feat: enhance booking operations to support freight forwarder variants and improve consolidation handling

This commit is contained in:
Marshal
2026-06-23 23:48:45 +00:00
parent 9d81a2e1ee
commit 07cd7dc111
8 changed files with 338 additions and 64 deletions

View File

@@ -242,12 +242,23 @@ export class BookingPricingService {
)
: 0;
// Consolidation is system-managed: the CONSOLIDATION_ENABLED surcharge fires
// whenever any container line leaves a wagon partially filled. Derived from
// the container quantities there is no persisted opt-in flag.
// Consolidation is system-managed: the CONSOLIDATION surcharge fires whenever
// a container type leaves a wagon partially filled. Aggregate by type first —
// two lines of the same type share wagons, so 2× 20FT (= one full wagon) must
// NOT count as a partial wagon. Mirrors ConsolidationService.slotsFromContainerLines.
const remainderByType = new Map<string, { quantity: number; perWagon: number }>();
for (const l of lines) {
const prev = remainderByType.get(l.container.containerTypeId);
remainderByType.set(l.container.containerTypeId, {
quantity: (prev?.quantity ?? 0) + Number(l.quantity || 0),
perWagon: l.perWagon,
});
}
const allowConsolidation =
booking.freightType === 'CONTAINER' &&
lines.some((l) => wagonRemainder(l.quantity, l.perWagon) > 0);
[...remainderByType.values()].some(
(t) => wagonRemainder(t.quantity, t.perWagon) > 0,
);
return {
freightType: booking.freightType as 'CONTAINER' | 'BULK',

View File

@@ -57,16 +57,29 @@ export class ConsolidationService {
async slotsFromContainerLines(
lines: Array<{ containerTypeId: string; quantity: number }>,
): Promise<ConsolidationSlot[]> {
const slots: 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) {
const ct = await this.containerTypesService.findById(line.containerTypeId);
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(line.quantity, perWagon);
const remainder = wagonRemainder(quantity, perWagon);
if (remainder === 0) continue;
slots.push({
containerTypeId: line.containerTypeId,
containerTypeId,
containerTypeCode: ct.code,
quantity: line.quantity,
quantity,
containersPerWagon: perWagon,
remainder,
slotsNeeded: perWagon - remainder,