Governed operator lists (organizations, grants, people, billing×4) gain
server-side search, status filters, and 50-row pages with true totals
from count(*) OVER(); state is URL-addressable, out-of-range pages
clamp,
and no-match is distinct from true-empty.
People is the eighth flat sidebar entry: /operator/persons lists persons
newest-joined first (excluding the reserved system person), rows linking
to the existing detail.
Billing gains an operator invoice detail at
/operator/billing/invoices/{invoiceID} reusing the member projection;
open invoices past due present as Overdue (derived, filterable, stored
status untouched); all four views lead with the linked organization and
mute object IDs.
Grants filter over the derived Live/Superseded/Inactive state, the SQL
HAVING predicate pinned to the Go derivation by test. Embedded lists
(org composite ledger, Tier changes) adopt the shared controls under
namespaced params with sibling-state-preserving URLs and scoped htmx
swaps that hold the viewport.
Review corrections: blocked ladder Delete renders disabled with tooltip
and mutations fire toasts; collapse triggers paint their open state;
sections use outside headings; plan topology drops the orphan-product
check; domains policy collapses behind a disclosure.
490 lines
14 KiB
Go
490 lines
14 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.29.0
|
|
// source: invoices.sql
|
|
|
|
package billing
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/lib/pq"
|
|
)
|
|
|
|
const countOpenInvoices = `-- name: CountOpenInvoices :one
|
|
SELECT COUNT(*) FROM core.invoices
|
|
WHERE status = 'open'
|
|
`
|
|
|
|
// Receivables headline for the operator landing surface. `open` is the only
|
|
// actionable non-terminal invoice status (design/billing/model.md): draft is
|
|
// not yet issued, and paid/void/uncollectible/refunded are settled or written
|
|
// off. A count, not a balance sum — currency is per-row, and a cross-currency
|
|
// sum would be a number the tile could not stand behind.
|
|
func (q *Queries) CountOpenInvoices(ctx context.Context) (int64, error) {
|
|
row := q.db.QueryRowContext(ctx, countOpenInvoices)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
const createInvoice = `-- name: CreateInvoice :one
|
|
INSERT INTO core.invoices (
|
|
billing_account_id, subscription_id, status,
|
|
amount_due, amount_paid, currency,
|
|
period_start, period_end, due_date
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
RETURNING invoice_id, billing_account_id, subscription_id, status, amount_due, amount_paid, currency, period_start, period_end, due_date, paid_at, voided_at, created_at, updated_at
|
|
`
|
|
|
|
type CreateInvoiceParams struct {
|
|
BillingAccountID string `json:"billing_account_id"`
|
|
SubscriptionID uuid.NullUUID `json:"subscription_id"`
|
|
Status string `json:"status"`
|
|
AmountDue int32 `json:"amount_due"`
|
|
AmountPaid int32 `json:"amount_paid"`
|
|
Currency string `json:"currency"`
|
|
PeriodStart sql.NullTime `json:"period_start"`
|
|
PeriodEnd sql.NullTime `json:"period_end"`
|
|
DueDate sql.NullTime `json:"due_date"`
|
|
}
|
|
|
|
func (q *Queries) CreateInvoice(ctx context.Context, arg CreateInvoiceParams) (Invoice, error) {
|
|
row := q.db.QueryRowContext(ctx, createInvoice,
|
|
arg.BillingAccountID,
|
|
arg.SubscriptionID,
|
|
arg.Status,
|
|
arg.AmountDue,
|
|
arg.AmountPaid,
|
|
arg.Currency,
|
|
arg.PeriodStart,
|
|
arg.PeriodEnd,
|
|
arg.DueDate,
|
|
)
|
|
var i Invoice
|
|
err := row.Scan(
|
|
&i.InvoiceID,
|
|
&i.BillingAccountID,
|
|
&i.SubscriptionID,
|
|
&i.Status,
|
|
&i.AmountDue,
|
|
&i.AmountPaid,
|
|
&i.Currency,
|
|
&i.PeriodStart,
|
|
&i.PeriodEnd,
|
|
&i.DueDate,
|
|
&i.PaidAt,
|
|
&i.VoidedAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getInvoiceByID = `-- name: GetInvoiceByID :one
|
|
SELECT invoice_id, billing_account_id, subscription_id, status, amount_due, amount_paid, currency, period_start, period_end, due_date, paid_at, voided_at, created_at, updated_at FROM core.invoices
|
|
WHERE invoice_id = $1
|
|
`
|
|
|
|
func (q *Queries) GetInvoiceByID(ctx context.Context, invoiceID string) (Invoice, error) {
|
|
row := q.db.QueryRowContext(ctx, getInvoiceByID, invoiceID)
|
|
var i Invoice
|
|
err := row.Scan(
|
|
&i.InvoiceID,
|
|
&i.BillingAccountID,
|
|
&i.SubscriptionID,
|
|
&i.Status,
|
|
&i.AmountDue,
|
|
&i.AmountPaid,
|
|
&i.Currency,
|
|
&i.PeriodStart,
|
|
&i.PeriodEnd,
|
|
&i.DueDate,
|
|
&i.PaidAt,
|
|
&i.VoidedAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getInvoicesByBillingAccountID = `-- name: GetInvoicesByBillingAccountID :many
|
|
SELECT invoice_id, billing_account_id, subscription_id, status, amount_due, amount_paid, currency, period_start, period_end, due_date, paid_at, voided_at, created_at, updated_at FROM core.invoices
|
|
WHERE billing_account_id = $1
|
|
ORDER BY created_at DESC
|
|
`
|
|
|
|
func (q *Queries) GetInvoicesByBillingAccountID(ctx context.Context, billingAccountID string) ([]Invoice, error) {
|
|
rows, err := q.db.QueryContext(ctx, getInvoicesByBillingAccountID, billingAccountID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []Invoice{}
|
|
for rows.Next() {
|
|
var i Invoice
|
|
if err := rows.Scan(
|
|
&i.InvoiceID,
|
|
&i.BillingAccountID,
|
|
&i.SubscriptionID,
|
|
&i.Status,
|
|
&i.AmountDue,
|
|
&i.AmountPaid,
|
|
&i.Currency,
|
|
&i.PeriodStart,
|
|
&i.PeriodEnd,
|
|
&i.DueDate,
|
|
&i.PaidAt,
|
|
&i.VoidedAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listInvoicesPage = `-- name: ListInvoicesPage :many
|
|
SELECT
|
|
i.invoice_id,
|
|
i.billing_account_id,
|
|
ba.org_id,
|
|
i.subscription_id,
|
|
i.status,
|
|
i.amount_due,
|
|
i.amount_paid,
|
|
i.currency,
|
|
i.period_start,
|
|
i.period_end,
|
|
i.due_date,
|
|
i.paid_at,
|
|
i.voided_at,
|
|
i.created_at,
|
|
i.updated_at,
|
|
ba.name as billing_account_name,
|
|
count(*) OVER() AS total_count
|
|
FROM core.invoices i
|
|
JOIN core.accounts ba ON i.billing_account_id = ba.billing_account_id
|
|
WHERE ($1::text IS NULL
|
|
OR ba.name ILIKE '%' || $1::text || '%'
|
|
OR ba.org_id = ANY($2::uuid[]))
|
|
AND ($3::text IS NULL
|
|
OR ($3::text = 'overdue'
|
|
AND i.status = 'open' AND i.due_date IS NOT NULL AND i.due_date < now())
|
|
OR ($3::text <> 'overdue' AND i.status = $3::text))
|
|
ORDER BY i.created_at DESC
|
|
LIMIT $5::int OFFSET $4::int
|
|
`
|
|
|
|
type ListInvoicesPageParams struct {
|
|
Q sql.NullString `json:"q"`
|
|
OrgIds []string `json:"org_ids"`
|
|
Status sql.NullString `json:"status"`
|
|
PageOffset int32 `json:"page_offset"`
|
|
PageLimit int32 `json:"page_limit"`
|
|
}
|
|
|
|
type ListInvoicesPageRow struct {
|
|
InvoiceID string `json:"invoice_id"`
|
|
BillingAccountID string `json:"billing_account_id"`
|
|
OrgID string `json:"org_id"`
|
|
SubscriptionID uuid.NullUUID `json:"subscription_id"`
|
|
Status string `json:"status"`
|
|
AmountDue int32 `json:"amount_due"`
|
|
AmountPaid int32 `json:"amount_paid"`
|
|
Currency string `json:"currency"`
|
|
PeriodStart sql.NullTime `json:"period_start"`
|
|
PeriodEnd sql.NullTime `json:"period_end"`
|
|
DueDate sql.NullTime `json:"due_date"`
|
|
PaidAt sql.NullTime `json:"paid_at"`
|
|
VoidedAt sql.NullTime `json:"voided_at"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
BillingAccountName string `json:"billing_account_name"`
|
|
TotalCount int64 `json:"total_count"`
|
|
}
|
|
|
|
// Operator invoices view, paged/searched/filtered (operator-list-scale
|
|
// UX-4; operator-billing-views D5/D8). Carries ba.org_id so the handler can
|
|
// resolve and link the organization without a cross-module join (see
|
|
// billing_accounts.sql's ListBillingAccountsPage comment for why).
|
|
//
|
|
// sqlc.narg(q): NULL matches every row; set, a case-insensitive substring
|
|
// match against the billing account's name (the view's leading identifier
|
|
// pre-reorder) or its organization via sqlc.narg(org_ids), pre-resolved in
|
|
// Go (operator_billing.go: matchingOrgIDs).
|
|
// sqlc.narg(status): NULL matches every status. The stored status
|
|
// vocabulary (design/data-model.md: draft, open, paid, void,
|
|
// uncollectible, refunded -- core.invoices carries no CHECK constraint,
|
|
// this is the documented closed set) matches exactly; the sentinel value
|
|
// 'overdue' instead selects the derived predicate design D5 defines --
|
|
// status = 'open' AND due_date IS NOT NULL AND due_date < now() -- so the
|
|
// Overdue filter's total is computed in SQL, never after LIMIT (the same
|
|
// predicate operator_billing.go's invoiceIsOverdue applies in Go for
|
|
// per-row presentation; a test pins their agreement).
|
|
// count(*) OVER() carries the true total for the filtered set (design D2).
|
|
func (q *Queries) ListInvoicesPage(ctx context.Context, arg ListInvoicesPageParams) ([]ListInvoicesPageRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, listInvoicesPage,
|
|
arg.Q,
|
|
pq.Array(arg.OrgIds),
|
|
arg.Status,
|
|
arg.PageOffset,
|
|
arg.PageLimit,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListInvoicesPageRow{}
|
|
for rows.Next() {
|
|
var i ListInvoicesPageRow
|
|
if err := rows.Scan(
|
|
&i.InvoiceID,
|
|
&i.BillingAccountID,
|
|
&i.OrgID,
|
|
&i.SubscriptionID,
|
|
&i.Status,
|
|
&i.AmountDue,
|
|
&i.AmountPaid,
|
|
&i.Currency,
|
|
&i.PeriodStart,
|
|
&i.PeriodEnd,
|
|
&i.DueDate,
|
|
&i.PaidAt,
|
|
&i.VoidedAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.BillingAccountName,
|
|
&i.TotalCount,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listRecentInvoices = `-- name: ListRecentInvoices :many
|
|
SELECT
|
|
i.invoice_id,
|
|
i.billing_account_id,
|
|
ba.org_id,
|
|
i.amount_due,
|
|
i.amount_paid,
|
|
i.currency,
|
|
i.status,
|
|
i.created_at
|
|
FROM core.invoices i
|
|
JOIN core.accounts ba ON ba.billing_account_id = i.billing_account_id
|
|
ORDER BY i.created_at DESC
|
|
LIMIT $1
|
|
`
|
|
|
|
type ListRecentInvoicesRow struct {
|
|
InvoiceID string `json:"invoice_id"`
|
|
BillingAccountID string `json:"billing_account_id"`
|
|
OrgID string `json:"org_id"`
|
|
AmountDue int32 `json:"amount_due"`
|
|
AmountPaid int32 `json:"amount_paid"`
|
|
Currency string `json:"currency"`
|
|
Status string `json:"status"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// Recent invoices for the operator landing activity timeline. Joins to
|
|
// core.accounts to resolve the org_id (same schema, safe for sqlc).
|
|
func (q *Queries) ListRecentInvoices(ctx context.Context, limit int32) ([]ListRecentInvoicesRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, listRecentInvoices, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListRecentInvoicesRow{}
|
|
for rows.Next() {
|
|
var i ListRecentInvoicesRow
|
|
if err := rows.Scan(
|
|
&i.InvoiceID,
|
|
&i.BillingAccountID,
|
|
&i.OrgID,
|
|
&i.AmountDue,
|
|
&i.AmountPaid,
|
|
&i.Currency,
|
|
&i.Status,
|
|
&i.CreatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const sumOpenInvoiceBalanceByCurrency = `-- name: SumOpenInvoiceBalanceByCurrency :many
|
|
SELECT currency, SUM(amount_due - amount_paid)::bigint AS outstanding_cents
|
|
FROM core.invoices
|
|
WHERE status = 'open'
|
|
GROUP BY currency
|
|
ORDER BY outstanding_cents DESC
|
|
`
|
|
|
|
type SumOpenInvoiceBalanceByCurrencyRow struct {
|
|
Currency string `json:"currency"`
|
|
OutstandingCents int64 `json:"outstanding_cents"`
|
|
}
|
|
|
|
// Outstanding receivables for the Open invoices caption, per currency —
|
|
// the caller applies the same largest-bucket display rule as the Monthly
|
|
// recurring headline.
|
|
func (q *Queries) SumOpenInvoiceBalanceByCurrency(ctx context.Context) ([]SumOpenInvoiceBalanceByCurrencyRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, sumOpenInvoiceBalanceByCurrency)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []SumOpenInvoiceBalanceByCurrencyRow{}
|
|
for rows.Next() {
|
|
var i SumOpenInvoiceBalanceByCurrencyRow
|
|
if err := rows.Scan(&i.Currency, &i.OutstandingCents); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const updateInvoicePaid = `-- name: UpdateInvoicePaid :one
|
|
UPDATE core.invoices
|
|
SET status = 'paid',
|
|
amount_paid = $2,
|
|
paid_at = $3
|
|
WHERE invoice_id = $1
|
|
RETURNING invoice_id, billing_account_id, subscription_id, status, amount_due, amount_paid, currency, period_start, period_end, due_date, paid_at, voided_at, created_at, updated_at
|
|
`
|
|
|
|
type UpdateInvoicePaidParams struct {
|
|
InvoiceID string `json:"invoice_id"`
|
|
AmountPaid int32 `json:"amount_paid"`
|
|
PaidAt sql.NullTime `json:"paid_at"`
|
|
}
|
|
|
|
func (q *Queries) UpdateInvoicePaid(ctx context.Context, arg UpdateInvoicePaidParams) (Invoice, error) {
|
|
row := q.db.QueryRowContext(ctx, updateInvoicePaid, arg.InvoiceID, arg.AmountPaid, arg.PaidAt)
|
|
var i Invoice
|
|
err := row.Scan(
|
|
&i.InvoiceID,
|
|
&i.BillingAccountID,
|
|
&i.SubscriptionID,
|
|
&i.Status,
|
|
&i.AmountDue,
|
|
&i.AmountPaid,
|
|
&i.Currency,
|
|
&i.PeriodStart,
|
|
&i.PeriodEnd,
|
|
&i.DueDate,
|
|
&i.PaidAt,
|
|
&i.VoidedAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateInvoiceStatus = `-- name: UpdateInvoiceStatus :one
|
|
UPDATE core.invoices
|
|
SET status = $2
|
|
WHERE invoice_id = $1
|
|
RETURNING invoice_id, billing_account_id, subscription_id, status, amount_due, amount_paid, currency, period_start, period_end, due_date, paid_at, voided_at, created_at, updated_at
|
|
`
|
|
|
|
type UpdateInvoiceStatusParams struct {
|
|
InvoiceID string `json:"invoice_id"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
func (q *Queries) UpdateInvoiceStatus(ctx context.Context, arg UpdateInvoiceStatusParams) (Invoice, error) {
|
|
row := q.db.QueryRowContext(ctx, updateInvoiceStatus, arg.InvoiceID, arg.Status)
|
|
var i Invoice
|
|
err := row.Scan(
|
|
&i.InvoiceID,
|
|
&i.BillingAccountID,
|
|
&i.SubscriptionID,
|
|
&i.Status,
|
|
&i.AmountDue,
|
|
&i.AmountPaid,
|
|
&i.Currency,
|
|
&i.PeriodStart,
|
|
&i.PeriodEnd,
|
|
&i.DueDate,
|
|
&i.PaidAt,
|
|
&i.VoidedAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateInvoiceVoided = `-- name: UpdateInvoiceVoided :one
|
|
UPDATE core.invoices
|
|
SET status = 'void',
|
|
voided_at = $2
|
|
WHERE invoice_id = $1
|
|
RETURNING invoice_id, billing_account_id, subscription_id, status, amount_due, amount_paid, currency, period_start, period_end, due_date, paid_at, voided_at, created_at, updated_at
|
|
`
|
|
|
|
type UpdateInvoiceVoidedParams struct {
|
|
InvoiceID string `json:"invoice_id"`
|
|
VoidedAt sql.NullTime `json:"voided_at"`
|
|
}
|
|
|
|
func (q *Queries) UpdateInvoiceVoided(ctx context.Context, arg UpdateInvoiceVoidedParams) (Invoice, error) {
|
|
row := q.db.QueryRowContext(ctx, updateInvoiceVoided, arg.InvoiceID, arg.VoidedAt)
|
|
var i Invoice
|
|
err := row.Scan(
|
|
&i.InvoiceID,
|
|
&i.BillingAccountID,
|
|
&i.SubscriptionID,
|
|
&i.Status,
|
|
&i.AmountDue,
|
|
&i.AmountPaid,
|
|
&i.Currency,
|
|
&i.PeriodStart,
|
|
&i.PeriodEnd,
|
|
&i.DueDate,
|
|
&i.PaidAt,
|
|
&i.VoidedAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|