mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-31 13:07:38 +00:00
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.
115 lines
4.1 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|