Files
edr-platform/apps/edr-freight-api/src/modules/contracts/contract-booking.consolidation.spec.ts
2026-07-23 13:54:09 +00:00

190 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
{} as never, // workflowService
invoiceService as never,
{ createdToStaff: jest.fn() } as never, // bookingNotifier
{} as never, // dataSource
{} as never, // trainSchedulingService
{} as never, // bookingBatchService
{} as never, // bookingTransitionService
);
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();
});
});