import { nanoid } from 'nanoid' import Stripe from 'stripe' import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' import * as ApiKeys from '../../../ApiKeys.js' import * as TestApp from '../../../../test/App.js' import * as TestStripe from '../../../../test/Stripe.js' import * as EnabledBillingSources from '../../../db/tables/enabledBillingSources.js' import * as Memberships from '../../../db/tables/memberships.js' import * as SponsoredTransactions from '../../../db/tables/sponsoredTransactions.js' import * as StripeCustomers from '../../../db/tables/stripeCustomers.js' import * as Viem from '../../../internal/Viem.js' import * as core_Billing from '../Billing.js' import type * as App from '../../../App.js' /** Origin pinned for SIWE domain binding; Hono test requests use this host. */ const origin = 'http://localhost' /** Super admin secret configured on the test app. */ const secret = 'tempo:sk:b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1' /** Webhook signing secret configured on the test app. */ const webhookSecret = `whsec_${'b'.repeat(32)}` /** Distinct webhook signing secret for the test app's sandbox source. */ const sandboxWebhookSecret = `whsec_${'d'.repeat(32)}` afterAll(() => TestStripe.sweep()) /** * Real Stripe SDK pointed at an unreachable host — nothing is faked. Guarded * paths never issue a request; the paths that do observe a real network * failure (mapped to the upstream error envelope). Stripe-flow coverage lives * in the sandbox-gated suites below. */ function billing(): App.Billing { return { stripe: { client: new Stripe(`sk_test_${'a'.repeat(24)}`, { host: '127.0.0.1', maxNetworkRetries: 0, port: 9, protocol: 'http', timeout: 2_000, }), webhookSecret, }, } } /** * Adds a sandbox source (same unreachable client) to the base capability. Reads * and writes never touch Stripe, so an unreachable client suffices for the * DB-only environment-isolation and webhook-verification suites. */ function billingWithSandbox(): App.Billing { const base = billing() return { stripe: { ...base.stripe, sandbox: { client: base.stripe.client, webhookSecret: sandboxWebhookSecret } }, // prettier-ignore } } function createApp( options: { billing?: App.Billing | undefined db?: TestApp.create.Options['db'] kv?: { store: ReturnType } } = {}, ) { // prettier-ignore return TestApp.create({ auth: { superAdmin: { secret } }, billing: options.billing ?? billing(), db: options.db ?? TestApp.database(), ...(options.kv ? { kv: options.kv } : {}), session: { wallet: { origin } }, }) } test('publishes generator-ready OpenAPI contracts', async () => { const spec = await (await createApp().request('/openapi.json')).json() const billing = spec.paths['/v1/orgs/{orgId}/billing'] const checkout = spec.paths['/v1/orgs/{orgId}/billing/stripe/checkout'].post const manage = spec.paths['/v1/orgs/{orgId}/billing/stripe/manage'].post const methods = spec.paths['/v1/orgs/{orgId}/billing/payment-methods'].get const remove = spec.paths['/v1/orgs/{orgId}/billing/payment-methods/{methodId}'].delete expect({ checkout: { errors: { 400: checkout.responses[400].content['application/json'].schema, 403: checkout.responses[403].content['application/json'].schema, 404: checkout.responses[404].content['application/json'].schema, 501: checkout.responses[501].content['application/json'].schema, }, operationId: checkout.operationId, response: checkout.responses[200].content['application/json'].schema, }, components: [ 'Billing', 'BillingPaymentMethod', 'BillingPaymentMethodList', 'BillingSession', 'BillingSpendLimit', 'BillingTransactionFeeLimit', 'UpdateBillingRequest', ].filter((name) => spec.components.schemas[name]), get: { errors: { 400: billing.get.responses[400].content['application/json'].schema, 404: billing.get.responses[404].content['application/json'].schema, }, operationId: billing.get.operationId, response: billing.get.responses[200].content['application/json'].schema, }, manage: { errors: { 400: manage.responses[400].content['application/json'].schema, 403: manage.responses[403].content['application/json'].schema, 404: manage.responses[404].content['application/json'].schema, 501: manage.responses[501].content['application/json'].schema, }, operationId: manage.operationId, response: manage.responses[200].content['application/json'].schema, }, methods: { errors: { 400: methods.responses[400].content['application/json'].schema, 404: methods.responses[404].content['application/json'].schema, 501: methods.responses[501].content['application/json'].schema, }, operationId: methods.operationId, response: methods.responses[200].content['application/json'].schema, }, remove: { errors: { 400: remove.responses[400].content['application/json'].schema, 403: remove.responses[403].content['application/json'].schema, 404: remove.responses[404].content['application/json'].schema, 501: remove.responses[501].content['application/json'].schema, }, operationId: remove.operationId, response: remove.responses[200].content['application/json'].schema, }, timestamps: { billing: spec.components.schemas.Billing.properties.updatedAt.format, paymentMethod: spec.components.schemas.BillingPaymentMethod.properties.createdAt.format, }, update: { errors: { 400: billing.patch.responses[400].content['application/json'].schema, 403: billing.patch.responses[403].content['application/json'].schema, 404: billing.patch.responses[404].content['application/json'].schema, }, operationId: billing.patch.operationId, request: billing.patch.requestBody.content['application/json'].schema, response: billing.patch.responses[200].content['application/json'].schema, }, }).toMatchInlineSnapshot(` { "checkout": { "errors": { "400": { "$ref": "#/components/schemas/ApiKeyMalformedOrParamInvalidOrQueryInvalidError", }, "403": { "$ref": "#/components/schemas/ApiKeyForbiddenOrApiKeyIpForbiddenOrBillingSourceDisabledOrForbiddenError", }, "404": { "$ref": "#/components/schemas/OrganizationNotFoundError", }, "501": { "$ref": "#/components/schemas/NotImplementedError", }, }, "operationId": "createStripeCheckout", "response": { "$ref": "#/components/schemas/BillingSession", }, }, "components": [ "Billing", "BillingPaymentMethod", "BillingPaymentMethodList", "BillingSession", "BillingSpendLimit", "BillingTransactionFeeLimit", "UpdateBillingRequest", ], "get": { "errors": { "400": { "$ref": "#/components/schemas/ApiKeyMalformedOrParamInvalidOrQueryInvalidError", }, "404": { "$ref": "#/components/schemas/OrganizationNotFoundError", }, }, "operationId": "getBilling", "response": { "$ref": "#/components/schemas/Billing", }, }, "manage": { "errors": { "400": { "$ref": "#/components/schemas/ApiKeyMalformedOrParamInvalidOrQueryInvalidError", }, "403": { "$ref": "#/components/schemas/ForbiddenError", }, "404": { "$ref": "#/components/schemas/BillingNotFoundOrOrganizationNotFoundError", }, "501": { "$ref": "#/components/schemas/NotImplementedError", }, }, "operationId": "createStripeManage", "response": { "$ref": "#/components/schemas/BillingSession", }, }, "methods": { "errors": { "400": { "$ref": "#/components/schemas/ApiKeyMalformedOrParamInvalidOrQueryInvalidError", }, "404": { "$ref": "#/components/schemas/OrganizationNotFoundError", }, "501": { "$ref": "#/components/schemas/NotImplementedError", }, }, "operationId": "getBillingPaymentMethods", "response": { "$ref": "#/components/schemas/BillingPaymentMethodList", }, }, "remove": { "errors": { "400": { "$ref": "#/components/schemas/ApiKeyMalformedOrParamInvalidOrQueryInvalidError", }, "403": { "$ref": "#/components/schemas/ForbiddenError", }, "404": { "$ref": "#/components/schemas/BillingNotFoundOrOrganizationNotFoundOrPaymentMethodNotFoundError", }, "501": { "$ref": "#/components/schemas/NotImplementedError", }, }, "operationId": "deleteBillingPaymentMethod", "response": { "$ref": "#/components/schemas/Billing", }, }, "timestamps": { "billing": "date-time", "paymentMethod": "date-time", }, "update": { "errors": { "400": { "$ref": "#/components/schemas/ApiKeyMalformedOrBodyInvalidOrParamInvalidOrQueryInvalidError", }, "403": { "$ref": "#/components/schemas/ForbiddenError", }, "404": { "$ref": "#/components/schemas/OrganizationNotFoundError", }, }, "operationId": "updateBilling", "request": { "$ref": "#/components/schemas/UpdateBillingRequest", }, "response": { "$ref": "#/components/schemas/Billing", }, }, } `) }) /** RequestInit presenting the given token. */ function as(token: string) { return { headers: { 'tempo-api-key': token } } as const } /** Signs in a fresh scripted account; returns its session cookie. */ async function session(app: TestApp.signIn.App) { const account = privateKeyToAccount(generatePrivateKey()) const { cookie } = await TestApp.signIn(app, account) return cookie! } /** * Signs in and creates an org owned by that session; returns the cookie and * org id. Org names are unique so sandbox customers are tellable apart in the * Stripe dashboard. */ async function seeded(app: ReturnType) { const cookie = await session(app) const response = await app.request('/v1/orgs', { body: JSON.stringify({ name: `Test Org ${nanoid(10)}` }), headers: { 'content-type': 'application/json', cookie }, method: 'POST', }) const { id } = (await response.json()) as { id: string } return { cookie, orgId: id } } /** Seeds an owned organization with Stripe setup enabled. */ async function seededWithStripe( app: ReturnType, db: ReturnType, ) { const result = await seeded(app) await EnabledBillingSources.add(db, { createdBy: 'test', orgId: result.orgId, source: 'stripe', }) return result } describe('GET /orgs/:orgId/billing', () => { test('behavior: synthesizes none for orgs without a billing source', async () => { const app = createApp() const { cookie, orgId } = await seeded(app) const response = await app.request(`/v1/orgs/${orgId}/billing`, { headers: { cookie } }) expect(response.status).toBe(200) expect(await response.json()).toMatchInlineSnapshot(` { "enabledSources": [], "spend": { "amount": "0", "currency": "usd", "period": "month", }, "status": "none", } `) }) test('behavior: reflects the stored status', async () => { const db = TestApp.database() const app = createApp({ db }) const { cookie, orgId } = await seeded(app) await StripeCustomers.create(db, { orgId, stripeCustomerId: 'cus_a' }) await StripeCustomers.setStatus(db, orgId, 'active') const response = await app.request(`/v1/orgs/${orgId}/billing`, { headers: { cookie } }) expect(response.status).toBe(200) const body = (await response.json()) as { status: string; updatedAt: string } expect(body.status).toMatchInlineSnapshot(`"active"`) expect(body.updatedAt).toBeDefined() }) test('behavior: lists organization-wide sources in canonical order', async () => { const db = TestApp.database() const app = createApp({ billing: billingWithSandbox(), db }) const { cookie, orgId } = await seeded(app) await EnabledBillingSources.add(db, { createdBy: 'test', orgId, source: 'tempo' }) await EnabledBillingSources.add(db, { createdBy: 'test', orgId, source: 'stripe' }) const production = await app.request(`/v1/orgs/${orgId}/billing`, { headers: { cookie } }) const sandbox = await app.request(`/v1/orgs/${orgId}/billing?environment=sandbox`, { headers: { cookie }, }) expect(await production.json()).toMatchObject({ enabledSources: ['stripe', 'tempo'], status: 'none', }) expect(await sandbox.json()).toMatchObject({ enabledSources: ['stripe', 'tempo'], status: 'none', }) }) test('behavior: the super admin reads any org', async () => { const db = TestApp.database() const app = createApp({ db }) const { orgId } = await seeded(app) const response = await app.request(`/v1/orgs/${orgId}/billing`, as(secret)) expect(response.status).toBe(200) expect(((await response.json()) as { status: string }).status).toBe('none') }) test('behavior: foreign sessions read 404', async () => { const app = createApp() const { orgId } = await seeded(app) const other = await session(app) const response = await app.request(`/v1/orgs/${orgId}/billing`, { headers: { cookie: other }, }) expect(response.status).toBe(404) }) test('behavior: 401 without a session', async () => { const app = createApp() const { orgId } = await seeded(app) const response = await app.request(`/v1/orgs/${orgId}/billing`) expect(response.status).toBe(401) }) test('behavior: reads database-backed state when Stripe is unconfigured', async () => { const app = TestApp.create({ auth: { superAdmin: { secret } }, db: TestApp.database(), session: { wallet: { origin } }, }) const { cookie, orgId } = await seeded(app) const response = await app.request(`/v1/orgs/${orgId}/billing`, { headers: { cookie } }) expect(response.status).toBe(200) expect(await response.json()).toMatchInlineSnapshot(` { "enabledSources": [], "spend": { "amount": "0", "currency": "usd", "period": "month", }, "status": "none", } `) }) }) /** RequestInit for a JSON PATCH carrying the session cookie. */ function patch(cookie: string, body: unknown) { return { body: JSON.stringify(body), headers: { 'content-type': 'application/json', cookie }, method: 'PATCH', } as const } describe('PATCH /orgs/:orgId/billing', () => { test('behavior: sets both limits and reads them back', async () => { const app = createApp() const { cookie, orgId } = await seeded(app) const response = await app.request( `/v1/orgs/${orgId}/billing`, patch(cookie, { spendLimit: { amount: '250' }, txFeeLimit: { amount: '0.50', currency: 'usd' }, }), ) expect(response.status).toBe(200) expect(await response.json()).toMatchInlineSnapshot(` { "enabledSources": [], "spend": { "amount": "0", "currency": "usd", "period": "month", }, "spendLimit": { "amount": "250", "currency": "usd", "period": "month", }, "status": "none", "txFeeLimit": { "amount": "0.50", "currency": "usd", }, } `) const read = await app.request(`/v1/orgs/${orgId}/billing`, { headers: { cookie } }) expect(((await read.json()) as { spendLimit: object }).spendLimit).toBeDefined() }) test('behavior: absent fields stay unchanged; null clears', async () => { const app = createApp() const { cookie, orgId } = await seeded(app) await app.request(`/v1/orgs/${orgId}/billing`, patch(cookie, { spendLimit: { amount: '250' }, txFeeLimit: { amount: '1' } })) // prettier-ignore const cleared = await app.request(`/v1/orgs/${orgId}/billing`, patch(cookie, { spendLimit: null })) // prettier-ignore expect(cleared.status).toBe(200) const body = (await cleared.json()) as { spendLimit?: object; txFeeLimit?: object } expect(body.spendLimit).toBeUndefined() expect(body.txFeeLimit).toStrictEqual({ amount: '1', currency: 'usd' }) }) test('behavior: reports current-window spend from the sponsorship ledger', async () => { const db = TestApp.database() const app = createApp({ db }) const { cookie, orgId } = await seeded(app) // Finalized fee plus a pending signed cap; sandbox rows stay invisible. const finalized = await SponsoredTransactions.upsert(db, { ...sponsorship(orgId), signPayload: `0x${'21'.repeat(32)}` }) // prettier-ignore await SponsoredTransactions.finalize(db, finalized.id, { feeAmount: '100000', finalizedAt: new Date().toISOString() }) // prettier-ignore await SponsoredTransactions.upsert(db, { ...sponsorship(orgId), feeMax: '250000', signPayload: `0x${'22'.repeat(32)}` }) // prettier-ignore await SponsoredTransactions.upsert(db, { ...sponsorship(orgId), billable: false, environment: 'sandbox', feeMax: '900000', signPayload: `0x${'23'.repeat(32)}` }) // prettier-ignore const response = await app.request(`/v1/orgs/${orgId}/billing`, { headers: { cookie } }) const body = (await response.json()) as { spend: { amount: string } } expect(body.spend).toStrictEqual({ amount: '0.35', currency: 'usd', period: 'month' }) }) test('behavior: requires the owner role', async () => { const db = TestApp.database() const app = createApp({ db }) const { orgId } = await seeded(app) const member = await session(app) const me = await app.request('/v1/me', { headers: { cookie: member } }) const { id: userId } = (await me.json()) as { id: string } await Memberships.create(db, { orgId, role: 'member', userId }) const response = await app.request( `/v1/orgs/${orgId}/billing`, patch(member, { spendLimit: null }), ) expect(response.status).toBe(403) }) test('behavior: rejects malformed limits', async () => { const app = createApp() const { cookie, orgId } = await seeded(app) for (const body of [ { spendLimit: { amount: '12.345' } }, // too many fraction digits { spendLimit: { amount: '1000000000' } }, // over 9 integer digits { spendLimit: { amount: 'abc' } }, { spendLimit: { amount: '10', currency: 'eur' } }, { spendLimit: { amount: '10', period: 'week' } }, { spendLimit: { amount: '0' } }, // zero blocks everything; null clears instead { txFeeLimit: { amount: '-1' } }, { txFeeLimit: { amount: '0.00' } }, ]) { const response = await app.request(`/v1/orgs/${orgId}/billing`, patch(cookie, body)) expect(response.status).toBe(400) expect(((await response.json()) as { error: { code: string } }).error.code).toBe('body_invalid') // prettier-ignore } }) test('behavior: foreign sessions read 404', async () => { const app = createApp() const { orgId } = await seeded(app) const other = await session(app) const response = await app.request( `/v1/orgs/${orgId}/billing`, patch(other, { spendLimit: null }), ) expect(response.status).toBe(404) }) test('behavior: updates database-backed state when Stripe is unconfigured', async () => { const app = TestApp.create({ auth: { superAdmin: { secret } }, db: TestApp.database(), session: { wallet: { origin } }, }) const { cookie, orgId } = await seeded(app) const response = await app.request( `/v1/orgs/${orgId}/billing`, patch(cookie, { spendLimit: { amount: '250' } }), ) expect(response.status).toBe(200) expect( ((await response.json()) as { spendLimit?: { amount: string } }).spendLimit?.amount, ).toBe('250') }) }) /** Baseline mainnet production sponsorship row attributed to the org. */ function sponsorship(orgId: string) { return { apiKeyId: 'key_1', billable: true, chainId: Viem.chainId.mainnet, environment: 'production', orgId, projectId: 'prj_1', transaction: `0x76${'cc'.repeat(16)}`, transactionHash: `0x${'aa'.repeat(32)}`, } as const } /** RequestInit for a bodyless POST carrying the session cookie. */ function post(cookie: string) { return { headers: { cookie }, method: 'POST' } as const } describe('POST /orgs/:orgId/billing/stripe/checkout', () => { test('behavior: requires the owner role', async () => { const db = TestApp.database() const app = createApp({ db }) const { orgId } = await seeded(app) // Seed a non-owner membership directly; roles gate writes, not reads. const member = await session(app) const me = await app.request('/v1/me', { headers: { cookie: member } }) const { id: userId } = (await me.json()) as { id: string } await Memberships.create(db, { orgId, role: 'member', userId }) const read = await app.request(`/v1/orgs/${orgId}/billing`, { headers: { cookie: member } }) expect(read.status).toBe(200) const write = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(member)) expect(write.status).toBe(403) }) test('behavior: foreign sessions read 404', async () => { const app = createApp() const { orgId } = await seeded(app) const other = await session(app) expect( (await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(other))).status, ).toBe(404) }) test('behavior: an unreachable Stripe maps to the upstream error envelope', async () => { const db = TestApp.database() const app = createApp({ db }) const { cookie, orgId } = await seededWithStripe(app, db) // First Stripe call the route makes is the customer create; the connection // is refused for real (no transport fake). const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie)) expect(response.status).toBe(502) const body = (await response.json()) as { error: { code: string } } expect(body.error.code).toMatchInlineSnapshot(`"upstream_error"`) }) }) describe('billing source access', () => { test('behavior: checkout requires Stripe to be enabled', async () => { const app = TestApp.create({ auth: { superAdmin: { secret } }, db: TestApp.database(), session: { wallet: { origin } }, }) const { cookie, orgId } = await seeded(app) const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie)) expect(response.status).toBe(403) const { requestId, ...body } = (await response.json()) as { requestId?: string } expect(requestId).toBeDefined() expect(body).toMatchInlineSnapshot(` { "error": { "code": "billing_source_disabled", "message": "Stripe billing is not enabled for this organization", }, } `) }) test('behavior: the source gate replaces temporary billing early access', async () => { const db = TestApp.database() const app = createApp({ db }) const { cookie, orgId } = await seededWithStripe(app, db) // The unreachable Stripe upstream proves source access, not user early access, is the gate. const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie)) expect(response.status).toBe(502) expect(((await response.json()) as { error: { code: string } }).error.code).toBe('upstream_error') // prettier-ignore }) test('behavior: an enabled management key reaches Stripe', async () => { const db = TestApp.database() const store = TestApp.kvStore({ keys: [] }) const app = createApp({ db, kv: { store } }) const { orgId } = await seededWithStripe(app, db) const { token } = await ApiKeys.mint(store, { orgId, scopes: ['management:write'] }) const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, { headers: { 'tempo-api-key': token }, method: 'POST', }) expect(response.status).toBe(502) }) test('behavior: an enabled super admin reaches Stripe', async () => { const db = TestApp.database() const app = createApp({ db }) const { orgId } = await seededWithStripe(app, db) const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, { ...as(secret), method: 'POST', }) expect(response.status).toBe(502) }) }) describe('POST /orgs/:orgId/billing/stripe/manage', () => { test('behavior: 404 before any billing exists', async () => { const app = createApp() const { cookie, orgId } = await seeded(app) const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/manage`, post(cookie)) expect(response.status).toBe(404) const body = (await response.json()) as { error: { code: string } } expect(body.error.code).toMatchInlineSnapshot(`"billing_not_found"`) }) }) describe('GET /orgs/:orgId/billing/payment-methods', () => { test('behavior: empty before any billing exists; members can read', async () => { const db = TestApp.database() const app = createApp({ db }) const { cookie, orgId } = await seeded(app) const response = await app.request(`/v1/orgs/${orgId}/billing/payment-methods`, { headers: { cookie }, }) expect(response.status).toBe(200) expect(await response.json()).toMatchInlineSnapshot(` { "data": [], } `) // Reads follow GET /billing: any member, not just owners. const member = await session(app) const me = await app.request('/v1/me', { headers: { cookie: member } }) const { id: userId } = (await me.json()) as { id: string } await Memberships.create(db, { orgId, role: 'member', userId }) const read = await app.request(`/v1/orgs/${orgId}/billing/payment-methods`, { headers: { cookie: member }, }) expect(read.status).toBe(200) }) test('behavior: foreign sessions read 404', async () => { const app = createApp() const { orgId } = await seeded(app) const other = await session(app) const response = await app.request(`/v1/orgs/${orgId}/billing/payment-methods`, { headers: { cookie: other }, }) expect(response.status).toBe(404) }) }) describe('DELETE /orgs/:orgId/billing/payment-methods/:methodId', () => { test('behavior: 404 before any billing exists', async () => { const app = createApp() const { cookie, orgId } = await seeded(app) const response = await app.request( `/v1/orgs/${orgId}/billing/payment-methods/pm_${'0'.repeat(24)}`, { headers: { cookie }, method: 'DELETE' }, ) expect(response.status).toBe(404) const body = (await response.json()) as { error: { code: string } } expect(body.error.code).toMatchInlineSnapshot(`"billing_not_found"`) }) test('behavior: requires the owner role', async () => { const db = TestApp.database() const app = createApp({ db }) const { orgId } = await seeded(app) const member = await session(app) const me = await app.request('/v1/me', { headers: { cookie: member } }) const { id: userId } = (await me.json()) as { id: string } await Memberships.create(db, { orgId, role: 'member', userId }) const response = await app.request( `/v1/orgs/${orgId}/billing/payment-methods/pm_${'0'.repeat(24)}`, { headers: { cookie: member }, method: 'DELETE' }, ) expect(response.status).toBe(403) }) }) describe('DELETE /orgs/:orgId', () => { test('behavior: deleting the organization removes its billing source', async () => { const db = TestApp.database() const app = createApp({ db }) const { cookie, orgId } = await seeded(app) await EnabledBillingSources.add(db, { createdBy: 'test', orgId, source: 'stripe' }) await StripeCustomers.create(db, { orgId, stripeCustomerId: 'cus_cascade' }) const response = await app.request(`/v1/orgs/${orgId}`, { headers: { cookie }, method: 'DELETE', }) expect(response.status).toBe(200) expect(await EnabledBillingSources.listByOrg(db, orgId)).toEqual([]) expect(await StripeCustomers.get(db, orgId)).toBeUndefined() }) }) describe('environment scoping', () => { test('behavior: sandbox billing state remains readable without a Stripe source', async () => { const app = createApp() const { cookie, orgId } = await seeded(app) const response = await app.request(`/v1/orgs/${orgId}/billing?environment=sandbox`, { headers: { cookie }, }) expect(response.status).toBe(200) expect(await response.json()).toMatchInlineSnapshot(` { "enabledSources": [], "spend": { "amount": "0", "currency": "usd", "period": "month", }, "status": "none", } `) }) test('behavior: rejects an unknown environment', async () => { const app = createApp({ billing: billingWithSandbox() }) const { cookie, orgId } = await seeded(app) const response = await app.request(`/v1/orgs/${orgId}/billing?environment=staging`, { headers: { cookie }, }) expect(response.status).toBe(400) expect(((await response.json()) as { error: { code: string } }).error.code).toBe( 'query_invalid', ) }) test('behavior: status reads are isolated per environment', async () => { const db = TestApp.database() const app = createApp({ billing: billingWithSandbox(), db }) const { cookie, orgId } = await seeded(app) // One org, two Stripe customers — production active, sandbox untouched. await StripeCustomers.create(db, { orgId, stripeCustomerId: 'cus_prod' }) await StripeCustomers.setStatus(db, orgId, 'active') await StripeCustomers.create(db, { environment: 'sandbox', orgId, stripeCustomerId: 'cus_sbx' }) const prod = await app.request(`/v1/orgs/${orgId}/billing`, { headers: { cookie } }) const sbx = await app.request(`/v1/orgs/${orgId}/billing?environment=sandbox`, { headers: { cookie } }) // prettier-ignore expect(((await prod.json()) as { status: string }).status).toBe('active') expect(((await sbx.json()) as { status: string }).status).toBe('none') }) test('behavior: PATCH writes only the targeted environment', async () => { const db = TestApp.database() const app = createApp({ billing: billingWithSandbox(), db }) const { cookie, orgId } = await seeded(app) await app.request(`/v1/orgs/${orgId}/billing?environment=sandbox`, patch(cookie, { spendLimit: { amount: '99' } })) // prettier-ignore const prod = await app.request(`/v1/orgs/${orgId}/billing`, { headers: { cookie } }) const sbx = await app.request(`/v1/orgs/${orgId}/billing?environment=sandbox`, { headers: { cookie } }) // prettier-ignore expect(((await prod.json()) as { spendLimit?: object }).spendLimit).toBeUndefined() expect(((await sbx.json()) as { spendLimit?: object }).spendLimit).toStrictEqual({ amount: '99', currency: 'usd', period: 'month', }) }) test('behavior: spend is scoped per environment', async () => { const db = TestApp.database() const app = createApp({ billing: billingWithSandbox(), db }) const { cookie, orgId } = await seeded(app) // Production: a finalized billable mainnet row (0.10). Sandbox: a pending // non-billable testnet row (0.25) — counted despite billable=false. const finalized = await SponsoredTransactions.upsert(db, { ...sponsorship(orgId), signPayload: `0x${'31'.repeat(32)}` }) // prettier-ignore await SponsoredTransactions.finalize(db, finalized.id, { feeAmount: '100000', finalizedAt: new Date().toISOString() }) // prettier-ignore await SponsoredTransactions.upsert(db, { ...sponsorship(orgId), billable: false, chainId: Viem.chainId.testnet, environment: 'sandbox', feeMax: '250000', signPayload: `0x${'32'.repeat(32)}` }) // prettier-ignore const prod = await app.request(`/v1/orgs/${orgId}/billing`, { headers: { cookie } }) const sbx = await app.request(`/v1/orgs/${orgId}/billing?environment=sandbox`, { headers: { cookie } }) // prettier-ignore expect(((await prod.json()) as { spend: { amount: string } }).spend.amount).toBe('0.1') expect(((await sbx.json()) as { spend: { amount: string } }).spend.amount).toBe('0.25') }) }) describe('POST /stripe/webhook', () => { /** Signs with the app's configured secret unless a test overrides it. */ function sign(payload: string, secret = webhookSecret) { return TestStripe.sign({ payload, secret }) } /** A minimal but shape-valid event addressing the given customer. */ function event(customer: string, type = 'customer.updated') { return JSON.stringify({ data: { object: { id: customer, object: 'customer' } }, id: 'evt_test_webhook', object: 'event', type, }) } function deliver(app: ReturnType, payload: string, signature?: string) { return app.request('/stripe/webhook', { body: payload, headers: signature ? { 'stripe-signature': signature } : {}, method: 'POST', }) } test('behavior: acks events for unknown customers without touching Stripe', async () => { const app = createApp() const payload = event('cus_unknown') const response = await deliver(app, payload, sign(payload)) expect(response.status).toBe(200) expect(await response.json()).toMatchInlineSnapshot(` { "received": true, } `) }) test('behavior: acks unhandled event types untouched', async () => { const app = createApp() const payload = event('cus_unknown', 'charge.succeeded') expect((await deliver(app, payload, sign(payload))).status).toBe(200) }) test('behavior: verifies events signed with the sandbox secret', async () => { const app = createApp({ billing: billingWithSandbox() }) const payload = event('cus_unknown') // Accepted (not signature_invalid) and acked as an unknown customer. const response = await deliver(app, payload, sign(payload, sandboxWebhookSecret)) expect(response.status).toBe(200) expect(((await response.json()) as { received: boolean }).received).toBe(true) }) test('behavior: rejects sandbox-signed events when sandbox is unconfigured', async () => { const app = createApp() const payload = event('cus_a') expect((await deliver(app, payload, sign(payload, sandboxWebhookSecret))).status).toBe(400) }) test('behavior: rejects a tampered payload', async () => { const app = createApp() const payload = event('cus_a') const response = await deliver(app, payload.replace('cus_a', 'cus_b'), sign(payload)) expect(response.status).toBe(400) const body = (await response.json()) as { error: { code: string } } expect(body.error.code).toMatchInlineSnapshot(`"signature_invalid"`) }) test('behavior: rejects a wrong secret and a missing header', async () => { const app = createApp() const payload = event('cus_a') expect((await deliver(app, payload, sign(payload, `whsec_${'c'.repeat(32)}`))).status).toBe(400) // prettier-ignore expect((await deliver(app, payload)).status).toBe(400) }) test('behavior: rejects a stale timestamp', async () => { const app = createApp() const payload = event('cus_a') const signature = TestStripe.sign({ payload, secret: webhookSecret, // Beyond the default 300s tolerance. timestamp: Math.floor(Date.now() / 1000) - 600, }) expect((await deliver(app, payload, signature)).status).toBe(400) }) test('behavior: 501 when billing is unconfigured', async () => { const app = TestApp.create({ auth: { superAdmin: { secret } }, db: TestApp.database(), session: { wallet: { origin } }, }) const payload = event('cus_a') expect((await deliver(app, payload, sign(payload))).status).toBe(501) }) }) /** Billing capability against the real sandbox. */ function sandboxBilling(): App.Billing { return { stripe: { client: TestStripe.client(), webhookSecret } } } /** * Real-Stripe capability with both sources configured (both hit the same * test-mode account, but each environment provisions its own customer row). */ function sandboxBillingBoth(): App.Billing { return { stripe: { client: TestStripe.client(), sandbox: { client: TestStripe.client(), webhookSecret: sandboxWebhookSecret }, webhookSecret, }, } } // Real-protocol suites against a Stripe sandbox (never faked, mirroring the // MPP-testnet convention). They run when `STRIPE_SECRET_KEY_DEV` is set; // CI always sets it. describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => { test('checkout provisions distinct customers per environment', { timeout: 30_000 }, async () => { const db = TestApp.database() const app = createApp({ billing: sandboxBillingBoth(), db }) const { cookie, orgId } = await seededWithStripe(app, db) const prod = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie)) const sbx = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout?environment=sandbox`, post(cookie)) // prettier-ignore // Track both before asserting so a failure never leaks a provisioned customer. const prodRecord = await StripeCustomers.get(db, orgId) const sbxRecord = await StripeCustomers.get(db, orgId, 'sandbox') if (prodRecord) TestStripe.track(prodRecord.stripeCustomerId) if (sbxRecord) TestStripe.track(sbxRecord.stripeCustomerId) expect(prod.status).toBe(200) expect(sbx.status).toBe(200) expect(prodRecord?.stripeCustomerId).toMatch(/^cus_/) expect(sbxRecord?.stripeCustomerId).toMatch(/^cus_/) // One org, two environments, two independent Stripe customers. expect(prodRecord!.stripeCustomerId).not.toBe(sbxRecord!.stripeCustomerId) // The sandbox session returns to the console in sandbox mode; the full-page // Stripe round-trip drops the client-retained `env`, so the URL carries it. const stripe = TestStripe.client() const sessions = await stripe.checkout.sessions.list({ customer: sbxRecord!.stripeCustomerId }) expect(sessions.data[0]?.success_url).toContain('env=sandbox') }) test('checkout provisions a real customer and setup session', { timeout: 30_000 }, async () => { const db = TestApp.database() const app = createApp({ billing: sandboxBilling(), db }) const { cookie, orgId } = await seededWithStripe(app, db) const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie)) // Track for teardown before asserting: a failed assertion must not leak // the customer the route already provisioned. const record = await StripeCustomers.get(db, orgId) if (record) TestStripe.track(record.stripeCustomerId) expect(response.status).toBe(200) const { url } = (await response.json()) as { url: string } expect(url).toMatch(/^https:\/\/checkout\.stripe\.com\//) expect(record?.stripeCustomerId).toMatch(/^cus_/) // The customer is real and carries org attribution. const stripe = TestStripe.client() const customer = await stripe.customers.retrieve(record!.stripeCustomerId) expect(customer.deleted).not.toBe(true) expect((customer as { metadata: { orgId?: string } }).metadata.orgId).toBe(orgId) // A second checkout reuses the customer; a browser `Origin` overrides the // configured console URL so the caller returns to the host it came from. const callerOrigin = 'http://localhost:23145' const again = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, { headers: { cookie, origin: callerOrigin }, method: 'POST', }) expect(again.status).toBe(200) expect((await StripeCustomers.get(db, orgId))?.stripeCustomerId).toBe(record!.stripeCustomerId) const sessions = await stripe.checkout.sessions.list({ customer: record!.stripeCustomerId }) expect(sessions.data.length).toBeGreaterThanOrEqual(2) expect(sessions.data.every(({ mode }) => mode === 'setup')).toBe(true) const urls = sessions.data.map(({ cancel_url, success_url }) => ({ cancel_url, success_url })) expect(urls).toContainEqual({ cancel_url: `https://console.tempo.xyz/${orgId}/billing?billing=canceled`, success_url: `https://console.tempo.xyz/${orgId}/billing?billing=success`, }) expect(urls).toContainEqual({ cancel_url: `${callerOrigin}/${orgId}/billing?billing=canceled`, success_url: `${callerOrigin}/${orgId}/billing?billing=success`, }) }) test( 'payment methods list live state; detach by id parks status', { timeout: 30_000 }, async () => { // prettier-ignore const db = TestApp.database() const app = createApp({ billing: sandboxBilling(), db }) const { cookie, orgId } = await seededWithStripe(app, db) const checkout = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie)) const customerId = (await StripeCustomers.get(db, orgId))?.stripeCustomerId if (customerId) TestStripe.track(customerId) expect(checkout.status).toBe(200) if (!customerId) throw new Error('checkout stored no customer') const attached = await TestStripe.attachCard(customerId) const stripe = TestStripe.client() await stripe.customers.update(customerId, { invoice_settings: { default_payment_method: attached.paymentMethod }, }) await StripeCustomers.setStatus(db, orgId, 'active') await EnabledBillingSources.remove(db, { orgId, source: 'stripe' }) // Disabling setup does not block management of the existing Stripe source. const list = await app.request(`/v1/orgs/${orgId}/billing/payment-methods`, { headers: { cookie }, }) expect(list.status).toBe(200) type Method = { card?: { brand: string; last4: string } default: boolean id: string provider: string type: string } const { data } = (await list.json()) as { data: Method[] } expect(data).toHaveLength(1) expect(data[0]!.id).toBe(attached.paymentMethod) expect(data[0]!.default).toBe(true) expect(data[0]!.provider).toBe('stripe') expect(data[0]!.type).toBe('card') expect(data[0]!.card?.last4).toMatch(/^\d{4}$/) // Ids outside the org's own list read as absent. const missing = await app.request( `/v1/orgs/${orgId}/billing/payment-methods/pm_${'0'.repeat(24)}`, { headers: { cookie }, method: 'DELETE' }, ) expect(missing.status).toBe(404) expect(((await missing.json()) as { error: { code: string } }).error.code).toBe( 'payment_method_not_found', ) const response = await app.request( `/v1/orgs/${orgId}/billing/payment-methods/${data[0]!.id}`, { headers: { cookie }, method: 'DELETE' }, ) expect(response.status).toBe(200) const body = (await response.json()) as { status: string } expect(body.status).toBe('none') expect((await StripeCustomers.get(db, orgId))?.status).toBe('none') // Nothing survives on the customer; the default cleared with the detach. const methods = await stripe.customers.listPaymentMethods(customerId, { limit: 100 }) expect(methods.data).toHaveLength(0) const customer = await stripe.customers.retrieve(customerId) expect(customer.deleted).not.toBe(true) if (!customer.deleted) expect(customer.invoice_settings.default_payment_method).toBeNull() }, ) test( 'checkout.session.completed promotes the payment method to default', { timeout: 30_000 }, async () => { // prettier-ignore const db = TestApp.database() const app = createApp({ billing: sandboxBilling(), db }) const { cookie, orgId } = await seededWithStripe(app, db) const checkout = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie)) const customerId = (await StripeCustomers.get(db, orgId))?.stripeCustomerId if (customerId) TestStripe.track(customerId) expect(checkout.status).toBe(200) if (!customerId) throw new Error('checkout stored no customer') const attached = await TestStripe.completeCheckout(app, { customer: customerId, webhookSecret, }) const stripe = TestStripe.client() const customer = await stripe.customers.retrieve(customerId) expect(customer.deleted).not.toBe(true) expect( (customer as { invoice_settings: { default_payment_method: unknown } }).invoice_settings .default_payment_method, ) // prettier-ignore .toBe(attached.paymentMethod) const billing = await app.request(`/v1/orgs/${orgId}/billing`, { headers: { cookie } }) expect(((await billing.json()) as { status: string }).status).toBe('active') }, ) test('manage links to a live portal session', { timeout: 30_000 }, async () => { await TestStripe.ensurePortalConfiguration() const db = TestApp.database() const app = createApp({ billing: sandboxBilling(), db }) const { cookie, orgId } = await seededWithStripe(app, db) const checkout = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie)) const customerId = (await StripeCustomers.get(db, orgId))?.stripeCustomerId if (customerId) TestStripe.track(customerId) expect(checkout.status).toBe(200) await EnabledBillingSources.remove(db, { orgId, source: 'stripe' }) const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/manage`, post(cookie)) expect(response.status).toBe(200) const { url } = (await response.json()) as { url: string } expect(url).toMatch(/^https:\/\/billing\.stripe\.com\//) }) }) describe.runIf(TestStripe.secretKey)('stripe sandbox (live delivery)', () => { let bridge: TestStripe.bridge.Bridge let app: ReturnType let db: ReturnType // Live webhook delivery: mutations fire real events, a `stripe listen` // container forwards them with Stripe's own signature, and the app // re-derives status from live state. beforeAll(async () => { bridge = await TestStripe.bridge() db = TestApp.database() app = createApp({ billing: { stripe: { client: TestStripe.client(), webhookSecret: bridge.secret } }, db }) // prettier-ignore bridge.connect((request) => app.fetch(request)) }, 180_000) afterAll(async () => { await bridge?.stop() }) test('attach → active, detach → none, delete → canceled', { timeout: 300_000 }, async () => { const { cookie, orgId } = await seededWithStripe(app, db) const checkout = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie)) const customerId = (await StripeCustomers.get(db, orgId))?.stripeCustomerId if (customerId) TestStripe.track(customerId) expect(checkout.status).toBe(200) if (!customerId) throw new Error('checkout stored no customer') const status = async () => { const response = await app.request(`/v1/orgs/${orgId}/billing`, { headers: { cookie } }) const body = (await response.json()) as { status?: string } return body.status } // Attach a real card: `payment_method.attached` + `setup_intent.succeeded` // deliver through the bridge and flip the derived status. await TestStripe.attachCard(customerId) await until(async () => (await status()) === 'active') // Detach every card: delivery re-derives back to `none`. await TestStripe.detachCards(customerId) await until(async () => (await status()) === 'none') // Deleting the customer lands `canceled`. await TestStripe.client().customers.del(customerId) await until(async () => (await status()) === 'canceled') }) }) /** Polls until the condition holds; live delivery typically lands within seconds. */ async function until(condition: () => Promise, timeout = 120_000) { const deadline = Date.now() + timeout while (Date.now() < deadline) { if (await condition()) return await new Promise((resolve) => setTimeout(resolve, 1_000)) } throw new Error('condition never held within the timeout') } // Metering against the real sandbox: account fixtures, subscription // provisioning, the reporter's exactly-once pass, and the priced preview. describe.runIf(TestStripe.secretKey)('stripe sandbox (metering)', () => { test('ensureFixtures is idempotent across clients', { timeout: 30_000 }, async () => { const first = await core_Billing.ensureFixtures(TestStripe.client()) // A fresh client skips the memo and exercises the real lookup path. const second = await core_Billing.ensureFixtures(TestStripe.client()) expect(second).toStrictEqual(first) expect(first.meterId).toMatch(/^mtr_/) expect(first.priceId).toMatch(/^price_/) }) test( 'ensureSubscription provisions exactly one live subscription', { timeout: 60_000 }, async () => { // prettier-ignore const stripe = TestStripe.client() const customer = await stripe.customers.create({ metadata: { orgId: 'org_sub' }, name: `Test Org ${nanoid(10)}`, }) TestStripe.track(customer.id) await core_Billing.ensureSubscription(stripe, customer.id) await core_Billing.ensureSubscription(stripe, customer.id) const { priceId } = await core_Billing.ensureFixtures(stripe) const subscriptions = await stripe.subscriptions.list({ customer: customer.id, limit: 100, price: priceId, status: 'all', }) const live = subscriptions.data.filter((s) => !['canceled', 'incomplete_expired'].includes(s.status)) // prettier-ignore expect(live).toHaveLength(1) }, ) test('the reporter reports each finalized row exactly once', { timeout: 120_000 }, async () => { const db = TestApp.database() const stripe = TestStripe.client() const customer = await stripe.customers.create({ metadata: { orgId: 'org_1' }, name: `Test Org ${nanoid(10)}`, }) TestStripe.track(customer.id) await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: customer.id }) const finalize = async (input: { feeAmount: string; orgId: string; seed: string }) => { const row = await SponsoredTransactions.upsert(db, { ...meterable(input.orgId), signPayload: `0x${input.seed.repeat(32)}` }) // prettier-ignore await SponsoredTransactions.finalize(db, row.id, { feeAmount: input.feeAmount, finalizedAt: new Date().toISOString() }) // prettier-ignore } await finalize({ feeAmount: '100000', orgId: 'org_1', seed: '41' }) await finalize({ feeAmount: '250000', orgId: 'org_1', seed: '42' }) // A deleted org's row marks without a meter event; a pending row waits. await finalize({ feeAmount: '900000', orgId: 'org_ghost', seed: '43' }) await SponsoredTransactions.upsert(db, { ...meterable('org_1'), signPayload: `0x${'44'.repeat(32)}` }) // prettier-ignore const reporter = core_Billing.createReporter({ db, stripe }) expect(await reporter.tick()).toStrictEqual({ failed: 0, reported: 2, skipped: 1 }) expect(await reporter.tick()).toStrictEqual({ failed: 0, reported: 0, skipped: 0 }) }) test('the metered invoice preview matches reported spend', { timeout: 240_000 }, async () => { const db = TestApp.database() const stripe = TestStripe.client() const customer = await stripe.customers.create({ metadata: { orgId: 'org_1' }, name: `Test Org ${nanoid(10)}`, }) TestStripe.track(customer.id) await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: customer.id }) await core_Billing.ensureSubscription(stripe, customer.id) // $0.35 of finalized spend: 100k + 250k base units. for (const [seed, feeAmount] of [ ['51', '100000'], ['52', '250000'], ] as const) { const row = await SponsoredTransactions.upsert(db, { ...meterable('org_1'), signPayload: `0x${seed.repeat(32)}` }) // prettier-ignore await SponsoredTransactions.finalize(db, row.id, { feeAmount, finalizedAt: new Date().toISOString() }) // prettier-ignore } await core_Billing.createReporter({ db, stripe }).tick() const { priceId } = await core_Billing.ensureFixtures(stripe) const [subscription] = (await stripe.subscriptions.list({ customer: customer.id, price: priceId, limit: 1 })).data // prettier-ignore // Meter events process asynchronously; poll the preview until usage lands. const deadline = Date.now() + 180_000 let line: { amount: number; quantity: number | null } | undefined while (Date.now() < deadline) { const preview = await stripe.invoices.createPreview({ customer: customer.id, subscription: subscription!.id, }) line = preview.lines.data.find((l) => l.pricing?.price_details?.price === priceId) if (line?.quantity === 350_000) break await new Promise((resolve) => setTimeout(resolve, 5_000)) } expect(line?.quantity).toBe(350_000) // 350k units × 0.0001¢ = 35¢. expect(line?.amount).toBe(35) }) test( 'a failed metered invoice parks the org past_due; payment restores active', { timeout: 600_000 }, async () => { // prettier-ignore const db = TestApp.database() const stripe = TestStripe.client() const app = createApp({ billing: sandboxBilling(), db }) // Frozen time lets the clock cross a real billing-period boundary. const clock = await stripe.testHelpers.testClocks.create({ frozen_time: Math.floor(Date.now() / 1000), }) try { const customer = await stripe.customers.create({ metadata: { orgId: 'org_1' }, name: `Test Org ${nanoid(10)}`, test_clock: clock.id, }) await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: customer.id }) // A card that saves fine but refuses charges: renewal will fail. const intent = await stripe.setupIntents.create({ confirm: true, customer: customer.id, payment_method: 'pm_card_chargeCustomerFail', payment_method_types: ['card'], }) await stripe.customers.update(customer.id, { invoice_settings: { default_payment_method: intent.payment_method as string }, }) await StripeCustomers.setStatus(db, 'org_1', 'active') await core_Billing.ensureSubscription(stripe, customer.id) // $5 of finalized usage so the invoice is worth charging. const row = await SponsoredTransactions.upsert(db, { ...meterable('org_1'), signPayload: `0x${'61'.repeat(32)}` }) // prettier-ignore await SponsoredTransactions.finalize(db, row.id, { feeAmount: '5000000', finalizedAt: new Date().toISOString() }) // prettier-ignore await core_Billing.createReporter({ db, stripe }).tick() // Cross the period boundary: Stripe invoices the metered usage and the // charge fails on the saved card, emitting a real payment_failed event. await stripe.testHelpers.testClocks.advance(clock.id, { frozen_time: Math.floor(Date.now() / 1000) + 32 * 24 * 60 * 60, }) const failed = await eventFor(stripe, 'invoice.payment_failed', customer.id) await deliverEvent(app, failed) expect((await StripeCustomers.get(db, 'org_1'))?.status).toBe('past_due') // Pay the open invoice with a working card; `invoice.paid` re-derives. const paying = await TestStripe.attachCard(customer.id) const invoice = (failed.data.object as { id: string }).id await stripe.invoices.pay(invoice, { payment_method: paying.paymentMethod }) const paid = await eventFor(stripe, 'invoice.paid', customer.id) await deliverEvent(app, paid) expect((await StripeCustomers.get(db, 'org_1'))?.status).toBe('active') } finally { // Deleting the clock deletes its customers and subscriptions. await stripe.testHelpers.testClocks.del(clock.id).catch(() => undefined) } }, ) }) /** Baseline mainnet production row the metering suites finalize per case. */ function meterable(orgId: string) { return { apiKeyId: 'key_1', billable: true, chainId: Viem.chainId.mainnet, environment: 'production', orgId, projectId: 'prj_1', transaction: `0x76${'cc'.repeat(16)}`, transactionHash: `0x${'ab'.repeat(32)}`, } as const } /** Polls the recorded event stream until one matches the customer. */ async function eventFor( stripe: Stripe, type: 'invoice.paid' | 'invoice.payment_failed', customerId: string, timeout = 240_000, ) { const deadline = Date.now() + timeout while (Date.now() < deadline) { const events = await stripe.events.list({ limit: 100, type }) const match = events.data.find( (event) => (event.data.object as { customer?: string }).customer === customerId, ) if (match) return match await new Promise((resolve) => setTimeout(resolve, 5_000)) } throw new Error(`no ${type} event recorded for ${customerId}`) } /** Delivers a recorded event to the app, signed with the suite's webhook secret. */ async function deliverEvent(app: ReturnType, event: Stripe.Event) { const payload = JSON.stringify(event) const response = await app.request('/stripe/webhook', { body: payload, headers: { 'stripe-signature': TestStripe.sign({ payload, secret: webhookSecret }) }, method: 'POST', }) if (response.status !== 200) throw new Error(`webhook delivery returned ${response.status}`) }