feat(WIP): filtering, exporting and more reports

This commit is contained in:
Nathnael
2026-08-19 13:51:18 +00:00
parent e964a9b8f4
commit fb21ad1541
56 changed files with 3750 additions and 27 deletions

View File

@@ -0,0 +1,525 @@
import { ObjectLiteral, SelectQueryBuilder } from 'typeorm';
import { Booking } from '../bookings/entities/booking.entity';
import { OperationsStandard } from '../operations-reporting/entities/operations-standard.entity';
import { CargoType } from '../rule-engine/entities/cargo-type.entity';
import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity';
import { TrainSetWagon } from '../train-sets/entities/train-set-wagon.entity';
import { WagonBookingAllocation } from '../train-schedules/entities/wagon-booking-allocation.entity';
import { Yard } from '../rule-engine/entities/yard.entity';
import { applyDirectionScope } from '../user-trade-access/trade-scope.util';
import { ReportContext, ReportFilterDef, ReportFilterOption } from './report.types';
import { yardOptions } from './revenue-classification';
/**
* The shared vocabulary and SQL behind every operations report — turnaround,
* delay, trainset, TEU and cargo volume.
*
* The fact table is `wagon_booking_allocations`: one row is one booking's cargo
* on one wagon of one departure. That is the marshalling record — what was
* actually put on the train — and it is the only grain that can answer both
* "how many TEU moved" and "how many wagons did it take", which the volume and
* trainset reports need together.
*
* Every consumer builds its FROM through {@link allocationLedgerQb}, so the
* table aliases below (`wba tsw ts b ct oy dy std`) are a fixed contract and
* the fragments here reference them directly.
*
* This is deliberately a SECOND classification module rather than an extension
* of `revenue-classification.ts`. That one classifies invoice lines by charge
* code; this one classifies physical cargo by booking and cargo type. The two
* answer different questions and a row that is one revenue category can be a
* different operational category — an incidental charge on a container booking,
* for instance, is INCIDENTAL revenue but container tonnage.
*/
// ---------------------------------------------------------------------------
// Cargo categories
// ---------------------------------------------------------------------------
export const CARGO_CATEGORIES: ReportFilterOption[] = [
{ value: 'CONTAINER_IMPORT_MULTIMODAL', label: 'Multimodal container import' },
{ value: 'CONTAINER_IMPORT_UNIMODAL', label: 'Unimodal container import' },
{ value: 'CONTAINER_EXPORT', label: 'Export container' },
{ value: 'EMPTY_CONTAINER', label: 'Empty container' },
{ value: 'FERTILIZER', label: 'Fertilizer' },
{ value: 'RORO', label: 'RoRo' },
{ value: 'BREAK_BULK', label: 'Break bulk' },
{ value: 'SAND', label: 'Sand' },
{ value: 'BULK', label: 'Bulk' },
{ value: 'OTHER_IMPORT', label: 'Other imports' },
{ value: 'OTHER_EXPORT', label: 'Other export cargo' },
{ value: 'UNCLASSIFIED', label: 'Unclassified' },
];
/**
* Container classes for the TEU report — the four the spec names.
* `EMPTY_CONTAINER_RETURN` is the empty re-export leg.
*/
export const CONTAINER_CLASSES: ReportFilterOption[] = [
{ value: 'CONTAINER_IMPORT_MULTIMODAL', label: 'Multimodal container import' },
{ value: 'CONTAINER_IMPORT_UNIMODAL', label: 'Unimodal container import' },
{ value: 'CONTAINER_EXPORT', label: 'Full export container' },
{ value: 'EMPTY_CONTAINER_RETURN', label: 'Empty container return' },
];
/** `cargo_types.code` is admin-managed, so each set absorbs every spelling seeded so far. */
export const RORO_CODES = ['TRUCK', 'AUTOMOBILE', 'CARS', 'RORO'];
export const BREAK_BULK_CODES = [
'BREAK_BULK',
'STEEL_BILLET',
'STEEL',
'MACHINERY',
'PIPES',
'TIMBER',
];
export const FERTILIZER_CODES = ['FERTILIZER'];
export const SAND_CODES = ['SAND'];
/** Cargo charged at the lighter per-wagon rate — vegetables, milk, meat, livestock. */
export const PERISHABLE_CODES = ['PERISHABLE', 'LIVESTOCK'];
const quote = (values: string[]): string => values.map((v) => `'${v}'`).join(', ');
/**
* A booking whose equipment_return is RETURN is the empty-container movement
* itself; WITH_RETURN / WITHOUT_RETURN describe a laden booking's obligation.
* This is the only booking-level marker of an empty box — no table records
* laden-vs-empty on the container row.
*/
const IS_EMPTY_CONTAINER = "b.equipment_return = 'RETURN'";
/**
* Multimodal means a named sea carrier is on the booking — the same proxy the
* revenue reports use. There is no explicit multimodal flag; confirm with the
* business before treating this as definitive.
*/
const IS_MULTIMODAL = 'b.shipping_line_id IS NOT NULL';
const IS_CONTAINER = "COALESCE(b.freight_type, wba.load_type) = 'CONTAINER'";
export const CARGO_CATEGORY_EXPR = `CASE
WHEN ${IS_EMPTY_CONTAINER} THEN 'EMPTY_CONTAINER'
WHEN ${IS_CONTAINER} AND b.trade_direction = 'EXPORT' THEN 'CONTAINER_EXPORT'
WHEN ${IS_CONTAINER} AND ${IS_MULTIMODAL} THEN 'CONTAINER_IMPORT_MULTIMODAL'
WHEN ${IS_CONTAINER} THEN 'CONTAINER_IMPORT_UNIMODAL'
WHEN ct.code IN (${quote(FERTILIZER_CODES)}) THEN 'FERTILIZER'
WHEN ct.code IN (${quote(RORO_CODES)}) THEN 'RORO'
WHEN ct.code IN (${quote(BREAK_BULK_CODES)}) THEN 'BREAK_BULK'
WHEN ct.code IN (${quote(SAND_CODES)}) THEN 'SAND'
WHEN b.trade_direction = 'EXPORT' THEN 'OTHER_EXPORT'
WHEN b.trade_direction = 'IMPORT' THEN 'OTHER_IMPORT'
WHEN b.id IS NOT NULL THEN 'BULK'
ELSE 'UNCLASSIFIED'
END`;
export const CONTAINER_CLASS_EXPR = `CASE
WHEN ${IS_EMPTY_CONTAINER} THEN 'EMPTY_CONTAINER_RETURN'
WHEN b.trade_direction = 'EXPORT' THEN 'CONTAINER_EXPORT'
WHEN ${IS_MULTIMODAL} THEN 'CONTAINER_IMPORT_MULTIMODAL'
ELSE 'CONTAINER_IMPORT_UNIMODAL'
END`;
/**
* Every fixed key a planner may enter on the targets screen — the category and
* container-class vocabularies. Station targets are keyed on a yard code, which
* is reference data rather than a fixed list, so they are not enumerated here.
*/
export const TARGET_DIMENSION_KEYS: string[] = [
...CARGO_CATEGORIES.map((o) => o.value),
...CONTAINER_CLASSES.map((o) => o.value),
];
/** Turns a key-emitting CASE into a label-emitting one, so a report shows business names. */
const labelCase = (keyExpr: string, options: ReportFilterOption[]): string =>
`CASE ${options
.map((o) => `WHEN (${keyExpr}) = '${o.value}' THEN '${o.label.replace(/'/g, "''")}'`)
.join(' ')} ELSE (${keyExpr}) END`;
export const CARGO_CATEGORY_LABEL_EXPR = labelCase(CARGO_CATEGORY_EXPR, CARGO_CATEGORIES);
export const CONTAINER_CLASS_LABEL_EXPR = labelCase(CONTAINER_CLASS_EXPR, CONTAINER_CLASSES);
// ---------------------------------------------------------------------------
// Standards
// ---------------------------------------------------------------------------
/**
* A standard, read off the joined `operations_standards` row.
*
* The fallback is not decoration: the row is seeded by migration, but a report
* must not return zeros — or divide by zero — on an environment where the seed
* has not run. The fallbacks are the spec's own figures.
*/
const stdRow = (column: string, fallback: number): string =>
`COALESCE(std.${column}, ${fallback})`;
/**
* The same value in an aggregate select. `std` is a single joined row, so the
* column is constant across the group — but Postgres still demands it be
* grouped or aggregated, and wrapping it in MAX() is cheaper than dragging it
* through every report's GROUP BY.
*/
const stdAgg = (column: string, fallback: number): string =>
`MAX(COALESCE(std.${column}, ${fallback}))`;
/** Standard hours a train may stand at a station, by the station's country. */
export const STATION_STANDARD_HOURS_EXPR = `CASE
WHEN y.country = 'Djibouti' THEN ${stdRow('station_standard_hours_djibouti', 13)}
ELSE ${stdRow('station_standard_hours_ethiopia', 10)}
END`;
/** Whichever end of the corridor is on the Djibouti side, if either is. */
export const DJIBOUTI_YARD_CODE_EXPR = `CASE
WHEN oy.country = 'Djibouti' THEN oy.code
WHEN dy.country = 'Djibouti' THEN dy.code
END`;
/** True when the departure carried any container allocation. */
export const SCHEDULE_IS_CONTAINER = `EXISTS (
SELECT 1 FROM freight.wagon_booking_allocations a
JOIN freight.train_set_wagons w ON w.id = a.train_set_wagon_id AND w.deleted_at IS NULL
WHERE w.train_set_id = ts.train_set_id
AND a.deleted_at IS NULL AND a.load_type = 'CONTAINER'
)`;
/**
* Standard turn-around cycle for a departure, in hours. Container trains run
* the 65-hour cycle; a bulk cycle depends on which Djibouti terminal it works.
* Schedule grain — it reads `ts`, `oy` and `dy`, not the allocation aliases.
*/
export const CYCLE_STANDARD_HOURS_EXPR = `CASE
WHEN ${SCHEDULE_IS_CONTAINER} THEN ${stdRow('cycle_standard_hours_container', 65)}
WHEN ${DJIBOUTI_YARD_CODE_EXPR} = 'DORALEH_MULTIPURPOSE_PORT_DMP' THEN ${stdRow('cycle_standard_hours_bulk_dmp', 88)}
WHEN ${DJIBOUTI_YARD_CODE_EXPR} = 'BCC' THEN ${stdRow('cycle_standard_hours_bulk_bcc', 96)}
ELSE ${stdRow('cycle_standard_hours_bulk_nagad', 96)}
END`;
export const DELAY_TOLERANCE_HOURS_EXPR = `(${stdRow('delay_tolerance_minutes', 30)} / 60.0)`;
/**
* Joins the single standards row. Restricted by id to the earliest live row so
* a stray second row could never fan a report's result out.
*/
export const STANDARDS_JOIN = `std.id = (
SELECT s.id FROM freight.operations_standards s
WHERE s.deleted_at IS NULL ORDER BY s.created_at ASC LIMIT 1
)`;
// ---------------------------------------------------------------------------
// Distance
// ---------------------------------------------------------------------------
/**
* Configured rail distance for a yard pair, in km. Symmetric: `yard_distances`
* stores one row per pair and an A→B row governs B→A.
*
* Returns NULL when the pair is not configured, and every caller must let that
* null through rather than coalescing to zero — a missing distance is not a
* zero distance, and Ton/Km computed from one would understate silently.
*/
export const distanceKmBetween = (fromCol: string, toCol: string): string => `(
SELECT yd.distance_km FROM freight.yard_distances yd
WHERE yd.deleted_at IS NULL
AND ((yd.from_yard_id = ${fromCol} AND yd.to_yard_id = ${toCol})
OR (yd.from_yard_id = ${toCol} AND yd.to_yard_id = ${fromCol}))
LIMIT 1
)`;
/** Standard running time for a leg, falling back to the default leg standard. */
export const legStandardHours = (fromCol: string, toCol: string): string => `COALESCE((
SELECT yd.standard_hours FROM freight.yard_distances yd
WHERE yd.deleted_at IS NULL
AND ((yd.from_yard_id = ${fromCol} AND yd.to_yard_id = ${toCol})
OR (yd.from_yard_id = ${toCol} AND yd.to_yard_id = ${fromCol}))
LIMIT 1
), ${stdRow('default_leg_standard_hours', 21)})`;
/** The schedule's own corridor, origin to destination. */
export const SCHEDULE_KM_EXPR = distanceKmBetween('ts.origin_station_id', 'ts.destination_station_id');
// ---------------------------------------------------------------------------
// Volume — TEU, charged and actual
// ---------------------------------------------------------------------------
/** Per-allocation aggregate over its container items. */
const containerItems = (selection: string): string => `(
SELECT ${selection}
FROM freight.wagon_allocation_container_items ci
LEFT JOIN freight.container_types cty ON cty.id = ci.container_type_id
WHERE ci.wagon_booking_allocation_id = wba.id AND ci.deleted_at IS NULL
)`;
/**
* TEU for one allocation: a 40ft box is two twenty-foot equivalents, anything
* else one.
*
* Note this is the third TEU derivation in the codebase and the only one taken
* from the marshalling record. `revenue-classification.ts` derives TEU from the
* charge code's size suffix (billing truth, blind to unsized codes) and
* `wagon-teu-utilization.report.ts` from a wagon's currently pinned containers
* (live state). This one answers "what did we actually move", which is what the
* reporting spec asks for.
*/
export const ALLOC_TEU = containerItems(
'COALESCE(SUM(CASE WHEN cty.size_ft >= 40 THEN 2 ELSE 1 END), 0)',
);
export const ALLOC_CONTAINERS_20 = containerItems('COUNT(*) FILTER (WHERE cty.size_ft = 20)');
export const ALLOC_CONTAINERS_40 = containerItems('COUNT(*) FILTER (WHERE cty.size_ft >= 40)');
export const ALLOC_CONTAINERS = containerItems('COUNT(*)');
export const TEU_EXPR = `COALESCE(SUM(${ALLOC_TEU}), 0)::int`;
export const CONTAINERS_EXPR = `COALESCE(SUM(${ALLOC_CONTAINERS}), 0)::int`;
/**
* Actual volume — "loading capacity from marshalling" in the spec.
* `allocated_weight_tons` is what the allocation flow recorded onto the wagon,
* and is populated for every allocation in the system.
*/
export const ACTUAL_TONS_EXPR = 'COALESCE(SUM(wba.allocated_weight_tons), 0)::float8';
const IS_PERISHABLE = `COALESCE(ct.code, '') IN (${quote(PERISHABLE_CODES)})`;
const IS_BULK_LOAD = "wba.load_type <> 'CONTAINER'";
/**
* Charged volume — the standard weight capacity the spec bills against, not
* what was weighed.
*
* Containers are charged per box (20/40 tons laden, 2.24/3.88 empty). Bulk is
* charged per WAGON (70 tons, or 38 for perishables), so it counts distinct
* wagons rather than allocations: two bookings sharing one wagon are one
* wagon's charge, not two.
*/
export const CHARGED_TONS_EXPR = `(
COALESCE(SUM(
CASE WHEN ${IS_BULK_LOAD} THEN 0 ELSE
${ALLOC_CONTAINERS_20} * CASE WHEN ${IS_EMPTY_CONTAINER}
THEN ${stdRow('charged_tons_empty_20ft', 2.24)}
ELSE ${stdRow('charged_tons_full_20ft', 20)} END
+ ${ALLOC_CONTAINERS_40} * CASE WHEN ${IS_EMPTY_CONTAINER}
THEN ${stdRow('charged_tons_empty_40ft', 3.88)}
ELSE ${stdRow('charged_tons_full_40ft', 40)} END
END), 0)
+ COUNT(DISTINCT tsw.id) FILTER (WHERE ${IS_BULK_LOAD} AND ${IS_PERISHABLE})
* ${stdAgg('charged_tons_per_wagon_perishable', 38)}
+ COUNT(DISTINCT tsw.id) FILTER (WHERE ${IS_BULK_LOAD} AND NOT ${IS_PERISHABLE})
* ${stdAgg('charged_tons_per_wagon_general', 70)}
)::float8`;
/** Wagons actually carrying cargo in the grouped set. */
export const LOADED_WAGONS_EXPR = 'COUNT(DISTINCT tsw.id)::int';
/**
* Wagons on the departure with nothing allocated to them — the Vehicle-Km base.
*
* A train-level figure: it belongs to the departure, not to any one cargo type
* riding on it, so a report grouped finer than the schedule repeats it rather
* than splitting it. Callers that need a total must de-duplicate by schedule.
*/
export const SCHEDULE_EMPTY_WAGONS = `(
SELECT COUNT(*) FROM freight.train_set_wagons tw
WHERE tw.train_set_id = ts.train_set_id AND tw.deleted_at IS NULL
AND NOT EXISTS (
SELECT 1 FROM freight.wagon_booking_allocations a
WHERE a.train_set_wagon_id = tw.id AND a.deleted_at IS NULL)
)`;
/**
* Trainsets operated: wagons loaded divided by a full trainset for this cargo.
* Seven full multimodal trains plus 30 of a 50-wagon set reads 7.6 — the
* fraction the spec's worked example asks for.
*/
export const TRAINSETS_EXPR = `ROUND(
COUNT(DISTINCT tsw.id)::numeric
/ NULLIF(MAX(COALESCE(ct.full_trainset_wagons, ${stdRow('default_full_trainset_wagons', 50)})), 0)
, 2)::float8`;
// ---------------------------------------------------------------------------
// Rates
// ---------------------------------------------------------------------------
/**
* Implement rate — operated against plan, as a percentage.
*
* NULL when there is no plan, never 100 and never 0: an unplanned period has no
* achievement to report, and coercing a missing plan to zero would read as
* infinite achievement.
*/
export const implementRateExpr = (operated: string, planned: string): string =>
`ROUND(100 * (${operated})::numeric / NULLIF(${planned}, 0), 1)::float8`;
/**
* Turn-around implement rate, the spec's own formula:
* `[((SC AD) / SC) + 1] × 100`. Finishing exactly on standard scores 100;
* a cycle an hour quicker than a 65-hour standard scores ~101.5.
*/
export const cycleRateExpr = (actual: string, standard: string): string =>
`ROUND((((${standard}) - (${actual})) / NULLIF(${standard}, 0) + 1) * 100, 1)::float8`;
/** Hours between two timestamps, one decimal place. */
export const hoursBetween = (from: string, to: string): string =>
`ROUND(EXTRACT(EPOCH FROM ((${to}) - (${from})))::numeric / 3600, 1)::float8`;
// ---------------------------------------------------------------------------
// Filters and the shared ledger
// ---------------------------------------------------------------------------
/**
* The date every operations report buckets and filters on: when the train
* actually left, falling back to the plan for a departure not yet dispatched.
*/
export const OPS_DATE = 'COALESCE(ts.actual_departure_at, ts.scheduled_departure_date)';
export const DIRECTION_FILTER: ReportFilterDef = {
key: 'direction',
label: 'Direction',
type: 'select',
options: [
{ value: 'IMPORT', label: 'Import' },
{ value: 'EXPORT', label: 'Export' },
{ value: 'DOMESTIC', label: 'Domestic' },
],
};
export const COUNTRY_FILTER: ReportFilterDef = {
key: 'country',
label: 'Country',
type: 'select',
options: [
{ value: 'Ethiopia', label: 'Ethiopia' },
{ value: 'Djibouti', label: 'Djibouti' },
],
};
/** Shared by every operations report, so they read the same way side by side. */
export const OPERATIONS_FILTERS: ReportFilterDef[] = [
{ key: 'date', label: 'Departure', type: 'daterange' },
DIRECTION_FILTER,
{ key: 'trainNumber', label: 'Train No.', type: 'text' },
{ key: 'origin', label: 'Origin', type: 'select', optionsQuery: yardOptions },
{ key: 'destination', label: 'Destination', type: 'select', optionsQuery: yardOptions },
];
export const CARGO_CATEGORY_FILTER: ReportFilterDef = {
key: 'categories',
label: 'Cargo category',
type: 'multiselect',
options: CARGO_CATEGORIES,
};
/** Schedule states that never represent an operated train. */
const DEAD_SCHEDULE_STATUSES = ['DRAFT', 'CANCELLED'];
/**
* Every operations report starts here: one wagon allocation, joined out to the
* departure that carried it and the booking that explains it.
*
* The booking is LEFT joined — a wagon can be allocated before its booking data
* is complete, and dropping those rows would understate wagon usage.
*/
export function allocationLedgerQb(ctx: ReportContext): SelectQueryBuilder<ObjectLiteral> {
const { params, directions } = ctx;
const qb = ctx.ds
.createQueryBuilder()
.from(WagonBookingAllocation, 'wba')
.innerJoin(TrainSetWagon, 'tsw', 'tsw.id = wba.train_set_wagon_id AND tsw.deleted_at IS NULL')
.innerJoin(TrainSchedule, 'ts', 'ts.train_set_id = tsw.train_set_id AND ts.deleted_at IS NULL')
.leftJoin(Booking, 'b', 'b.id = wba.booking_id AND b.deleted_at IS NULL')
.leftJoin(CargoType, 'ct', 'ct.id = b.cargo_type_id')
.leftJoin(Yard, 'oy', 'oy.id = ts.origin_station_id')
.leftJoin(Yard, 'dy', 'dy.id = ts.destination_station_id')
.leftJoin(OperationsStandard, 'std', STANDARDS_JOIN)
.where('wba.deleted_at IS NULL')
.andWhere('ts.status NOT IN (:...deadScheduleStatuses)', {
deadScheduleStatuses: DEAD_SCHEDULE_STATUSES,
});
applyOperationsFilters(qb, params);
applyDirectionScope(qb, 'COALESCE(b.trade_direction, ts.direction)', directions);
return qb;
}
/**
* The schedule-grain query, for reports that measure trains rather than cargo —
* turnaround, delay, station stay. Same aliases, minus the allocation.
*/
export function scheduleLedgerQb(ctx: ReportContext): SelectQueryBuilder<ObjectLiteral> {
const { params, directions } = ctx;
const qb = ctx.ds
.createQueryBuilder()
.from(TrainSchedule, 'ts')
.leftJoin(Yard, 'oy', 'oy.id = ts.origin_station_id')
.leftJoin(Yard, 'dy', 'dy.id = ts.destination_station_id')
.leftJoin(OperationsStandard, 'std', STANDARDS_JOIN)
.where('ts.deleted_at IS NULL')
.andWhere('ts.status NOT IN (:...deadScheduleStatuses)', {
deadScheduleStatuses: DEAD_SCHEDULE_STATUSES,
});
applyOperationsFilters(qb, params);
applyDirectionScope(qb, 'ts.direction', directions);
return qb;
}
export function applyOperationsFilters(
qb: SelectQueryBuilder<ObjectLiteral>,
params: Record<string, unknown>,
): void {
if (params.dateFrom) qb.andWhere(`${OPS_DATE} >= :dateFrom`, { dateFrom: params.dateFrom });
if (params.dateTo) qb.andWhere(`${OPS_DATE} < :dateTo`, { dateTo: params.dateTo });
if (params.direction) qb.andWhere('ts.direction = :direction', { direction: params.direction });
if (params.trainNumber) {
qb.andWhere('ts.train_number ILIKE :trainNumber', {
trainNumber: `%${params.trainNumber as string}%`,
});
}
if (params.origin) qb.andWhere('oy.code = :origin', { origin: params.origin });
if (params.destination) qb.andWhere('dy.code = :destination', { destination: params.destination });
}
/**
* Restricts an allocation-grain query to a set of cargo categories. Kept
* separate from {@link applyOperationsFilters} because the schedule-grain
* query has no cargo to filter by.
*/
export function applyCategoryFilter(
qb: SelectQueryBuilder<ObjectLiteral>,
params: Record<string, unknown>,
): void {
const categories = params.categories as string[] | null;
if (categories?.length) {
qb.andWhere(`${CARGO_CATEGORY_EXPR} IN (:...categories)`, { categories });
}
}
/**
* The planned value for a group, as a correlated subselect against
* `operations_targets`.
*
* Correlated rather than joined because the period bucket is an expression, not
* a column: joining would need the same `date_trunc` repeated in the ON clause
* and in the GROUP BY, and a mismatch between the two silently drops targets.
*
* Wrapped in MAX() so the correlated references sit inside an aggregate's
* argument. Postgres does not recognise a grouped EXPRESSION as grouped when it
* appears inside a subquery — `subquery uses ungrouped column` — and an
* aggregate argument is the one place ungrouped columns are legal. The value is
* constant within the group, so MAX() picks it exactly.
*/
export const plannedValueExpr = (
metric: string,
dimension: string,
dimensionKeyExpr: string,
periodTypeExpr: string,
periodStartExpr: string,
): string => `MAX((
SELECT ot.planned_value FROM freight.operations_targets ot
WHERE ot.deleted_at IS NULL
AND ot.metric = '${metric}'
AND ot.dimension = '${dimension}'
AND ot.dimension_key = ${dimensionKeyExpr}
AND ot.period_type = ${periodTypeExpr}
AND ot.period_start = (${periodStartExpr})::date
LIMIT 1
))`;