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'. const injection = "day'); DROP TABLE freight.invoices; --"; expect(periodExpr({ period: injection })).toContain("date_trunc('month'"); expect(periodExpr({ period: injection })).not.toContain('DROP TABLE'); 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).toEqual([ 'day', 'week', 'month', 'quarter', 'half_year', 'nine_month', 'ninety_day', 'year', ]); // Every offered unit resolves to its own expression rather than silently // falling through to the month default — which is what a missing entry or a // typo'd key would look like. const expressions = offered.map((unit) => periodExpr({ period: unit })); expect(new Set(expressions).size).toBe(offered.length); }); /** * Half-year, nine-month and ninety-day have no `date_trunc` unit, so they are * offset arithmetic anchored to January 1st. These pin the anchor: they are * the SQL half of a pair whose other half is `normalisePeriodStart` in * `operations-targets.service.ts`, and a target that snaps to a boundary the * report does not bucket on plans against a period that does not exist. */ it('anchors the irregular units to the start of the calendar year', () => { for (const unit of ['half_year', 'nine_month', 'ninety_day']) { const expr = periodExpr({ period: unit }); expect(expr).toContain("date_trunc('year'"); expect(expr).not.toContain(`date_trunc('${unit}'`); } // Six- and nine-month blocks count whole months from January. expect(periodExpr({ period: 'half_year' })).toContain("INTERVAL '6 months'"); expect(periodExpr({ period: 'nine_month' })).toContain("INTERVAL '9 months'"); // 90-day blocks count days, and cap at the fourth so the last days of // December widen block four instead of forming a 5-day stub of their own. const ninety = periodExpr({ period: 'ninety_day' }); expect(ninety).toContain("INTERVAL '90 days'"); expect(ninety).toContain('LEAST('); expect(ninety).toContain('/ 90, 3)'); }); });