mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
/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
92 lines
4.1 KiB
TypeScript
92 lines
4.1 KiB
TypeScript
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()
|
||
}
|
||
})
|