import { CARGO_CATEGORIES, CARGO_CATEGORY_EXPR, CARGO_CATEGORY_LABEL_EXPR, CONTAINER_CLASSES, CONTAINER_CLASS_EXPR, HANDLING_STANDARD_HOURS_EXPR, TARGET_DIMENSION_KEYS, cycleRateExpr, handlingHours, implementRateExpr, loadingHours, otherActivityHours, plannedRowsSql, } from './operations-classification'; import { TARGET_DIMENSIONS, TARGET_METRICS } from '../operations-reporting/entities/operations-target.entity'; /** * Every key a classification CASE can emit, read straight off the expression. * The categories are the join key between a report and its planned target, so a * key the reports emit but the target dimension list does not offer is a plan * nobody can ever enter. */ function emittedKeys(expr: string): string[] { return [...expr.matchAll(/THEN '([A-Z_]+)'/g)] .map(([, key]) => key) .concat([...expr.matchAll(/ELSE '([A-Z_]+)'/g)].map(([, key]) => key)); } describe('operations classification', () => { /** * The loading window falls back to the bookings that boarded at the stop, off * the DEPARTING schedule — a turnaround loads for the leg it leaves on, not * the one it arrived on. Losing either half of that silently turns a * populated report back into an empty one. */ it('falls back to the booking-derived loading window, off the departing leg', () => { for (const expr of [loadingHours('s'), handlingHours('s')]) { expect(expr).toContain('COALESCE(s.loading_started_at'); expect(expr).toContain('s.departed_schedule_id'); expect(expr).toContain('b.origin_yard_id = (s.yard_id)'); } }); /** Unloading is never derived — auto-unload would report it as ~0 hours. */ it('never derives the unloading half', () => { expect(handlingHours('s')).toContain('s.unloading_started_at'); expect(handlingHours('s')).not.toContain('b.destination_yard_id'); }); /** * Every other standard coalesces to the spec's figure. This one must not: * the spec names no handling standard, and a fallback would publish a rate * against a number nobody agreed to. */ it('leaves the handling standard null when nobody has set one', () => { expect(HANDLING_STANDARD_HOURS_EXPR).toContain('std.handling_standard_hours_container'); expect(HANDLING_STANDARD_HOURS_EXPR).not.toContain('COALESCE(std.handling'); }); /** An unmeasured stop reports unknown activity, not a full stay of it. */ it('keeps other activity null when there is no handling window', () => { expect(otherActivityHours('stay', 'handling')).toContain('IS NULL THEN NULL'); }); it('offers every cargo category the expression can emit as a filter option', () => { const offered = new Set(CARGO_CATEGORIES.map((o) => o.value)); const missing = [...new Set(emittedKeys(CARGO_CATEGORY_EXPR))].filter((k) => !offered.has(k)); expect(missing).toEqual([]); }); it('offers every container class the expression can emit', () => { const offered = new Set(CONTAINER_CLASSES.map((o) => o.value)); const missing = [...new Set(emittedKeys(CONTAINER_CLASS_EXPR))].filter((k) => !offered.has(k)); expect(missing).toEqual([]); }); it('labels every category, leaving none showing a raw key', () => { for (const option of CARGO_CATEGORIES) { expect(CARGO_CATEGORY_LABEL_EXPR).toContain(`'${option.label}'`); } }); /** * A planner types a dimension key into the targets screen; the reports match * it against what their CASE emits. If the two lists ever drift, a target is * silently ignored — the report shows no plan and nobody is told why. */ it('accepts every emitted key as a target dimension key', () => { const emitted = [ ...new Set([ ...emittedKeys(CARGO_CATEGORY_EXPR), ...emittedKeys(CONTAINER_CLASS_EXPR), ]), ]; const unplannable = emitted.filter((k) => !TARGET_DIMENSION_KEYS.includes(k)); expect(unplannable).toEqual([]); }); it('keeps the target metric and dimension vocabularies non-empty and distinct', () => { expect(new Set(TARGET_METRICS).size).toBe(TARGET_METRICS.length); expect(new Set(TARGET_DIMENSIONS).size).toBe(TARGET_DIMENSIONS.length); }); /** * The spec's worked example: a full trainset holds 50 wagons, 30 of them * carry multimodal cargo, so that cargo operated 0.6 trainsets. The SQL does * this division; this checks the arithmetic the SQL encodes. */ it('matches the spec worked example for trainsets', () => { expect(Number((30 / 50).toFixed(2))).toBe(0.6); }); /** Ten 40ft boxes and thirty 20ft boxes is fifty TEU, not forty. */ it('matches the spec worked example for TEU', () => { expect(10 * 2 + 30 * 1).toBe(50); }); it('divides by NULLIF so a missing plan yields no rate rather than infinity', () => { expect(implementRateExpr('operated', 'planned')).toContain('NULLIF(planned, 0)'); }); /** * [(SC − AD) / SC + 1] × 100 — finishing exactly on standard scores 100, and * beating it scores above 100. Guards the sign, which is easy to invert. */ it('encodes the turnaround rate so on-standard is 100 and faster is more', () => { const rate = (sc: number, ad: number) => ((sc - ad) / sc + 1) * 100; expect(rate(65, 65)).toBe(100); expect(rate(65, 52)).toBeGreaterThan(100); expect(rate(65, 78)).toBeLessThan(100); expect(cycleRateExpr('ad', 'sc')).toContain('NULLIF(sc, 0)'); }); /** * The plan side is FULL OUTER JOINed to the operated side, so a plan row for * a category the user filtered out comes back as a row of zeros — the bug * where `?categories=FERTILIZER` still returned all ten planned categories. */ describe('plannedRowsSql cargo filter', () => { const sqlFor = (dimension: string, params: Record): string => plannedRowsSql('VOLUME_TONS', dimension, params, 'SELECT 1'); it('restricts targets to the selected categories', () => { expect(sqlFor('cargo_category', { categories: ['FERTILIZER'] })).toContain( "AND ot.dimension_key IN ('FERTILIZER')", ); }); it('restricts a station target on its cargo type, not its key', () => { const sql = sqlFor('station', { categories: ['SAND'] }); expect(sql).toContain("AND ot.cargo_category IN ('SAND')"); expect(sql).not.toContain('ot.dimension_key IN'); }); it('filters a container class report on its own vocabulary', () => { expect(sqlFor('container_class', { classes: ['CONTAINER_EXPORT'] })).toContain( "AND ot.dimension_key IN ('CONTAINER_EXPORT')", ); }); it('leaves every target when nothing is selected', () => { expect(sqlFor('cargo_category', {})).not.toContain('ot.dimension_key IN'); }); it('matches nothing on a value no category expression can emit', () => { expect(sqlFor('cargo_category', { categories: ["x'; DROP TABLE"] })).toContain('AND FALSE'); }); it('narrows a station plan to the chosen country rather than suppressing it', () => { const sql = sqlFor('station', { country: 'Djibouti' }); expect(sql).toContain("y.country = 'Djibouti'"); expect(sql).not.toContain('AND FALSE'); }); it('ignores a country that is not one of the two sides', () => { expect(sqlFor('station', { country: "' OR true --" })).not.toContain('y.country'); }); it('leaves the country alone on a plan not keyed by station', () => { expect(sqlFor('cargo_category', { country: 'Djibouti' })).not.toContain('y.country'); }); /** * No target carries a route, a train or a direction, so beside a * route-filtered actual the plan would be the whole corridor's target. */ it.each(['origin', 'destination', 'trainNumber', 'direction'])( 'reports no plan at all when %s narrows below the target grain', (key) => { expect(sqlFor('cargo_category', { [key]: 'X' })).toContain('AND FALSE'); }, ); it('keeps the plan when only period, date and category are set', () => { const sql = sqlFor('cargo_category', { period: 'month', dateFrom: '2026-01-01', categories: ['SAND'], }); expect(sql).not.toContain('AND FALSE'); }); }); });