/** * What the customer said is IN the booking, per freight type — the list * filter, the summary and the export all read this one expression so the * column, the pill and the sheet can never disagree. * * BULK the commodity picked from the cargo tree (`cargo_types`), falling * back to the free-text description for a bare group or a legacy row * that has no commodity. * CONTAINER the wizard asks for no description at all — VGM and contents are * captured later in operations — so the closest thing to the * customer's own words is the container lines they entered: * "2 × 40FT, 1 × 20FT". * * Containers are checked FIRST: a container booking has no `cargo_type_id` * (the API rejects one), so the order only matters for a mixed legacy row, * where the physical lines are the better answer. */ export function bookingContentSql(alias = 'b'): string { return `COALESCE( NULLIF((SELECT string_agg(bc.quantity || ' × ' || COALESCE(cnt.label, cnt.code), ', ' ORDER BY cnt.size_ft DESC NULLS LAST, cnt.code) FROM freight.booking_container bc JOIN freight.container_types cnt ON cnt.id = bc.container_type_id WHERE bc.booking_id = ${alias}.id AND bc.deleted_at IS NULL), ''), NULLIF((SELECT cgt.cargo_type_name FROM freight.cargo_types cgt WHERE cgt.id = ${alias}.cargo_type_id), ''), NULLIF(${alias}.cargo_free_text, ''))`; } /** * Cargo types at or under `:cargoTypeId`, so picking a GROUP in the filter * matches every commodity beneath it — the same group→commodity drill-down the * booking wizard offers, read back. Recursive because `cargo_types` is an * arbitrary-depth tree (Bulk → Steel Billet → S1 → …), not two levels. */ export const CARGO_TYPE_SUBTREE_SQL = `( WITH RECURSIVE sub AS ( SELECT id FROM freight.cargo_types WHERE id = :cargoTypeId UNION ALL SELECT c.id FROM freight.cargo_types c JOIN sub ON c.parent_group_id = sub.id ) SELECT id FROM sub)`; /** * Contains-match over every part of the content a customer can type or pick: * their own description, the commodity's name, and the container types on the * booking. Bind `:cargoText` already wrapped in `%`. */ export function bookingContentMatchSql(alias = 'b'): string { return `(${alias}.cargo_free_text ILIKE :cargoText OR EXISTS (SELECT 1 FROM freight.cargo_types cgt WHERE cgt.id = ${alias}.cargo_type_id AND cgt.cargo_type_name ILIKE :cargoText) OR EXISTS (SELECT 1 FROM freight.booking_container bc JOIN freight.container_types cnt ON cnt.id = bc.container_type_id WHERE bc.booking_id = ${alias}.id AND bc.deleted_at IS NULL AND (cnt.label ILIKE :cargoText OR cnt.code ILIKE :cargoText)))`; } /** * Containers on a booking, as a count of physical boxes — `booking_container` * is one row PER LINE with a `quantity`, not one row per box, so this sums the * quantity rather than counting rows. * * `scopedToType` narrows the sum to `:containerTypeId`, which is what makes one * number filter answer both "10 containers in total" and "10 forty-footers": * the count filter reads the container-type filter when one is set, and counts * every type when it is not. */ export function bookingContainerCountSql(alias = 'b', scopedToType = false): string { return `(SELECT COALESCE(SUM(bc.quantity), 0) FROM freight.booking_container bc WHERE bc.booking_id = ${alias}.id AND bc.deleted_at IS NULL${ scopedToType ? '\n AND bc.container_type_id = :containerTypeId' : '' })`; } /** Bookings carrying at least one line of `:containerTypeId`. */ export function bookingHasContainerTypeSql(alias = 'b'): string { return `EXISTS (SELECT 1 FROM freight.booking_container bc WHERE bc.booking_id = ${alias}.id AND bc.deleted_at IS NULL AND bc.container_type_id = :containerTypeId)`; } /** * Container VGM on a booking, in tons — the sum of the per-line totals. * * NOT `bookings.cargo_total_weight_vgm`: the portal wizard leaves that at 0 for * container freight (VGM is captured per container, later, in operations), so * reading the booking-level column showed every portal container booking as * weighing nothing. Same reason `bookingTonsSql` falls through to these lines. */ export function bookingContainerVgmSql(alias = 'b'): string { return `(SELECT COALESCE(SUM(bc.total_vgm_tons), 0) FROM freight.booking_container bc WHERE bc.booking_id = ${alias}.id AND bc.deleted_at IS NULL)`; } /** * Cargo the customer declared on the SHIPMENT REQUEST behind a booking, which * is not the same fact as cargo on the booking itself. * * On a GENERAL + customs contract the customer cannot book directly: they * submit a request (day + quantities), and `initiateForShipmentRequest` opens a * BARE instance from it — "the request itself carries the quantities; the * instance carries none". So between initiation and `completeUnderContract` the * booking legitimately holds no cargo while the customer's declared quantities * sit on `booking_requests.requested_lines`. * * Kept in its own column rather than folded into the real container count: a * declared 2 × 20FT is a request, not two boxes on a booking, and merging the * two would overstate operational totals. */ const REQUESTED_CONTAINER_LINES = `jsonb_array_elements( CASE WHEN jsonb_typeof(br.requested_lines->'containers') = 'array' THEN br.requested_lines->'containers' ELSE '[]'::jsonb END)`; /** Human-readable declared cargo: "2 × 20FT", "12 t", "40 items". */ export function bookingRequestedCargoSql(alias = 'b'): string { return `(SELECT COALESCE( (SELECT string_agg((l->>'quantity') || ' × ' || upper(l->>'containerSize'), ', ' ORDER BY l->>'containerSize') FROM ${REQUESTED_CONTAINER_LINES} AS l), NULLIF(br.requested_lines->'bulk'->>'cargoWeightTons', '') || ' t', NULLIF(br.requested_lines->'bulk'->>'itemCount', '') || ' items') FROM freight.booking_requests br WHERE br.created_booking_id = ${alias}.id AND br.deleted_at IS NULL ORDER BY br.created_at DESC LIMIT 1)`; } /** * Boxes declared on the shipment request. Pairs with the real container count: * `Containers = 0` AND `Requested containers >= 1` is exactly the set awaiting * completion. */ export function bookingRequestedContainerCountSql(alias = 'b'): string { return `(SELECT COALESCE(SUM((l->>'quantity')::int), 0) FROM freight.booking_requests br CROSS JOIN LATERAL ${REQUESTED_CONTAINER_LINES} AS l WHERE br.created_booking_id = ${alias}.id AND br.deleted_at IS NULL)`; }