Files
edr-platform/apps/edr-freight-api/src/modules/operations-reporting/dto/create-operations-target.dto.ts
ghost2023 c8b44d4d04 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.
2026-08-21 17:37:21 +03:00

80 lines
2.1 KiB
TypeScript

import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import {
IsDateString,
IsIn,
IsNumber,
IsOptional,
IsString,
MaxLength,
Min,
} from 'class-validator';
import {
TARGET_DIMENSIONS,
TARGET_METRICS,
TARGET_PERIOD_TYPES,
TargetDimension,
TargetMetric,
TargetPeriodType,
} from '../entities/operations-target.entity';
const toNumber = ({ value }: { value: unknown }) =>
value === '' || value == null ? value : Number(value);
export class CreateOperationsTargetDto {
@ApiProperty({ enum: TARGET_PERIOD_TYPES })
@IsIn(TARGET_PERIOD_TYPES as unknown as string[])
periodType!: TargetPeriodType;
@ApiProperty({
example: '2026-08-01',
description: 'Any date inside the bucket — normalised to the bucket start on write.',
})
@IsDateString()
periodStart!: string;
@ApiProperty({ enum: TARGET_METRICS })
@IsIn(TARGET_METRICS as unknown as string[])
metric!: TargetMetric;
@ApiProperty({ enum: TARGET_DIMENSIONS })
@IsIn(TARGET_DIMENSIONS as unknown as string[])
dimension!: TargetDimension;
@ApiProperty({
example: 'CONTAINER_IMPORT_MULTIMODAL',
description: 'Category key, container-class key or yard code — not a display label.',
})
@IsString()
@MaxLength(60)
dimensionKey!: string;
@ApiProperty({ example: 1200 })
@Transform(toNumber)
@IsNumber()
@Min(0)
plannedValue!: number;
@ApiPropertyOptional({
description:
'Station targets only: which cargo category this station plan covers. Ignored for the ' +
'other dimensions, whose key already carries the category.',
example: 'CONTAINER_IMPORT_MULTIMODAL',
})
@IsOptional()
// `'' ?? null` is `''`, and an empty string matches neither the unique
// index's `COALESCE(cargo_category, '')` nor the report's join — it reads as
// a category that does not exist. Blank means absent.
@Transform(({ value }) => (value === '' ? null : value))
@IsString()
@MaxLength(60)
cargoCategory?: string | null;
@ApiPropertyOptional()
@IsOptional()
@IsString()
@MaxLength(500)
note?: string;
}