This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274// SPDX-License-Identifier: AGPL-3.0-or-later
import {describe, expect, test} from 'vitest';import {createUserID} from '../../BrandedTypes';import type {GiftCodeDurationType, GiftCodeRow} from '../../database/types/PaymentTypes';import {addGiftCodeDuration, GiftCode, mapGiftCodeDurationToMonths, mapGiftDurationMonthsToFields} from '../GiftCode';
const TEST_USER_ID = createUserID(1n);const BASE_DATE = new Date('2026-03-01T00:00:00.000Z');const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
function createGiftCodeRow(overrides: Partial<GiftCodeRow> = {}): GiftCodeRow { return { code: 'TESTCODE', duration_months: null, duration_type: 'months', duration_quantity: 1, created_at: BASE_DATE, created_by_user_id: TEST_USER_ID, redeemed_at: null, redeemed_by_user_id: null, stripe_payment_intent_id: null, visionary_sequence_number: null, checkout_session_id: null, version: 1, ...overrides, };}
describe('GiftCode', () => { describe('constructor normalisation', () => { test('prefers duration_type and duration_quantity when present', () => { const giftCode = new GiftCode( createGiftCodeRow({ duration_type: 'weeks', duration_quantity: 2, duration_months: 6, }), ); expect(giftCode.durationType).toBe('weeks'); expect(giftCode.durationQuantity).toBe(2); }); test('falls back to duration_months when duration_type and duration_quantity are absent', () => { const giftCode = new GiftCode( createGiftCodeRow({ duration_type: undefined, duration_quantity: undefined, duration_months: 3, }), ); expect(giftCode.durationType).toBe('months'); expect(giftCode.durationQuantity).toBe(3); }); test('converts 12 duration_months to 1 year when falling back', () => { const giftCode = new GiftCode( createGiftCodeRow({ duration_type: undefined, duration_quantity: undefined, duration_months: 12, }), ); expect(giftCode.durationType).toBe('years'); expect(giftCode.durationQuantity).toBe(1); }); test('converts 24 duration_months to 2 years when falling back', () => { const giftCode = new GiftCode( createGiftCodeRow({ duration_type: undefined, duration_quantity: undefined, duration_months: 24, }), ); expect(giftCode.durationType).toBe('years'); expect(giftCode.durationQuantity).toBe(2); }); test('throws when only duration_type is set without duration_quantity', () => { expect( () => new GiftCode( createGiftCodeRow({ duration_type: 'months', duration_quantity: undefined, duration_months: null, }), ), ).toThrow('Gift code duration_type and duration_quantity must both be set when either is present'); }); test('throws when only duration_quantity is set without duration_type', () => { expect( () => new GiftCode( createGiftCodeRow({ duration_type: undefined, duration_quantity: 3, duration_months: null, }), ), ).toThrow('Gift code duration_type and duration_quantity must both be set when either is present'); }); test('throws when all duration fields are missing', () => { expect( () => new GiftCode( createGiftCodeRow({ duration_type: undefined, duration_quantity: undefined, duration_months: null, }), ), ).toThrow('Gift code duration is missing from both duration_type/duration_quantity and duration_months'); }); test('sets durationMonths to null for days duration type', () => { const giftCode = new GiftCode( createGiftCodeRow({ duration_type: 'days', duration_quantity: 14, }), ); expect(giftCode.durationType).toBe('days'); expect(giftCode.durationQuantity).toBe(14); expect(giftCode.durationMonths).toBeNull(); }); test('sets durationMonths to null for weeks duration type', () => { const giftCode = new GiftCode( createGiftCodeRow({ duration_type: 'weeks', duration_quantity: 2, }), ); expect(giftCode.durationType).toBe('weeks'); expect(giftCode.durationQuantity).toBe(2); expect(giftCode.durationMonths).toBeNull(); }); test('computes durationMonths for months duration type', () => { const giftCode = new GiftCode( createGiftCodeRow({ duration_type: 'months', duration_quantity: 6, }), ); expect(giftCode.durationMonths).toBe(6); }); test('computes durationMonths for years duration type', () => { const giftCode = new GiftCode( createGiftCodeRow({ duration_type: 'years', duration_quantity: 2, }), ); expect(giftCode.durationMonths).toBe(24); }); }); describe('toRow round-trip', () => { test.each<{ durationType: GiftCodeDurationType; durationQuantity: number; }>([ {durationType: 'days', durationQuantity: 7}, {durationType: 'weeks', durationQuantity: 3}, {durationType: 'months', durationQuantity: 6}, {durationType: 'years', durationQuantity: 1}, ])('preserves $durationType/$durationQuantity through toRow', ({durationType, durationQuantity}) => { const original = new GiftCode( createGiftCodeRow({ duration_type: durationType, duration_quantity: durationQuantity, }), ); const row = original.toRow(); const restored = new GiftCode(row); expect(restored.durationType).toBe(durationType); expect(restored.durationQuantity).toBe(durationQuantity); }); });});
describe('mapGiftDurationMonthsToFields', () => { test('converts non-divisible-by-12 months to months type', () => { expect(mapGiftDurationMonthsToFields(3)).toEqual({durationType: 'months', durationQuantity: 3}); expect(mapGiftDurationMonthsToFields(1)).toEqual({durationType: 'months', durationQuantity: 1}); expect(mapGiftDurationMonthsToFields(6)).toEqual({durationType: 'months', durationQuantity: 6}); }); test('converts multiples of 12 months to years type', () => { expect(mapGiftDurationMonthsToFields(12)).toEqual({durationType: 'years', durationQuantity: 1}); expect(mapGiftDurationMonthsToFields(24)).toEqual({durationType: 'years', durationQuantity: 2}); expect(mapGiftDurationMonthsToFields(36)).toEqual({durationType: 'years', durationQuantity: 3}); }); test('converts 0 months to months/0 (lifetime sentinel)', () => { expect(mapGiftDurationMonthsToFields(0)).toEqual({durationType: 'months', durationQuantity: 0}); }); test('rejects negative values', () => { expect(() => mapGiftDurationMonthsToFields(-1)).toThrow('non-negative integer'); }); test('rejects non-integer values', () => { expect(() => mapGiftDurationMonthsToFields(1.5)).toThrow('non-negative integer'); });});
describe('mapGiftCodeDurationToMonths', () => { test('returns the quantity for months type', () => { expect(mapGiftCodeDurationToMonths('months', 3)).toBe(3); expect(mapGiftCodeDurationToMonths('months', 1)).toBe(1); }); test('converts years to months', () => { expect(mapGiftCodeDurationToMonths('years', 1)).toBe(12); expect(mapGiftCodeDurationToMonths('years', 2)).toBe(24); }); test('returns null for days type', () => { expect(mapGiftCodeDurationToMonths('days', 14)).toBeNull(); }); test('returns null for weeks type', () => { expect(mapGiftCodeDurationToMonths('weeks', 2)).toBeNull(); });});
describe('addGiftCodeDuration', () => { test('adds days correctly', () => { const result = addGiftCodeDuration(BASE_DATE, 'days', 14); expect(result).not.toBeNull(); expect(result!.getTime()).toBe(BASE_DATE.getTime() + 14 * MILLISECONDS_PER_DAY); }); test('adds weeks correctly', () => { const result = addGiftCodeDuration(BASE_DATE, 'weeks', 2); expect(result).not.toBeNull(); expect(result!.getTime()).toBe(BASE_DATE.getTime() + 14 * MILLISECONDS_PER_DAY); }); test('adds months correctly', () => { const result = addGiftCodeDuration(BASE_DATE, 'months', 1); expect(result).not.toBeNull(); expect(result!.toISOString()).toBe('2026-04-01T00:00:00.000Z'); }); test('adds years correctly', () => { const result = addGiftCodeDuration(BASE_DATE, 'years', 1); expect(result).not.toBeNull(); expect(result!.toISOString()).toBe('2027-03-01T00:00:00.000Z'); }); test('returns null for zero quantity (lifetime)', () => { expect(addGiftCodeDuration(BASE_DATE, 'months', 0)).toBeNull(); expect(addGiftCodeDuration(BASE_DATE, 'days', 0)).toBeNull(); expect(addGiftCodeDuration(BASE_DATE, 'weeks', 0)).toBeNull(); expect(addGiftCodeDuration(BASE_DATE, 'years', 0)).toBeNull(); }); test('clamps month addition to end of shorter month', () => { const jan31 = new Date('2026-01-31T00:00:00.000Z'); const result = addGiftCodeDuration(jan31, 'months', 1); expect(result).not.toBeNull(); expect(result!.toISOString()).toBe('2026-02-28T00:00:00.000Z'); }); test('2 weeks equals 14 days', () => { const weeksResult = addGiftCodeDuration(BASE_DATE, 'weeks', 2); const daysResult = addGiftCodeDuration(BASE_DATE, 'days', 14); expect(weeksResult).not.toBeNull(); expect(daysResult).not.toBeNull(); expect(weeksResult!.getTime()).toBe(daysResult!.getTime()); }); test('does not mutate the base date when adding months', () => { const baseDate = new Date('2026-01-31T00:00:00.000Z'); addGiftCodeDuration(baseDate, 'months', 1); expect(baseDate.toISOString()).toBe('2026-01-31T00:00:00.000Z'); }); test('clamps year addition from leap day to non-leap year end of february', () => { const leapDay = new Date('2024-02-29T00:00:00.000Z'); const result = addGiftCodeDuration(leapDay, 'years', 1); expect(result).not.toBeNull(); expect(result!.toISOString()).toBe('2025-02-28T00:00:00.000Z'); }); test('throws for negative duration quantity', () => { expect(() => addGiftCodeDuration(BASE_DATE, 'days', -1)).toThrow('non-negative integer'); }); test('throws for non-integer duration quantity', () => { expect(() => addGiftCodeDuration(BASE_DATE, 'weeks', 1.5)).toThrow('non-negative integer'); });});