Merge pull request #1390 from Tria-plc/freight_feature/usermanagement

fix issue
This commit is contained in:
marshal
2026-08-22 04:31:27 +03:00
committed by GitHub
2 changed files with 39 additions and 12 deletions

View File

@@ -1516,6 +1516,7 @@ export class BookingBatchService implements OnModuleInit {
stock, stock,
leg, leg,
this.scarcityRankForPool([primary], allowedWagonTypes), this.scarcityRankForPool([primary], allowedWagonTypes),
wagonTypeIds,
) )
: null; : null;
const seated = useSmart const seated = useSmart
@@ -2357,7 +2358,7 @@ export class BookingBatchService implements OnModuleInit {
!perItemBulk && !perItemBulk &&
wagonTypeIds.length > 0; wagonTypeIds.length > 0;
const smart = useSmart const smart = useSmart
? this.smartBulkNeed(booking, wagonDims, stock, leg, scarcityRank) ? this.smartBulkNeed(booking, wagonDims, stock, leg, scarcityRank, wagonTypeIds)
: null; : null;
const admitted = useSmart const admitted = useSmart
? smart != null && budget.fits(smart.need, leg) ? smart != null && budget.fits(smart.need, leg)
@@ -2653,7 +2654,14 @@ export class BookingBatchService implements OnModuleInit {
const leg = legOn(t); const leg = legOn(t);
if (leg == null) continue; if (leg == null) continue;
if (useSmart) { if (useSmart) {
const probe = this.smartBulkNeed(booking, wagonDims, t.stock, leg, scarcityRank); const probe = this.smartBulkNeed(
booking,
wagonDims,
t.stock,
leg,
scarcityRank,
wagonTypeIds,
);
if (probe != null && t.budget.fits(probe.need, leg)) { if (probe != null && t.budget.fits(probe.need, leg)) {
smart = probe; smart = probe;
target = t; target = t;
@@ -2858,10 +2866,14 @@ export class BookingBatchService implements OnModuleInit {
// other 4 leave as the usual remainder booking, instead of paying for // other 4 leave as the usual remainder booking, instead of paying for
// 20 and stalling at allocation on wagon 17. // 20 and stalling at allocation on wagon 17.
if (cappedBulk && wagonDims) { if (cappedBulk && wagonDims) {
const best = this.allowedDimsWithTypes(booking, wagonDims) // Types resolved from the id list (join tables), never the pool
.filter((o): o is { wagonTypeId: string; dims: PerWagonDims } => // entity's unloaded cargoType.wagonTypes relation — see smartBulkNeed.
o.wagonTypeId != null, const best = [...new Set(wagonTypeIds)]
) .map((wagonTypeId) => ({
wagonTypeId,
dims: wagonDims.byWagonTypeId.get(wagonTypeId),
}))
.filter((o): o is { wagonTypeId: string; dims: PerWagonDims } => o.dims != null)
.map((o) => ({ .map((o) => ({
...o, ...o,
free: c.stock?.availableFor([o.wagonTypeId], leg) ?? 0, free: c.stock?.availableFor([o.wagonTypeId], leg) ?? 0,
@@ -5074,7 +5086,7 @@ export class BookingBatchService implements OnModuleInit {
Number(b.bulkTotalWeightTons ?? 0) > 0 && Number(b.bulkTotalWeightTons ?? 0) > 0 &&
Number(b.cargoTotalWeightVgm ?? 0) > 0; Number(b.cargoTotalWeightVgm ?? 0) > 0;
if (b.freightType === "BULK" && !perItemBulk && typeIds.length) { if (b.freightType === "BULK" && !perItemBulk && typeIds.length) {
const smart = this.smartBulkNeed(b, wagonDims, ledger, leg, rank); const smart = this.smartBulkNeed(b, wagonDims, ledger, leg, rank, typeIds);
if (smart) { if (smart) {
for (const part of smart.perType) { for (const part of smart.perType) {
ledger.consume([part.wagonTypeId], part.wagons, leg); ledger.consume([part.wagonTypeId], part.wagons, leg);
@@ -5168,9 +5180,19 @@ export class BookingBatchService implements OnModuleInit {
stock: WagonStockLedger, stock: WagonStockLedger,
leg: CorridorLeg, leg: CorridorLeg,
scarcityRank: Map<string, number>, scarcityRank: Map<string, number>,
/**
* Wagon-type ids this booking may ride, from {@link loadAllowedWagonTypeIds}
* — NEVER from `booking.cargoType.wagonTypes`. The batch pool finders
* deliberately do not join that relation (hot path), so on a pool entity
* it is always empty; resolving through it made every PER_TON bulk booking
* unseatable — no whole fit and no partial offer, silently READY forever
* (the S-2026-00020 / BK-2026-000036 incident).
*/
wagonTypeIds: readonly string[],
): { need: Capacity; perType: Array<{ wagonTypeId: string; wagons: number }> } | null { ): { need: Capacity; perType: Array<{ wagonTypeId: string; wagons: number }> } | null {
const options = this.allowedDimsWithTypes(booking, wagonDims) const options = [...new Set(wagonTypeIds)]
.filter((o): o is { wagonTypeId: string; dims: PerWagonDims } => o.wagonTypeId != null) .map((wagonTypeId) => ({ wagonTypeId, dims: wagonDims.byWagonTypeId.get(wagonTypeId) }))
.filter((o): o is { wagonTypeId: string; dims: PerWagonDims } => o.dims != null)
.map((o) => ({ .map((o) => ({
...o, ...o,
free: stock.availableFor([o.wagonTypeId], leg), free: stock.availableFor([o.wagonTypeId], leg),

View File

@@ -22,17 +22,21 @@ describe('BookingBatchService.smartBulkNeed', () => {
s: WagonStockLedger, s: WagonStockLedger,
l: { fromEdge: number; toEdge: number }, l: { fromEdge: number; toEdge: number },
r: Map<string, number>, r: Map<string, number>,
ids: readonly string[],
) => { need: { wagons: number }; perType: Array<{ wagonTypeId: string; wagons: number }> } | null; ) => { need: { wagons: number }; perType: Array<{ wagonTypeId: string; wagons: number }> } | null;
} }
).smartBulkNeed(booking, wagonDims, stock, { fromEdge: 0, toEdge: 1 }, rank); ).smartBulkNeed(booking, wagonDims, stock, { fromEdge: 0, toEdge: 1 }, rank, allowedIds);
const nw5 = { id: 'wt-nw5', capacityTons: 70 }; const nw5 = { id: 'wt-nw5', capacityTons: 70 };
const pw2 = { id: 'wt-pw2', capacityTons: 70 }; const pw2 = { id: 'wt-pw2', capacityTons: 70 };
// Shaped like a BATCH POOL entity: cargoType WITHOUT the wagonTypes
// relation (the pool query never joins it) — allowed types must come from
// the ids parameter, or every pool bulk booking reads as unseatable.
const perishable = { const perishable = {
id: 'cargo-perishable', id: 'cargo-perishable',
wagonTypes: [nw5, pw2],
tonsPerWagonMap: { [nw5.id]: 30, [pw2.id]: 20 }, tonsPerWagonMap: { [nw5.id]: 30, [pw2.id]: 20 },
}; };
const allowedIds = [nw5.id, pw2.id];
const wagonDims = { const wagonDims = {
container: { lengthMeters: 14, tareWeightTons: 24, capacityTons: 70 }, container: { lengthMeters: 14, tareWeightTons: 24, capacityTons: 70 },
bulk: { lengthMeters: 14, tareWeightTons: 24, capacityTons: 70 }, bulk: { lengthMeters: 14, tareWeightTons: 24, capacityTons: 70 },
@@ -113,9 +117,10 @@ describe('BookingBatchService.smartBulkNeed', () => {
s: WagonStockLedger, s: WagonStockLedger,
l: { fromEdge: number; toEdge: number }, l: { fromEdge: number; toEdge: number },
r: Map<string, number>, r: Map<string, number>,
ids: readonly string[],
) => { need: { wagons: number }; perType: Array<{ wagonTypeId: string; wagons: number }> } | null; ) => { need: { wagons: number }; perType: Array<{ wagonTypeId: string; wagons: number }> } | null;
} }
).smartBulkNeed(booking(200), wagonDims, stock, { fromEdge: 0, toEdge: 2 }, contested); ).smartBulkNeed(booking(200), wagonDims, stock, { fromEdge: 0, toEdge: 2 }, contested, allowedIds);
expect(smart).not.toBeNull(); expect(smart).not.toBeNull();
expect(smart!.perType).toEqual([{ wagonTypeId: pw2.id, wagons: 10 }]); expect(smart!.perType).toEqual([{ wagonTypeId: pw2.id, wagons: 10 }]);
}); });