Files
edr-platform/apps/edr-freight-api/src/modules/bookings/consolidation.service.ts

138 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Injectable } from '@nestjs/common';
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[];
}
/** Containers that fit on one wagon for a given container type (inverse of wagons_per_unit). */
export function containersPerWagon(wagonsPerUnit: number): number {
const wpu = Number(wagonsPerUnit);
if (!wpu || wpu <= 0) return 1;
return Math.max(1, Math.round(1 / wpu));
}
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 = containersPerWagon(Number(ct.wagonsPerUnit));
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('; ')}.`
);
}
}