Skip to content

billing: add tenant billing contact fields and per-tenant controller#2902

Open
jshearer wants to merge 1 commit into
masterfrom
jshearer/billing_fields
Open

billing: add tenant billing contact fields and per-tenant controller#2902
jshearer wants to merge 1 commit into
masterfrom
jshearer/billing_fields

Conversation

@jshearer
Copy link
Copy Markdown
Contributor

@jshearer jshearer commented Apr 29, 2026

Summary

Customers cannot self-serve billing email changes today. Every request requires manual Stripe intervention. This adds admin-editable billing contact fields on tenants, a GraphQL mutation for managing them, and a per-tenant controller that reconciles the Stripe-backed subset to Stripe asynchronously.

  • Adds billing_email, billing_name, and billing_address fields to tenants, letting admins self-serve billing contact changes through a new setBillingContact GraphQL mutation instead of requesting manual Stripe edits
  • Introduces a per-tenant TenantController automation that reconciles DB-authoritative billing contact data to Stripe asynchronously
  • Updates existing customer-creation paths (createBillingSetupIntent, billing-integrations) to prefer tenant-managed billing email over JWT claims when creating new Stripe customers

How it works

The setBillingContact mutation writes to the DB. A trigger then wakes the tenant's controller task, which reads current DB state, compares against the Stripe customer, and calls update_customer_billing_profile to sync the changes to the Stripe customer record if they differ.

Migration

20260429120000_tenant_controller_billing_contact.sql:

  • Adds columns. New tenants get a controller task via an insert trigger. Existing tenants get one lazily: wake_tenant_controller creates the task on-demand if controller_task_id is null, so setBillingContact or customer-creation paths work without a pre-existing task row.
  • Any source of change to these billing fields will trigger the automation to sync to stripe
  • Backfills billing_email and billing_address from CDC-synced stripe.customers so existing data matches Stripe without triggering reconciliation. Only tenants that received billing data from this backfill get a controller task row; the rest get one on first use.

Testing

I tested this e2e in a local stack with a testmode Stripe API key

@jshearer jshearer force-pushed the jshearer/billing_fields branch 3 times, most recently from dd24bcb to 2d02827 Compare April 29, 2026 21:35
@jshearer jshearer self-assigned this Apr 29, 2026
@jshearer jshearer added change:planned This is a planned change control-plane-api Change affecting the API of control-plane, may impact the UI, flowctl, etc labels Apr 29, 2026
@jshearer jshearer marked this pull request as ready for review April 29, 2026 21:40
@jshearer jshearer force-pushed the jshearer/billing_fields branch from 2d02827 to 769438a Compare April 29, 2026 22:42
@jshearer jshearer force-pushed the jshearer/billing_graphql branch from cc73e15 to 2184f2e Compare April 29, 2026 22:42
@jshearer jshearer force-pushed the jshearer/billing_fields branch from 769438a to 4203194 Compare April 29, 2026 22:43
@jshearer jshearer added waiting This change is waiting on something else and removed change:planned This is a planned change control-plane-api Change affecting the API of control-plane, may impact the UI, flowctl, etc waiting This change is waiting on something else labels May 4, 2026
@jshearer jshearer force-pushed the jshearer/billing_graphql branch 2 times, most recently from 183cb12 to e3ba37e Compare May 4, 2026 17:35
@jshearer jshearer force-pushed the jshearer/billing_fields branch from 4203194 to 583eeca Compare May 5, 2026 15:24
@jshearer jshearer force-pushed the jshearer/billing_graphql branch 5 times, most recently from ca57448 to 98a8373 Compare May 6, 2026 20:06
@jshearer jshearer added control-plane waiting This change is waiting on something else labels May 8, 2026
@jshearer jshearer force-pushed the jshearer/billing_fields branch from 583eeca to 4d347ab Compare May 8, 2026 19:02
@jshearer jshearer force-pushed the jshearer/billing_graphql branch 3 times, most recently from 955f630 to d1317da Compare May 18, 2026 17:13
@jshearer jshearer force-pushed the jshearer/billing_graphql branch from 0b50ea6 to 29df22e Compare May 19, 2026 14:53
@jshearer jshearer force-pushed the jshearer/billing_fields branch from 4d347ab to c8fc53b Compare May 21, 2026 16:10
@jshearer jshearer changed the base branch from jshearer/billing_graphql to master May 26, 2026 16:08
@jshearer jshearer removed the waiting This change is waiting on something else label May 26, 2026
@jshearer jshearer requested a review from a team May 26, 2026 16:11
@jshearer jshearer force-pushed the jshearer/billing_fields branch 3 times, most recently from a3979b1 to 7d830f8 Compare May 27, 2026 14:38
Customers cannot self-serve billing email changes today. Every request requires manual Stripe intervention. This adds admin-editable billing contact fields on `tenants`, a GraphQL mutation for managing them, and a per-tenant controller that reconciles the Stripe-backed subset to Stripe asynchronously.

* `billing_email`, `billing_name`, `billing_address` columns on `tenants`. Insert trigger creates a controller task per tenant, update trigger wakes the controller when `billing_email` or `billing_address` change. Existing tenants are backfilled from `stripe.customers` CDC data.
* `setBillingContact` mutation writes the DB and returns immediately. `TenantBilling.contact` query field reads from the DB. No Stripe call in the request path.
* `BillingProvider::update_customer_billing_profile` for updating Stripe `Customer.email` and `Customer.address`.
* `TenantController` executor (`TaskType(12)`) with a `billing_contact` sub-controller that compares DB desired state against actual Stripe state and reconciles on mismatch, with retry backoff.
* `createBillingSetupIntent` and `billing-integrations` customer creation now prefer `billing_email` from the tenants table over the JWT user's email when creating new Stripe customers.
@jshearer jshearer force-pushed the jshearer/billing_fields branch from 7d830f8 to ceb1d56 Compare May 27, 2026 17:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant