mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 22:18:12 +00:00
132 lines
4.3 KiB
TypeScript
132 lines
4.3 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
||
|
||
import { containersPerWagonForSize } from '../rule-engine/container-type.util';
|
||
import { ContainerTypesService } from '../rule-engine/services/container-types.service';
|
||
import { Booking } from './entities/booking.entity';
|
||
|
||
export interface ConsolidationSlot {
|
||
containerTypeId: string;
|
||
containerTypeCode: string;
|
||
quantity: number;
|
||
containersPerWagon: number;
|
||
remainder: number;
|
||
slotsNeeded: number;
|
||
}
|
||
|
||
export interface ConsolidationAttemptResult {
|
||
booking: Booking;
|
||
partner: Booking | null;
|
||
paired: boolean;
|
||
messages: string[];
|
||
}
|
||
|
||
export function wagonRemainder(quantity: number, perWagon: number): number {
|
||
const r = quantity % perWagon;
|
||
return r;
|
||
}
|
||
|
||
export function slotsNeededToFillWagon(quantity: number, perWagon: number): number {
|
||
const remainder = wagonRemainder(quantity, perWagon);
|
||
if (remainder === 0) return 0;
|
||
return perWagon - remainder;
|
||
}
|
||
|
||
/** Two bookings' quantities for the same type complete whole wagon(s). */
|
||
export function quantitiesComplementWagon(
|
||
q1: number,
|
||
q2: number,
|
||
perWagon: number,
|
||
): boolean {
|
||
return (
|
||
wagonRemainder(q1, perWagon) > 0 &&
|
||
wagonRemainder(q2, perWagon) > 0 &&
|
||
(q1 + q2) % perWagon === 0
|
||
);
|
||
}
|
||
|
||
@Injectable()
|
||
export class ConsolidationService {
|
||
constructor(private readonly containerTypesService: ContainerTypesService) {}
|
||
|
||
async slotsFromContainerLines(
|
||
lines: Array<{ containerTypeId: string; quantity: number }>,
|
||
): Promise<ConsolidationSlot[]> {
|
||
// Aggregate by container type first: two lines of the same type on one
|
||
// booking share the same wagons. Counting them separately would flag a
|
||
// self-complete booking (e.g. 2× 20FT = exactly one wagon) as a partial
|
||
// wagon and wrongly park it in PENDING_CONSOLIDATION.
|
||
const quantityByType = new Map<string, number>();
|
||
for (const line of lines) {
|
||
if (!line.containerTypeId) continue;
|
||
quantityByType.set(
|
||
line.containerTypeId,
|
||
(quantityByType.get(line.containerTypeId) ?? 0) + Number(line.quantity || 0),
|
||
);
|
||
}
|
||
|
||
const slots: ConsolidationSlot[] = [];
|
||
for (const [containerTypeId, quantity] of quantityByType) {
|
||
const ct = await this.containerTypesService.findById(containerTypeId);
|
||
const perWagon = containersPerWagonForSize(ct.sizeFt);
|
||
const remainder = wagonRemainder(quantity, perWagon);
|
||
if (remainder === 0) continue;
|
||
slots.push({
|
||
containerTypeId,
|
||
containerTypeCode: ct.code,
|
||
quantity,
|
||
containersPerWagon: perWagon,
|
||
remainder,
|
||
slotsNeeded: perWagon - remainder,
|
||
});
|
||
}
|
||
return slots;
|
||
}
|
||
|
||
async slotsFromBooking(booking: Booking): Promise<ConsolidationSlot[]> {
|
||
const lines = (booking.bookingContainers ?? [])
|
||
.filter((bc): bc is typeof bc & { containerTypeId: string } => bc.containerTypeId != null)
|
||
.map((bc) => ({
|
||
containerTypeId: bc.containerTypeId,
|
||
quantity: bc.quantity,
|
||
}));
|
||
return this.slotsFromContainerLines(lines);
|
||
}
|
||
|
||
async needsConsolidation(
|
||
lines: Array<{ containerTypeId: string; quantity: number }>,
|
||
): Promise<boolean> {
|
||
const slots = await this.slotsFromContainerLines(lines);
|
||
return slots.length > 0;
|
||
}
|
||
|
||
async needsConsolidationFromBooking(booking: Booking): Promise<boolean> {
|
||
const slots = await this.slotsFromBooking(booking);
|
||
return slots.length > 0;
|
||
}
|
||
|
||
describePending(_booking: Booking, slots: ConsolidationSlot[]): string {
|
||
if (slots.length === 0) {
|
||
return 'Booking does not require wagon consolidation.';
|
||
}
|
||
const parts = slots.map(
|
||
(s) =>
|
||
`${s.slotsNeeded} more × ${s.containerTypeCode} (${s.containersPerWagon} per wagon; you have ${s.quantity})`,
|
||
);
|
||
return (
|
||
`No compatible partner found yet. Your booking is queued (PENDING_CONSOLIDATION). ` +
|
||
`Waiting for: ${parts.join('; ')}. You will be notified when another customer fills the wagon.`
|
||
);
|
||
}
|
||
|
||
describePaired(partnerReference: string, slots: ConsolidationSlot[]): string {
|
||
const parts = slots.map(
|
||
(s) =>
|
||
`${s.containerTypeCode}: ${s.quantity} + partner fills wagon (${s.containersPerWagon} per wagon)`,
|
||
);
|
||
return (
|
||
`Consolidation partner found (${partnerReference}). ` +
|
||
`Shared wagon confirmed: ${parts.join('; ')}.`
|
||
);
|
||
}
|
||
}
|