feat(billing): checkout carries the GPU wanted and the room is made when payment lands

This commit is contained in:
Wanjohi
2026-10-19 22:38:51 +03:00
parent 32f62c1b83
commit 5c2bfa0bb3
9 changed files with 4709 additions and 5 deletions
+47 -1
View File
@@ -1,5 +1,6 @@
import { Actor } from '@nestri/core/actor';
import { Billing } from '@nestri/core/billing/index';
import { CheckoutRoom } from '@nestri/core/billing/checkout-room';
import { Polar } from '@nestri/core/billing/polar';
import { ErrorCodes, VisibleError } from '@nestri/core/error';
import { Team } from '@nestri/core/team/index';
@@ -134,6 +135,44 @@ export namespace BillingApi {
...ids
});
// The room they paid for. Never allowed to fail the delivery:
// the plan is already applied, and a retry loop cannot make a
// GPU free.
if (
updated &&
delivery.gpu &&
delivery.subscriptionId &&
delivery.standing.plan === 'paid' &&
delivery.standing.status === 'active'
) {
try {
const room = await CheckoutRoom.forSubscription({
teamId: delivery.teamId,
subscriptionId: delivery.subscriptionId,
gpu: delivery.gpu
});
console.log('billing checkout room', {
team: delivery.teamId,
requested: delivery.gpu,
...room
});
if (room.reason === 'no gpu free') {
console.error('a paid checkout asked for a GPU and none was free', {
team: delivery.teamId,
requested: delivery.gpu,
...ids
});
}
} catch (err) {
console.error('a paid checkout room failed to be created', {
team: delivery.teamId,
requested: delivery.gpu,
...ids,
err
});
}
}
return c.json({
data: { applied: Boolean(updated), type: delivery.type }
});
@@ -197,19 +236,26 @@ export namespace BillingApi {
.object({
successUrl: z.url().optional().meta({
description: 'Where to return to once they have paid'
}),
gpu: z.string().min(1).max(128).optional().meta({
description:
'A fleet GPU model, as `GET /location` lists it. When payment lands, a room on it is created (or on the closest free model if none is free)'
})
})
.strict()
),
async (c) => {
mustBeConfigured();
const { successUrl, gpu } = c.req.valid('json');
if (gpu) await CheckoutRoom.assertKnown(gpu);
const teamId = await payingTeam();
const user = await User.fromID(Actor.userID);
return c.json({
data: await Polar.checkout({
teamId,
email: user?.email ?? undefined,
successUrl: c.req.valid('json').successUrl
successUrl,
gpu
})
});
}
+232
View File
@@ -0,0 +1,232 @@
import { createHmac } from 'node:crypto';
import { afterAll, afterEach, describe, expect, test } from 'bun:test';
import { AccessToken } from '@nestri/core/access-token/index';
import { Polar } from '@nestri/core/billing/polar';
import { Fixtures } from '@nestri/core/db/fixtures';
import { testDb } from '@nestri/core/db/test';
import { Env } from '@nestri/core/env';
import { Identifier } from '@nestri/core/id';
import { Machine } from '@nestri/core/machine/index';
import { app } from '../app/index';
import { TEST_FRONTEND_URL } from './setup';
const sql = testDb();
const createdUserIds: string[] = [];
const createdOrgIds: string[] = [];
const realFetch = globalThis.fetch;
const PAID = 'prod_paid';
const V710 = { model: 'AMD Radeon Pro V710', vramMib: 28672, pciId: '1002:7461' };
const RX = { model: 'AMD Radeon RX 9070', vramMib: 16384, pciId: '1002:7550' };
const H100 = { model: 'NVIDIA H100', vramMib: 81920, pciId: '10de:2330' };
const RTX = { model: 'NVIDIA RTX 4090', vramMib: 24576, pciId: '10de:2684' };
const secret = `whsec_${Buffer.from('d'.repeat(32)).toString('base64')}`;
function configure(organisationId?: string) {
Env.init({
NODE_ENV: 'test',
FRONTEND_URL: TEST_FRONTEND_URL,
POLAR_ACCESS_TOKEN: 'polar_test',
POLAR_PRODUCT_ID: PAID,
POLAR_WEBHOOK_SECRET: secret,
...(organisationId ? { FLEET_ORGANISATION_ID: organisationId } : {})
});
Polar.reset();
}
async function person(label: string) {
const owner = await Fixtures.owner(label);
createdUserIds.push(owner.userId);
const pat = await AccessToken.create({
id: Identifier.ascending('accessToken'),
ownerUserId: owner.userId,
teamId: null,
name: label
});
return {
owner,
headers: {
authorization: `Bearer ${pat.token}`,
'content-type': 'application/json'
} as Record<string, string>
};
}
async function fleet(gpus: (typeof V710)[][]) {
const operator = await Fixtures.owner('operator');
createdUserIds.push(operator.userId);
const f = await Fixtures.fleet(operator, gpus.length);
createdOrgIds.push(f.organisationId);
for (const [i, g] of gpus.entries()) await Machine.setGpus({ id: f.machines[i]!.id, gpus: g });
configure(f.organisationId);
return f;
}
/** Stand in for Polar, remembering what was sent to create a checkout. */
function polarCheckout() {
const sent: any[] = [];
globalThis.fetch = (async (input: Request | string | URL, init?: RequestInit) => {
const req = input instanceof Request ? input : new Request(String(input), init);
if (req.url.includes('polar.sh')) {
sent.push(await req.json());
return new Response(JSON.stringify({ id: 'chk_1', url: 'https://polar.sh/pay/chk_1' }), {
status: 201,
headers: { 'content-type': 'application/json' }
});
}
return realFetch(input as never, init);
}) as typeof fetch;
return sent;
}
let n = 0;
async function deliver(type: string, data: Record<string, unknown>) {
const body = JSON.stringify({ type, data });
const id = `msg_cg_${++n}`;
const ts = Math.floor(Date.now() / 1000);
// Standard Webhooks: HMAC-SHA256 over `id.timestamp.body`, keyed by the decoded secret.
const signature = `v1,${createHmac('sha256', Buffer.from(secret.slice(6), 'base64'))
.update(`${id}.${ts}.${body}`)
.digest('base64')}`;
return app.request('/billing/webhook', {
method: 'POST',
headers: {
'webhook-id': id,
'webhook-timestamp': String(ts),
'webhook-signature': signature
},
body
});
}
const paid = (teamId: string, sub: string, gpu?: string) => ({
id: sub,
productId: PAID,
customer: { externalId: teamId },
metadata: gpu ? { teamId, gpu } : { teamId }
});
const rooms = (userId: string) =>
sql`select b.gpu_pci_id, b.label from box b where b.user_id = ${userId} and b.time_deleted is null`;
afterEach(() => {
globalThis.fetch = realFetch;
Polar.reset();
});
afterAll(async () => {
await sql`delete from checkout_room where team_id in (select id from team where owner_id = any(${createdUserIds}))`;
await sql`delete from box where user_id = any(${createdUserIds})`;
await sql`delete from install_token where created_by_user_id = any(${createdUserIds})`;
await sql`delete from machine where owner_user_id = any(${createdUserIds})`;
await sql`delete from organisation where id = any(${createdOrgIds})`;
await sql`delete from "user" where id = any(${createdUserIds})`;
});
describe('POST /billing/checkout with a GPU', () => {
test('the model travels to the provider as checkout metadata', async () => {
await fleet([[V710], [H100]]);
const p = await person('co-meta');
const sent = polarCheckout();
const res = await app.request('/billing/checkout', {
method: 'POST',
headers: p.headers,
body: JSON.stringify({ gpu: H100.model })
});
// The stand-in's reply is not a full checkout, so the SDK may refuse
// to parse it; what matters here is what was sent.
void res;
expect(sent[0].metadata).toEqual({ teamId: p.owner.teamId, gpu: H100.model });
});
test('no gpu leaves the metadata as it was', async () => {
await fleet([[V710]]);
const p = await person('co-plain');
const sent = polarCheckout();
const res = await app.request('/billing/checkout', {
method: 'POST',
headers: p.headers,
body: JSON.stringify({})
});
void res;
expect(sent[0].metadata).toEqual({ teamId: p.owner.teamId });
});
test('a model the fleet does not have is refused, and nothing reaches the provider', async () => {
await fleet([[V710]]);
const p = await person('co-unknown');
const sent = polarCheckout();
const res = await app.request('/billing/checkout', {
method: 'POST',
headers: p.headers,
body: JSON.stringify({ gpu: 'Imaginary 9000' })
});
expect(res.status).toBe(400);
expect(sent).toHaveLength(0);
});
});
describe('the webhook makes the room that was paid for', () => {
test('a paid subscription with a gpu creates a room on that model', async () => {
await fleet([[V710], [H100]]);
const p = await person('wh-room');
const res = await deliver('subscription.active', paid(p.owner.teamId, 'sub_wh_room', H100.model));
expect(res.status).toBe(200);
const made = await rooms(p.owner.userId);
expect(made.map((r) => [r.gpu_pci_id, r.label])).toEqual([[H100.pciId, H100.model]]);
const [row] = await sql`select substituted, placed_model from checkout_room where subscription_id = 'sub_wh_room'`;
expect(row).toMatchObject({ substituted: false, placed_model: H100.model });
});
test('a redelivery, and the created event after the active one, add no second room', async () => {
await fleet([[H100], [H100]]);
const p = await person('wh-twice');
const data = paid(p.owner.teamId, 'sub_wh_twice', H100.model);
await deliver('subscription.created', data);
await deliver('subscription.active', data);
await deliver('subscription.active', data);
expect(await rooms(p.owner.userId)).toHaveLength(1);
});
test('with the model taken, the closest free one of the same vendor is used and recorded', async () => {
await fleet([[H100], [V710], [RX], [RTX]]);
const taker = await person('wh-taker');
await deliver('subscription.active', paid(taker.owner.teamId, 'sub_wh_taker', H100.model));
const p = await person('wh-sub');
// H100 is gone. Nvidia first, so the 4090 beats the closer-in-memory AMD cards.
await deliver('subscription.active', paid(p.owner.teamId, 'sub_wh_sub', H100.model));
const made = await rooms(p.owner.userId);
expect(made.map((r) => r.gpu_pci_id)).toEqual([RTX.pciId]);
const [row] = await sql`select substituted, requested_model, placed_model from checkout_room where subscription_id = 'sub_wh_sub'`;
expect(row).toMatchObject({
substituted: true,
requested_model: H100.model,
placed_model: RTX.model
});
});
test('with nothing free, no room is made, the plan is still applied, and a retry stays quiet', async () => {
await fleet([[V710]]);
const taker = await person('wh-none-taker');
await deliver('subscription.active', paid(taker.owner.teamId, 'sub_wh_nt', V710.model));
const p = await person('wh-none');
const data = paid(p.owner.teamId, 'sub_wh_none', V710.model);
expect((await deliver('subscription.active', data)).status).toBe(200);
expect(await rooms(p.owner.userId)).toHaveLength(0);
const [team] = await sql`select plan from team where id = ${p.owner.teamId}`;
expect(team!.plan).toBe('paid');
await deliver('subscription.active', data);
expect(await rooms(p.owner.userId)).toHaveLength(0);
});
test('a paid subscription with no gpu creates nothing', async () => {
await fleet([[V710]]);
const p = await person('wh-nogpu');
await deliver('subscription.active', paid(p.owner.teamId, 'sub_wh_nogpu'));
expect(await rooms(p.owner.userId)).toHaveLength(0);
});
});