Billing operations β
Audience
This article is for GCM platform operators. Church admins should see the per-org Giving module for their congregation's donations β this page is about subscription payments to GCM.
The Billing operations tab (/platform/billing-ops) is where staff resolve everything money-shaped: a card that declined this morning, a customer who wants their last invoice refunded, a renewal that needs to be pushed a week, a missing invoice PDF. Everything here is gated by superadmin or platform_admin and every write lands in platform_audit_log with a mandatory reason.

The cron-health banner β
If you ever see a red banner at the top reading "Recurring billing is not scheduled", stop and fix it before doing anything else. It means the charge_recurring job is not registered in pg_cron, so no renewals will fire until someone re-runs the cron migration. The banner only renders when the dashboard RPC reports cron_health.charge_recurring_scheduled = false β it is the single most important alert on the page.
The five summary cards β
- Estimated MRR β the same number as the Organisations KPI strip. Formatted as USD.
- Due today β orgs with
subscription_status = 'active'andnext_billing_date <= today. These will be charged on the next cron tick. - Past due β
subscription_status = 'past_due'. The retry counter on each is visible in the table. - Failed attempts 7d β
payment_sessionswith terminal error status in the last week. A spike here usually means a gateway outage; check Status from the gateway provider. - Refunds 30d β count of
payment_historyrows withstatus = 'refunded'in the last 30 days.
The four sub-tabs β
Subscriptions β
The default view. Every org with billing data, with their plan, status, next billing date, retry posture, card on file, and last successful payment. The four action buttons per row are:
- Details β opens the right-hand sheet with a fuller breakdown, the last few payments, and recent gateway attempts for that org alone.
- Settings gear β opens the Edit subscription dialog (covered below).
- Refresh icon β opens the Renew / retry dialog.
- Open β closes this tab and pushes the standard org detail sheet from the Organisations page, so you can jump into impersonation or staff management.
Payments β
The payment ledger. Every row in payment_history across the platform, newest first. Click the file icon to (re)generate the invoice PDF β it calls adminGenerateInvoice which writes a signed URL into the row. Click the rotate-counter-clockwise icon to open the standard Refund modal; it is the same component the per-org giving flow uses, so partial / full / off-platform refunds all work the same way.
A refund is only available when the payment is in approved status and the amount is greater than zero. Refunded and pending payments grey the button out.
Attempts β
A redacted view of payment_sessions β every transaction attempt, successful or not, with the gateway response code and message. This is the first place to look when "the customer says they were charged twice": each attempt has an order_identifier you can correlate with the gateway's dashboard.
Timeline β
A scrollable audit feed scoped to billing actions: subscription edits, renewals fired, refunds processed, invoices generated. It is a filtered slice of platform_audit_log joined to org names β useful when a customer asks "what changed on our account between Tuesday and Friday?".
Editing a subscription β
The Edit subscription dialog is the override panel. Every field is editable but Reason is required (minimum 3 characters) and is appended to the audit row alongside your email. Fields:
- Plan and Status β same enum values as the Organisations tab.
- Trial end / Next billing / Billing cycle start β date pickers. Used to push or pull the next charge.
- Billing cycle day β 1-31. If the renewal day differs from the start date (e.g. signed up on the 14th but bills on the 1st).
- Member limit β free-form integer. Use this when a plan upgrade is not appropriate but the customer needs headroom.
- Retry count / Retry date β set to zero and clear the date to stop the retry storm on a declined card.
- Pause until β sets the resume date when a customer asks to suspend service.
- Send customer email β toggle. Off by default; turn on when the change is customer-visible (e.g. plan downgrade).
Save calls adminUpdateSubscription which both writes the new columns and inserts an audit event.
Renewing or retrying β
The Renew / retry dialog has two modes:
- Preview only (default) β runs the renewal logic in dry-run. The gateway is not called; the response shows what would happen and is dumped into a
<pre>block for inspection. Use this whenever you are unsure about a card's state. - Charge now β actually invokes the gateway with the stored payment token. The button turns red so you cannot mis-click it. A reason field is required and gets stamped onto the resulting payment-history row.
The dialog hits adminTriggerRenewal with a fresh requestId so a duplicate click cannot double-charge β the function dedupes server-side on that key.
Off-cycle charges
Charging outside the normal billing day is fine, but it shifts the renewal cadence: the next next_billing_date will be computed from today, not from the previously scheduled date. If you want to keep the cycle aligned, edit next_billing_date back manually in the Edit subscription dialog after the charge succeeds.
Refunds β
The Refund modal is shared with the per-org giving flow but always operates in platform-admin mode here. You can issue:
- Gateway refund β calls the gateway's refund endpoint with the original transaction ID. Funds return to the customer's card.
- Off-platform refund β records a refund row without touching the gateway. Use when you have already refunded the customer via wire / check and just need the ledger to reflect it. The
manualMethodandreferenceNumberfields are required so the auditor can trace it.
A successful refund writes to payment_history (status becomes refunded), updates the related payment_sessions row, and inserts a platform_audit_log entry with the reason.
Common situations β
"The customer's card declined three nights in a row and they want to know why." Open Details on their row, scroll to recent attempts, and read the gateway response message. 99% of the time it is INSUFFICIENT_FUNDS, EXPIRED_CARD, or DO_NOT_HONOR. Open the org's account (impersonate) and have them update the card via the customer-facing billing page.
"We need to push the renewal a week so the customer can wire-transfer." Edit subscription -> set next_billing_date to today + 7. Reset billing_retry_count to 0 and clear billing_retry_at so the retry storm stops. Reason: "wire-transfer arrangement, T-7".
"The invoice PDF is missing." Payments tab -> find the row -> click the file icon. adminGenerateInvoice re-renders the PDF, uploads it to the invoices bucket, and stamps the signed URL on the row.
"This morning's cron didn't run." Check the red banner first. If it is not showing but you suspect it, look at the Due Today number an hour after the usual run time β if it has not dropped, the job did not fire. The fix is to re-run the pg_cron migration; see the runbook in docs/runbooks/.
