mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 16:00:56 +00:00
Every facility raises a GRN — the goods changed hands, whether or not anyone stores them. What differs is what happens next: Indode has a warehouse, so cargo left there goes through the existing warehouse flow and accrues storage and demurrage; Sebeta, Modjo, Adama and Dire Dawa only move cargo between train and truck, so the handling event and its GRN are the whole record. facility_handling_events carries that record because warehouse_inventory cannot: its warehouse/yard/zone are NOT NULL, so a facility with equipment but no warehouse could never have a row there. inventory_id links the storage record when the facility does keep the cargo, which is what ties an Indode handover to its demurrage. generateGrnNumber moves to common/grn.util.ts so a GRN raised at a facility is indistinguishable from one raised in a warehouse — the two live in different tables, and a second generator would let the formats drift. Recording is best-effort: the cargo moved regardless, so paperwork must never fail the journey. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
98 lines
3.4 KiB
TypeScript
98 lines
3.4 KiB
TypeScript
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<string | null> {
|
|
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;
|
|
}
|
|
}
|
|
}
|