mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Replace the fixed 5-minute grace with a settlement check at expiry: expire() calls the payment API's reconcile endpoint — paid intents are kept and allocated via payment.succeeded, unverifiable results defer expiry to the next tick, only verifiably unpaid holds expire.
122 lines
4.0 KiB
TypeScript
122 lines
4.0 KiB
TypeScript
import { BookingSplitService } from './booking-split.service';
|
|
import { Booking } from '../bookings/entities/booking.entity';
|
|
import { BookingContainer } from '../bookings/entities/booking-container.entity';
|
|
import { BookingContainerUnit } from '../bookings/entities/booking-container-unit.entity';
|
|
import { Contract } from '../contracts/entities/contract.entity';
|
|
import { BookingBatchOffer } from './entities/booking-batch-offer.entity';
|
|
|
|
/**
|
|
* applySplit split-marking behaviour: the reduced booking is flagged is_split
|
|
* and keeps a pre_split_quantities snapshot (the remainder ledger for ONE_TIME
|
|
* contracts). The contract kind is NEVER changed — a ONE_TIME contract stays
|
|
* ONE_TIME through the split chain.
|
|
*/
|
|
describe('BookingSplitService — applySplit split marking', () => {
|
|
const bookingId = 'bk-1';
|
|
const contractId = 'ct-1';
|
|
const offerId = 'of-1';
|
|
|
|
const buildService = (bookingContractKind: 'ONE_TIME' | 'GENERAL') => {
|
|
const offer = {
|
|
id: offerId,
|
|
bookingId,
|
|
status: 'OFFERED',
|
|
offeredWagons: 3,
|
|
totalWagons: 5,
|
|
offeredWeightTons: 30,
|
|
offeredAmount: 300,
|
|
offeredPricingBreakdown: {},
|
|
offeredLines: null,
|
|
} as unknown as BookingBatchOffer;
|
|
|
|
const bookingRepo = {
|
|
update: jest.fn().mockResolvedValue(undefined),
|
|
findOne: jest.fn().mockResolvedValue({
|
|
id: bookingId,
|
|
contractId,
|
|
contractKind: bookingContractKind,
|
|
cargoTotalWeightVgm: 50,
|
|
}),
|
|
find: jest.fn().mockResolvedValue([]),
|
|
softDelete: jest.fn().mockResolvedValue(undefined),
|
|
};
|
|
const contractRepo = { update: jest.fn().mockResolvedValue(undefined) };
|
|
const offerRepo = {
|
|
findOne: jest.fn().mockResolvedValue(offer),
|
|
update: jest.fn().mockResolvedValue(undefined),
|
|
};
|
|
const containerRepo = {
|
|
find: jest.fn().mockResolvedValue([]),
|
|
update: jest.fn(),
|
|
softDelete: jest.fn(),
|
|
};
|
|
const unitRepo = { find: jest.fn().mockResolvedValue([]), softDelete: jest.fn() };
|
|
|
|
const repoFor = (entity: unknown) => {
|
|
if (entity === Booking) return bookingRepo;
|
|
if (entity === Contract) return contractRepo;
|
|
if (entity === BookingBatchOffer) return offerRepo;
|
|
if (entity === BookingContainer) return containerRepo;
|
|
if (entity === BookingContainerUnit) return unitRepo;
|
|
return { find: jest.fn().mockResolvedValue([]), update: jest.fn() };
|
|
};
|
|
|
|
const dataSource = {
|
|
getRepository: jest.fn(repoFor),
|
|
transaction: jest.fn(async (fn: (m: unknown) => Promise<void>) => {
|
|
await fn({ getRepository: repoFor });
|
|
}),
|
|
};
|
|
|
|
const service = new BookingSplitService(
|
|
dataSource as never,
|
|
{} as never,
|
|
{} as never,
|
|
{
|
|
expirePayable: jest.fn(),
|
|
reconcilePayable: jest
|
|
.fn()
|
|
.mockResolvedValue({ paid: false, unverifiable: false }),
|
|
} as never,
|
|
{ payNowPartial: jest.fn() } as never,
|
|
);
|
|
return { service, bookingRepo, contractRepo };
|
|
};
|
|
|
|
it('flags the reduced booking is_split with a pre-split bulk snapshot', async () => {
|
|
const { service, bookingRepo } = buildService('ONE_TIME');
|
|
|
|
await service.applySplit(bookingId);
|
|
|
|
expect(bookingRepo.update).toHaveBeenCalledWith(
|
|
bookingId,
|
|
expect.objectContaining({
|
|
isSplit: true,
|
|
preSplitQuantities: { bulkTons: 50 },
|
|
cargoTotalWeightVgm: 30,
|
|
wagonsRequired: 3,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('never changes the contract kind — ONE_TIME stays ONE_TIME', async () => {
|
|
const { service, bookingRepo, contractRepo } = buildService('ONE_TIME');
|
|
|
|
await service.applySplit(bookingId);
|
|
|
|
expect(contractRepo.update).not.toHaveBeenCalled();
|
|
expect(bookingRepo.update).not.toHaveBeenCalledWith(
|
|
bookingId,
|
|
expect.objectContaining({ contractKind: expect.anything() }),
|
|
);
|
|
});
|
|
|
|
it('leaves a GENERAL contract untouched too', async () => {
|
|
const { service, contractRepo } = buildService('GENERAL');
|
|
|
|
await service.applySplit(bookingId);
|
|
|
|
expect(contractRepo.update).not.toHaveBeenCalled();
|
|
});
|
|
});
|