Files
edr-platform/apps/edr-freight-api/src/modules/train-scheduling/booking-split.service.spec.ts
Marshal acb6f286d4 fix(payments): reconcile with gateway before expiring unpaid holds
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.
2026-07-30 12:01:59 +00:00

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();
});
});