Skip to content

Migrate billing invoices to GraphQL and decouple from the Billing store#2012

Open
GregorShear wants to merge 3 commits into
mainfrom
greg/billing-invoices-gql-narrow
Open

Migrate billing invoices to GraphQL and decouple from the Billing store#2012
GregorShear wants to merge 3 commits into
mainfrom
greg/billing-invoices-gql-narrow

Conversation

@GregorShear

Copy link
Copy Markdown
Contributor

What this does

Two changes to the billing admin page, kept tightly scoped:

  1. Retires PostgREST from billing. The invoice history was read from the invoices_ext view via PostgREST (getInvoicesBetween). It now comes from the tenant(name).billing.invoices GraphQL connection. This was billing's only direct PostgREST read.
  2. Decouples billing data from the Billing store. Invoices were hydrated imperatively into a Zustand store and reset on tenant change. They now flow through a tenant-keyed hook (useBillingInvoices), so switching orgs re-fetches automatically and never shows the previous tenant's data.

The second change is what unblocks the tenant/prefixSelector refactor: TenantOptions no longer needs to pass updateStoreState to reset the store on tenant switch, so it drops to a bare <TenantSelector />. Billing was the only caller passing that prop, so it's now dead — the refactor can remove it as its first step. (Left in place here to keep this PR purely about billing.)

How it works

  • useBillingInvoices queries invoices via urql, keyed on the selected tenant. urql caches on the query variables, so revisiting a tenant is instant and the previous tenant's data is never shown.
  • The GraphQL InvoiceFilter is narrower than the old PostgREST predicate (no "window OR manual" clause), so the six-month-window + manual-invoice filter and the newest-first sort are reproduced client-side over a generous fetch limit.
  • The GQL invoice node is camelCased, returns invoiceType as an enum, and omits billed_prefix (the tenant is the query parent). The hook maps each node back to the existing Invoice shape, so no consumer of the hook changed behavior — the history table, line-items table, usage graphs, and graph-state wrapper all keep reading through it.
  • Tenant, TenantBilling, and Invoice are registered as keyless in the urql cache (they have no stable id).

Out of scope (deferred)

This branch is the narrow slice of a larger billing branch. Deliberately excluded, to be split into follow-ups:

  • Payment methods edge-function → GraphQL migration and the actions-column UI rework
  • "Show all invoices" + table pagination
  • Surfacing invoice PDF/receipt links from the GQL node (the line-items PDF/Pay buttons still use the existing getTenantInvoice edge function, unchanged)
  • Layout changes (Grid → Stack, column wrapping) and inline react-intl strings

Verification

  • npm run typecheck — clean
  • npm run lint — clean
  • No getInvoicesBetween / invoices_ext references remain in billing

Needs a runtime check before merge

Confirm the GraphQL billing.invoices connection returns the same set as the old invoices_ext view — specifically the in-progress preview invoice and manual invoices, which the usage graphs and current-month line items depend on. Worth checking against a tenant that has a current open invoice plus a historical manual one.

Billing invoices are now fetched through a tenant-keyed SWR hook
(useBillingInvoices) instead of an imperative fetch into the Billing
store. Because the SWR key derives from the selected tenant and the
rolling six-month window, switching orgs re-fetches automatically and
never shows the previous tenant's data — so the store no longer needs to
be manually reset when the tenant changes.

Removes the useTenantChangeReset hook, the unmount reset in AdminBilling,
and the StoreWithHydration machinery (invoices, active/hydrated/
networkFailed/hydrationErrorsExist, resetState) from the Billing store,
which is now just the invoice selection and paymentMethodExists. The
selected invoice resolves to the stored selection or falls back to the
newest invoice, so a stale selection from another tenant self-corrects.

All invoice/loading/error reads across the billing page, history table,
line-items table, usage graphs, and graph-state wrapper now come from the
hook. SWR dedupes the shared key to a single request, and revisiting a
tenant is served instantly from cache.
Swap the transport behind useBillingInvoices from the PostgREST
invoices_ext view to the GraphQL tenant(name).billing.invoices
connection. The hook's public shape is unchanged, so no consumer is
touched — the invoice list, history table, line-items table, usage
graphs, and graph-state wrapper all keep reading through the hook.

The GQL invoice node is camelCased, returns invoiceType as an enum, and
omits billed_prefix (the tenant is the query parent), so the hook maps
each node back to the existing Invoice shape (billed_prefix = the queried
tenant, lowercased invoice_type, JSON line items/extra cast to the
established types). Money fields stay integer cents, matching the
existing /100 display.

GraphQL's InvoiceFilter is narrower than the old PostgREST predicate (no
"window OR manual" clause, only gt/lt date bounds), so the six-month
window + manual-invoice filter and the newest-first sort are reproduced
client-side over a generous fetch limit. urql keys its cache on the query
variables, preserving the tenant-keyed refetch and instant cached
revisits. Removes the now-unused getInvoicesBetween PostgREST query.

Note: the line-item/extra JSON field shapes are assumed identical to the
PostgREST payload (same data source); pending runtime verification.
The TenantBillingInvoices query selects Tenant, TenantBilling, and Invoice,
none of which expose an id/_id, so graphcache logged 'Invalid key' warnings
and fell back to embedding them on the parent. Add the three types to the
cacheExchange keys config returning null (the same convention already used
for Alert/LiveSpecRef/etc.), making the embed-on-parent behavior explicit
and silencing the warnings. Read-only billing data fetched per tenant, so
normalization isn't needed.
@GregorShear GregorShear requested a review from a team as a code owner June 21, 2026 11:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant