mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 15:18:11 +00:00
- Add migration for consolidation approvals table and status enum - Create ConsolidationApprovalService to handle approval logic - Implement repository for managing consolidation approvals - Add entity for consolidation approval with necessary fields - Develop frontend components for displaying and managing consolidation approvals - Create tests for consolidation approval service to ensure correct behavior
193 lines
7.0 KiB
TypeScript
193 lines
7.0 KiB
TypeScript
import { ContractBookingService } from './contract-booking.service';
|
||
import { Booking } from '../bookings/entities/booking.entity';
|
||
|
||
/**
|
||
* The GL contract-drawdown path must run wagon consolidation before invoicing.
|
||
* A partial-wagon drawdown (e.g. 21× 20FT → one leftover container) parks in
|
||
* PENDING_CONSOLIDATION and is NOT finalized (no invoice / milestones) until it
|
||
* pairs with a wagon partner. These tests exercise the two new hooks directly.
|
||
*/
|
||
describe('ContractBookingService — drawdown consolidation gate', () => {
|
||
function makeService(overrides: {
|
||
consolidationService?: Partial<Record<string, jest.Mock>>;
|
||
bookingsRepository?: Partial<Record<string, jest.Mock>>;
|
||
invoiceService?: Partial<Record<string, jest.Mock>>;
|
||
milestoneService?: Partial<Record<string, jest.Mock>>;
|
||
contractsRepository?: Partial<Record<string, jest.Mock>>;
|
||
}) {
|
||
const consolidationService = {
|
||
slotsFromBooking: jest.fn().mockResolvedValue([]),
|
||
describePaired: jest.fn().mockReturnValue('paired'),
|
||
describePending: jest.fn().mockReturnValue('pending'),
|
||
needsConsolidationFromBooking: jest.fn().mockResolvedValue(false),
|
||
...overrides.consolidationService,
|
||
};
|
||
const bookingsRepository = {
|
||
findConsolidationPartner: jest.fn().mockResolvedValue(null),
|
||
pairConsolidation: jest.fn().mockResolvedValue(undefined),
|
||
parkForConsolidation: jest.fn().mockResolvedValue(undefined),
|
||
findByIdWithFiles: jest.fn(),
|
||
...overrides.bookingsRepository,
|
||
};
|
||
const invoiceService = {
|
||
ensureInvoiceForBooking: jest.fn().mockResolvedValue({ id: 'inv-1' }),
|
||
...overrides.invoiceService,
|
||
};
|
||
const milestoneService = {
|
||
seedPostBookingMilestones: jest.fn().mockResolvedValue(undefined),
|
||
seedPreBookingMilestonesOnBooking: jest.fn().mockResolvedValue(undefined),
|
||
ensureBookingMilestones: jest.fn().mockResolvedValue(undefined),
|
||
...overrides.milestoneService,
|
||
};
|
||
const contractsRepository = {
|
||
findByIdWithRelations: jest.fn(),
|
||
currentCycle: jest.fn().mockResolvedValue(null),
|
||
linkBooking: jest.fn().mockResolvedValue(undefined),
|
||
update: jest.fn().mockResolvedValue(undefined),
|
||
...overrides.contractsRepository,
|
||
};
|
||
|
||
const service = new ContractBookingService(
|
||
contractsRepository as never,
|
||
bookingsRepository as never,
|
||
{} as never, // bookingPricingService
|
||
consolidationService as never,
|
||
{} as never, // containerTypesService
|
||
{} as never, // ruleEngineService
|
||
milestoneService as never,
|
||
invoiceService as never,
|
||
{
|
||
createdToStaff: jest.fn(),
|
||
createdByGlForCustomer: jest.fn(),
|
||
} as never, // bookingNotifier
|
||
{} as never, // dataSource
|
||
{} as never, // trainSchedulingService
|
||
{} as never, // bookingBatchService
|
||
{} as never, // bookingTransitionService
|
||
{} as never, // consolidationApprovalService
|
||
);
|
||
return {
|
||
service,
|
||
consolidationService,
|
||
bookingsRepository,
|
||
invoiceService,
|
||
milestoneService,
|
||
contractsRepository,
|
||
};
|
||
}
|
||
|
||
const booking = { id: 'b-1', reference: 'BK-1' } as Booking;
|
||
|
||
it('parks (not pairs) when no complementary partner exists', async () => {
|
||
const { service, bookingsRepository } = makeService({
|
||
consolidationService: {
|
||
slotsFromBooking: jest
|
||
.fn()
|
||
.mockResolvedValue([{ containerTypeId: 'ct', slotsNeeded: 1 }]),
|
||
},
|
||
bookingsRepository: {
|
||
findConsolidationPartner: jest.fn().mockResolvedValue(null),
|
||
},
|
||
});
|
||
|
||
const result = await (service as never as {
|
||
consolidateDrawdown: (b: Booking, s: string) => Promise<{ paired: boolean }>;
|
||
}).consolidateDrawdown(booking, 'OPERATION_REQUEST_PENDING');
|
||
|
||
expect(result.paired).toBe(false);
|
||
expect(bookingsRepository.parkForConsolidation).toHaveBeenCalledWith(
|
||
'b-1',
|
||
'OPERATION_REQUEST_PENDING',
|
||
);
|
||
expect(bookingsRepository.pairConsolidation).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('pairs when a complementary partner exists', async () => {
|
||
const { service, bookingsRepository } = makeService({
|
||
consolidationService: {
|
||
slotsFromBooking: jest
|
||
.fn()
|
||
.mockResolvedValue([{ containerTypeId: 'ct', slotsNeeded: 1 }]),
|
||
},
|
||
bookingsRepository: {
|
||
findConsolidationPartner: jest
|
||
.fn()
|
||
.mockResolvedValue({ id: 'p-1', reference: 'BK-2' }),
|
||
},
|
||
});
|
||
|
||
const result = await (service as never as {
|
||
consolidateDrawdown: (b: Booking, s: string) => Promise<{ paired: boolean }>;
|
||
}).consolidateDrawdown(booking, 'AWAITING_DOCUMENTS');
|
||
|
||
expect(result.paired).toBe(true);
|
||
expect(bookingsRepository.pairConsolidation).toHaveBeenCalledWith('b-1', 'p-1');
|
||
expect(bookingsRepository.parkForConsolidation).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('onConsolidationPaired finalizes a resumed contract booking (invoice + milestones)', async () => {
|
||
const paired = {
|
||
id: 'b-1',
|
||
reference: 'BK-1',
|
||
contractId: 'c-1',
|
||
status: 'OPERATION_REQUEST_PENDING',
|
||
} as Booking;
|
||
const contract = {
|
||
id: 'c-1',
|
||
contractKind: 'GENERAL',
|
||
customsClearingEnabled: true,
|
||
tradeDirection: 'EXPORT',
|
||
};
|
||
const { service, invoiceService, milestoneService } = makeService({
|
||
bookingsRepository: {
|
||
findByIdWithFiles: jest.fn().mockResolvedValue(paired),
|
||
},
|
||
contractsRepository: {
|
||
findByIdWithRelations: jest.fn().mockResolvedValue(contract),
|
||
},
|
||
});
|
||
|
||
await service.onConsolidationPaired({ bookingIds: ['b-1'] });
|
||
|
||
expect(invoiceService.ensureInvoiceForBooking).toHaveBeenCalledTimes(1);
|
||
// GENERAL customs → per-booking milestones, via the idempotent ensure so a
|
||
// pairing replay (or an initiated instance's pre-seeded timeline) never
|
||
// duplicates rows.
|
||
expect(milestoneService.ensureBookingMilestones).toHaveBeenCalledWith(
|
||
'b-1',
|
||
'EXPORT',
|
||
);
|
||
});
|
||
|
||
it('onConsolidationPaired ignores a booking still PENDING_CONSOLIDATION', async () => {
|
||
const stillPending = {
|
||
id: 'b-1',
|
||
contractId: 'c-1',
|
||
status: 'PENDING_CONSOLIDATION',
|
||
} as Booking;
|
||
const { service, invoiceService } = makeService({
|
||
bookingsRepository: {
|
||
findByIdWithFiles: jest.fn().mockResolvedValue(stillPending),
|
||
},
|
||
});
|
||
|
||
await service.onConsolidationPaired({ bookingIds: ['b-1'] });
|
||
|
||
expect(invoiceService.ensureInvoiceForBooking).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('onConsolidationPaired ignores a non-contract (direct) booking', async () => {
|
||
const direct = { id: 'd-1', status: 'SUBMITTED', contractId: null } as Booking;
|
||
const { service, invoiceService, contractsRepository } = makeService({
|
||
bookingsRepository: {
|
||
findByIdWithFiles: jest.fn().mockResolvedValue(direct),
|
||
},
|
||
});
|
||
|
||
await service.onConsolidationPaired({ bookingIds: ['d-1'] });
|
||
|
||
expect(contractsRepository.findByIdWithRelations).not.toHaveBeenCalled();
|
||
expect(invoiceService.ensureInvoiceForBooking).not.toHaveBeenCalled();
|
||
});
|
||
});
|