Compare commits
2 Commits
a2bfd707e2
...
f74b85bb88
| Author | SHA1 | Date | |
|---|---|---|---|
| f74b85bb88 | |||
| 2996b1670f |
@ -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' },
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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) => {
|
||||||
@ -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';
|
||||||
|
|||||||
@ -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).',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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 {
|
||||||
Loading…
x
Reference in New Issue
Block a user