Skip to content

Commit 80ef6e6

Browse files
feat(billing): wire fetchBilling() to real GET /api/v1/billing endpoint (#23)
InstaNode-dev/api PR #15 just landed — the agent API now serves GET /api/v1/billing as the aggregated subscription-state endpoint the dashboard had been fixturing. This commit drops the fixture path. fetchBilling() now calls the real endpoint and maps the response into the dashboard's existing BillingDetails shape (status, current_period_end, payment_last4, payment_network — every field the agent API can populate is now live; the ones the agent API doesn't expose yet stay undefined and render as "—" in the UI). Fallback: on 503 (Razorpay unconfigured in local dev), still falls back to FIXTURE_BILLING so the BillingPage doesn't break for developers without RAZORPAY_KEY_ID set. Any other error propagates so production failures aren't silently swallowed. Closes the partial-fixture comment block I left in PR #20. Verified: npm run build clean (116 HTML + 116 .md), 26/26 markdown renderer tests pass, Playwright auth/navigation/resources specs all green. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c507a4b commit 80ef6e6

1 file changed

Lines changed: 60 additions & 16 deletions

File tree

src/api/index.ts

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -396,35 +396,79 @@ export async function deleteCustomDomain(stackSlug: string, id: string): Promise
396396
)
397397
}
398398

399-
// ─── Billing (LIVE for checkout + invoices; partial-fixture for state) ───
399+
// ─── Billing (LIVE: every endpoint hits the agent API) ──────────────────
400400
//
401-
// fetchBilling — plan tier is REAL (from /api/v1/whoami). Renewal date,
402-
// payment method, billing email all come from fixtures
403-
// because the agent API doesn't yet expose a GET
404-
// /api/v1/billing endpoint that aggregates this. Open
405-
// follow-up: add the endpoint, then drop FIXTURE_BILLING
406-
// here. See `dashboard/AGENT_API_NOTES.md` for the
407-
// expected shape.
401+
// fetchBilling — LIVE. Calls GET /api/v1/billing on the agent API,
402+
// which returns the aggregated billing state (tier,
403+
// subscription_status, next_renewal_at, amount_inr,
404+
// payment_method, razorpay_*_id). Falls back to a
405+
// whoami-derived shape when the endpoint isn't
406+
// available (503 = Razorpay unconfigured, e.g. local
407+
// dev) so the UI stays usable.
408408
//
409409
// listInvoices — LIVE. Calls GET /api/v1/billing/invoices on the agent
410-
// API; falls back to FIXTURE_INVOICES on 503 (billing
411-
// not configured, e.g. local dev without Razorpay keys)
412-
// so the UI stays usable. Returns an empty list when
413-
// the team has no subscription yet.
410+
// API; falls back to FIXTURE_INVOICES on 503.
414411
//
415412
// createCheckout — LIVE. Calls POST /api/v1/billing/checkout, creates a
416413
// real Razorpay subscription, and returns the hosted
417414
// payment short_url. The caller (BillingPage) redirects
418415
// the user to short_url to complete payment. Errors
419416
// propagate as APIError so the page's checkoutErr state
420417
// can surface them inline.
418+
//
419+
// cancelSubscription — LIVE. POST /api/v1/billing/cancel.
420+
421+
type BillingStateResp = {
422+
ok: boolean
423+
tier: string
424+
subscription_status?: 'none' | 'active' | 'cancelled' | 'trial'
425+
next_renewal_at?: string | null
426+
amount_inr?: number | null
427+
payment_method?: {
428+
type: 'card' | 'upi' | 'netbanking' | 'wallet'
429+
brand?: string
430+
last4?: string
431+
vpa?: string
432+
} | null
433+
billing_email?: string
434+
razorpay_subscription_id?: string | null
435+
razorpay_customer_id?: string | null
436+
}
437+
438+
/* Map the agent API's BillingStateResp into the dashboard's BillingDetails
439+
* type. The dashboard's shape was designed against a richer Stripe-style
440+
* payload; the agent API returns the bare minimum for now. Anything the
441+
* agent API doesn't expose stays undefined so the UI renders "—". */
442+
function mapBillingState(r: BillingStateResp): BillingDetails {
443+
return {
444+
status: r.subscription_status ?? 'none',
445+
current_period_end: r.next_renewal_at ?? null,
446+
razorpay_configured: r.subscription_status !== 'none',
447+
subscription_status: r.subscription_status,
448+
payment_last4: r.payment_method?.last4,
449+
payment_network: r.payment_method?.brand,
450+
cancel_at_period_end: false,
451+
}
452+
}
421453

422454
export async function fetchBilling(): Promise<{ ok: true; plan: string; billing: BillingDetails }> {
423455
try {
424-
const me = await fetchMe()
425-
return { ok: true as const, plan: me.user.tier, billing: FIXTURE_BILLING }
426-
} catch {
427-
return { ok: true as const, plan: 'hobby', billing: FIXTURE_BILLING }
456+
const r = await call<BillingStateResp>('/api/v1/billing')
457+
return { ok: true as const, plan: r.tier, billing: mapBillingState(r) }
458+
} catch (e: any) {
459+
// 503 = Razorpay unconfigured in this env (e.g. local dev without
460+
// RAZORPAY_KEY_ID). Fall back to the whoami-derived shape +
461+
// FIXTURE_BILLING so the page still renders. Any other error
462+
// propagates so the caller sees a real failure.
463+
if (e?.status === 503) {
464+
try {
465+
const me = await fetchMe()
466+
return { ok: true as const, plan: me.user.tier, billing: FIXTURE_BILLING }
467+
} catch {
468+
return { ok: true as const, plan: 'hobby', billing: FIXTURE_BILLING }
469+
}
470+
}
471+
throw e
428472
}
429473
}
430474

0 commit comments

Comments
 (0)