test(reports): pin the receivable/payable ledger sides

Guards the two facts the report exists to get right: the cancellation
fee is never a payable, and exactly one wagon-cancellation status
(CREDIT_AVAILABLE) is a live liability. Adding a status to
WAGON_CANCELLATION_STATUSES now fails here until someone decides which
side of the ledger it lands on.

Also pins the sort expressions to the union wrapper alias — a branch
alias would resolve at build time and 42P01 at runtime, since the runner
appends ORDER BY outside the subquery.
This commit is contained in:
ghost2023
2026-08-21 17:49:02 +03:00
parent 6d113ca00c
commit 53e4b93ac9

View File

@@ -0,0 +1,71 @@
import { WAGON_CANCELLATION_STATUSES } from '../../bookings/entities/booking-wagon-cancellation.entity';
import { ShippingLineCreditStatus } from '../../shipping-lines/entities/shipping-line-credit.entity';
import {
CREDIT_LIABILITY_STATUS,
INVOICE_SIDE_EXPR,
LEDGER_SIDES,
UNINVOICED_CREDIT_STATUS,
receivablesPayablesReport,
} from './receivables-payables.report';
/**
* The report's whole point is the sign of the money: a cancellation FEE is
* owed TO EDR, and the cancelled freight is owed BACK to the customer as
* bookable credit. These tests pin the two down at the string level — the SQL
* itself is validated against the database, not here.
*/
describe('receivables-payables report', () => {
it('treats exactly one wagon-cancellation status as a liability', () => {
expect(WAGON_CANCELLATION_STATUSES).toContain(CREDIT_LIABILITY_STATUS);
// Every other status owes nothing: nothing cut yet (FEE_PENDING), redeemed
// (REBOOKED), or voided (WITHDRAWN / EXPIRED). If a new status appears,
// this fails until someone decides which side of the ledger it lands on.
expect(WAGON_CANCELLATION_STATUSES.filter((s) => s !== CREDIT_LIABILITY_STATUS).sort()).toEqual(
['EXPIRED', 'FEE_PENDING', 'REBOOKED', 'WITHDRAWN'],
);
});
it('counts only the shipping-line credit status that has no invoice behind it', () => {
expect(UNINVOICED_CREDIT_STATUS).toBe(ShippingLineCreditStatus.Unbilled);
// BILLED is debt too, but it is counted through its invoice on the invoice
// branch — taking it here as well would double it.
expect(UNINVOICED_CREDIT_STATUS).not.toBe(ShippingLineCreditStatus.Billed);
});
it('never classifies the cancellation fee as a payable', () => {
// The fee invoice rides the booking's invoice list; while it is open it is
// an ordinary receivable balance, and it must not reach a PAYABLE arm.
expect(INVOICE_SIDE_EXPR).not.toContain('WAGON_CANCEL_FEE');
expect(INVOICE_SIDE_EXPR).not.toContain('CANCELLATION_FEE');
});
it('does not double-count a booking already carried by the cancellation ledger', () => {
expect(INVOICE_SIDE_EXPR).toContain('NOT EXISTS');
expect(INVOICE_SIDE_EXPR).toContain('booking_wagon_cancellations');
});
it('emits exactly the side keys the filter offers', () => {
const declared = LEDGER_SIDES.map((s) => s.value).sort();
expect(declared).toEqual([
'PAYABLE_PREPAID',
'PAYABLE_WAGON_CREDIT',
'RECEIVABLE_OPEN',
'RECEIVABLE_SL_INVOICED',
'RECEIVABLE_SL_UNBILLED',
]);
// The summary KPIs split on these prefixes; a key matching neither would
// silently vanish from both totals.
for (const key of declared) {
expect(key.startsWith('RECEIVABLE') || key.startsWith('PAYABLE')).toBe(true);
}
});
it('sorts on the union wrapper, never on a branch-local alias', () => {
// The runner appends ORDER BY outside the union subquery, where `i.*`,
// `b.*` and `bwc.*` do not exist.
for (const col of receivablesPayablesReport.columns) {
if (!col.sortExpr) continue;
expect(col.sortExpr).toMatch(/^r\./);
}
});
});