mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 19:00:55 +00:00
- Updated the logic in to ensure that a booking only earns its gate pass once the freight charges are settled. - Added logging for bookings that have not settled freight payment when securing gate passes. - Modified seeders to ensure that bookings have associated company profiles to prevent data inconsistencies. - Updated freight permissions to include new clearance actions for bookings. - Enhanced the UI to reflect changes in the clearance process, including new shipment request pages and improved status handling in the clearance action panel. - Adjusted the contract clearance list to accommodate both customs contracts and shipment bookings. - Improved the handling of GENERAL contracts in various components to ensure proper booking flow and visibility.
119 lines
4.3 KiB
TypeScript
119 lines
4.3 KiB
TypeScript
import { TrainSchedulingService } from './train-scheduling.service';
|
|
import { Booking } from '../bookings/entities/booking.entity';
|
|
import { ClearanceMilestone } from '../contracts/entities/clearance-milestone.entity';
|
|
|
|
type Row = Pick<ClearanceMilestone, 'bookingId' | 'milestoneCode' | 'status'> & {
|
|
metadata?: Record<string, unknown> | null;
|
|
triggeredAt?: Date | null;
|
|
};
|
|
|
|
/**
|
|
* The gate pass is secured once per train schedule, but each booking only earns
|
|
* its GATEPASS_GRANTED milestone after settling freight payment. An unpaid
|
|
* booking must not ride a paid neighbour's grant — the train proceeds, that
|
|
* booking stays pending.
|
|
*/
|
|
function makeService(bookings: Array<Partial<Booking>>, rows: Row[]) {
|
|
const milestoneRepo = {
|
|
find: jest.fn().mockResolvedValue(rows),
|
|
save: jest.fn((row: Row) => Promise.resolve(row)),
|
|
};
|
|
const bookingRepo = { find: jest.fn().mockResolvedValue(bookings) };
|
|
const dataSource = {
|
|
getRepository: (entity: unknown) =>
|
|
entity === Booking ? bookingRepo : milestoneRepo,
|
|
};
|
|
|
|
const service = Object.create(
|
|
TrainSchedulingService.prototype,
|
|
) as TrainSchedulingService;
|
|
Object.assign(service, {
|
|
dataSource,
|
|
logger: { warn: jest.fn(), log: jest.fn() },
|
|
});
|
|
return { service, milestoneRepo };
|
|
}
|
|
|
|
/** Reach the private bridge write under test. */
|
|
function grant(service: TrainSchedulingService, at: Date): Promise<void> {
|
|
return (
|
|
service as unknown as {
|
|
completeGatepassMilestoneForSchedule(id: string, at: Date): Promise<void>;
|
|
}
|
|
).completeGatepassMilestoneForSchedule('sched-1', at);
|
|
}
|
|
|
|
const securedAt = new Date('2026-07-09T08:00:00.000Z');
|
|
|
|
describe('gate pass is withheld from bookings that have not paid freight', () => {
|
|
it('grants the paid booking and leaves the unpaid one pending', async () => {
|
|
const rows: Row[] = [
|
|
{ bookingId: 'paid', milestoneCode: 'FREIGHT_PAYMENT_SETTLED', status: 'COMPLETED' },
|
|
{ bookingId: 'paid', milestoneCode: 'GATEPASS_GRANTED', status: 'PENDING' },
|
|
{ bookingId: 'unpaid', milestoneCode: 'FREIGHT_PAYMENT_SETTLED', status: 'PENDING' },
|
|
{ bookingId: 'unpaid', milestoneCode: 'GATEPASS_GRANTED', status: 'PENDING' },
|
|
];
|
|
const { service, milestoneRepo } = makeService(
|
|
[
|
|
{ id: 'paid', status: 'CONFIRMED', paymentStatus: 'PENDING' },
|
|
{ id: 'unpaid', status: 'CONFIRMED', paymentStatus: 'PENDING' },
|
|
],
|
|
rows,
|
|
);
|
|
|
|
await grant(service, securedAt);
|
|
|
|
const saved = milestoneRepo.save.mock.calls.map(([r]: [Row]) => r);
|
|
expect(saved).toHaveLength(1);
|
|
expect(saved[0]!.bookingId).toBe('paid');
|
|
expect(saved[0]!.status).toBe('COMPLETED');
|
|
expect(saved[0]!.triggeredAt).toBe(securedAt);
|
|
|
|
const unpaid = rows.find(
|
|
(r) => r.bookingId === 'unpaid' && r.milestoneCode === 'GATEPASS_GRANTED',
|
|
);
|
|
expect(unpaid!.status).toBe('PENDING');
|
|
});
|
|
|
|
it('treats a booking paid outside the milestone path as paid', async () => {
|
|
// Some payment paths settle the invoice without writing the milestone; the
|
|
// clearance views self-heal it on read, so the gate pass must not lag.
|
|
const rows: Row[] = [
|
|
{ bookingId: 'b-1', milestoneCode: 'FREIGHT_PAYMENT_SETTLED', status: 'PENDING' },
|
|
{ bookingId: 'b-1', milestoneCode: 'GATEPASS_GRANTED', status: 'PENDING' },
|
|
];
|
|
const { service, milestoneRepo } = makeService(
|
|
[{ id: 'b-1', status: 'CONFIRMED', paymentStatus: 'PAID' }],
|
|
rows,
|
|
);
|
|
|
|
await grant(service, securedAt);
|
|
|
|
expect(milestoneRepo.save).toHaveBeenCalledTimes(1);
|
|
expect(milestoneRepo.save.mock.calls[0]![0].bookingId).toBe('b-1');
|
|
});
|
|
|
|
it('leaves an already-granted milestone untouched', async () => {
|
|
const rows: Row[] = [
|
|
{ bookingId: 'b-1', milestoneCode: 'FREIGHT_PAYMENT_SETTLED', status: 'COMPLETED' },
|
|
{ bookingId: 'b-1', milestoneCode: 'GATEPASS_GRANTED', status: 'COMPLETED' },
|
|
];
|
|
const { service, milestoneRepo } = makeService(
|
|
[{ id: 'b-1', status: 'PAID', paymentStatus: 'PAID' }],
|
|
rows,
|
|
);
|
|
|
|
await grant(service, securedAt);
|
|
|
|
expect(milestoneRepo.save).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does nothing when the schedule carries no customs bookings', async () => {
|
|
const { service, milestoneRepo } = makeService([], []);
|
|
|
|
await grant(service, securedAt);
|
|
|
|
expect(milestoneRepo.save).not.toHaveBeenCalled();
|
|
});
|
|
});
|