Files
hanzo.ai/lib/coupon.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

123 lines
5.0 KiB
TypeScript

/**
* A coupon code, as commerce quotes it and charges it.
*
* The quote is `GET /v1/billing/plans?coupon=<code>`: the catalog, with every
* row the code covers annotated — `couponCode`, `couponPercent`,
* `couponFirstCents` (a discounted month's charge), `couponMonths` (how many
* monthly billing cycles it discounts; absent means one) and `couponEnds` — and
* `price`, the list price, untouched. A row the code does not cover says so in
* `couponError` (`not_applicable`); a code commerce refuses outright says why on
* every row (expired, unknown, used up). Commerce bills a code it covers: the
* first `couponMonths` monthly cycles at `couponFirstCents`, every month after
* at the list price. Nothing here prices a charge; it reads the quote.
*
* The code arrives on an offer link (/onboarding?coupon=), rides the sign-up
* back there as `?coupon=`, and opens pay's plans priced with it
* (components/onboarding/flow.tsx), where pay says the reason for a code it
* refuses. It is also kept in this tab, and dropped once commerce refuses it. A
* code inside a checkout's `next` is said on the sign-in page that checkout sent
* the buyer to (components/auth/page.tsx) and rides back to pay inside that
* `next`; a kept code is added to a checkout that names none only for a plan
* commerce quoted it on.
*/
/** A row of the quote, as far as the coupon reads it. */
export interface Row {
slug: string
/** The plan's name, as a buyer reads it. */
name?: string
/** The monthly list price, in cents. */
price: number
couponCode?: string
couponPercent?: number
couponFirstCents?: number
couponMonths?: number
couponError?: string
}
/** What the code does to one plan: its name for the order, the percent, and what it bills for how long. */
export interface Deal {
code: string
percent: number
/** A discounted month's charge, in cents. */
first: number
/** How many monthly billing cycles are charged `first`. */
months: number
/** The monthly list price, in cents, charged from the cycle after them. */
price: number
/** The plan's name, when the row gives one. */
name?: string
}
/** The quote read: the plans the code covers by slug, or commerce's reason for refusing it ('' when there was no answer). */
export type Quote = { deals: Record<string, Deal> } | { refused: string }
/** The shape a code may have. Anything else is not sent, and the picker is drawn as it is without one. */
export const SHAPE = /^[A-Za-z0-9_-]{1,32}$/
/** A code from an address, when it has a code's shape. */
export function clean(raw: string | null | undefined): string | null {
const code = raw?.trim() ?? ''
return SHAPE.test(code) ? code : null
}
/** The months a deal discounts, in words: "your first month", "your first 3 months". */
export const span = (months: number): string => (months > 1 ? `your first ${months} months` : 'your first month')
/** Commerce's reason on a row the code does not cover. */
const NOT_COVERED = 'not_applicable'
/**
* The quote, read: a deal on each plan the code covers, where a row is a deal
* only when it names the code and charges a first month between nothing and its
* list price. A code that covers no plan is refused, for commerce's reason about
* the code (expired, unknown, used up) before its reason about a plan
* (`not_applicable`). Null when the body is not a catalog at all.
*/
export function read(body: unknown): Quote | null {
const wrapped = body as { plans?: unknown; couponError?: unknown } | null
const rows = (Array.isArray(body) ? body : Array.isArray(wrapped?.plans) ? wrapped.plans : null) as Row[] | null
if (!rows) return null
const deals: Record<string, Deal> = {}
for (const r of rows) {
const first = r?.couponFirstCents
if (!r?.couponCode || !(typeof first === 'number' && first > 0 && first < r.price)) continue
const months = Number.isInteger(r.couponMonths) && (r.couponMonths as number) > 1 ? (r.couponMonths as number) : 1
deals[r.slug] = { code: r.couponCode, percent: r.couponPercent ?? 0, first, months, price: r.price, name: r.name }
}
if (Object.keys(deals).length > 0) return { deals }
const reasons = [wrapped?.couponError, ...rows.map((r) => r?.couponError)].filter((e): e is string => typeof e === 'string' && e.trim() !== '')
const refused = reasons.find((e) => e !== NOT_COVERED) ?? reasons[0]
return refused ? { refused } : { deals }
}
/** Where the code is kept in this tab. */
const KEY = 'checkout.coupon'
/** Keep the code for this tab. A browser that refuses storage still has it in the address. */
export function keep(code: string): void {
try {
window.sessionStorage.setItem(KEY, code)
} catch {
/* no store: the address carries it */
}
}
/** The code kept in this tab, if any. */
export function kept(): string | null {
try {
return clean(window.sessionStorage.getItem(KEY))
} catch {
return null
}
}
/** Drop the kept code: commerce refused it. */
export function drop(): void {
try {
window.sessionStorage.removeItem(KEY)
} catch {
/* nothing kept */
}
}