Compare commits

..

2 Commits

10 changed files with 216 additions and 8 deletions

View File

@ -15,6 +15,8 @@ const paymentSchema = new Schema(
}, },
paymentDate: { type: Date, required: false }, paymentDate: { type: Date, required: false },
postedAt: { type: Date, required: false }, postedAt: { type: Date, required: false },
authorisedAt: { type: Date, required: false },
declinedAt: { type: Date, required: false },
cancelledAt: { type: Date, required: false }, cancelledAt: { type: Date, required: false },
paymentMethod: { type: String, required: false }, paymentMethod: { type: String, required: false },
notes: { type: String, required: false }, notes: { type: String, required: false },
@ -39,6 +41,22 @@ const rollupConfigs = [
{ name: 'postedAmount', property: 'amount', operation: 'sum' }, { 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', name: 'cancelled',
filter: { 'state.type': 'cancelled' }, filter: { 'state.type': 'cancelled' },

View File

@ -36,7 +36,7 @@ import { fileModel } from './management/file.schema.js';
import { courierServiceModel } from './management/courierservice.schema.js'; import { courierServiceModel } from './management/courierservice.schema.js';
import { courierModel } from './management/courier.schema.js'; import { courierModel } from './management/courier.schema.js';
import { taxRateModel } from './management/taxrate.schema.js'; import { taxRateModel } from './management/taxrate.schema.js';
import { taxRecordModel } from './management/taxrecord.schema.js'; import { taxRecordModel } from './finance/taxrecord.schema.js';
import { shipmentModel } from './inventory/shipment.schema.js'; import { shipmentModel } from './inventory/shipment.schema.js';
import { invoiceModel } from './finance/invoice.schema.js'; import { invoiceModel } from './finance/invoice.schema.js';
import { clientModel } from './sales/client.schema.js'; import { clientModel } from './sales/client.schema.js';

View File

@ -14,6 +14,8 @@ import {
getPaymentStatsRouteHandler, getPaymentStatsRouteHandler,
getPaymentHistoryRouteHandler, getPaymentHistoryRouteHandler,
postPaymentRouteHandler, postPaymentRouteHandler,
authorisePaymentRouteHandler,
declinePaymentRouteHandler,
cancelPaymentRouteHandler, cancelPaymentRouteHandler,
} from '../../services/finance/payments.js'; } from '../../services/finance/payments.js';
@ -88,6 +90,14 @@ router.post('/:id/post', isAuthenticated, async (req, res) => {
postPaymentRouteHandler(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) => { router.post('/:id/cancel', isAuthenticated, async (req, res) => {
cancelPaymentRouteHandler(req, res); cancelPaymentRouteHandler(req, res);
}); });

View File

@ -12,7 +12,7 @@ import {
listTaxRecordsByPropertiesRouteHandler, listTaxRecordsByPropertiesRouteHandler,
getTaxRecordStatsRouteHandler, getTaxRecordStatsRouteHandler,
getTaxRecordHistoryRouteHandler, getTaxRecordHistoryRouteHandler,
} from '../../services/management/taxrecords.js'; } from '../../services/finance/taxrecords.js';
// list of tax records // list of tax records
router.get('/', isAuthenticated, (req, res) => { router.get('/', isAuthenticated, (req, res) => {

View File

@ -36,7 +36,7 @@ import documentJobsRoutes from './management/documentjobs.js';
import courierRoutes from './management/courier.js'; import courierRoutes from './management/courier.js';
import courierServiceRoutes from './management/courierservice.js'; import courierServiceRoutes from './management/courierservice.js';
import taxRateRoutes from './management/taxrates.js'; import taxRateRoutes from './management/taxrates.js';
import taxRecordRoutes from './management/taxrecords.js'; import taxRecordRoutes from './finance/taxrecords.js';
import invoiceRoutes from './finance/invoices.js'; import invoiceRoutes from './finance/invoices.js';
import paymentRoutes from './finance/payments.js'; import paymentRoutes from './finance/payments.js';
import clientRoutes from './sales/clients.js'; import clientRoutes from './sales/clients.js';

View File

@ -38,6 +38,9 @@ const {
getPaymentRouteHandler, getPaymentRouteHandler,
newPaymentRouteHandler, newPaymentRouteHandler,
postPaymentRouteHandler, postPaymentRouteHandler,
authorisePaymentRouteHandler,
declinePaymentRouteHandler,
cancelPaymentRouteHandler,
} = await import('../payments.js'); } = await import('../payments.js');
const { listObjects, getObject, editObject, newObject, checkStates } = await import( 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).',
})
);
});
});
}); });

View File

@ -11,7 +11,7 @@ jest.unstable_mockModule('../../../database/database.js', () => ({
getModelHistory: jest.fn(), getModelHistory: jest.fn(),
})); }));
jest.unstable_mockModule('../../../database/schemas/management/taxrecord.schema.js', () => ({ jest.unstable_mockModule('../../../database/schemas/finance/taxrecord.schema.js', () => ({
taxRecordModel: { modelName: 'TaxRecord' }, taxRecordModel: { modelName: 'TaxRecord' },
})); }));
@ -37,7 +37,7 @@ const {
const { listObjects, getObject, editObject, newObject } = await import( const { listObjects, getObject, editObject, newObject } = await import(
'../../../database/database.js' '../../../database/database.js'
); );
const { taxRecordModel } = await import('../../../database/schemas/management/taxrecord.schema.js'); const { taxRecordModel } = await import('../../../database/schemas/finance/taxrecord.schema.js');
describe('Tax Record Service Route Handlers', () => { describe('Tax Record Service Route Handlers', () => {
let req, res; let req, res;

View File

@ -322,6 +322,88 @@ export const postPaymentRouteHandler = async (req, res) => {
res.send(result); 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) => { export const cancelPaymentRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id); const id = new mongoose.Types.ObjectId(req.params.id);
@ -330,7 +412,7 @@ export const cancelPaymentRouteHandler = async (req, res) => {
const checkStatesResult = await checkStates({ const checkStatesResult = await checkStates({
model: paymentModel, model: paymentModel,
id, id,
states: ['draft', 'posted'], states: ['posted'],
}); });
if (checkStatesResult.error) { if (checkStatesResult.error) {
@ -342,7 +424,7 @@ export const cancelPaymentRouteHandler = async (req, res) => {
if (checkStatesResult === false) { if (checkStatesResult === false) {
logger.error('Payment is not in a cancellable state.'); logger.error('Payment is not in a cancellable state.');
res.status(400).send({ 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, code: 400,
}); });
return; return;

View File

@ -1,5 +1,5 @@
import config from '../../config.js'; import config from '../../config.js';
import { taxRecordModel } from '../../database/schemas/management/taxrecord.schema.js'; import { taxRecordModel } from '../../database/schemas/finance/taxrecord.schema.js';
import log4js from 'log4js'; import log4js from 'log4js';
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { import {