mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 20:10:56 +00:00
Three export rules that the flow left open. An unpaid export booking could be received at the warehouse. Receiving is what starts storage and mints a GRN, so it must not happen against cargo the customer has not settled. receive() now rejects an unpaid EXPORT booking. Import is untouched — it arrives OFF a train and its receive is the unload, so gating that on payment would strand cargo already at the yard. An allocated export booking could be marked loaded onto its train without ever reaching the warehouse. An allocation is a plan; the GRN is the proof the goods are in hand. Two loading paths skipped that check — the per-yard loadBooking and the workspace confirmScheduleLoading — and both now require every export booking to be received with a GRN first, however it arrived (first-mile or the customer's own truck) and whatever it is allocated to. The rule lives in one shared guard (assertExportReceivedWithGrn) so the two paths cannot drift. Export self-haul without a first-mile leg already worked and is unchanged: assertSelfHaulPaid allows a customer truck when there is no EDR mile leg and the booking is paid, and addTruck applies the same one-40ft-or-two-20ft rule to containers and the tonnage drawdown to bulk, exactly as import does. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
51 lines
1.9 KiB
TypeScript
51 lines
1.9 KiB
TypeScript
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;
|
|
}
|
|
|
|
/**
|
|
* 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 async function assertExportReceivedWithGrn(
|
|
db: DataSource | EntityManager,
|
|
booking: ExportLoadGateBooking,
|
|
): Promise<void> {
|
|
if (booking.tradeDirection !== 'EXPORT') 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.',
|
|
);
|
|
}
|
|
}
|