mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 21:48:18 +00:00
chore: more reporting
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
BULK_FREIGHT_CHARGES,
|
||||
PAYMENT_CLASSES,
|
||||
PAYMENT_CLASS_EXPR,
|
||||
PERIOD_FILTER,
|
||||
REVENUE_CATEGORIES,
|
||||
REVENUE_CATEGORY_EXPR,
|
||||
periodExpr,
|
||||
} from './revenue-classification';
|
||||
|
||||
/**
|
||||
* `invoice_lines.charge_type` is an unconstrained varchar written by eight
|
||||
* unrelated code paths. Nothing at the type level stops someone adding a ninth
|
||||
* spelling, whose revenue would then land silently in the ELSE arm.
|
||||
*
|
||||
* This list is every value the codebase writes today. When it grows, these
|
||||
* tests are what fail — which is the whole trade the const-map design makes.
|
||||
*/
|
||||
const KNOWN_CHARGE_TYPES = [
|
||||
// booking base freight (rate_type codes)
|
||||
'CONTAINER_IMPORT', 'CONTAINER_EXPORT', 'CONTAINER_20FT', 'CONTAINER_40FT',
|
||||
'BULK_IMPORT', 'BULK_EXPORT', 'INTERCITY_BULK', 'INTERCITY_CONTAINER', 'FREIGHT',
|
||||
// surcharges
|
||||
'FUEL_SURCHARGE', 'LASHING', 'OVERWEIGHT_PER_TON', 'HAZARD_SURCHARGE',
|
||||
'REEFER_SURCHARGE', 'PIL_EXTRA_FEE', 'RETURN_SURCHARGE', 'RETURN_SURCHARGE_20FT',
|
||||
'RETURN_SURCHARGE_40FT', 'CONTAINER_WITH_RETURN', 'ADJUSTMENT', 'RATE_ADJUSTMENT',
|
||||
// customs
|
||||
'CUSTOMS_CLEARANCE', 'CUSTOMS_CLEARANCE_20FT', 'CUSTOMS_CLEARANCE_40FT',
|
||||
// mile legs
|
||||
'FIRST_MILE', 'LAST_MILE', 'DELIVERY', 'LAST_MILE_ADVANCE',
|
||||
// warehouse fees
|
||||
'CONTAINER_DEMURRAGE', 'BULK_DEMURRAGE', 'DEMURRAGE', 'STORAGE_FEE',
|
||||
'HANDLING_FEE', 'DOUBLE_HANDLING', 'TRUCK_DETENTION',
|
||||
// other producers
|
||||
'CANCELLATION_FEE', 'SHIPPING_LINE_SERVICE',
|
||||
];
|
||||
|
||||
/**
|
||||
* Does the expression name this charge type — either as a literal or through
|
||||
* one of its `LIKE 'PREFIX%'` arms?
|
||||
*
|
||||
* Deliberately a substring check, not a SQL parser: a parser would be more
|
||||
* fragile than the expression it is guarding. This catches the failure that
|
||||
* actually happens (a new charge type nobody added to the map) and nothing
|
||||
* pretends it verifies the branch order.
|
||||
*/
|
||||
function isNamed(expr: string, chargeType: string): boolean {
|
||||
if (expr.includes(`'${chargeType}'`)) return true;
|
||||
return [...expr.matchAll(/LIKE '([^']*)%'/g)].some(([, prefix]) =>
|
||||
chargeType.startsWith(prefix),
|
||||
);
|
||||
}
|
||||
|
||||
describe('revenue classification', () => {
|
||||
it('names every charge type the codebase writes in the payment-class map', () => {
|
||||
const unmapped = KNOWN_CHARGE_TYPES.filter((c) => !isNamed(PAYMENT_CLASS_EXPR, c));
|
||||
expect(unmapped).toEqual([]);
|
||||
});
|
||||
|
||||
it('names every ancillary charge type in the revenue-category map', () => {
|
||||
// Bulk freight lines carry no category of their own — the CASE falls
|
||||
// through to the booking's cargo type and trade direction for those.
|
||||
const cargoDerived = new Set(BULK_FREIGHT_CHARGES);
|
||||
const unmapped = KNOWN_CHARGE_TYPES.filter(
|
||||
(c) => !cargoDerived.has(c) && !isNamed(REVENUE_CATEGORY_EXPR, c),
|
||||
);
|
||||
expect(unmapped).toEqual([]);
|
||||
});
|
||||
|
||||
it('emits only categories that are offered as filter options', () => {
|
||||
const declared = new Set(REVENUE_CATEGORIES.map((c) => c.value));
|
||||
const emitted = [...REVENUE_CATEGORY_EXPR.matchAll(/THEN '([A-Z_]+)'/g)].map((m) => m[1]);
|
||||
expect(emitted.length).toBeGreaterThan(0);
|
||||
expect(emitted.filter((c) => !declared.has(c))).toEqual([]);
|
||||
expect(declared.has('UNCLASSIFIED')).toBe(true);
|
||||
});
|
||||
|
||||
it('emits only payment classes that are offered as filter options', () => {
|
||||
const declared = new Set(PAYMENT_CLASSES.map((c) => c.value));
|
||||
const emitted = [...PAYMENT_CLASS_EXPR.matchAll(/THEN '([A-Z_]+)'/g)].map((m) => m[1]);
|
||||
expect(emitted.filter((c) => !declared.has(c))).toEqual([]);
|
||||
expect(declared.has('ADDITIONAL')).toBe(true);
|
||||
});
|
||||
|
||||
it('falls back to a whitelisted period unit instead of interpolating input', () => {
|
||||
expect(periodExpr({ period: 'quarter' })).toContain("date_trunc('quarter'");
|
||||
expect(periodExpr({ period: 'year' })).toContain("date_trunc('year'");
|
||||
// Anything unrecognised — including an injection attempt — becomes 'month'.
|
||||
expect(periodExpr({ period: "day'); DROP TABLE freight.invoices; --" })).toContain(
|
||||
"date_trunc('month'",
|
||||
);
|
||||
expect(periodExpr({})).toContain("date_trunc('month'");
|
||||
});
|
||||
|
||||
it('offers exactly the period units the expression understands', () => {
|
||||
const offered = (PERIOD_FILTER.options ?? []).map((o) => o.value);
|
||||
expect(offered.length).toBe(5);
|
||||
for (const unit of offered) {
|
||||
expect(periodExpr({ period: unit })).toContain(`date_trunc('${unit}'`);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user