95 lines
3.5 KiB
TypeScript
95 lines
3.5 KiB
TypeScript
import { decimal, minor, discountFor } from '../src/checkout/money';
|
|
import { couponSchema } from '../src/coupons/coupon.schema';
|
|
import {
|
|
cartLineSchema,
|
|
checkoutSchema,
|
|
} from '../src/checkout/checkout.schemas';
|
|
import { assertCoupon } from '../src/coupons/coupon-policy';
|
|
import { Prisma, type Coupon } from '../src/generated/prisma/client';
|
|
import { couponInput } from './helpers/checkout';
|
|
|
|
describe('checkout money and eligibility policies', () => {
|
|
it('uses exact minor units and round-half-up percentage discounts', () => {
|
|
expect(minor('0.10') + minor('0.20')).toBe(30n);
|
|
expect(minor('499')).toBe(49900n);
|
|
expect(minor('1.5')).toBe(150n);
|
|
expect(decimal(1n)).toBe('0.01');
|
|
expect(
|
|
discountFor(101n, { kind: 'PERCENT', percentBps: 5000, amount: null }),
|
|
).toBe(51n);
|
|
expect(
|
|
discountFor(100n, { kind: 'FIXED', amount: '5.00', percentBps: null }),
|
|
).toBe(100n);
|
|
expect(
|
|
discountFor(1000n, { kind: 'FIXED', amount: '1.50', percentBps: null }),
|
|
).toBe(150n);
|
|
expect(decimal(9999999999999999n)).toBe('99999999999999.99');
|
|
for (const value of ['-1', '0.001', 'NaN', '1e3'])
|
|
expect(() => minor(value)).toThrow();
|
|
expect(() => decimal(-1n)).toThrow();
|
|
expect(() => decimal(10000000000000000n)).toThrow();
|
|
});
|
|
it('validates coupon dates, currency, amount, quotas and mutually exclusive rules', () => {
|
|
const input = couponInput();
|
|
expect(couponSchema.parse(input).code).toBe(input.code.toUpperCase());
|
|
for (const change of [
|
|
{ endsAt: input.startsAt },
|
|
{ perUserLimit: 11 },
|
|
{ percentBps: 10001 },
|
|
{ amount: '2.00' },
|
|
{ currency: 'XXX' },
|
|
{ code: '<script>' },
|
|
])
|
|
expect(couponSchema.safeParse({ ...input, ...change }).success).toBe(
|
|
false,
|
|
);
|
|
const { percentBps: _percent, ...fixed } = input;
|
|
expect(
|
|
couponSchema.safeParse({ ...fixed, kind: 'FIXED', amount: '2.00' })
|
|
.success,
|
|
).toBe(true);
|
|
expect(
|
|
couponSchema.safeParse({ ...fixed, kind: 'FIXED', amount: '0.00' })
|
|
.success,
|
|
).toBe(false);
|
|
});
|
|
it('rejects every coupon eligibility boundary', () => {
|
|
const now = new Date();
|
|
const coupon = {
|
|
...couponInput(),
|
|
id: 'id',
|
|
organizationId: 'org',
|
|
kind: 'PERCENT',
|
|
startsAt: new Date(now.getTime() - 1000),
|
|
endsAt: new Date(now.getTime() + 1000),
|
|
active: true,
|
|
amount: null,
|
|
minimumSubtotal: new Prisma.Decimal(100),
|
|
} as Coupon;
|
|
expect(() => assertCoupon(coupon, 'INR', 10000n, now, 0, 0)).not.toThrow();
|
|
for (const invalid of [
|
|
null,
|
|
{ ...coupon, active: false },
|
|
{ ...coupon, currency: 'USD' },
|
|
{ ...coupon, startsAt: new Date(now.getTime() + 1) },
|
|
{ ...coupon, endsAt: now },
|
|
])
|
|
expect(() => assertCoupon(invalid, 'INR', 10000n, now, 0, 0)).toThrow();
|
|
expect(() => assertCoupon(coupon, 'INR', 9999n, now, 0, 0)).toThrow();
|
|
expect(() => assertCoupon(coupon, 'INR', 10000n, now, 10, 0)).toThrow();
|
|
expect(() => assertCoupon(coupon, 'INR', 10000n, now, 0, 1)).toThrow();
|
|
});
|
|
it('bounds carts and rejects client totals and unknown fields', () => {
|
|
for (const quantity of [0, -1, 101, 0.5])
|
|
expect(cartLineSchema.safeParse({ quantity, version: 0 }).success).toBe(
|
|
false,
|
|
);
|
|
expect(cartLineSchema.safeParse({ quantity: 1, version: -1 }).success).toBe(
|
|
false,
|
|
);
|
|
expect(
|
|
checkoutSchema.safeParse({ cartVersion: 0, total: '0.00' }).success,
|
|
).toBe(false);
|
|
});
|
|
});
|