Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions app/schemas/customerpayment.schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,31 @@ const isoDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
message: 'Must be an ISO 8601 date (YYYY-MM-DD).',
});

// A payment amount must be a real, non-zero number. Zero is operator
// error (recording a "$0 payment" against a customer ledger has no
// business meaning); Infinity/-Infinity is data-quality error from
// pathological clients — `z.number()` rejects NaN but allows the
// infinities by default, and a DOUBLE column will happily store them.
// Negative values pass — some operators model refunds that way.
const cpayAmountField = z.coerce.number().finite({
message: 'cpayAmount must be a finite number.',
}).refine((n) => n !== 0, {
message: 'cpayAmount must not be zero.',
});

const createCustomerPaymentBody = z.object({
cpayCustId: z.coerce.number().int().positive(),
cpayDescription: z.string().max(10000).optional(),
cpayDate: isoDate,
cpayAmount: z.coerce.number(),
cpayAmount: cpayAmountField,
}).strict({
message: 'Unexpected field in body. Whitelist: cpayCustId, cpayDescription, cpayDate, cpayAmount.',
});

const updateCustomerPaymentBody = z.object({
cpayDescription: z.string().max(10000).optional(),
cpayDate: isoDate.optional(),
cpayAmount: z.coerce.number().optional(),
cpayAmount: cpayAmountField.optional(),
}).strict({
message: 'Unexpected field in body. Whitelist: cpayDescription, cpayDate, cpayAmount.',
});
Expand Down
21 changes: 21 additions & 0 deletions tests/api/customerpayment.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,25 @@ describe('CustomerPayment body validation', () => {
const res = await request(app).post('/v1/customerpayment').set('authKey', 'any').send({ cpayCustId: 1, cpayDate: '2026-01-01' });
expect(res.status).toBe(400);
});

test('POST rejects zero cpayAmount', async () => {
const res = await request(app).post('/v1/customerpayment').set('authKey', 'any')
.send({ cpayCustId: 1, cpayDate: '2026-01-01', cpayAmount: 0 });
expect(res.status).toBe(400);
});

test('POST accepts a negative cpayAmount (refund model)', async () => {
// Some operators record refunds as negative payments. The
// schema only blocks 0 and the infinities, not negatives —
// pin that so a future tightening surfaces here.
const res = await request(app).post('/v1/customerpayment').set('authKey', 'any')
.send({ cpayCustId: 1, cpayDate: '2026-01-01', cpayAmount: -50 });
expect(res.status).not.toBe(400);
});

test('PATCH rejects zero cpayAmount', async () => {
const res = await request(app).patch('/v1/customerpayment/1').set('authKey', 'any')
.send({ cpayAmount: 0 });
expect(res.status).toBe(400);
});
});