mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 01:48:12 +00:00
add Excel import functionality for container bookings
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { ContractBookingService } from './contract-booking.service';
|
||||
import { Contract } from './entities/contract.entity';
|
||||
|
||||
/**
|
||||
* Contract auto-completion by quantity cap. Once a GENERAL contract's capped
|
||||
* scope is fully consumed (e.g. a split remainder rebooked), the contract moves
|
||||
* to CONTRACT_CLOSED even inside its validity window, and further bookings are
|
||||
* blocked — including while a booking window is open. Released capacity
|
||||
* (cancelled/expired booking) reopens the contract on the next attempt.
|
||||
*/
|
||||
describe('ContractBookingService — quantity-cap completion', () => {
|
||||
function makeService() {
|
||||
const contractsRepository = {
|
||||
findByIdWithRelations: jest.fn(),
|
||||
update: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const service = new ContractBookingService(
|
||||
contractsRepository as never,
|
||||
{} as never, // bookingsRepository
|
||||
{} as never, // bookingPricingService
|
||||
{} as never, // consolidationService
|
||||
{} as never, // containerTypesService
|
||||
{} as never, // ruleEngineService
|
||||
{} as never, // milestoneService
|
||||
{} as never, // workflowService
|
||||
{} as never, // invoiceService
|
||||
{} as never, // dataSource
|
||||
{} as never, // trainSchedulingService
|
||||
);
|
||||
return { service, contractsRepository };
|
||||
}
|
||||
|
||||
type WithPrivate = {
|
||||
maybeCompleteContract: (c: Contract) => Promise<void>;
|
||||
};
|
||||
|
||||
const generalContract = (status: string): Contract =>
|
||||
({
|
||||
id: 'c-1',
|
||||
reference: 'CTR-1',
|
||||
contractKind: 'GENERAL',
|
||||
status,
|
||||
}) as Contract;
|
||||
|
||||
it('closes a GENERAL contract when every capped line is exhausted', async () => {
|
||||
const { service, contractsRepository } = makeService();
|
||||
jest.spyOn(service, 'computeCapacity').mockResolvedValue([
|
||||
{ containerSize: '20FT', cap: 10, booked: 10, remaining: 0 },
|
||||
{ containerSize: '40FT', cap: 4, booked: 4, remaining: 0 },
|
||||
]);
|
||||
|
||||
await (service as never as WithPrivate).maybeCompleteContract(
|
||||
generalContract('CONTRACT_ACTIVE'),
|
||||
);
|
||||
|
||||
expect(contractsRepository.update).toHaveBeenCalledWith('c-1', {
|
||||
status: 'CONTRACT_CLOSED',
|
||||
});
|
||||
});
|
||||
|
||||
it('absorbs bulk-ton float dust when judging exhaustion', async () => {
|
||||
const { service, contractsRepository } = makeService();
|
||||
jest
|
||||
.spyOn(service, 'computeCapacity')
|
||||
.mockResolvedValue([{ cap: 100, booked: 99.9995, remaining: 0.0005 }]);
|
||||
|
||||
await (service as never as WithPrivate).maybeCompleteContract(
|
||||
generalContract('FULLY_EXECUTED'),
|
||||
);
|
||||
|
||||
expect(contractsRepository.update).toHaveBeenCalledWith('c-1', {
|
||||
status: 'CONTRACT_CLOSED',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the contract open while any capped line has capacity left', async () => {
|
||||
const { service, contractsRepository } = makeService();
|
||||
jest.spyOn(service, 'computeCapacity').mockResolvedValue([
|
||||
{ containerSize: '20FT', cap: 10, booked: 10, remaining: 0 },
|
||||
{ containerSize: '40FT', cap: 4, booked: 3, remaining: 1 },
|
||||
]);
|
||||
|
||||
await (service as never as WithPrivate).maybeCompleteContract(
|
||||
generalContract('CONTRACT_ACTIVE'),
|
||||
);
|
||||
|
||||
expect(contractsRepository.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('never closes an uncapped contract', async () => {
|
||||
const { service, contractsRepository } = makeService();
|
||||
jest.spyOn(service, 'computeCapacity').mockResolvedValue([]);
|
||||
|
||||
await (service as never as WithPrivate).maybeCompleteContract(
|
||||
generalContract('CONTRACT_ACTIVE'),
|
||||
);
|
||||
|
||||
expect(contractsRepository.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('never closes a ONE_TIME contract (single-slot rule governs it)', async () => {
|
||||
const { service, contractsRepository } = makeService();
|
||||
const spy = jest.spyOn(service, 'computeCapacity');
|
||||
|
||||
await (service as never as WithPrivate).maybeCompleteContract({
|
||||
id: 'c-1',
|
||||
contractKind: 'ONE_TIME',
|
||||
status: 'FULLY_EXECUTED',
|
||||
} as Contract);
|
||||
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
expect(contractsRepository.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects a new booking on a completed contract even inside an open window', async () => {
|
||||
const { service, contractsRepository } = makeService();
|
||||
contractsRepository.findByIdWithRelations.mockResolvedValue(
|
||||
generalContract('CONTRACT_CLOSED'),
|
||||
);
|
||||
jest
|
||||
.spyOn(service, 'computeCapacity')
|
||||
.mockResolvedValue([{ cap: 10, booked: 10, remaining: 0 }]);
|
||||
|
||||
await expect(
|
||||
service.createUnderContract('c-1', {} as never, null, null),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
expect(contractsRepository.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reopens a completed contract when capacity was released', async () => {
|
||||
const { service, contractsRepository } = makeService();
|
||||
contractsRepository.findByIdWithRelations.mockResolvedValue(
|
||||
generalContract('CONTRACT_CLOSED'),
|
||||
);
|
||||
jest
|
||||
.spyOn(service, 'computeCapacity')
|
||||
.mockResolvedValue([{ cap: 10, booked: 8, remaining: 2 }]);
|
||||
|
||||
// The create path continues past the gate and dies later on the bare mocks —
|
||||
// only the reopen transition is under test here.
|
||||
await service.createUnderContract('c-1', {} as never, null, null).catch(() => undefined);
|
||||
|
||||
expect(contractsRepository.update).toHaveBeenCalledWith('c-1', {
|
||||
status: 'CONTRACT_ACTIVE',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user