From a95667fde45c3805ecb4179783389fac38bd9b3e Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Thu, 16 Jul 2026 13:44:02 +0000 Subject: [PATCH] fix(warehouse): raise a GRN on every warehouse receipt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Requiring a GRN before loading only works if every path into the warehouse issues one. Two did not: autoUnloadArrived and unloadBooking created RECEIVED inventory with a null grn_number, so cargo that genuinely arrived — by first mile or self haul — would have been stuck un-loadable behind the new gate. Both now stamp a GRN, derived from the booking's trade direction, matching receive/bulkReceive/autoUnloadArrivedBookings. unloadBooking keeps an already-issued GRN when it re-unloads an existing row rather than reissuing one. Every path that creates warehouse inventory now issues a GRN, so the chain is seamless: booking arrives (first mile or self haul) -> received -> GRN -> loadable onto its allocated wagon. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../warehouses/warehouse-inventory.service.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts index 19b1c0159..3630aa9de 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts @@ -1032,6 +1032,8 @@ export class WarehouseInventoryService { result.results.push({ bookingId: booking.id, status: 'FAILED', reason: 'No warehouse/yard/zone configured' }); continue; } + // Goods reaching the warehouse always get a GRN, whichever path brought + // them in — nothing loads onto a train without one. const saved = await this.inventoryRepository.create({ warehouseId: location.warehouseId, yardId: location.yardId, @@ -1041,6 +1043,11 @@ export class WarehouseInventoryService { weight: Number(booking.weight) || 0, status: 'RECEIVED', arrivedAt: new Date(), + grnNumber: this.generateGrnNumber( + booking.tradeDirection ?? 'WH', + booking.id, + new Date(), + ), notes: allocated?.rule ? `Auto-unloaded → ${allocated.path}` : 'Auto-unloaded from arrival queue', }); result.processedCount += 1; @@ -1061,6 +1068,14 @@ export class WarehouseInventoryService { /** Unload a single arrived booking into a chosen (or default) location. */ async unloadBooking(bookingId: string, dto: UnloadBookingDto): Promise { const existing = await this.inventoryRepository.findAll({ where: { bookingId } }); + // Goods reaching the warehouse always get a GRN, whichever path brought them + // in — nothing loads onto a train without one. + const [bookingRow]: Array<{ tradeDirection: string | null }> = await this.dataSource.query( + `SELECT trade_direction AS "tradeDirection" + FROM freight.bookings WHERE id = $1 AND deleted_at IS NULL`, + [bookingId], + ); + const grnDirection = bookingRow?.tradeDirection ?? 'WH'; let location: DefaultLocation | null = dto.warehouseId && dto.yardId && dto.zoneId @@ -1081,6 +1096,10 @@ export class WarehouseInventoryService { zoneId: location.zoneId, status: 'RECEIVED', arrivedAt, + // Keep an already-issued GRN; only raise one if this row never got it. + ...(existing[0].grnNumber + ? {} + : { grnNumber: this.generateGrnNumber(grnDirection, bookingId, arrivedAt) }), notes: dto.notes ?? existing[0].notes ?? 'Unloaded', }); return this.findById(existing[0].id); @@ -1095,6 +1114,7 @@ export class WarehouseInventoryService { weight: 0, status: 'RECEIVED', arrivedAt, + grnNumber: this.generateGrnNumber(grnDirection, bookingId, arrivedAt), notes: dto.notes ?? 'Unloaded', }); return this.findById(saved.id);