diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-journey.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-journey.service.ts index 87e756ba6..e5933d35f 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/booking-journey.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-journey.service.ts @@ -327,19 +327,48 @@ export class BookingJourneyService { RETURNING b.id, b.trade_direction`, [schedule.id, schedule.destinationStationId, now], ); + if (rows.length === 0) return []; + + // The facility took the cargo off the train at the final yard — raise its + // GRN, same as the per-booking unloadBooking() path does. Only when that + // yard also has a warehouse (or has no facility at all, e.g. Kality) does + // WarehouseInventoryService additionally get to allocate a warehouse/yard/ + // zone row: a pure facility yard (Dire Dawa, Modjo, Sebeta, Adama) is + // fully represented by the facility event alone — there is nothing there + // for warehouse_inventory's NOT NULL warehouse/yard/zone to point at. + const facility = await this.yardFacilities.facilityForYard(schedule.destinationStationId); + const bookings = await manager + .getRepository(Booking) + .find({ where: { id: In(rows.map((r) => r.id)) }, relations: ['company'] }); + const bookingById = new Map(bookings.map((b) => [b.id, b])); + for (const row of rows) { // Intercity rows just completed — let a ONE_TIME contract close on delivery. if (row.trade_direction === 'DOMESTIC') { this.events.emit('booking.completed', { bookingId: row.id }); } + + const booking = bookingById.get(row.id); + if (booking) { + await this.facilityHandling.recordHandling(manager, { + booking, + yardId: schedule.destinationStationId, + trainScheduleId: schedule.id, + eventType: 'UNLOAD', + occurredAt: now, + }); + } + // Same event the per-booking unloadBooking() path emits — WarehouseInventoryService // listens for this to auto-create the warehouse_inventory row (import/intercity only, // it filters EXPORT itself). The bulk SQL update above skipped this entirely, so // bookings caught by this fallback never left "awaiting unload". - this.events.emit('booking.unloadedAtYard', { - bookingId: row.id, - tradeDirection: row.trade_direction, - }); + if (row.trade_direction !== 'EXPORT' && (!facility?.hasFacility || facility.hasWarehouse)) { + this.events.emit('booking.unloadedAtYard', { + bookingId: row.id, + tradeDirection: row.trade_direction, + }); + } } return rows.map((r) => r.id); } diff --git a/apps/edr-freight-api/src/scripts/backfill-missing-unload-inventory.ts b/apps/edr-freight-api/src/scripts/backfill-missing-unload-inventory.ts index 9d51107fe..608fba613 100644 --- a/apps/edr-freight-api/src/scripts/backfill-missing-unload-inventory.ts +++ b/apps/edr-freight-api/src/scripts/backfill-missing-unload-inventory.ts @@ -27,12 +27,20 @@ async function main() { const dataSource = app.get(DataSource); const inventory = app.get(WarehouseInventoryService); + // Skip bookings destined for a pure facility yard (has a facility but no + // warehouse, e.g. Dire Dawa) — those are fully represented by their + // facility_handling_events UNLOAD record, not a warehouse_inventory row. + // Same gate as BookingJourneyService.autoArriveAtFinalYard. const bookings: { id: string; tradeDirection: string }[] = await dataSource.query( `SELECT b.id, b.trade_direction AS "tradeDirection" FROM freight.bookings b + JOIN freight.yards dy ON dy.id = b.destination_yard_id + LEFT JOIN freight.yard_facilities yf + ON yf.yard_id = dy.id AND yf.deleted_at IS NULL AND yf.is_active = true WHERE b.deleted_at IS NULL AND b.trade_direction IN ('IMPORT', 'DOMESTIC') AND b.status IN ('ARRIVED', 'COMPLETED') + AND NOT (dy.has_facility = true AND COALESCE(yf.has_warehouse, false) = false) AND NOT EXISTS ( SELECT 1 FROM freight.warehouse_inventory wi WHERE wi.booking_id = b.id AND wi.deleted_at IS NULL