mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 09:00:57 +00:00
feat(freight): support direct truck-to-train export handover
Export cargo reaches a train two ways, but the platform only modelled one. Direct truck-to-train cargo loads straight onto the wagon, never enters a warehouse and so never has a GRN — yet assertExportReceivedWithGrn required one before the carriage acceptance sheet could be issued or the booking loaded from inside its schedule. Adds export_handover_mode to freight.bookings (null = WAREHOUSE, so existing bookings are unaffected) and teaches the shared gate to skip DIRECT_TO_TRAIN. Both call sites are fixed by that single early return. For direct bookings the carriage acceptance sheet builds its lines from the booking's own containers, falling back to the declared bulk tonnage, and is issuable as soon as the mode is chosen. Direct bookings are also removed from the warehouse receive queue, since that cargo is never coming to the shed. Staff choose the mode from the booking detail page via a new endpoint reusing bookings:operations. Switching to direct is refused once warehouse inventory exists, so the two flows cannot cross. Warehouse-then-train keeps every gate it had.
This commit is contained in:
@@ -36,4 +36,27 @@ describe('assertExportReceivedWithGrn', () => {
|
||||
assertExportReceivedWithGrn(db([]), { id: 'b-1', tradeDirection: 'DOMESTIC' }),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('never blocks direct truck-to-train export — that cargo has no GRN by design', async () => {
|
||||
const source = db([]);
|
||||
await expect(
|
||||
assertExportReceivedWithGrn(source, {
|
||||
id: 'b-1',
|
||||
tradeDirection: 'EXPORT',
|
||||
exportHandoverMode: 'DIRECT_TO_TRAIN',
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
// Direct short-circuits before querying — there is no inventory to look for.
|
||||
expect(source.query as jest.Mock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('still gates a warehouse export booking', async () => {
|
||||
await expect(
|
||||
assertExportReceivedWithGrn(db([]), {
|
||||
id: 'b-1',
|
||||
tradeDirection: 'EXPORT',
|
||||
exportHandoverMode: 'WAREHOUSE',
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,8 +5,15 @@ import type { DataSource, EntityManager } from 'typeorm';
|
||||
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
|
||||
@@ -21,12 +28,18 @@ export interface ExportLoadGateBooking {
|
||||
* "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<void> {
|
||||
if (booking.tradeDirection !== 'EXPORT') return;
|
||||
if (booking.exportHandoverMode === DIRECT_TO_TRAIN) return;
|
||||
|
||||
const [row] = await db.query(
|
||||
`SELECT 1
|
||||
|
||||
Reference in New Issue
Block a user