Files
hanzo.ai/e2e/unit/tier.spec.ts
zeekay 6c65e3e941 @hanzo/app 0.2.0: the app SDK, renamed from @hanzo/rooms
Every import, the catalog pin and the docs name @hanzo/app and
github.com/hanzoai/app. lib/rooms.ts is lib/app.ts and
components/rooms.tsx is components/app.tsx.
2026-10-22 19:07:20 -07:00

165 lines
6.2 KiB
TypeScript

import { test, expect } from '@playwright/test'
import { readSubscription, readTier, renewal } from '@hanzo/app/lib/tier'
import { label, said } from '@hanzo/build/plan'
/**
* WHAT THE SIDEBAR SAYS IS LEFT IS WHAT THE LEDGER SAYS.
*
* `/v1/billing/tier` names the plan; its `effectiveAvailable` comes from
* commerce's store, which a usage debit never reaches, and carries a $1,000
* floor for the ecosystem orgs. `/v1/billing/balance` reads the ledger the gate
* spends from. `readTier` states the money from the second read, and a balance
* it could not read is a refusal, never a number.
*/
function bearer(claims: Record<string, unknown>): string {
return `header.${Buffer.from(JSON.stringify(claims)).toString('base64url')}.signature`
}
const tier = {
user: 'acme',
tier: { name: 'pro', displayName: 'Pro', dailyCreditsCents: 0, maxAgents: 10, unlimitedAgents: false },
balance: { currency: 'usd', prepaidAvailable: 2000, creditsRemaining: 0, dailyRemaining: 0, effectiveAvailable: 2000 },
}
/** A browser signed in to `acme`, answering each path from `answers`. */
function world(answers: Record<string, { status: number; body: unknown }>): { asked: string[]; done: () => void } {
const global = globalThis as { window?: unknown; fetch: typeof fetch }
const hadWindow = 'window' in global
const wasWindow = global.window
const wasFetch = global.fetch
const store = new Map([['hanzo_iam_access_token', bearer({ owner: 'hanzo', orgs: [{ org: 'acme' }] })]])
global.window = {
location: { hostname: 'hanzo.ai', origin: 'https://hanzo.ai' },
localStorage: { getItem: (k: string) => store.get(k) ?? null },
}
const asked: string[] = []
global.fetch = (async (input: string | URL, init?: RequestInit) => {
const url = new URL(String(input))
asked.push(`${url.pathname} ${new Headers(init?.headers).get('X-Org-Id')}`)
const a = answers[url.pathname]
return new Response(JSON.stringify(a?.body ?? {}), { status: a?.status ?? 404 })
}) as typeof fetch
return {
asked,
done: () => {
global.fetch = wasFetch
if (hadWindow) global.window = wasWindow
else delete global.window
},
}
}
test('the money is the ledger balance, in the organization this browser works in', async () => {
const w = world({
'/v1/billing/tier': { status: 200, body: tier },
'/v1/billing/balance': { status: 200, body: { balance: 1234, holds: 0, available: 1234, account: 'acme' } },
})
try {
const got = await readTier()
expect(got.tier.displayName).toBe('Pro')
expect(got.balance.effectiveAvailable).toBe(1234)
expect(w.asked.sort()).toEqual(['/v1/billing/balance acme', '/v1/billing/tier acme'])
} finally {
w.done()
}
})
test('the plan slug on GET /v1/billing/tier passes through and names the catalog row', async () => {
const w = world({
'/v1/billing/tier': { status: 200, body: { ...tier, plan: 'max-20x' } },
'/v1/billing/balance': { status: 200, body: { balance: 1234, holds: 0, available: 1234, account: 'acme' } },
})
try {
const got = await readTier()
expect(got.plan).toBe('max-20x')
expect(said(label(got.plan))).toBe('Max 20x')
} finally {
w.done()
}
})
test('an account with no plan slug names none, and the reader falls back to the tier', async () => {
expect(label('')).toBeNull()
expect(label(undefined)).toBeNull()
expect(said(label(''))).toBe('')
})
test('a balance that could not be read is a refusal, not a figure', async () => {
const w = world({
'/v1/billing/tier': { status: 200, body: tier },
'/v1/billing/balance': { status: 502, body: { detail: 'billing upstream unreachable' } },
})
try {
await expect(readTier()).rejects.toMatchObject({ status: 502, message: 'billing upstream unreachable' })
} finally {
w.done()
}
})
test('the plan is the one billing answered, whatever the organization is called', async () => {
const w = world({
'/v1/billing/tier': { status: 200, body: { ...tier, user: 'hanzo', tier: { ...tier.tier, name: 'free', displayName: 'Free' } } },
'/v1/billing/balance': { status: 200, body: { balance: 0, holds: 0, available: 0, account: 'hanzo' } },
})
try {
const got = await readTier()
expect(got.tier.name).toBe('free')
expect(got.tier.displayName).toBe('Free')
expect(got.tier.unlimitedAgents).toBe(false)
} finally {
w.done()
}
})
/**
* WHAT A PLAN COSTS IS ITS SUBSCRIPTION, NOT THE BALANCE.
*
* GET /v1/billing/subscriptions lists the org's rows; the one that is live is
* the plan's term: its price per seat times its seats, by its interval, and the
* day the period ends — renewed, or ended where it was cancelled.
*/
const sub = {
id: 'sub_1',
status: 'active',
quantity: 1,
currentPeriodEnd: '2026-10-30T00:00:00Z',
cancelAtPeriodEnd: false,
plan: { id: 'max-20x', name: 'Max 20x', price: 20000, currency: 'usd', interval: 'month' },
}
test('the live subscription is the plan, priced by its seats and renewed at its period end', async () => {
const w = world({
'/v1/billing/subscriptions': {
status: 200,
body: { subscriptions: [{ ...sub, id: 'old', status: 'canceled' }, sub], count: 2 },
},
})
try {
const got = await readSubscription()
expect(got?.id).toBe('sub_1')
expect(renewal(got!)).toBe('$200/month subscription, renews Oct 30, 2026')
expect(w.asked).toEqual(['/v1/billing/subscriptions acme'])
} finally {
w.done()
}
expect(renewal({ ...sub, quantity: 3, plan: { ...sub.plan, price: 3000 } })).toBe('$90/month subscription, renews Oct 30, 2026')
expect(renewal({ ...sub, plan: { ...sub.plan, price: 200000, interval: 'year' } })).toBe('$2,000/year subscription, renews Oct 30, 2026')
expect(renewal({ ...sub, cancelAtPeriodEnd: true })).toBe('$200/month subscription, ends Oct 30, 2026')
})
test('an org on no plan has no subscription, and a refusal is not an empty list', async () => {
const none = world({ '/v1/billing/subscriptions': { status: 200, body: { subscriptions: [], count: 0 } } })
try {
expect(await readSubscription()).toBeNull()
} finally {
none.done()
}
const down = world({ '/v1/billing/subscriptions': { status: 502, body: { detail: 'commerce unreachable' } } })
try {
await expect(readSubscription()).rejects.toMatchObject({ status: 502 })
} finally {
down.done()
}
})