Fetch billing invoices via GraphQL#2004
Open
GregorShear wants to merge 16 commits into
Open
Conversation
This was referenced Jun 16, 2026
* Theme the Stripe payment form for dark mode * gql api changed - running codegen
45430fd to
9209897
Compare
* Migrate refresh token UI from PostgREST to GraphQL * rework refresh token UI * clean up * Adopt gql updates * remove validFor from refresh token query - not exposed anymore * Regenerate gql-types against current schema Resyncs the generated GraphQL types with the live schema after rebasing onto main. Drops the service-account schema (ApiKeyInfo, ServiceAccount, createServiceAccount, revokeApiKey, serviceAccounts, the ManageServiceAccounts capability bit, TenantFilter) that an earlier codegen run had incidentally captured, and removes validFor from the RefreshTokenInfo output type and the RefreshTokens query, which the server no longer exposes. * Clean up * intl strings * address review feedback on refresh token table - Gate create dialog dismissal on the in-flight create state so backdrop/Escape can't close mid-request and orphan the new token's secret - Realign table headers with their cells: add a Uses header and move Status over the expired column - Suppress the empty-state message on load error so it no longer renders beneath the error banner - Recover from an empty non-first page with a useEffect (mirrors AccessLinksTable) instead of the row-level page-back heuristic, and drop the now-unused onRevoked prop; revoke marks a token expired rather than removing it, so a revoke never empties the page
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.
The Recent History table was hard-capped at the 4 most recent invoices within the rolling six-month window (a slice(0, 4) placeholder). Show all of a tenant's invoices instead, paginated four per page (PRD #2). - useBillingInvoices now exposes allInvoices (the full newest-first list) alongside invoices (the windowed+manual subset the usage graphs chart). selectedInvoice resolves against the full list, so any row is clickable. - BillingHistoryTable paginates allInvoices with MUI TablePagination + the shared TablePaginationActions, resetting to the first page when the tenant changes. Rows no longer slices. The usage graphs and graph-state wrapper still read the windowed invoices, so they remain a six-month view. Fetch is capped at 100 invoices; true cursor pagination beyond that is a later step.
The GraphQL invoice node carries the Stripe-sourced fields inline (status, invoicePdf, hostedInvoiceUrl, paymentDetails.receiptUrl), so the line-items view no longer needs a separate getTenantInvoice round-trip to the billing edge function to render the download/pay/receipt buttons. - Add those fields to the TenantBillingInvoices query and carry them onto the Invoice shape via the hook mapping. - BillLineItems reads them off the selected invoice directly: drops the getTenantInvoice fetch, the stripeInvoice state, and the loading skeletons. - Add a "View Receipt" button shown when the invoice has a receipt URL (PRD #3 — receipts for paid invoices). - Remove the now-unused getTenantInvoice / StripeInvoice from the billing API. Verified locally that the new fields are requested and the page renders (seeded data has no Stripe linkage, so the fields come back null and the buttons degrade to disabled, with no getTenantInvoice call). Populated PDF/receipt rendering needs a Stripe-enabled environment.
When the selected invoice has no Stripe artifact (no PDF, hosted page, receipt, or status), replace the disabled Download/Pay buttons with a single 'No invoice available' button rather than two dead controls. The external invoice links (Download PDF, Pay Invoice, View Receipt) now open in a new tab (component=a + target=_blank, rel=noopener noreferrer) so they don't navigate the user out of the app.
BillingHistoryTable reset to the first page whenever the allInvoices array changed reference. The billing query is keyless and revalidates on the 30s requestPolicy TTL, so a background refetch handed back a new but identical array and bounced the user back to page one. Key the reset on the selected tenant instead, the only case where returning to the newest invoices is wanted, and expose selectedTenant from useBillingInvoices. The existing currentPage clamp still keeps the view in range when the invoice count changes.
In narrow layouts the End Date wrapped to two lines because the combined Data / Tasks cell was set to nowrap and claimed the available width. Let the Data / Tasks cell wrap instead, and set the date cell to nowrap so the End Date stays on one line.
Move the payment-methods admin flow off the Supabase `billing` edge function onto GraphQL. A new useBillingPaymentMethods hook reads the list via tenant.billing.paymentMethods and wraps the createBillingSetupIntent, setBillingPaymentMethod, and deleteBillingPaymentMethod mutations, so the Add dialog's SetupIntent secret and the set-primary/delete actions now go through GraphQL. The edge function is kept only for the multi-tenant trial-warning batch (getPaymentMethodsForTenants); the now-dead REST exports are removed. Table and UX changes: - Extract the Add Payment Method button out of the dialog; it is full-width and stacked under the header on narrow screens. - Replace the Primary column and the per-row Make Primary / Delete buttons with a single actions column: a star (filled = primary, outline-on-hover = make primary) and a hover-revealed red trash icon, each with a tooltip. - Set-primary updates optimistically and rolls back if the mutation fails; the list is only refetched on add/delete, not on a primary toggle. - Confirm before deleting a payment method, and reload the list before dismissing the dialog. - Center the Last 4 / Exp / actions columns and slim PaymentMethodProps to the fields actually rendered. Regenerate gql-types for the new operations.
2b0cf99 to
6a0159b
Compare
Replace FormattedMessage/useIntl usage across the billing admin page, graphs, and invoice/payment-method tables with inline English copy, and remove the message keys that this orphans from AdminPage.ts. Swap intl.formatDate/FormattedDate for date-fns format and intl.formatNumber for native toLocaleString, dropping react-intl from the graph, totals, and timestamp components entirely. Reproduce the taskHoursByMonth ICU plural as a JS ternary. Unwind the TimeStamp tooltipMessageId indirection into a getTooltip callback and the PaymentMethods column headers into literal labels. Table column headers and empty-state messages rendered by the shared EntityTableHeader/EntityTableBody still use message keys, since those components are outside this change and shared across many tables.
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.
What
Swap the transport behind
useBillingInvoicesfrom the PostgRESTinvoices_extview to the GraphQLtenant(name).billing.invoicesconnection. Internal to the hook — no consumer changes.Changes
TenantBillingInvoicesquery (src/api/gql/billing.ts).useBillingInvoicesmaps the GQL node back to the existingInvoiceshape:billed_prefix= the queried tenant, lowercasedinvoiceType, JSONlineItems/extracast to the established types. Money stays integer cents.InvoiceFiltercan't express the old "six-month window OR any manual invoice, newest-first" predicate, so that filter + sort are reproduced client-side over a generous fetch limit.Tenant/TenantBilling/Invoiceas keyless in the urql graphcache (these types have noid), matching the existing convention.getInvoicesBetweenPostgREST query.Verification
Validated locally against seeded data (final / preview / manual invoices):
line_itemsJSON; money formats correctly;extrausage drives the history columns and the usage graphextra: nullon manual invoices renders without crashingStacked on
greg/billing-tenant-keyed-fetch.