Files
member-console/internal/billing/queries/billing_accounts.sql
T
cgalo5758 dd3962990b Adopt entity keys and add invoice numbers
Replace the entity slugs on organizations, workspaces, resource pools,
and
plan ladders with nullable `key` columns and add keys to products,
prices,
and entitlement sets. Rename `providers.slug` to `provider` and add
partial
unique indexes for system and org role names.

Assign invoice numbers per billing account from a gapless transactional
counter; Stripe's number moves to the invoice mapping as an external
reference.

Seeds, fixtures, and the operator lookup address rows by key, and the
returning-login resync no longer blanks a display name when the IdP
sends
no `name` claim.
2026-08-29 20:12:04 -05:00

86 lines
3.6 KiB
SQL

-- name: CreateBillingAccount :one
INSERT INTO core.accounts (org_id, name, status, metadata)
VALUES ($1, $2, $3, $4)
RETURNING *;
-- name: GetBillingAccountByID :one
SELECT * FROM core.accounts
WHERE billing_account_id = $1;
-- name: ListBillingAccountsByOrgID :many
SELECT * FROM core.accounts
WHERE org_id = $1
ORDER BY created_at ASC;
-- name: UpdateBillingAccountStatus :one
UPDATE core.accounts
SET status = $2
WHERE billing_account_id = $1
RETURNING *;
-- name: ListBillingAccountsPage :many
-- Operator billing-accounts view, paged and searched (operator-list-scale
-- UX-4; operator-billing-views D8). No status facet: core.accounts.status
-- carries no CHECK-enforced vocabulary and operator-list-scale's "Status
-- filters exist where a status vocabulary exists" requirement does not
-- name accounts, so this query offers search + pagination only.
--
-- sqlc.narg(q): NULL matches every row; set, a case-insensitive substring
-- match against the account's own name, or its organization via
-- sqlc.narg(org_ids) (design D3: "billing: organization name plus the
-- view's leading identifier"). Organization-name search resolves through
-- a pre-resolved ID array rather than a join to core.organizations: no
-- existing internal/billing query crosses that module boundary
-- (grep 'core\.' internal/billing/queries/ turns up none), so
-- operator_billing.go's matchingOrgIDs resolves matching org IDs in Go and
-- passes them here, mirroring the grants lane's product-name pattern.
-- count(*) OVER() carries the true total for the filtered set (design D2).
SELECT a.billing_account_id, a.org_id, a.name, a.status, a.metadata, a.created_at, a.updated_at,
count(*) OVER() AS total_count
FROM core.accounts a
WHERE (sqlc.narg(q)::text IS NULL
OR a.name ILIKE '%' || sqlc.narg(q)::text || '%'
OR a.org_id = ANY(sqlc.narg(org_ids)::uuid[]))
ORDER BY a.created_at DESC
LIMIT sqlc.arg(page_limit)::int OFFSET sqlc.arg(page_offset)::int;
-- name: GetLatestSubscriptionByBillingAccountID :one
-- Most recent subscription for a billing account (any status). Used by the
-- per-org composite's billing-summary section.
SELECT * FROM core.subscriptions
WHERE billing_account_id = $1
ORDER BY created_at DESC
LIMIT 1;
-- name: GetLatestInvoiceByBillingAccountID :one
-- Most recent invoice for a billing account (any status). Used by the
-- per-org composite's billing-summary section.
SELECT * FROM core.invoices
WHERE billing_account_id = $1
ORDER BY created_at DESC
LIMIT 1;
-- name: GetOutstandingBalanceByBillingAccountID :one
-- Sum of (amount_due - amount_paid) across invoices with status='open' — the
-- only actionable non-terminal invoice status per design/billing/model.md.
-- Returns 0 if no open invoices exist.
SELECT COALESCE(SUM(amount_due - amount_paid), 0)::BIGINT AS outstanding_balance
FROM core.invoices
WHERE billing_account_id = $1
AND status = 'open';
-- name: AssignNextInvoiceNumber :one
-- The transactional, gapless counter invoice-numbers D1 specifies: the
-- account's next_invoice_number is incremented and the prior value
-- returned in the same statement, so the caller runs this inside the same
-- transaction that inserts the invoice row (invoice.finalized projection).
-- Never a Postgres sequence — a sequence burns values on rollback, and this
-- counter must not: a projection that fails after this call rolls back
-- with the transaction, leaving the counter exactly where it was. Row
-- locking is implicit in the UPDATE and serializes only within one
-- account.
UPDATE core.accounts
SET next_invoice_number = next_invoice_number + 1, updated_at = NOW()
WHERE billing_account_id = $1
RETURNING next_invoice_number - 1 AS assigned;