Added authorisation and decline functionality for payments, including new route handlers and schema updates to track authorisation and decline timestamps. Updated payment cancellation logic to reflect changes in payment states.
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
This commit is contained in:
parent
2996b1670f
commit
f74b85bb88
@ -15,6 +15,8 @@ const paymentSchema = new Schema(
|
||||
},
|
||||
paymentDate: { type: Date, required: false },
|
||||
postedAt: { type: Date, required: false },
|
||||
authorisedAt: { type: Date, required: false },
|
||||
declinedAt: { type: Date, required: false },
|
||||
cancelledAt: { type: Date, required: false },
|
||||
paymentMethod: { type: String, required: false },
|
||||
notes: { type: String, required: false },
|
||||
@ -39,6 +41,22 @@ const rollupConfigs = [
|
||||
{ name: 'postedAmount', property: 'amount', operation: 'sum' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'authorised',
|
||||
filter: { 'state.type': 'authorised' },
|
||||
rollups: [
|
||||
{ name: 'authorisedCount', property: 'state.type', operation: 'count' },
|
||||
{ name: 'authorisedAmount', property: 'amount', operation: 'sum' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'declined',
|
||||
filter: { 'state.type': 'declined' },
|
||||
rollups: [
|
||||
{ name: 'declinedCount', property: 'state.type', operation: 'count' },
|
||||
{ name: 'declinedAmount', property: 'amount', operation: 'sum' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'cancelled',
|
||||
filter: { 'state.type': 'cancelled' },
|
||||
|
||||
@ -14,6 +14,8 @@ import {
|
||||
getPaymentStatsRouteHandler,
|
||||
getPaymentHistoryRouteHandler,
|
||||
postPaymentRouteHandler,
|
||||
authorisePaymentRouteHandler,
|
||||
declinePaymentRouteHandler,
|
||||
cancelPaymentRouteHandler,
|
||||
} from '../../services/finance/payments.js';
|
||||
|
||||
@ -88,6 +90,14 @@ router.post('/:id/post', isAuthenticated, async (req, res) => {
|
||||
postPaymentRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.post('/:id/authorise', isAuthenticated, async (req, res) => {
|
||||
authorisePaymentRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.post('/:id/decline', isAuthenticated, async (req, res) => {
|
||||
declinePaymentRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.post('/:id/cancel', isAuthenticated, async (req, res) => {
|
||||
cancelPaymentRouteHandler(req, res);
|
||||
});
|
||||
|
||||
@ -38,6 +38,9 @@ const {
|
||||
getPaymentRouteHandler,
|
||||
newPaymentRouteHandler,
|
||||
postPaymentRouteHandler,
|
||||
authorisePaymentRouteHandler,
|
||||
declinePaymentRouteHandler,
|
||||
cancelPaymentRouteHandler,
|
||||
} = await import('../payments.js');
|
||||
|
||||
const { listObjects, getObject, editObject, newObject, checkStates } = await import(
|
||||
@ -120,5 +123,100 @@ describe('Payment Service Route Handlers', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('authorisePaymentRouteHandler', () => {
|
||||
it('should authorise a posted payment', async () => {
|
||||
req.params.id = '507f1f77bcf86cd799439011';
|
||||
checkStates.mockResolvedValue(true);
|
||||
editObject.mockResolvedValue({
|
||||
_id: '507f1f77bcf86cd799439011',
|
||||
state: { type: 'authorised' },
|
||||
});
|
||||
|
||||
await authorisePaymentRouteHandler(req, res);
|
||||
|
||||
expect(checkStates).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ states: ['posted'] })
|
||||
);
|
||||
expect(editObject).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
updateData: expect.objectContaining({
|
||||
state: { type: 'authorised' },
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(res.send).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail if payment is not in posted state', async () => {
|
||||
req.params.id = '507f1f77bcf86cd799439011';
|
||||
checkStates.mockResolvedValue(false);
|
||||
|
||||
await authorisePaymentRouteHandler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.send).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ error: 'Payment is not in posted state.' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('declinePaymentRouteHandler', () => {
|
||||
it('should decline a posted payment', async () => {
|
||||
req.params.id = '507f1f77bcf86cd799439011';
|
||||
checkStates.mockResolvedValue(true);
|
||||
editObject.mockResolvedValue({
|
||||
_id: '507f1f77bcf86cd799439011',
|
||||
state: { type: 'declined' },
|
||||
});
|
||||
|
||||
await declinePaymentRouteHandler(req, res);
|
||||
|
||||
expect(checkStates).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ states: ['posted'] })
|
||||
);
|
||||
expect(editObject).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
updateData: expect.objectContaining({
|
||||
state: { type: 'declined' },
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(res.send).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelPaymentRouteHandler', () => {
|
||||
it('should cancel a posted payment', async () => {
|
||||
req.params.id = '507f1f77bcf86cd799439011';
|
||||
checkStates.mockResolvedValue(true);
|
||||
editObject.mockResolvedValue({
|
||||
_id: '507f1f77bcf86cd799439011',
|
||||
state: { type: 'cancelled' },
|
||||
});
|
||||
|
||||
await cancelPaymentRouteHandler(req, res);
|
||||
|
||||
expect(checkStates).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ states: ['posted'] })
|
||||
);
|
||||
expect(editObject).toHaveBeenCalled();
|
||||
expect(res.send).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail if payment is not in posted state', async () => {
|
||||
req.params.id = '507f1f77bcf86cd799439011';
|
||||
checkStates.mockResolvedValue(false);
|
||||
|
||||
await cancelPaymentRouteHandler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.send).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
error: 'Payment is not in a cancellable state (must be posted).',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -322,6 +322,88 @@ export const postPaymentRouteHandler = async (req, res) => {
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const authorisePaymentRouteHandler = async (req, res) => {
|
||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||
|
||||
logger.trace(`Payment with ID: ${id}`);
|
||||
|
||||
const checkStatesResult = await checkStates({ model: paymentModel, id, states: ['posted'] });
|
||||
|
||||
if (checkStatesResult.error) {
|
||||
logger.error('Error checking payment states:', checkStatesResult.error);
|
||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkStatesResult === false) {
|
||||
logger.error('Payment is not in posted state.');
|
||||
res.status(400).send({ error: 'Payment is not in posted state.', code: 400 });
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
updatedAt: new Date(),
|
||||
state: { type: 'authorised' },
|
||||
authorisedAt: new Date(),
|
||||
};
|
||||
const result = await editObject({
|
||||
model: paymentModel,
|
||||
id,
|
||||
updateData,
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
logger.error('Error authorising payment:', result.error);
|
||||
res.status(result.code).send(result);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`Authorised payment with ID: ${id}`);
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const declinePaymentRouteHandler = async (req, res) => {
|
||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||
|
||||
logger.trace(`Payment with ID: ${id}`);
|
||||
|
||||
const checkStatesResult = await checkStates({ model: paymentModel, id, states: ['posted'] });
|
||||
|
||||
if (checkStatesResult.error) {
|
||||
logger.error('Error checking payment states:', checkStatesResult.error);
|
||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkStatesResult === false) {
|
||||
logger.error('Payment is not in posted state.');
|
||||
res.status(400).send({ error: 'Payment is not in posted state.', code: 400 });
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
updatedAt: new Date(),
|
||||
state: { type: 'declined' },
|
||||
declinedAt: new Date(),
|
||||
};
|
||||
const result = await editObject({
|
||||
model: paymentModel,
|
||||
id,
|
||||
updateData,
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
logger.error('Error declining payment:', result.error);
|
||||
res.status(result.code).send(result);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`Declined payment with ID: ${id}`);
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const cancelPaymentRouteHandler = async (req, res) => {
|
||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||
|
||||
@ -330,7 +412,7 @@ export const cancelPaymentRouteHandler = async (req, res) => {
|
||||
const checkStatesResult = await checkStates({
|
||||
model: paymentModel,
|
||||
id,
|
||||
states: ['draft', 'posted'],
|
||||
states: ['posted'],
|
||||
});
|
||||
|
||||
if (checkStatesResult.error) {
|
||||
@ -342,7 +424,7 @@ export const cancelPaymentRouteHandler = async (req, res) => {
|
||||
if (checkStatesResult === false) {
|
||||
logger.error('Payment is not in a cancellable state.');
|
||||
res.status(400).send({
|
||||
error: 'Payment is not in a cancellable state (must be draft or posted).',
|
||||
error: 'Payment is not in a cancellable state (must be posted).',
|
||||
code: 400,
|
||||
});
|
||||
return;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user