mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +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
117 lines
4.2 KiB
TypeScript
117 lines
4.2 KiB
TypeScript
import { ContractBookingService } from './contract-booking.service';
|
|
|
|
/**
|
|
* OPERATION_CHANGES_REQUESTED resubmit with restated cargo. Operations can ask
|
|
* for the cargo itself to change, so a completion payload that restates
|
|
* containers must cancel the unpaid invoice, wipe the persisted cargo and
|
|
* re-run the fresh-completion path (re-persist, re-price, re-invoice). A
|
|
* payload without cargo keeps the day-only resubmit behavior.
|
|
*/
|
|
describe('ContractBookingService — changes-requested resubmit restating cargo', () => {
|
|
const CONTRACT = {
|
|
id: 'c-1',
|
|
reference: 'CTR-1',
|
|
contractKind: 'GENERAL',
|
|
freightType: 'CONTAINER',
|
|
tradeDirection: 'IMPORT',
|
|
customsClearingEnabled: false,
|
|
contractValidUntil: null,
|
|
cargoScope: [],
|
|
};
|
|
|
|
const bookingWithCargo = () => ({
|
|
id: 'b-1',
|
|
contractId: 'c-1',
|
|
reference: 'BKG-1',
|
|
status: 'OPERATION_CHANGES_REQUESTED',
|
|
bookingContainers: [{ containerSize: '20FT', quantity: 4 }],
|
|
cargoTotalWeightVgm: 80,
|
|
originYardId: 'y-o',
|
|
destinationYardId: 'y-d',
|
|
});
|
|
|
|
function makeService() {
|
|
const bookingsRepository = {
|
|
findByIdWithFiles: jest.fn().mockResolvedValue(bookingWithCargo()),
|
|
deleteContainers: jest.fn().mockResolvedValue(undefined),
|
|
update: jest.fn().mockResolvedValue(undefined),
|
|
};
|
|
const invoiceService = {
|
|
cancelUnpaidInvoiceForBooking: jest.fn().mockResolvedValue(undefined),
|
|
};
|
|
const trainSchedulingService = {
|
|
assertBookingWindowOpen: jest.fn().mockResolvedValue(undefined),
|
|
};
|
|
const contractsRepository = {
|
|
findByIdWithRelations: jest.fn().mockResolvedValue(CONTRACT),
|
|
};
|
|
const service = new ContractBookingService(
|
|
contractsRepository as never,
|
|
bookingsRepository as never,
|
|
{} as never, // bookingPricingService
|
|
{} as never, // consolidationService
|
|
{} as never, // containerTypesService
|
|
{} as never, // ruleEngineService
|
|
{} as never, // milestoneService
|
|
invoiceService as never,
|
|
{} as never, // bookingNotifier
|
|
{} as never, // dataSource
|
|
trainSchedulingService as never,
|
|
{} as never, // bookingBatchService
|
|
{} as never, // bookingTransitionService
|
|
{} as never, // consolidationApprovalService
|
|
);
|
|
return { service, bookingsRepository, invoiceService };
|
|
}
|
|
|
|
// Both paths dead-end into a downstream private assert we replace with a
|
|
// sentinel — which path threw tells us which branch the resubmit took.
|
|
const SENTINEL = new Error('reached-branch');
|
|
|
|
it('restated cargo cancels the invoice, wipes cargo and re-runs fresh completion', async () => {
|
|
const { service, bookingsRepository, invoiceService } = makeService();
|
|
// First gate inside the fresh-completion (!hasCargo) path.
|
|
jest
|
|
.spyOn(
|
|
service as never as { assertWithinQuantityCap: () => Promise<void> },
|
|
'assertWithinQuantityCap',
|
|
)
|
|
.mockRejectedValue(SENTINEL);
|
|
|
|
await expect(
|
|
service.completeUnderContract('c-1', 'b-1', {
|
|
scheduledDate: new Date().toISOString(),
|
|
containers: [{ containerSize: '20FT', quantity: 2 }],
|
|
} as never),
|
|
).rejects.toBe(SENTINEL);
|
|
|
|
expect(invoiceService.cancelUnpaidInvoiceForBooking).toHaveBeenCalledWith('b-1');
|
|
expect(bookingsRepository.deleteContainers).toHaveBeenCalledWith('b-1');
|
|
expect(bookingsRepository.update).toHaveBeenCalledWith('b-1', {
|
|
cargoTotalWeightVgm: 0,
|
|
});
|
|
});
|
|
|
|
it('a day-only resubmit keeps the persisted cargo and invoice untouched', async () => {
|
|
const { service, bookingsRepository, invoiceService } = makeService();
|
|
// First call inside the day-only (hasCargo) resubmit path.
|
|
jest
|
|
.spyOn(
|
|
service as never as {
|
|
assertPersistedContainersAvailable: () => Promise<void>;
|
|
},
|
|
'assertPersistedContainersAvailable',
|
|
)
|
|
.mockRejectedValue(SENTINEL);
|
|
|
|
await expect(
|
|
service.completeUnderContract('c-1', 'b-1', {
|
|
scheduledDate: new Date().toISOString(),
|
|
} as never),
|
|
).rejects.toBe(SENTINEL);
|
|
|
|
expect(invoiceService.cancelUnpaidInvoiceForBooking).not.toHaveBeenCalled();
|
|
expect(bookingsRepository.deleteContainers).not.toHaveBeenCalled();
|
|
});
|
|
});
|