Files
hanzo.ai/e2e/unit/coupon.spec.ts
zeekay 9f02758d25 beta: two questions, then Pro's checkout; the rest after payment
/beta asks two required questions and the second opens pay's card step
for Pro with 50OFFBETA (/pay/confirm/card?plan=dev&coupon=50OFFBETA): no
plan picker, no sign-in first, no referral card in the way. The lead is
filed under the account's address before checkout when signed in (the
token's address until userinfo answers), else on the paid return from the
answers this browser kept. Paid, the page offers the optional questions
(onto the beta record) and then the referral card.

What a code does is commerce's quote: lib/coupon reads couponMonths beside
couponPercent, and /beta, /pricing and the sign-in strip say the months
commerce bills instead of a typed "3 months (90 days)". Until commerce
quotes three, they say the first month, which is what it charges. The
strip under the header says 50% off Pro with no duration.

SLUG_ALIASES goes: every caller passes a catalog slug. A price and its
unit read as a phrase again ("$27 USD/member/month").

Claude-Session: https://claude.ai/code/session_01BLLMyBFSvAfCt7ZGqYuiEz
2026-10-22 00:46:38 -07:00

92 lines
4.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect } from '@playwright/test'
import { clean, read, span } from '../../lib/coupon'
/**
* THE COUPON IS READ FROM COMMERCE'S QUOTE AND NEVER PRICED HERE.
*
* `GET /v1/billing/plans?coupon=<code>` annotates the rows the code covers with
* the first month's charge; a refused code annotates nothing and says why in
* `couponError`. lib/coupon reads that and nothing else: the first month on
* screen is `couponFirstCents`, the list price stays `price`.
*/
const ROWS = [
{ slug: 'free', price: 0 },
{ slug: 'dev', price: 2000 },
{ slug: 'max-5x', price: 10000 },
{ slug: 'max-20x', price: 20000 },
]
const quoted = (code: string) =>
ROWS.map((r) =>
r.price > 0
? { ...r, couponCode: code, couponPercent: 50, couponFirstCents: r.price / 2, couponEnds: '2026-10-12T06:59:59Z' }
: { ...r, couponError: 'not_applicable' },
)
test('a code commerce takes is a deal on each plan it covers, at the first month it quotes', () => {
const q = read(quoted('50OFF'))
expect(q).toEqual({
deals: {
dev: { code: '50OFF', percent: 50, first: 1000, months: 1, price: 2000 },
'max-5x': { code: '50OFF', percent: 50, first: 5000, months: 1, price: 10000 },
'max-20x': { code: '50OFF', percent: 50, first: 10000, months: 1, price: 20000 },
},
})
// The wrapped shape reads the same.
expect(read({ plans: quoted('50OFF') })).toEqual(q)
})
test('a code that discounts several months says how many, and a quote that names none is one month', () => {
const rows = quoted('50OFFBETA').map((r) => (r.slug === 'dev' ? { ...r, couponMonths: 3 } : r))
const q = read(rows)
expect(q && 'deals' in q ? q.deals.dev : null).toEqual({ code: '50OFFBETA', percent: 50, first: 1000, months: 3, price: 2000 })
expect(q && 'deals' in q ? q.deals['max-5x'].months : null).toBe(1)
for (const odd of [0, -2, 1.5, '3', null]) {
const r = read([{ slug: 'dev', price: 2000, couponCode: 'X', couponPercent: 50, couponFirstCents: 1000, couponMonths: odd as number }])
expect(r && 'deals' in r ? r.deals.dev.months : null, String(odd)).toBe(1)
}
expect(span(1)).toBe('your first month')
expect(span(3)).toBe('your first 3 months')
})
test('a refused code is commerce’s reason, on the rows or beside them, and no deal', () => {
expect(read(ROWS.map((r) => ({ ...r, couponError: 'expired' })))).toEqual({ refused: 'expired' })
expect(read({ plans: ROWS, couponError: 'used up' })).toEqual({ refused: 'used up' })
// A code that covers no plan is refused, for its reason about the code before its reason about a plan.
expect(read(ROWS.map((r) => ({ ...r, couponError: 'not_applicable' })))).toEqual({ refused: 'not_applicable' })
expect(read([{ slug: 'free', price: 0, couponError: 'not_applicable' }, ...ROWS.slice(1).map((r) => ({ ...r, couponError: 'exhausted' }))])).toEqual({
refused: 'exhausted',
})
})
test('a plan the code does not cover is left as it is beside the plans it does', () => {
const q = read([...quoted('50OFF'), { slug: 'team', price: 2500, couponError: 'not_applicable' }])
expect(q).toEqual(read(quoted('50OFF')))
})
test('a first month that is nothing, the whole price or more is not a deal', () => {
const rows = [
{ slug: 'dev', price: 2000, couponCode: 'X', couponPercent: 100, couponFirstCents: 0 },
{ slug: 'max-5x', price: 10000, couponCode: 'X', couponPercent: 0, couponFirstCents: 10000 },
{ slug: 'max-20x', price: 20000, couponCode: 'X', couponPercent: 50, couponFirstCents: 25000 },
{ slug: 'team', price: 2500, couponFirstCents: 1250 },
]
expect(read(rows)).toEqual({ deals: {} })
expect(read(ROWS)).toEqual({ deals: {} })
})
test('a body that is not a catalog is no quote', () => {
expect(read(null)).toBeNull()
expect(read({ error: 'nope' })).toBeNull()
expect(read('[]')).toBeNull()
})
test('a code is sent only when it has a code’s shape', () => {
expect(clean(' 50OFF ')).toBe('50OFF')
expect(clean('summer_2026-a')).toBe('summer_2026-a')
for (const bad of [null, undefined, '', ' ', 'YOUR ACCOUNT IS LOCKED', '<b>x</b>', 'a'.repeat(33), '50%OFF']) {
expect(clean(bad), String(bad)).toBeNull()
}
})