import { Injectable, Logger } from '@nestjs/common'; import { EntityManager } from 'typeorm'; import { generateGrnNumber } from '../../common/grn.util'; import { YardFacilitiesService } from '../rule-engine/services/yard-facilities.service'; import { Booking } from '../bookings/entities/booking.entity'; import { FacilityHandlingEvent, FacilityHandlingEventType, } from './entities/facility-handling-event.entity'; /** * Records cargo being loaded/unloaded at a yard's facility, and raises its GRN. * * Every facility raises a GRN — the goods changed hands, whether or not anyone * stores them. What differs is what happens next: a facility with a warehouse * (Indode) keeps the cargo, so it goes through the normal warehouse flow and * accrues storage/demurrage; the rest only move it between train and truck, so * the event and its GRN are the whole record. * * Best-effort by design: a failure here must not undo a load/unload that * physically happened. */ @Injectable() export class FacilityHandlingService { private readonly logger = new Logger(FacilityHandlingService.name); constructor(private readonly yardFacilities: YardFacilitiesService) {} /** * Write the handling event and mint its GRN. Returns the GRN, or null when the * yard has no facility (nothing to record) or the write failed. */ async recordHandling( manager: EntityManager, input: { booking: Booking; yardId: string; trainScheduleId?: string | null; eventType: FacilityHandlingEventType; performedBy?: string | null; occurredAt?: Date; }, ): Promise { const { booking, yardId, eventType } = input; try { const facility = await this.yardFacilities.facilityForYard(yardId); if (!facility?.hasFacility) return null; const occurredAt = input.occurredAt ?? new Date(); const grnNumber = generateGrnNumber( booking.tradeDirection ?? 'DOMESTIC', booking.id, occurredAt, ); // Link the storage record when this facility keeps cargo — that link is // what ties an Indode handover to its storage/demurrage. let inventoryId: string | null = null; if (facility.hasWarehouse) { const [inv]: Array<{ id: string }> = await manager.query( `SELECT id FROM freight.warehouse_inventory WHERE booking_id = $1 AND deleted_at IS NULL ORDER BY created_at DESC LIMIT 1`, [booking.id], ); inventoryId = inv?.id ?? null; } const repo = manager.getRepository(FacilityHandlingEvent); await repo.save( repo.create({ bookingId: booking.id, yardId, trainScheduleId: input.trainScheduleId ?? null, eventType, grnNumber, weightTons: Number(booking.cargoTotalWeightVgm) || null, inventoryId, performedBy: input.performedBy ?? null, occurredAt, }), ); this.logger.log( `GRN ${grnNumber} raised on ${eventType} at ${facility.yardCode ?? yardId} for booking ${booking.reference ?? booking.id}`, ); return grnNumber; } catch (err) { // The cargo moved regardless — never fail the journey over the paperwork. this.logger.error( `Facility ${eventType} record failed for booking ${booking.id} at yard ${yardId}: ${String(err)}`, ); return null; } } }