import { BadRequestException } from '@nestjs/common'; import type { DataSource, EntityManager } from 'typeorm'; /** The booking fields the gate needs. */ export interface ExportLoadGateBooking { id: string; tradeDirection?: string | null; /** 'DIRECT_TO_TRAIN' skips the gate entirely; null/'WAREHOUSE' keeps it. */ exportHandoverMode?: string | null; } /** Direct truck-to-train: the cargo never sees a warehouse, so it never has a GRN. */ export const DIRECT_TO_TRAIN = 'DIRECT_TO_TRAIN'; /** Warehouse-then-train: the existing flow. Also what a null mode means. */ export const WAREHOUSE = 'WAREHOUSE'; /** * Export cargo may not be loaded onto its train until it has physically reached * the warehouse and been issued a GRN — whether it got there by first-mile or by * the customer's own truck, and even though a wagon is already allocated. An * allocation is a plan; the GRN is the proof the goods are actually in hand. * * Several loading paths (per-yard load, workspace confirm-loaded) marked cargo * loaded straight off the allocation, skipping the warehouse, so a booking could * ride the train with nothing ever received. This closes that for export; import * loads off a train and is unaffected. * * "Received with a GRN" = an inventory row that has reached the warehouse * (RECEIVED or any later stage) and carries a GRN, in the column or the notes * fallback older rows use. * * Export has a second, warehouse-free shape: the customer's truck loads straight * onto the wagon. That cargo is never received and never GRN'd, so a booking * marked DIRECT_TO_TRAIN is outside this gate by definition — its custody is * attested by the carriage acceptance sheet instead. */ export async function assertExportReceivedWithGrn( db: DataSource | EntityManager, booking: ExportLoadGateBooking, ): Promise { if (booking.tradeDirection !== 'EXPORT') return; if (booking.exportHandoverMode === DIRECT_TO_TRAIN) return; const [row] = await db.query( `SELECT 1 FROM freight.warehouse_inventory inv WHERE inv.booking_id = $1 AND inv.deleted_at IS NULL AND inv.status IN ('RECEIVED', 'STORED', 'RESERVED', 'READY_FOR_LOADING', 'LOADED', 'DISPATCHED') AND COALESCE( NULLIF(TRIM(inv.grn_number), ''), substring(inv.notes FROM 'GRN Number: ([^\\n\\r]+)') ) IS NOT NULL LIMIT 1`, [booking.id], ); if (!row) { throw new BadRequestException( 'This export booking has not been received at the warehouse yet — receive its cargo and generate a GRN before loading it onto the train.', ); } }