manicanldes-backend/test/checkout-policy.spec.ts

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);
});
});