Files
edr-platform/apps/edr-freight-api/src/modules/train-scheduling/facility-handling.service.ts
Hagernesh 101bf69271 feat(warehouses): map GRN numbers to the goods owner
GRN-<DIR>-<DATE>-<REF8> carried no owner, so a note couldn't be
identified by who owns the cargo. Add an owner segment sourced from the
booking's company at every generation point (import, export, facility,
manual receive), keep REF8 for uniqueness, and label the GRN document
row Owner's Name.
2026-07-27 09:57:59 +00:00

115 lines
4.1 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();
// Mapped to the goods owner, same as every warehouse-raised GRN.
const grnNumber = generateGrnNumber(
booking.tradeDirection ?? 'DOMESTIC',
booking.id,
occurredAt,
booking.company?.name ?? null,
);
// 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;
}
// The handed-over weight: the booking's declared VGM, else what its
// containers actually carry. A GRN without a weight is not a receipt.
let weightTons = Number(booking.cargoTotalWeightVgm) || null;
if (!weightTons) {
const [sum]: Array<{ tons: string | null }> = await manager.query(
`SELECT SUM(bcu.vgm_tons) AS tons
FROM freight.booking_container_units bcu
JOIN freight.booking_container bc
ON bc.id = bcu.booking_container_id AND bc.deleted_at IS NULL
WHERE bc.booking_id = $1 AND bcu.deleted_at IS NULL`,
[booking.id],
);
weightTons = Number(sum?.tons) || null;
}
const repo = manager.getRepository(FacilityHandlingEvent);
await repo.save(
repo.create({
bookingId: booking.id,
yardId,
trainScheduleId: input.trainScheduleId ?? null,
eventType,
grnNumber,
weightTons,
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;
}
}
}