From c48e9b8905e622346a996be640cbe87fb110c197 Mon Sep 17 00:00:00 2001 From: Marshal Date: Fri, 21 Aug 2026 23:28:59 +0000 Subject: [PATCH] fix issue and add consolidation --- .../booking-batch.service.spec.ts | 68 +++++++++++++++++++ .../train-scheduling/booking-batch.service.ts | 4 +- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.spec.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.spec.ts index ce0e77a3e..1c6b66ea8 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.spec.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.spec.ts @@ -1536,4 +1536,72 @@ describe('BookingBatchService — physical wagon-type gate', () => { // Those 16 are now held, so the next booking in the pass cannot re-take them. expect(stock.availableFor([NW5], WHOLE_LEG)).toBe(0); }); + + it('sizes a capped-bulk partial on ONE type at the cargo cap, not the 70T rating', async () => { + const svc = service(); + const inner = internals(svc); + (inner as { isSplitEligible: unknown }).isSplitEligible = () => true; + const dims = { lengthMeters: 14, tareWeightTons: 24, capacityTons: 70 }; + (inner as unknown as { loadWagonDims: unknown }).loadWagonDims = async () => ({ + container: dims, + bulk: dims, + byWagonTypeId: new Map([ + [NW5, dims], + [PW2, dims], + ]), + }); + const tryPartial = jest + .fn() + .mockResolvedValue({ wagons: 16, weightTons: 864, lengthMeters: 224 }); + (inner as { tryPartialOffer: unknown }).tryPartialOffer = tryPartial; + + const stock = mixedStock(); + const candidate = { + id: 'schedule-1', + budget: { + legOf: () => WHOLE_LEG, + remainingFor: () => ({ wagons: 20, weightTons: 99_999, lengthMeters: 99_999 }), + subtract: jest.fn(), + }, + armed: false, + stock, + }; + const booking = { + id: 'b2', + reference: 'BK-2', + originYardId: 'a', + destinationYardId: 'b', + freightType: 'BULK', + cargoTotalWeightVgm: 695, + cargoType: { + id: 'cargo-perishable', + wagonTypes: [ + { id: NW5, capacityTons: 70 }, + { id: PW2, capacityTons: 70 }, + ], + tonsPerWagonMap: { [NW5]: 30, [PW2]: 20 }, + }, + bookingContainers: [], + } as unknown as Booking; + + const offered = await inner.maybeOfferPartial( + booking, + false, + [candidate], + { wagons: 24, weightTons: 1400, lengthMeters: 336 }, + [NW5, PW2], + ); + + expect(offered).toBe(true); + // Room capped to the 16 NW5 that exist (biggest capped take), and the seat + // carries the 30T cargo cap — never the wagon's raw 70T rating. + expect(tryPartial.mock.calls[0][2]).toMatchObject({ wagons: 16 }); + expect(tryPartial.mock.calls[0][4]).toMatchObject({ + wagonTypeId: NW5, + perWagon: { capacityTons: 30 }, + }); + // Only the seated type is held; the PW2s stay free for bulk-only cargo. + expect(stock.availableFor([NW5], WHOLE_LEG)).toBe(0); + expect(stock.availableFor([PW2], WHOLE_LEG)).toBe(4); + }); }); diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts index 72ffb2a96..79a902a65 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts @@ -2831,7 +2831,6 @@ export class BookingBatchService implements OnModuleInit { wagonTypeIds: string[] = [], ): Promise { if (!this.isSplitEligible(booking, isPair)) return false; - const wagonDims = await this.loadWagonDims(); // PER_TON bulk partials are sized on ONE concrete wagon type at the // cargo's per-wagon cap — sizing on the first type's raw 70T rating // offered tonnage the wagons could never carry (Perishable caps at @@ -2846,6 +2845,7 @@ export class BookingBatchService implements OnModuleInit { booking.freightType === "BULK" && !perItemBulk && wagonTypeIds.length > 0; + const wagonDims = cappedBulk ? await this.loadWagonDims() : null; const target = candidates .map((c) => { const leg = c.budget.legOf(booking.originYardId, booking.destinationYardId); @@ -2856,7 +2856,7 @@ export class BookingBatchService implements OnModuleInit { // them NW5" into an offer for 16 — the customer pays for 16 and the // other 4 leave as the usual remainder booking, instead of paying for // 20 and stalling at allocation on wagon 17. - if (cappedBulk) { + if (cappedBulk && wagonDims) { const best = this.allowedDimsWithTypes(booking, wagonDims) .filter((o): o is { wagonTypeId: string; dims: PerWagonDims } => o.wagonTypeId != null,