import { ContractBookingService } from './contract-booking.service'; import { BookingPricingService } from '../bookings/booking-pricing.service'; import type { Contract } from './entities/contract.entity'; import type { ContractRateSnapshot } from './entities/contract-rate-snapshot.entity'; const contract = (over: Partial): Contract => ({ tradeDirection: 'IMPORT', paymentCurrency: 'USD', ...over }) as Contract; /** The private resolver, reached without standing up the whole Nest graph. */ const resolveCurrency = (c: Contract, requested?: string | null): string => ( ContractBookingService.prototype as unknown as { resolveShipmentCurrency: (c: Contract, r?: string | null) => string; } ).resolveShipmentCurrency(c, requested); const snapshot = (currency: string, unitPrice: number): ContractRateSnapshot => ({ rateCode: 'CONTAINER_20FT', currency, unitPrice }) as ContractRateSnapshot; const frozenByCode = ( snap: ContractRateSnapshot | null, bookingCurrency: string, usdToEtb: number, ): ContractRateSnapshot | null => ( BookingPricingService.prototype as unknown as { frozenRateByCode: ( m: Map | null, code: string, bookingCurrency: string, usdToEtb: number, ) => ContractRateSnapshot | null; } ).frozenRateByCode( snap ? new Map([['CONTAINER_20FT', snap]]) : null, 'CONTAINER_20FT', bookingCurrency, usdToEtb, ); describe('per-shipment billing currency', () => { it('takes the customer choice over the contract', () => { expect(resolveCurrency(contract({}), 'ETB')).toBe('ETB'); expect(resolveCurrency(contract({}), 'USD')).toBe('USD'); }); it('falls back to the contract currency when none is chosen', () => { // Grandfathered ETB contract with no explicit choice. expect(resolveCurrency(contract({ paymentCurrency: 'ETB' }))).toBe('ETB'); expect(resolveCurrency(contract({}), ' ')).toBe('USD'); }); it('forces ETB on intercity whatever was requested', () => { const domestic = contract({ tradeDirection: 'DOMESTIC' }); expect(resolveCurrency(domestic, 'USD')).toBe('ETB'); expect(resolveCurrency(domestic)).toBe('ETB'); }); }); describe('frozen contract rate in the booking currency', () => { it('converts a USD snapshot for an ETB booking instead of dropping it', () => { // The old behaviour returned null here, which silently re-priced the // booking at live rates and lost the agreed contract price. expect(frozenByCode(snapshot('USD', 400), 'ETB', 150)?.unitPrice).toBe(60_000); }); it('converts a grandfathered ETB snapshot back for a USD booking', () => { expect(frozenByCode(snapshot('ETB', 60_000), 'USD', 150)?.unitPrice).toBe(400); }); it('passes a matching-currency snapshot through untouched', () => { const snap = snapshot('USD', 400); expect(frozenByCode(snap, 'USD', 1)).toBe(snap); }); it('refuses to price off an unusable exchange rate', () => { // Converting with 0 would zero the whole line. expect(frozenByCode(snapshot('USD', 400), 'ETB', 0)).toBeNull(); expect(frozenByCode(snapshot('USD', 400), 'ETB', Number.NaN)).toBeNull(); }); it('returns null when there is no snapshot', () => { expect(frozenByCode(null, 'ETB', 150)).toBeNull(); }); });