const { normalizeCode, buildCouponLines, computeCouponDiscount, validateCoupon, describeCoupon, } = require('../src/utils/coupons'); const { computeRegistrationTotalDue, staffPriceFor } = require('../src/utils/pricing'); const NOW = new Date('2026-06-15T10:00:00Z'); const PAST = new Date('2026-06-01T00:00:00Z'); const FUTURE = new Date('2026-07-01T00:00:00Z'); const coupon = (over = {}) => ({ id: 'c1', code: 'STAFF50', type: 'percent', value: 50, stackEarlyBird: false, active: true, validFrom: null, validUntil: null, maxUses: null, maxUsesPerUser: 1, minQuantity: null, restrictRoles: [], restrictDomain: null, events: [], options: [], allowedUsers: [], ...over, }); // 2 adult tickets at R200 (standard) and 1 T-shirt at R100. const LINES = [ { eventOptionId: 'adult', variantId: null, quantity: 2, unitPrice: 200, basePrice: 200 }, { eventOptionId: 'shirt', variantId: 'L', quantity: 1, unitPrice: 100, basePrice: 100 }, ]; describe('normalizeCode', () => { test('trims and uppercases', () => { expect(normalizeCode(' staff50 ')).toBe('STAFF50'); expect(normalizeCode(null)).toBe(''); }); }); describe('computeCouponDiscount', () => { test('percentage over the whole event', () => { expect(computeCouponDiscount(coupon(), LINES)).toBe(250); }); test('fixed amount, capped at what is in scope', () => { expect(computeCouponDiscount(coupon({ type: 'fixed', value: 120 }), LINES)).toBe(120); expect(computeCouponDiscount(coupon({ type: 'fixed', value: 9999 }), LINES)).toBe(500); }); test('comp covers only in-scope options', () => { const c = coupon({ type: 'comp', options: [{ eventOptionId: 'adult', variantId: null }] }); expect(computeCouponDiscount(c, LINES)).toBe(400); }); test('scope can target a single variant', () => { const c = coupon({ type: 'comp', options: [{ eventOptionId: 'shirt', variantId: 'M' }] }); expect(computeCouponDiscount(c, LINES)).toBe(0); const c2 = coupon({ type: 'comp', options: [{ eventOptionId: 'shirt', variantId: 'L' }] }); expect(computeCouponDiscount(c2, LINES)).toBe(100); }); test('best price wins against early-bird when not stacking', () => { // Early-bird R150 vs standard R200: 10% off standard = R180, early-bird already beats it. const eb = [{ eventOptionId: 'adult', variantId: null, quantity: 1, unitPrice: 150, basePrice: 200 }]; expect(computeCouponDiscount(coupon({ value: 10 }), eb)).toBe(0); // 50% off standard = R100, beats R150 early-bird by R50. expect(computeCouponDiscount(coupon({ value: 50 }), eb)).toBe(50); // Fixed R80 off standard = R120, beats R150 by R30. expect(computeCouponDiscount(coupon({ type: 'fixed', value: 80 }), eb)).toBe(30); }); test('stacking applies on top of the early-bird price', () => { const eb = [{ eventOptionId: 'adult', variantId: null, quantity: 1, unitPrice: 150, basePrice: 200 }]; expect(computeCouponDiscount(coupon({ value: 10, stackEarlyBird: true }), eb)).toBe(15); expect(computeCouponDiscount(coupon({ type: 'fixed', value: 80, stackEarlyBird: true }), eb)).toBe(80); }); test('never goes below zero or above the price', () => { expect(computeCouponDiscount(coupon({ value: 150 }), LINES)).toBe(500); expect(computeCouponDiscount(coupon({ type: 'fixed', value: -10 }), LINES)).toBe(0); expect(computeCouponDiscount(coupon(), [])).toBe(0); }); }); describe('validateCoupon', () => { const ctx = (over = {}) => ({ eventId: 'e1', user: { id: 'u1', role: 'user', email: 'a@church.org' }, now: NOW, usesTotal: 0, usesByUser: 0, lines: LINES, ...over }); test('accepts an unrestricted active code', () => { expect(validateCoupon(coupon(), ctx())).toEqual({ ok: true }); }); test('rejects inactive, not-yet-valid and expired codes', () => { expect(validateCoupon(coupon({ active: false }), ctx()).ok).toBe(false); expect(validateCoupon(coupon({ validFrom: FUTURE }), ctx()).message).toMatch(/isn't valid yet/); expect(validateCoupon(coupon({ validUntil: PAST }), ctx()).message).toMatch(/expired/); }); test('limits to the coupon events when any are set', () => { expect(validateCoupon(coupon({ events: [{ eventId: 'e2' }] }), ctx()).ok).toBe(false); expect(validateCoupon(coupon({ events: [{ eventId: 'e1' }, { eventId: 'e2' }] }), ctx()).ok).toBe(true); }); test('restricted codes need a known account matching any restriction', () => { const staffOnly = coupon({ restrictRoles: ['staff', 'supervisor', 'admin'] }); expect(validateCoupon(staffOnly, ctx({ user: null })).message).toMatch(/Log in/); expect(validateCoupon(staffOnly, ctx()).ok).toBe(false); expect(validateCoupon(staffOnly, ctx({ user: { id: 'u1', role: 'staff', email: 'x@y.z' } })).ok).toBe(true); expect(validateCoupon(coupon({ restrictDomain: '@church.org' }), ctx()).ok).toBe(true); expect(validateCoupon(coupon({ restrictDomain: 'other.org' }), ctx()).ok).toBe(false); expect(validateCoupon(coupon({ allowedUsers: [{ userId: 'u1' }] }), ctx()).ok).toBe(true); }); test('enforces total and per-user use limits', () => { expect(validateCoupon(coupon({ maxUses: 5 }), ctx({ usesTotal: 5 })).message).toMatch(/fully used/); expect(validateCoupon(coupon({ maxUsesPerUser: 1 }), ctx({ usesByUser: 1 })).message).toMatch(/already used/); expect(validateCoupon(coupon({ maxUsesPerUser: null }), ctx({ usesByUser: 9 })).ok).toBe(true); }); test('needs qualifying items and the minimum quantity', () => { expect(validateCoupon(coupon({ options: [{ eventOptionId: 'other' }] }), ctx()).message).toMatch(/doesn't apply/); expect(validateCoupon(coupon({ minQuantity: 4 }), ctx()).message).toMatch(/at least 4/); expect(validateCoupon(coupon({ minQuantity: 3 }), ctx()).ok).toBe(true); }); }); describe('buildCouponLines', () => { test('one line per tranche; early-bird tranches keep the standard price as base', () => { const reg = { registrationOptions: [{ eventOptionId: 'adult', variantId: null, quantity: 3, eventOption: { price: 200, variants: [] }, tranches: [ { quantity: 2, priceSnapshot: 150, appliedTierId: 'tier1' }, { quantity: 1, priceSnapshot: 200, appliedTierId: null }, ], }], }; expect(buildCouponLines(reg)).toEqual([ { eventOptionId: 'adult', variantId: null, quantity: 2, unitPrice: 150, basePrice: 200 }, { eventOptionId: 'adult', variantId: null, quantity: 1, unitPrice: 200, basePrice: 200 }, ]); }); }); describe('computeRegistrationTotalDue with a discount', () => { const reg = (discountTotal) => ({ discountTotal, registrationOptions: [{ quantity: 2, tranches: [{ quantity: 2, priceSnapshot: 100 }] }], }); test('subtracts the coupon discount', () => { expect(computeRegistrationTotalDue(reg(50), NOW)).toBe(150); }); test('never goes below zero', () => { expect(computeRegistrationTotalDue(reg(500), NOW)).toBe(0); }); }); describe('staffPriceFor', () => { const option = { price: 200, staffPrice: 50, variants: [{ id: 'v1', staffPrice: 20 }, { id: 'v2', staffPrice: null }] }; test('applies to Staff and above only', () => { expect(staffPriceFor(option, null, { role: 'staff' })).toBe(50); expect(staffPriceFor(option, null, { role: 'admin' })).toBe(50); expect(staffPriceFor(option, null, { role: 'user' })).toBeNull(); expect(staffPriceFor(option, null, null)).toBeNull(); }); test('a variant staff price overrides the option one', () => { expect(staffPriceFor(option, 'v1', { role: 'staff' })).toBe(20); expect(staffPriceFor(option, 'v2', { role: 'staff' })).toBe(50); }); }); describe('describeCoupon', () => { test('summarises each type', () => { expect(describeCoupon(coupon())).toBe('50% off'); expect(describeCoupon(coupon({ type: 'fixed', value: 25 }))).toBe('R25.00 off'); expect(describeCoupon(coupon({ type: 'comp' }))).toBe('Free'); }); });