mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 22:18:12 +00:00
fix(reports): filter the plan side of plan-versus-actual reports
The cargo filter was applied only to the operated and attainment subqueries. The plan side selected targets by metric and dimension alone, and the FULL OUTER JOIN put every filtered-out key back as a row of zeros — ?categories=FERTILIZER returned all ten planned categories. planKeyFilter restricts targets to the selected categories (or container classes), reading ot.cargo_category for a station plan and ot.dimension_key otherwise. Values are whitelisted against the vocabulary and inlined, because the fragment is assembled into raw CTE text and the runner does not validate multiselect values. Two related grain leaks close with it: - planCountryFilter narrows a station plan to the chosen country. All seven station targets are Ethiopian, so the Djibouti view was listing 33 Ethiopian targets as stations that moved nothing. - planGrainFilter drops the plan entirely when origin, destination, train number or direction is set. No target carries a route, so the plan there was the whole corridor's target sitting beside one slice of its work, and the implement rate read as a miss that never happened. Fixes cargo-volume-performance, cargo-volume-by-station, trainset-performance and teu-performance together. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
TARGET_DIMENSION_KEYS,
|
||||
cycleRateExpr,
|
||||
implementRateExpr,
|
||||
plannedRowsSql,
|
||||
} from './operations-classification';
|
||||
import { TARGET_DIMENSIONS, TARGET_METRICS } from '../operations-reporting/entities/operations-target.entity';
|
||||
|
||||
@@ -91,4 +92,74 @@ describe('operations classification', () => {
|
||||
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, unknown>): 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user