fix(operations-targets): derive cargo category from its dimension

A station target's plan is per station AND per cargo type; the other two
dimensions already carry the category inside dimension_key. update() kept
whatever was stored whenever the payload omitted the key, and the admin
form builds its payload from visible fields only — so switching "Plan by"
away from Station left the old category behind on a row that no longer has
any use for one.

That row is not merely untidy. It survives the COALESCE(cargo_category,'')
unique index alongside the legitimate null-category row for the same key,
plannedRowsSql groups by the column, and the two plan rows then join the
same operated row through the FULL OUTER JOIN: the category lists twice,
each line carrying the full operated tonnage, while the summary tiles are
computed separately and stay correct — so the table disagrees with its own
totals and nothing says why.

create() and update() now resolve the slot through one place, which is the
point: the two paths cannot drift again. cargoCategory is derived from the
effective dimension rather than carried over, and a station target without
one is rejected instead of stored as a plan the report can never match.

dimensionKey is checked against the vocabulary the reports actually emit —
CARGO_CATEGORIES / CONTAINER_CLASSES, or live yards.code for stations. An
unknown key used to store fine, list fine and fall back to showing the raw
key, while being a plan no report would ever find.

Also drops @Global from the module. Nothing outside it injects either
service; the reports read both tables in raw SQL, so the docstring's stated
reason for being global was not true.
This commit is contained in:
ghost2023
2026-08-21 17:37:21 +03:00
parent 2317684db9
commit c8b44d4d04
4 changed files with 138 additions and 20 deletions

View File

@@ -149,6 +149,12 @@ const TRADE_DIRECTIONS = [
* a report matches a target by this exact key, so a value here that the API
* does not emit is a plan the report will never find. The API spec
* `operations-classification.spec.ts` guards the API side of the pair.
*
* Drift is no longer silent: `OperationsTargetsService.assertDimensionKey`
* rejects any key outside the API's own vocabulary, so a stale entry here
* surfaces as a 400 on save rather than a plan that quietly never joins.
* `UNCLASSIFIED` is left out deliberately — the API accepts it, but there is no
* sense in planning against cargo nobody has classified.
*/
export const OPERATIONS_CARGO_CATEGORIES = [
{ label: "Multimodal container import", value: "CONTAINER_IMPORT_MULTIMODAL" },