const { getEffectiveUnitPrice, computeOptionLineTotal, computeRegistrationTotalDue, attachComputedTotals, } = require('../src/utils/pricing'); const DAY = 24 * 60 * 60 * 1000; const NOW = new Date('2026-06-01T00:00:00Z'); const PAST = new Date(NOW.getTime() - DAY); const FUTURE = new Date(NOW.getTime() + DAY); describe('getEffectiveUnitPrice', () => { test('returns base price when there are no early-bird tiers', () => { const option = { price: 100, earlyBirdTiers: [] }; expect(getEffectiveUnitPrice(option, null, NOW)).toBe(100); }); test('returns tier price when the tier deadline is still in the future', () => { const option = { price: 100, earlyBirdTiers: [{ id: 't1', price: 50, deadline: FUTURE }], }; expect(getEffectiveUnitPrice(option, null, NOW)).toBe(50); }); test('falls back to base price once the tier deadline has passed', () => { const option = { price: 100, earlyBirdTiers: [{ id: 't1', price: 50, deadline: PAST }], }; expect(getEffectiveUnitPrice(option, null, NOW)).toBe(100); }); test('requires the deadline to be after both referenceTime and atTime', () => { const midDeadline = new Date(NOW.getTime() - DAY / 2); const option = { price: 100, earlyBirdTiers: [{ id: 't1', price: 50, deadline: midDeadline }], }; // referenceTime (PAST) is before the deadline, but atTime (NOW) is after it — tier no longer applies expect(getEffectiveUnitPrice(option, PAST, NOW)).toBe(100); }); test('with multiple applicable tiers, picks the one with the earliest deadline', () => { const soonerDeadline = new Date(NOW.getTime() + DAY); const laterDeadline = new Date(NOW.getTime() + 2 * DAY); const option = { price: 100, earlyBirdTiers: [ { id: 'later', price: 80, deadline: laterDeadline }, { id: 'sooner', price: 60, deadline: soonerDeadline }, ], }; expect(getEffectiveUnitPrice(option, null, NOW)).toBe(60); }); test('returns 0 for a missing eventOption', () => { expect(getEffectiveUnitPrice(null, null, NOW)).toBe(0); }); }); describe('computeOptionLineTotal', () => { test('the 1.8.0 regression: buying more after an early-bird tier expires only re-prices the new quantity', () => { // 5 tickets bought at R50 (early-bird), then 1 more bought after the price rose to R100. // Must total 5*50 + 1*100 = 350, not 6*100 = 600. const ro = { quantity: 6, priceSnapshot: 100, tranches: [ { quantity: 5, priceSnapshot: 50, createdAt: PAST }, { quantity: 1, priceSnapshot: 100, createdAt: NOW }, ], }; expect(computeOptionLineTotal(ro, null, NOW)).toBe(350); }); test('sums a single tranche correctly', () => { const ro = { quantity: 3, priceSnapshot: 40, tranches: [{ quantity: 3, priceSnapshot: 40 }] }; expect(computeOptionLineTotal(ro, null, NOW)).toBe(120); }); test('legacy fallback: no tranches, uses priceSnapshot directly', () => { const ro = { quantity: 4, priceSnapshot: 25, tranches: [] }; expect(computeOptionLineTotal(ro, null, NOW)).toBe(100); }); test('legacy fallback: no tranches and no priceSnapshot, re-evaluates from tier deadlines', () => { const ro = { quantity: 2, priceSnapshot: null, tranches: [], eventOption: { price: 100, earlyBirdTiers: [{ id: 't1', price: 70, deadline: FUTURE }] }, }; expect(computeOptionLineTotal(ro, null, NOW)).toBe(140); }); }); describe('computeRegistrationTotalDue', () => { test('sums tranche-aware totals across multiple RegistrationOptions', () => { const registration = { registrationOptions: [ { quantity: 6, priceSnapshot: 100, tranches: [ { quantity: 5, priceSnapshot: 50, createdAt: PAST }, { quantity: 1, priceSnapshot: 100, createdAt: NOW }, ], }, { quantity: 2, priceSnapshot: 20, tranches: [{ quantity: 2, priceSnapshot: 20, createdAt: PAST }], }, ], payments: [], }; // 350 (first option, see 1.8.0 regression case) + 40 (second option) expect(computeRegistrationTotalDue(registration, NOW)).toBe(390); }); test('returns 0 for a registration with no options', () => { expect(computeRegistrationTotalDue({ registrationOptions: [] }, NOW)).toBe(0); expect(computeRegistrationTotalDue(null, NOW)).toBe(0); }); }); describe('attachComputedTotals', () => { test('mutates the registration with per-line and total computed amounts', () => { const registration = { registrationOptions: [ { quantity: 2, priceSnapshot: 50, tranches: [{ quantity: 2, priceSnapshot: 50 }] }, ], payments: [], }; const result = attachComputedTotals(registration); expect(result).toBe(registration); // mutated in place expect(result.registrationOptions[0].lineTotal).toBe(100); expect(result.totalDueComputed).toBe(100); }); test('handles a null registration gracefully', () => { expect(attachComputedTotals(null)).toBeNull(); }); });