feat(dashboard): self-serve Stripe billing, free-credit accounting, and quota guard#141
Draft
JacobZuliani wants to merge 7 commits into
Draft
feat(dashboard): self-serve Stripe billing, free-credit accounting, and quota guard#141JacobZuliani wants to merge 7 commits into
JacobZuliani wants to merge 7 commits into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The dashboard had no self-serve path for a customer to hand us a payment method — adding a card meant a human touching Firestore. The "Billing" tab only showed usage and had no link to the mechanism that's supposed to stop cluster creation once free credits run out. Separately, saving a cluster config above the customer's per-region machine-type quota succeeded in the UI and then failed opaquely at boot time.
This PR wires three tracks that together make the dashboard a real self-serve front door.
What changes
1. Stripe self-serve billing (
main_service/endpoints/settings.py,BillingPortalSettings.tsx)Settings gains a third tab, Billing, visible once a payment method is on file (or from the free-credits prompt). It opens Stripe Checkout (
mode=setup) in a new tab to collect a card and returns to the dashboard via/billing/confirm-setup-session, which resolves the setup intent → payment method and writes the result to Firestorebilling/billing(stripe_customer_id,has_payment_method,default_payment_method_id,billing_email). Subsequent updates go through Stripe's Customer Portal via/billing/portal-session[/redirect], scoped to the payment-method-update flow./billing/webhookmirrorscheckout.session.completed,setup_intent.succeeded, andpayment_method.attachedinto the same doc. The Stripe secret is pulled from Secret Manager (stripe_secret_key_testby default, overridable viaSTRIPE_SECRET_KEY_SECRET_NAME; project fallback chainBURLA_BILLING_SECRETS_PROJECT_ID→burla-prod→PROJECT_ID).2. Free-credit accounting and "free credits used" modal (
main_service/endpoints/cluster.py,UsageSettings.tsx,UsageContext.tsx)New
monthly_usage/{YYYY-MM}Firestore collection caches each month'sspend_dollars,usage_hours,credits_applied_dollars,billable_spend_dollars, andstatus(openfor the current month,closedotherwise)._reconcile_monthly_usage_credits_from_scratchwalks months chronologically and appliesdiscount_credit_usdgreedily, then flips the cluster-widecreditsflag off once credits are exhausted and a payment method is on file./v1/nodes/daily_hoursnow returns this summary (credits,has_payment_method,credits_usd,credits_used_usd,remaining_free_credit_usd,monthly_usage_hours,monthly_spend_dollars) alongside the daily breakdown so the dashboard can render:credits == true && has_payment_method == false && remaining_free_credit_usd <= 0, with a single "Add payment method" CTA that starts the Stripe setup flowNew endpoints:
POST /v1/billing/reconcile-credits,GET /v1/billing/invoiceable-months,POST /v1/settings/credits/disable.3. Quota guard on settings save (
main_service/endpoints/settings.py,useSaveSettings.ts,Settings.tsx)POST /v1/settingsnow readscluster_quota/cluster_quota.{region}.machine_type_limits.{machine_type}and rejects any save that exceeds it with a structured 400:{ "detail": { "error_code": "quota_exceeded", "message": "Quota exceeded. Limit for a3-highgpu-8g in us-central1 is 2. ...", "machine_type": "a3-highgpu-8g", "region": "us-central1", "limit": 2, "requested": 8 } }The frontend catches
quota_exceededspecifically and either:limitwhenlimit >= 1(so the user can retry with the allowed amount), orlimit == 0(no quota at all for that config)…and renders a "GCP Quota Limit Reached" dialog that names the configuration, region, requested count, available quota, and how to get it raised.
Smaller things rolled in
SettingsFormandconstants.tsswitch to"X vCPUs / Y GB RAM"(from"XvCPU / YG RAM")._as_utc_datetimenow tolerates epoch values in seconds, milliseconds, microseconds, nanoseconds, and numeric strings — the previous heuristic mis-scaled anything between2e9and2e12.Test plan
billing/billingdoc. Settings → Usage → "Free credits used" modal appears once credits hit zero; Add payment method → Stripe Checkout in test mode → return to dashboard; billing doc gainsstripe_customer_id,has_payment_method: true,default_payment_method_id;cluster_config.creditsflips tofalse.default_payment_method_idvia webhook.POST /v1/settingswith{machineType: "a3-highgpu-8g", gcpRegion: "us-central1", machineQuantity: 999}against a quota of{us-central1: {machine_type_limits: {"a3-highgpu-8g": 2}}}returns 400 witherror_code: "quota_exceeded",limit: 2; frontend caps the quantity field to 2 and shows the dialog.0for that config reverts hardware fields to the last-saved snapshot./v1/nodes/daily_hoursresponse includescredits,has_payment_method,credits_usd,credits_used_usd,remaining_free_credit_usd,monthly_usage_hours,monthly_spend_dollars.POST /v1/billing/reconcile-creditstwice in a row produces the samecredits_used_usdand does not double-apply credits to any month.setup_intent.succeededwith no prior billing doc still writeshas_payment_method: trueand flipscredits: false.Made with Cursor