mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 09:58:12 +00:00
implement contract cancellation feature and update contract statuses
- Added functionality to cancel contracts, allowing users to provide a reason for cancellation. - Updated contract statuses to include SUSPENDED and changed CLOSED to COMPLETED. - Enhanced the UI to reflect the new cancellation option and updated messaging for contract statuses. - Refactored contract booking actions to accommodate changes in booking logic for ONE_TIME and GENERAL contracts. - Removed clearance document management from the contract detail page, as it is now handled per booking. - Introduced a SQL script to reset bookings and train schedules for development purposes.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { BookingBatchService } from './booking-batch.service';
|
||||
import { Booking } from '../bookings/entities/booking.entity';
|
||||
import { WagonStockLedger } from './wagon-stock-ledger.util';
|
||||
|
||||
describe('BookingBatchService — PAID reconcile', () => {
|
||||
const scheduleId = 'schedule-1';
|
||||
@@ -40,10 +41,12 @@ describe('BookingBatchService — PAID reconcile', () => {
|
||||
previewPaidBookingWagonShortage: jest.Mock;
|
||||
getBookableSchedules: jest.Mock;
|
||||
getWindowConfig: jest.Mock;
|
||||
wagonStockForSchedule: jest.Mock;
|
||||
};
|
||||
let dataSource: {
|
||||
getRepository: jest.Mock;
|
||||
transaction: jest.Mock;
|
||||
query: jest.Mock;
|
||||
};
|
||||
let notifier: {
|
||||
payNow: jest.Mock;
|
||||
@@ -90,6 +93,13 @@ describe('BookingBatchService — PAID reconcile', () => {
|
||||
}),
|
||||
// No shortage by default — paid bookings link as before.
|
||||
previewPaidBookingWagonShortage: jest.fn().mockResolvedValue(null),
|
||||
// No physical stock configured → the wagon-type gate stands down and these
|
||||
// specs keep testing the abstract capacity budget on its own.
|
||||
wagonStockForSchedule: jest.fn().mockResolvedValue({
|
||||
mode: 'YARD',
|
||||
remainingByTypeId: new Map<string, number>(),
|
||||
codesByTypeId: new Map<string, string>(),
|
||||
}),
|
||||
getBookableSchedules: jest.fn().mockResolvedValue([]),
|
||||
getWindowConfig: jest.fn().mockResolvedValue({
|
||||
importWindowLeadDays: 3,
|
||||
@@ -116,6 +126,10 @@ describe('BookingBatchService — PAID reconcile', () => {
|
||||
};
|
||||
await fn(manager);
|
||||
}),
|
||||
// cargo/container type -> allowed wagon type lookups (loadAllowedWagonTypeIds).
|
||||
// Empty = unresolvable, so the physical-stock gate stands down and these
|
||||
// specs keep exercising the abstract capacity budget alone.
|
||||
query: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
|
||||
notifier = {
|
||||
@@ -1237,6 +1251,7 @@ describe('BookingBatchService — built-train wagon capacity', () => {
|
||||
return genericRepo;
|
||||
}),
|
||||
transaction: jest.fn(),
|
||||
query: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
const service = new BookingBatchService(
|
||||
dataSource as never,
|
||||
@@ -1344,3 +1359,106 @@ describe('BookingBatchService — built-train wagon capacity', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The reported failure: a train advertising 20 free wagons where only 16 are of
|
||||
* the type the booking can ride. Selecting all 20 took the customer's money for
|
||||
* space that never existed and then stalled at allocation on wagon 17.
|
||||
*/
|
||||
describe('BookingBatchService — physical wagon-type gate', () => {
|
||||
const NW5 = 'wagon-type-nw5';
|
||||
const PW2 = 'wagon-type-pw2';
|
||||
const WHOLE_LEG = { fromEdge: 0, toEdge: 1 };
|
||||
|
||||
/** 16 NW5 + 4 PW2 = 20 wagons on the train, but only 16 usable by an NW5 booking. */
|
||||
const mixedStock = () => new WagonStockLedger(new Map([[NW5, 16], [PW2, 4]]), 1);
|
||||
|
||||
const internals = (svc: BookingBatchService) =>
|
||||
svc as unknown as {
|
||||
hasWagonStock: (
|
||||
stock: WagonStockLedger,
|
||||
ids: string[],
|
||||
needed: number,
|
||||
leg: { fromEdge: number; toEdge: number },
|
||||
) => boolean;
|
||||
maybeOfferPartial: (
|
||||
booking: Booking,
|
||||
isPair: boolean,
|
||||
candidates: unknown[],
|
||||
need: { wagons: number; weightTons: number; lengthMeters: number },
|
||||
ids: string[],
|
||||
) => Promise<boolean>;
|
||||
tryPartialOffer: unknown;
|
||||
isSplitEligible: unknown;
|
||||
};
|
||||
|
||||
const service = () =>
|
||||
new BookingBatchService(
|
||||
{ getRepository: jest.fn(), transaction: jest.fn(), query: jest.fn() } as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
undefined,
|
||||
{ findOpenOffer: jest.fn() } as never,
|
||||
);
|
||||
|
||||
it('refuses a 20-wagon NW5 booking on a train holding only 16 NW5', () => {
|
||||
const svc = internals(service());
|
||||
const stock = mixedStock();
|
||||
expect(svc.hasWagonStock(stock, [NW5], 20, WHOLE_LEG)).toBe(false);
|
||||
expect(svc.hasWagonStock(stock, [NW5], 16, WHOLE_LEG)).toBe(true);
|
||||
// A booking that may ride either type sees all 20.
|
||||
expect(svc.hasWagonStock(stock, [NW5, PW2], 20, WHOLE_LEG)).toBe(true);
|
||||
});
|
||||
|
||||
it('stands down when the booking has no allowed wagon type configured', () => {
|
||||
// Unresolvable configuration must not strand every booking that uses it —
|
||||
// the abstract capacity budget still governs.
|
||||
expect(internals(service()).hasWagonStock(mixedStock(), [], 999, WHOLE_LEG)).toBe(true);
|
||||
});
|
||||
|
||||
it('sizes the split offer to the wagons that physically exist, not the free slots', async () => {
|
||||
const svc = service();
|
||||
const inner = internals(svc);
|
||||
// Isolate the sizing decision: eligibility and offer creation are covered
|
||||
// elsewhere, what matters here is the room handed to tryPartialOffer.
|
||||
(inner as { isSplitEligible: unknown }).isSplitEligible = () => true;
|
||||
const tryPartial = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ wagons: 16, weightTons: 1600, lengthMeters: 224 });
|
||||
(inner as { tryPartialOffer: unknown }).tryPartialOffer = tryPartial;
|
||||
|
||||
const stock = mixedStock();
|
||||
const candidate = {
|
||||
id: 'schedule-1',
|
||||
// 20 abstract slots free, weight and length wide open.
|
||||
budget: {
|
||||
legOf: () => WHOLE_LEG,
|
||||
remainingFor: () => ({ wagons: 20, weightTons: 99_999, lengthMeters: 99_999 }),
|
||||
subtract: jest.fn(),
|
||||
},
|
||||
armed: false,
|
||||
stock,
|
||||
};
|
||||
|
||||
const offered = await inner.maybeOfferPartial(
|
||||
{ id: 'b1', reference: 'BK-1', originYardId: 'a', destinationYardId: 'b' } as Booking,
|
||||
false,
|
||||
[candidate],
|
||||
{ wagons: 20, weightTons: 2000, lengthMeters: 280 },
|
||||
[NW5],
|
||||
);
|
||||
|
||||
expect(offered).toBe(true);
|
||||
// 16, not the 20 free slots — the customer is billed for what can be loaded.
|
||||
expect(tryPartial.mock.calls[0][2]).toMatchObject({ wagons: 16 });
|
||||
// Those 16 are now held, so the next booking in the pass cannot re-take them.
|
||||
expect(stock.availableFor([NW5], WHOLE_LEG)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user