50 KiB
Billing Module Model Reference
This file is a projection of the billing section of data-model.md. It contains the exact DDL and table descriptions for all twenty-two tables in the billing module. Cross-module foreign key references use schema-qualified notation: FK -> schema.table_name (Decision 114). For cross-cutting policies (soft-delete, JSONB governance, UUIDv7 primary keys, temporal modeling, polymorphic patterns), see the root-level policy documents referenced in modules/CONVENTIONS.md.
Financial Identity
billing_accounts
A billing_account is the entity that pays. It holds payment method references, receives invoices, and accumulates patronage. A billing account is separate from the organization that owns it.
This separation matters because:
- An organization may have multiple billing accounts (e.g., for different cost centers, clients, or funding sources).
- Billing contacts and addresses may differ from organization contacts.
- The payer (billing account) is the patron for cooperative purposes, which may differ from organizational structure.
- A billing account can provision pools in other organizations (the GCP orthogonality principle).
| Field | Type | Purpose |
|---|---|---|
billing_account_id |
UUID | Primary key |
org_id |
UUID | FK -> organization.organizations. The organization that owns this billing account. |
name |
VARCHAR(255) | Display name (e.g., "Primary", "Marketing Budget", "Client: Acme"). |
is_default |
BOOLEAN | Whether this is the default billing account for the organization. Each org has exactly one default. |
billing_email |
VARCHAR(255) | Where to send invoices. May differ from user or org email. |
billing_name |
VARCHAR(255) | Name to appear on invoices. |
billing_address_line1 |
VARCHAR(255) | Billing street address. |
billing_address_line2 |
VARCHAR(255) | Billing address line 2. |
billing_city |
VARCHAR(100) | Billing city. |
billing_state |
VARCHAR(100) | Billing state/province. |
billing_postal_code |
VARCHAR(20) | Billing postal code. |
billing_country |
VARCHAR(2) | Billing country (ISO 3166-1 alpha-2). |
tax_id |
VARCHAR(100) | Tax identifier for this billing entity (for invoices, receipts). |
tax_exempt |
BOOLEAN | Whether this billing account is tax exempt. |
tax_exempt_reason |
VARCHAR(255) | Reason for tax exemption if applicable. |
currency |
VARCHAR(3) | Preferred currency for billing (ISO 4217). |
platform_billing_mode |
VARCHAR(20) | Platform-enforced billing mode. postpaid, prepaid_only, or hybrid. Default: hybrid. |
customer_billing_mode |
VARCHAR(20) | Customer-selected billing mode preference. prepaid_only or hybrid. Default: hybrid. |
auto_recharge_enabled |
BOOLEAN DEFAULT FALSE | Whether auto-recharge of credit balance is active. |
auto_recharge_threshold |
INTEGER | Credit balance below which a recharge is triggered (smallest currency unit). |
auto_recharge_amount |
INTEGER | Amount to purchase when auto-recharge triggers (smallest currency unit). |
settings |
JSONB | Billing preferences and configuration. |
status |
VARCHAR(20) | State: active, suspended, closed. |
suspended_at |
TIMESTAMPTZ | When most recently suspended. |
suspended_by |
UUID | FK -> identity.persons. Who suspended. |
closed_at |
TIMESTAMPTZ | When closed. |
closed_by |
UUID | FK -> identity.persons. Who closed. |
created_at |
TIMESTAMPTZ | When this billing account was created. |
updated_at |
TIMESTAMPTZ | Last modification timestamp. |
Constraints:
- Each organization has exactly one default billing account (
is_default= TRUE).
Status lifecycle: active -> suspended -> closed. closed is terminal. Governed by the Soft-Delete and Terminal State Policy.
Relationships:
billing_accounts(0..*) -> (1)organizations: FK -> organization.organizations. Billing accounts belong to an organization.billing_accounts(1) -> (0..*)subscriptions: Billing accounts have subscriptions.billing_accounts(1) -> (0..*)purchases: Billing accounts make purchases.billing_accounts(1) -> (0..*)invoices: Billing accounts receive invoices.billing_accounts(1) -> (0..*)payments: Billing accounts make payments.billing_accounts(1) -> (0..*)pool_provisions: Billing accounts may fund pool provisions.billing_accounts(1) -> (0..*)pool_ondemand_config: Billing accounts may be metered usage payers.billing_accounts(1) -> (0..*)credit_grants: Billing accounts may hold prepaid credit grants.billing_accounts(1) -> (0..*)payment_methods: Billing accounts have payment methods on file.billing_accounts(1) -> (0..*)pending_charges: Billing accounts accumulate pending charges.
Product Catalog
products
A product represents something the cooperative sells. It is the commercial instrument -- name, description, type, visibility, and pricing -- that wraps an entitlement set. The product defines how capabilities are packaged and sold; the entitlement set defines what capabilities are conferred. Multiple products may reference the same entitlement set (e.g., monthly vs. annual pricing for identical capabilities). This factoring ensures that commercial concerns (pricing, catalog visibility, Stripe integration) remain separate from capability specification.
| Field | Type | Purpose |
|---|---|---|
product_id |
UUID | Primary key |
entitlement_set_id |
UUID | FK -> entitlements.entitlement_sets. The entitlement set this product confers. Resolved at provision-creation time and denormalized onto pool_provisions.entitlement_set_id. |
name |
VARCHAR(255) | Display name (e.g., "Pro Plan", "Premium Support", "Custom Domain"). |
description |
TEXT | Marketing/explanatory description. |
product_type |
VARCHAR(50) | Classification for non-plan kinds: addon (supplementary capability), usage (metered resource), one_time (single purchase). The 'plan' kind is not represented here; it is derived structurally from the presence of rows in plan_ladder_tiers. See the product_kinds view below and Doc 31 Amendment #3. |
lifecycle_status |
VARCHAR(20) | One of draft, published, retired. Orthogonal to kind: carries the editorial-intent axis (is this product definition complete and offered?) separately from the structural-kind axis (what role does the product play in the catalog?). Defaults to draft. |
metadata |
JSONB | Additional product attributes. |
is_active |
BOOLEAN | Whether this product can be purchased. |
is_public |
BOOLEAN | Whether this product appears on public pricing pages. |
created_at |
TIMESTAMPTZ | |
updated_at |
TIMESTAMPTZ |
Constraints:
ALTER TABLE billing.products
ADD CONSTRAINT product_type_non_plan_domain
CHECK (product_type IN ('addon', 'usage', 'one_time')),
ADD CONSTRAINT lifecycle_status_domain
CHECK (lifecycle_status IN ('draft', 'published', 'retired'));
The 'plan' value is deliberately absent from the product_type domain. A product's status as a plan is a structural fact — the presence of rows in plan_ladder_tiers — not a label the product declares. The earlier bidirectional product_type_plan_iff_ladder_member CHECK introduced by Doc 31 Amendment #1 is retired under Amendment #3; the denormalization it policed has been dissolved.
Derived kind:
CREATE VIEW billing.product_kinds AS
SELECT
p.product_id,
CASE
WHEN EXISTS (
SELECT 1 FROM billing.plan_ladder_tiers t
WHERE t.product_id = p.product_id
) THEN 'plan'
WHEN p.product_type = 'addon' THEN 'addon'
WHEN p.product_type = 'usage' THEN 'usage'
WHEN p.product_type = 'one_time' THEN 'one_time'
END AS product_kind
FROM billing.products p;
The view is the authoritative articulation of the kind taxonomy. Its explicit per-kind cases make the taxonomy auditable: reading the view enumerates every kind the catalog recognizes and the rule by which each is recognized. The 'plan' case is derived structurally from the junction; the non-plan cases delegate to product_type as provisional sources of truth, pending future amendments that identify their distinguishing structural facts (see Doc 31 Amendment #3 §3 for sketches of what those derivations might eventually look like). A new kind introduced without a structural fact is, by this discipline, not yet a kind — merely a label awaiting a relation to anchor it.
Relationships:
products(0..*) -> (1)entitlement_sets: Products reference an entitlement set to declare what capabilities they confer.FK -> entitlements.entitlement_setsproducts(0..) -> (0..)plan_ladders(viaplan_ladder_tiers): Plan products occupy one or more ladders through the junction; the junction is the sole representation of ladder membership.products(1) -> (0..*)prices: Products have one or more prices.products(1) -> (0..*)grants: Grants may optionally reference a product for audit clarity.FK <- entitlements.grants.product_id
prices
A price defines the commercial terms for purchasing a product: how much, in what currency, on what schedule, with what trial period.
| Field | Type | Purpose |
|---|---|---|
price_id |
UUID | Primary key |
product_id |
UUID | FK -> products. The product this price is for. |
nickname |
VARCHAR(255) | Display name (e.g., "Monthly", "Annual", "Starter"). |
currency |
VARCHAR(3) | Currency code (ISO 4217). |
unit_amount |
INTEGER | Price in smallest currency unit (e.g., cents for USD). |
billing_scheme |
VARCHAR(20) | How pricing works: flat (fixed amount), per_unit (multiplied by quantity), tiered (volume brackets). |
recurring_interval |
VARCHAR(20) | Billing frequency: month, year, or NULL for one-time prices. |
recurring_interval_count |
INTEGER | Number of intervals between billings. |
trial_period_days |
INTEGER | Number of days in trial period. NULL for no trial. Trial specification is placed at the price layer by deliberate architectural choice: a ladder-tier-level trial_days column was considered and rejected on definiteness grounds, as it would conflate catalog shape with subscription terms and force duplicate ladders for cooperatives offering identical structural plans under different trial policies (see documents/doc-34-trial-handling-recommendation.md). |
usage_type |
VARCHAR(20) | For metered products: metered or licensed. NULL for non-usage pricing. |
metadata |
JSONB | Additional price attributes. |
is_active |
BOOLEAN | Whether this price can be used for new transactions. |
created_at |
TIMESTAMPTZ | |
updated_at |
TIMESTAMPTZ |
Relationships:
prices(0..*) -> (1)products: Prices belong to a product.prices(1) -> (0..*)subscription_items: Prices are referenced by subscription line items.prices(1) -> (0..*)purchases: Prices are referenced by purchases.prices(1) -> (0..*)pool_ondemand_config: Prices may serve as metered rate plans.
plan_ladders
A plan_ladder is a named set of plan products declared mutually exclusive within a commercial domain. It is a catalog-shape construct: it declares how products relate to one another commercially, not what capabilities they confer. Capability composition lives in entitlement_sets; pricing lives in prices. The ladder says only "these plans are alternatives to each other." Ordered membership is expressed through plan_ladder_tiers; this table carries only the named set's identity and descriptive metadata.
CREATE TABLE billing.plan_ladders (
plan_ladder_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
ladder_key VARCHAR(64) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
| Field | Type | Purpose |
|---|---|---|
plan_ladder_id |
UUID | Primary key |
ladder_key |
VARCHAR(64) | Stable slug identifying the ladder in code and configuration (e.g., nextcloud, discourse). Unique; survives renames of name. |
name |
VARCHAR(255) | Human-readable label for display (e.g., "Nextcloud Storage Plans"). |
description |
TEXT | Optional prose describing the ladder's commercial scope. |
created_at |
TIMESTAMPTZ | |
updated_at |
TIMESTAMPTZ |
Relationships:
plan_ladders(1) -> (0..*)plan_ladder_tiers: Ladders have one or more tier memberships. The junction is the sole representation of product-ladder membership (Doc 31 Amendment #3).plan_ladders(1) -> (0..*)pool_provision_ladders: FK <- entitlements.pool_provision_ladders.plan_ladder_id. Provisions track which ladder they occupy at the entitlement layer.
plan_ladder_tiers
A plan_ladder_tier is the ordered-membership junction between a ladder and a plan product. Each row places a product at a specific rank within a ladder. Rank establishes ordinal position within the ladder; it is not a pricing tier — see glossary §5.2 for the disambiguation between ladder tiers and volume-bracketed pricing tiers.
The junction is the sole and universal representation of product-ladder membership: single-ladder plans and multi-ladder bundles alike express their ladder cardinality through rows in this table. A single-ladder plan is a plan whose tier-row set has cardinality one; no structural distinction separates it from a bundle beyond that cardinality, and the schema carries none. Correspondingly, a product's status as a plan is read from the presence of rows here — a structural fact exposed through the product_kinds view on products — rather than from any label the product carries (Doc 31 Amendment #3).
CREATE TABLE billing.plan_ladder_tiers (
plan_ladder_id UUID NOT NULL REFERENCES billing.plan_ladders(plan_ladder_id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES billing.products(product_id),
rank INTEGER NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (plan_ladder_id, product_id),
UNIQUE (plan_ladder_id, rank)
);
| Field | Type | Purpose |
|---|---|---|
plan_ladder_id |
UUID | FK -> plan_ladders. The ladder this row places a product on. |
product_id |
UUID | FK -> products. The plan product occupying this rung. |
rank |
INTEGER | Ordinal position within the ladder. 1-based by convention; lower values are lower tiers. Unique per ladder. |
created_at |
TIMESTAMPTZ |
Constraints:
- Primary key on
(plan_ladder_id, product_id). UNIQUE (plan_ladder_id, rank)ensures no two products share the same rank on a given ladder.
Relationships:
plan_ladder_tiers(0..*) -> (1)plan_ladders: Tiers belong to a ladder.plan_ladder_tiers(0..*) -> (1)products: Tiers reference a product.
Access Mechanisms
subscriptions
A subscription is a stateful binding agreement between a billing account and one or more priced products, with a recurring billing lifecycle. It is an invoice generator: at each billing cycle, it produces an invoice. Its status determines whether the pool provisions it creates remain active. The subscription_changes table provides complete transition history; paused_at and resumed_at capture the most recent occurrence for hot-path queries.
| Field | Type | Purpose |
|---|---|---|
subscription_id |
UUID | Primary key |
billing_account_id |
UUID | FK -> billing_accounts. The entity paying. |
current_period_start |
TIMESTAMPTZ | Start of current billing period. |
current_period_end |
TIMESTAMPTZ | End of current billing period. |
trial_start |
TIMESTAMPTZ | When trial started, if applicable. |
trial_end |
TIMESTAMPTZ | When trial ends, if applicable. |
cancel_at_period_end |
BOOLEAN | Whether to cancel at end of current period. |
cancel_at |
TIMESTAMPTZ | Scheduled cancellation time. |
canceled_at |
TIMESTAMPTZ | When cancellation was requested. |
cancellation_reason |
TEXT | Why the subscription was canceled. |
ended_at |
TIMESTAMPTZ | When the subscription actually ended. |
paused_at |
TIMESTAMPTZ | When most recently paused. |
resumed_at |
TIMESTAMPTZ | When most recently resumed. |
status |
VARCHAR(20) | State machine: incomplete, trialing, active, past_due, unpaid, paused, canceled. |
metadata |
JSONB | Additional subscription attributes. |
created_at |
TIMESTAMPTZ | |
updated_at |
TIMESTAMPTZ |
Relationships:
subscriptions(0..*) -> (1)billing_accounts: Subscriptions belong to a billing account.subscriptions(1) -> (1..*)subscription_items: Subscriptions contain one or more items.subscriptions(1) -> (0..*)subscription_changes: Changes are tracked.subscriptions(1) -> (0..*)invoices: Subscriptions generate invoices.subscriptions(1) -> (0..*)pool_provisions: Subscriptions create pool provisions (one per item).
subscription_items
A subscription_item is a line item within a subscription. Each item binds the subscription to a specific price (and by implication, a product) at a specific quantity.
| Field | Type | Purpose |
|---|---|---|
item_id |
UUID | Primary key |
subscription_id |
UUID | FK -> subscriptions. |
price_id |
UUID | FK -> prices. The price (and thus product) for this line item. |
quantity |
INTEGER | Number of units. |
metadata |
JSONB | Additional item attributes. |
created_at |
TIMESTAMPTZ | |
updated_at |
TIMESTAMPTZ |
Relationships:
subscription_items(1..*) -> (1)subscriptions: Items belong to a subscription.subscription_items(0..*) -> (1)prices: Items reference a price.
subscription_changes
A subscription_change records a modification to a subscription. This provides a complete audit trail of subscription lifecycle events with timestamps and actor attribution.
| Field | Type | Purpose |
|---|---|---|
change_id |
UUID | Primary key |
subscription_id |
UUID | FK -> subscriptions. The subscription that changed. |
change_type |
VARCHAR(50) | What kind of change: created, item_added, item_removed, item_quantity_changed, renewed, trial_started, trial_ended, paused, resumed, canceled, reactivated, ended. |
item_id |
UUID | FK -> subscription_items. Which item changed, if applicable. |
previous_price_id |
UUID | Price before the change. |
previous_quantity |
INTEGER | Quantity before the change. |
previous_status |
VARCHAR(20) | Status before the change. |
new_price_id |
UUID | Price after the change. |
new_quantity |
INTEGER | Quantity after the change. |
new_status |
VARCHAR(20) | Status after the change. |
changed_by_person_id |
UUID | FK -> identity.persons. Who made the change (NULL if system/automated). |
reason |
TEXT | Explanation for the change. |
effective_at |
TIMESTAMPTZ | When the change took/takes effect. |
created_at |
TIMESTAMPTZ |
Relationships:
subscription_changes(0..*) -> (1)subscriptions: Changes belong to a subscription.subscription_changes(0..*) -> (0..1)subscription_items: Changes may reference a specific item.subscription_changes(0..*) -> (0..1)persons: FK -> identity.persons. Changes may be attributed to a person.
purchases
A purchase represents a one-time transaction: a billing account pays for a product and receives access. There is no recurring billing, no state machine. It is a completed exchange.
| Field | Type | Purpose |
|---|---|---|
purchase_id |
UUID | Primary key |
billing_account_id |
UUID | FK -> billing_accounts. Who paid. |
price_id |
UUID | FK -> prices. What they paid (must be a one-time price). |
quantity |
INTEGER | Number of units purchased. |
amount |
INTEGER | Total amount charged (smallest currency unit). |
currency |
VARCHAR(3) | Currency of the transaction. |
purchased_at |
TIMESTAMPTZ | When the purchase was completed. |
status |
VARCHAR(20) | completed, refunded, partially_refunded. |
refunded_at |
TIMESTAMPTZ | When refunded. |
invoice_id |
UUID | FK -> invoices. The invoice generated for this purchase. |
metadata |
JSONB | Additional purchase attributes. |
created_at |
TIMESTAMPTZ | |
updated_at |
TIMESTAMPTZ |
Relationships:
purchases(0..*) -> (1)billing_accounts: Purchases belong to a billing account.purchases(0..*) -> (1)prices: Purchases reference a price.purchases(0..*) -> (0..1)invoices: Purchases may generate an invoice.purchases(1) -> (0..*)pool_provisions: Purchases create pool provisions.
Invoicing Pipeline
invoices
An invoice is a billing document requesting payment from a billing account. Invoices may be generated from subscriptions (periodically), purchases (once), or metered usage (aggregated). Invoice address fields snapshot billing identity at issuance time -- they record what appeared on the invoice regardless of subsequent billing account changes.
| Field | Type | Purpose |
|---|---|---|
invoice_id |
UUID | Primary key |
invoice_number |
VARCHAR(50) | Human-readable unique identifier. |
billing_account_id |
UUID | FK -> billing_accounts. The billing account being invoiced. |
subscription_id |
UUID | FK -> subscriptions. A subscription this invoice is for, if applicable. |
purchase_id |
UUID | FK -> purchases. A purchase this invoice is for, if applicable. |
billing_name |
VARCHAR(255) | Snapshot of billing name at invoice time. |
billing_email |
VARCHAR(255) | Snapshot of billing email at invoice time. |
billing_address_line1 |
VARCHAR(255) | Snapshot of billing street address at invoice time. |
billing_address_line2 |
VARCHAR(255) | Snapshot of billing address line 2 at invoice time. |
billing_city |
VARCHAR(100) | Snapshot of billing city at invoice time. |
billing_state |
VARCHAR(100) | Snapshot of billing state/province at invoice time. |
billing_postal_code |
VARCHAR(20) | Snapshot of billing postal code at invoice time. |
billing_country |
VARCHAR(2) | Snapshot of billing country (ISO 3166-1 alpha-2) at invoice time. |
currency |
VARCHAR(3) | Currency for this invoice. |
subtotal |
INTEGER | Amount before discounts and tax (smallest currency unit). |
discount_amount |
INTEGER | Total discounts applied. |
tax_amount |
INTEGER | Tax amount. |
total |
INTEGER | Full financial obligation before credit application. |
credit_applied |
INTEGER DEFAULT 0 | Total credit amount applied to this invoice from credit grants. |
amount_paid |
INTEGER | Amount paid via payment method so far. |
amount_due |
INTEGER | Remaining balance after credits. Equal to total - credit_applied - amount_paid. |
invoice_date |
DATE | Date the invoice was issued. |
due_date |
DATE | Date payment is due. |
period_start |
DATE | Start of billing period. |
period_end |
DATE | End of billing period. |
status |
VARCHAR(20) | draft, open, paid, void, uncollectible, refunded. |
paid_at |
TIMESTAMPTZ | When fully paid. |
voided_at |
TIMESTAMPTZ | When voided. |
invoice_pdf_url |
VARCHAR(500) | URL to downloadable PDF. |
hosted_invoice_url |
VARCHAR(500) | URL to hosted invoice page. |
memo |
TEXT | Notes or memo. |
metadata |
JSONB | Additional invoice attributes. |
created_at |
TIMESTAMPTZ | |
updated_at |
TIMESTAMPTZ |
Note on source FK non-exclusivity: The subscription_id and purchase_id columns are intentionally not mutually exclusive. An invoice may reference a subscription, a purchase, both (in mixed-source invoicing scenarios), or neither (for invoices generated from usage charges alone). These are optional associative references, not a polymorphic pattern.
Status lifecycle: draft -> open -> {paid | void | uncollectible | refunded}. All post-open states are terminal.
Relationships:
invoices(0..*) -> (1)billing_accounts: Invoices are sent to a billing account.invoices(0..*) -> (0..1)subscriptions: Invoices may be for a subscription.invoices(0..*) -> (0..1)purchases: Invoices may be for a purchase.invoices(1) -> (1..*)invoice_line_items: Invoices have one or more line items.invoices(1) -> (0..*)payments: Invoices may have payments applied.invoices(1) -> (0..*)pending_charges: Invoices may have pending charges swept into them.invoices(1) -> (0..*)credit_transactions: Invoices may have credits applied.
invoice_line_items
An invoice_line_item is a single charge, credit, or adjustment on an invoice. Every invoice has at least one line item. Line items are the atoms of invoice computation -- totals, discounts, and taxes are all expressed in terms of line items. The invoice's summary fields (subtotal, discount_amount, tax_amount, total) are denormalized aggregates maintained by the invoice generation process and verified against line items.
| Field | Type | Purpose |
|---|---|---|
line_item_id |
UUID | Primary key |
invoice_id |
UUID | FK -> invoices. The invoice this line item belongs to. |
line_type |
VARCHAR(30) | Classification: subscription, usage, one_time, proration_credit, proration_charge, discount, adjustment. |
description |
TEXT | Human-readable description displayed on the invoice. |
product_id |
UUID | FK -> products. The product this line item relates to. NULL for discounts and adjustments that are not product-specific. |
price_id |
UUID | FK -> prices. The price used for this line item. NULL for adjustments. |
subscription_item_id |
UUID | FK -> subscription_items. If this line item was generated from a subscription item. |
metered_key |
VARCHAR(100) | If this line item represents metered usage, the usage key. |
quantity |
INTEGER | Number of units. |
unit_amount |
INTEGER | Per-unit amount in smallest currency unit. |
amount |
INTEGER | Line total before tax (smallest currency unit). May be negative for credits, proration credits, and discounts. |
discount_amount |
INTEGER DEFAULT 0 | Discount apportioned to this line item (positive value representing reduction). |
tax_rate |
DECIMAL(7,4) | Tax rate applied (e.g., 0.0875 for 8.75%). NULL if not taxable. |
tax_amount |
INTEGER | Tax amount for this line item (smallest currency unit). |
period_start |
DATE | Start of the service period this line item covers. |
period_end |
DATE | End of the service period. |
metadata |
JSONB | Additional line item attributes. No PII. |
created_at |
TIMESTAMPTZ |
Sum invariant: SUM(amount) WHERE line_type != 'discount' = invoices.subtotal. invoices.total = subtotal - discount_amount + tax_amount.
Relationships:
invoice_line_items(1..*) -> (1)invoices: Line items belong to an invoice.invoice_line_items(0..*) -> (0..1)products: Line items may reference a product.invoice_line_items(0..*) -> (0..1)prices: Line items may reference a price.invoice_line_items(0..*) -> (0..1)subscription_items: Line items may trace to a subscription item.invoice_line_items(1) -> (0..*)discounts: Line items may have discounts scoped to them.
Payments
payments
A payment records money received from a billing account.
| Field | Type | Purpose |
|---|---|---|
payment_id |
UUID | Primary key |
billing_account_id |
UUID | FK -> billing_accounts. The billing account that paid. |
invoice_id |
UUID | FK -> invoices. The invoice this payment is for, if applicable. |
currency |
VARCHAR(3) | Currency of the payment. |
amount |
INTEGER | Gross amount received (smallest currency unit). |
processor_fee |
INTEGER | Fee charged by payment processor. |
payment_method_type |
VARCHAR(50) | Type: card, bank_transfer, crypto, etc. |
payment_method_details |
JSONB | Non-identifying descriptors only (e.g., {"brand": "visa", "last4": "4242"}). |
status |
VARCHAR(20) | pending, processing, succeeded, failed, refunded, partially_refunded. |
failure_code |
VARCHAR(100) | Error code if failed. |
failure_message |
TEXT | Human-readable failure explanation. |
paid_at |
TIMESTAMPTZ | When completed. |
failed_at |
TIMESTAMPTZ | When failed. |
refunded_at |
TIMESTAMPTZ | When refunded. |
metadata |
JSONB | Additional payment attributes. |
created_at |
TIMESTAMPTZ | |
updated_at |
TIMESTAMPTZ |
Relationships:
payments(0..*) -> (1)billing_accounts: Payments come from a billing account.payments(0..*) -> (0..1)invoices: Payments may be applied to an invoice.payments(1) -> (0..*)patronage_events: Successful payments create patronage events.payments(1) -> (0..*)refunds: Payments may have refunds.payments(1) -> (0..*)disputes: Payments may have disputes.
payment_methods
A payment_method records that a billing account has a means of payment on file. This is a provider-agnostic domain projection: it answers "does this account have a payment method?" and "what kind?" without consulting any external service. Provider-specific details (Stripe's pm_xxx ID, fingerprint, vault token) live in integration schemas.
| Field | Type | Purpose |
|---|---|---|
payment_method_id |
UUID | Primary key |
billing_account_id |
UUID | FK -> billing_accounts. The account this payment method belongs to. |
type |
VARCHAR(30) | Payment method type: card, bank_account, sepa_debit, etc. |
card_brand |
VARCHAR(20) | Card brand: visa, mastercard, amex, etc. NULL for non-card types. |
card_last4 |
VARCHAR(4) | Last four digits. NULL for non-card types. |
card_exp_month |
SMALLINT | Card expiration month. NULL for non-card types. |
card_exp_year |
SMALLINT | Card expiration year. NULL for non-card types. |
is_default |
BOOLEAN | Whether this is the default payment method for the billing account. |
status |
VARCHAR(20) | active, expired, revoked. |
created_at |
TIMESTAMPTZ | |
updated_at |
TIMESTAMPTZ |
Relationships:
payment_methods(0..*) -> (1)billing_accounts: Payment methods belong to a billing account.
refunds
A refund records a partial or full return of money from a payment. Refunds are separate from payments because a refund has its own amount (which may differ from the payment), reason, status, and timing. Modeling refunds as a status flag on payments (as v8 did) loses this information.
In the cooperative model, a refund triggers a patronage reversal -- the patronage event that credited value to the paying member must be partially or fully reversed. The patronage_events table references refunds via source_type.
| Field | Type | Purpose |
|---|---|---|
refund_id |
UUID | Primary key |
payment_id |
UUID | FK -> payments. The payment being refunded. |
amount |
INTEGER | Refund amount in smallest currency unit. May differ from payment amount (partial refund). |
currency |
VARCHAR(3) | Currency of the refund. |
reason |
VARCHAR(50) | Reason: requested_by_customer, duplicate, fraudulent, etc. |
status |
VARCHAR(20) | pending, succeeded, failed. |
refunded_at |
TIMESTAMPTZ | When the refund completed. |
failed_at |
TIMESTAMPTZ | When the refund failed, if applicable. |
created_at |
TIMESTAMPTZ | |
updated_at |
TIMESTAMPTZ |
Relationships:
refunds(0..*) -> (1)payments: Refunds apply to a payment.
disputes
A dispute (chargeback) records that a payment is being contested through the payment processor's dispute resolution process. Disputes affect revenue, patronage, and potentially account standing -- they are business entities, not integration details.
The dispute amount, status, reason, and resolution belong in the billing schema. Provider-specific dispute IDs, evidence submission details, and processor-specific reason codes live in per-provider integration schemas.
| Field | Type | Purpose |
|---|---|---|
dispute_id |
UUID | Primary key |
payment_id |
UUID | FK -> payments. The payment under dispute. |
amount |
INTEGER | Disputed amount in smallest currency unit. |
currency |
VARCHAR(3) | Currency of the dispute. |
reason |
VARCHAR(50) | Reason category: fraudulent, product_not_received, unrecognized, duplicate, subscription_canceled, other. |
status |
VARCHAR(20) | needs_response, under_review, won, lost. |
evidence_due_by |
TIMESTAMPTZ | Deadline for submitting evidence. NULL if not applicable. |
resolved_at |
TIMESTAMPTZ | When the dispute was resolved (won or lost). |
created_at |
TIMESTAMPTZ | |
updated_at |
TIMESTAMPTZ |
Relationships:
disputes(0..*) -> (1)payments: Disputes apply to a payment.
Discounts & Promotions
coupons
A coupon defines the terms of a price reduction: how much, for how long, for which products, and under what constraints. This is the rule layer -- the abstract definition of the discount logic. Coupons are distinct from credits: coupons reduce the obligation (the customer owes less), while credits reduce the collection (the customer has already paid).
| Field | Type | Purpose |
|---|---|---|
coupon_id |
UUID | Primary key |
org_id |
UUID | FK -> organization.organizations. The organization that created this coupon. NULL for platform-level coupons. |
name |
VARCHAR(255) | Internal name (e.g., "Launch Promotion", "Annual Loyalty"). |
display_name |
VARCHAR(255) | Customer-facing name displayed on invoices. |
discount_type |
VARCHAR(20) | percentage or fixed. |
percentage_off |
DECIMAL(5,2) | Percentage to deduct. NULL if discount_type = 'fixed'. |
amount_off |
INTEGER | Fixed amount to deduct (smallest currency unit). NULL if discount_type = 'percentage'. |
currency |
VARCHAR(3) | Currency for amount_off. NULL if discount_type = 'percentage'. |
duration |
VARCHAR(20) | once, repeating, forever. |
duration_months |
INTEGER | For repeating: number of billing cycles. NULL otherwise. |
applies_to_products |
UUID[] | Product IDs this discount is restricted to. NULL means all products. |
max_redemptions |
INTEGER | Maximum total redemptions. NULL for unlimited. |
redemption_count |
INTEGER DEFAULT 0 | Current redemption count. Denormalized. |
valid_from |
TIMESTAMPTZ | Earliest redemption time. NULL for immediate. |
valid_until |
TIMESTAMPTZ | Latest redemption time. NULL for no expiration. |
minimum_amount |
INTEGER | Minimum invoice subtotal for the discount to apply. NULL for no minimum. |
minimum_amount_currency |
VARCHAR(3) | Currency for minimum_amount. |
first_time_only |
BOOLEAN DEFAULT FALSE | Whether restricted to first-time subscribers/purchasers. |
is_active |
BOOLEAN DEFAULT TRUE | Whether available for new redemptions. |
created_by_person_id |
UUID | FK -> identity.persons. Who created this discount. |
metadata |
JSONB | Additional attributes. No PII. |
created_at |
TIMESTAMPTZ | |
updated_at |
TIMESTAMPTZ |
Constraints:
CHECK (
(discount_type = 'percentage' AND percentage_off IS NOT NULL AND amount_off IS NULL)
OR (discount_type = 'fixed' AND amount_off IS NOT NULL AND percentage_off IS NULL)
)
CHECK (
(duration = 'repeating' AND duration_months IS NOT NULL)
OR (duration != 'repeating' AND duration_months IS NULL)
)
Relationships:
coupons(0..*) -> (0..1)organizations: FK -> organization.organizations. Coupons may belong to an organization.coupons(0..*) -> (1)persons: FK -> identity.persons. Coupons are created by a person.coupons(1) -> (0..*)promotion_codes: Coupons may have distribution codes.coupons(1) -> (0..*)discounts: Coupons may have active discount applications.
promotion_codes
A promotion_code is a customer-facing key that maps to a coupon with additional distribution constraints. One coupon can have many promotion codes (e.g., five influencer codes all mapping to the same "20% off" coupon, with per-code tracking).
| Field | Type | Purpose |
|---|---|---|
promotion_code_id |
UUID | Primary key |
coupon_id |
UUID | FK -> coupons. The coupon this code provides access to. |
code |
VARCHAR(100) | Customer-facing code (e.g., "LAUNCH50"). Case-insensitive. |
max_redemptions |
INTEGER | Maximum redemptions for this code. NULL for unlimited. |
redemption_count |
INTEGER DEFAULT 0 | Current redemptions through this code. Denormalized. |
restrict_to_billing_account_id |
UUID | FK -> billing_accounts. If set, restricted to a specific billing account. |
first_time_only |
BOOLEAN DEFAULT FALSE | Whether restricted to first-time subscribers/purchasers. |
valid_from |
TIMESTAMPTZ | Earliest time this code can be used. |
valid_until |
TIMESTAMPTZ | Latest time this code can be used. |
is_active |
BOOLEAN DEFAULT TRUE | Whether this code is currently usable. |
created_by_person_id |
UUID | FK -> identity.persons. Who created this code. |
metadata |
JSONB | Additional attributes. No PII. |
created_at |
TIMESTAMPTZ | |
updated_at |
TIMESTAMPTZ |
Constraints:
codehas a partial unique index:UNIQUE (LOWER(code)) WHERE is_active = true.
Relationships:
promotion_codes(0..*) -> (1)coupons: Codes map to a coupon.promotion_codes(0..*) -> (0..1)billing_accounts: Codes may be restricted to a billing account.promotion_codes(0..*) -> (1)persons: FK -> identity.persons. Codes are created by a person.promotion_codes(1) -> (0..*)discounts: Codes may have active discount applications.
discounts
A discount records that a coupon has been applied to a specific billing relationship. It is the active state of a coupon on a billing account, subscription, or invoice line item.
| Field | Type | Purpose |
|---|---|---|
discount_id |
UUID | Primary key |
coupon_id |
UUID | FK -> coupons. The coupon being applied. |
promotion_code_id |
UUID | FK -> promotion_codes. The code used, if any. NULL if applied administratively. |
billing_account_id |
UUID | FK -> billing_accounts. If billing-account-level discount. NULL otherwise. |
subscription_id |
UUID | FK -> subscriptions. If subscription-level discount. NULL otherwise. |
invoice_line_item_id |
UUID | FK -> invoice_line_items. If line-item-level discount. NULL otherwise. |
applied_by_person_id |
UUID | FK -> identity.persons. Who applied this discount. NULL if self-service. |
code_used |
VARCHAR(100) | Snapshot of the code that was entered. |
duration_remaining |
INTEGER | For repeating: billing cycles remaining. NULL for once and forever. |
started_at |
TIMESTAMPTZ | When the discount was applied. |
ends_at |
TIMESTAMPTZ | When the discount expires based on duration. NULL for forever. |
ended_at |
TIMESTAMPTZ | When actually ended (if manually removed). |
status |
VARCHAR(20) | active, exhausted, expired, removed. |
removed_at |
TIMESTAMPTZ | When manually removed. |
removed_by_person_id |
UUID | FK -> identity.persons. Who removed it. |
created_at |
TIMESTAMPTZ | |
updated_at |
TIMESTAMPTZ |
Constraints (exclusive arc -- scope):
CHECK (
(billing_account_id IS NOT NULL AND subscription_id IS NULL AND invoice_line_item_id IS NULL)
OR (billing_account_id IS NULL AND subscription_id IS NOT NULL AND invoice_line_item_id IS NULL)
OR (billing_account_id IS NULL AND subscription_id IS NULL AND invoice_line_item_id IS NOT NULL)
)
Status lifecycle:
active: Discount currently applied and reducing charges on future invoices.exhausted: Foronceandrepeating-- all applicable cycles used.expired:ends_attimestamp passed.removed: Manually removed before natural expiration.
Relationships:
discounts(0..*) -> (1)coupons: Discounts reference a coupon.discounts(0..*) -> (0..1)promotion_codes: Discounts may reference a code.discounts(0..*) -> (0..1)billing_accounts: Billing-account-scoped. Exclusive arc.discounts(0..*) -> (0..1)subscriptions: Subscription-scoped. Exclusive arc.discounts(0..*) -> (0..1)invoice_line_items: Line-item-scoped. Exclusive arc.discounts(0..*) -> (0..1)persons: FK -> identity.persons (viaapplied_by_person_id). Who applied.discounts(0..*) -> (0..1)persons: FK -> identity.persons (viaremoved_by_person_id). Who removed.
Charge Assembly
pending_charges
A pending_charge represents a mutable, pre-invoice financial obligation. It bridges the gap between raw inputs (usage events, subscription cycles, ad hoc charges) and frozen outputs (invoice line items). Pending charges are the "invoice items" of the billing pipeline -- they represent "we intend to charge for this" and can be edited or voided before they are swept into a draft invoice.
| Field | Type | Purpose |
|---|---|---|
charge_id |
UUID | Primary key |
billing_account_id |
UUID | FK -> billing_accounts. Who will be charged. |
subscription_id |
UUID | FK -> subscriptions. If from a subscription cycle. NULL for ad hoc. |
subscription_item_id |
UUID | FK -> subscription_items. If from a specific subscription item. |
product_id |
UUID | FK -> products. The product this charge relates to. |
price_id |
UUID | FK -> prices. The price used. NULL for manual adjustments. |
charge_type |
VARCHAR(30) | subscription, usage, one_time, proration_credit, proration_charge, adjustment. |
description |
TEXT | Human-readable description. Carried forward to the invoice line item. |
metered_key |
VARCHAR(100) | If metered usage, the usage key. |
quantity |
INTEGER | Number of units. |
unit_amount |
INTEGER | Per-unit amount (smallest currency unit). |
amount |
INTEGER | Charge total. May be negative for credits. |
currency |
VARCHAR(3) | Currency of the charge. |
period_start |
DATE | Service period start. |
period_end |
DATE | Service period end. |
invoice_id |
UUID | FK -> invoices. Set when swept into a draft invoice. |
invoice_line_item_id |
UUID | FK -> invoice_line_items. Set when materialized as a line item. |
created_by_person_id |
UUID | FK -> identity.persons. For manual charges. NULL for system-generated. |
status |
VARCHAR(20) | pending, invoiced, voided. |
invoiced_at |
TIMESTAMPTZ | When swept into an invoice. |
voided_at |
TIMESTAMPTZ | When voided. |
voided_by_person_id |
UUID | FK -> identity.persons. Who voided. |
metadata |
JSONB | Additional attributes. No PII. |
created_at |
TIMESTAMPTZ | |
updated_at |
TIMESTAMPTZ |
Status lifecycle:
pending: Awaiting invoice sweep. Mutable. Can be voided.invoiced: Swept into a draft invoice. Immutable.voided: Cancelled before invoicing. Terminal.
Relationships:
pending_charges(0..*) -> (1)billing_accounts: Charges belong to a billing account.pending_charges(0..*) -> (0..1)subscriptions: Charges may originate from a subscription.pending_charges(0..*) -> (0..1)invoices: Charges may be swept into an invoice.pending_charges(0..*) -> (0..1)invoice_line_items: Charges may materialize as a line item.pending_charges(1) -> (0..*)pending_charge_usage_events: Charges may link to contributing usage events.
pending_charge_usage_events
A pending_charge_usage_event links a usage event to the pending charge it contributed to. This join table preserves the append-only semantics of usage_events while providing full traceability from consumption to billing.
| Field | Type | Purpose |
|---|---|---|
charge_id |
UUID | FK -> pending_charges. |
event_id |
UUID | FK -> entitlements.usage_events. |
created_at |
TIMESTAMPTZ |
Constraints:
- Primary key on
(charge_id, event_id). - Index on
event_idfor "which charge includes this event?" queries.
Relationships:
pending_charge_usage_events(0..*) -> (1)pending_charges: Links target a charge.pending_charge_usage_events(0..*) -> (1)usage_events: FK -> entitlements.usage_events. Links target an event.
Prepaid Credits
credit_grants
A credit grant represents a discrete allocation of prepaid or promotional billing credits to a billing account. Multiple grants may be active simultaneously; they are consumed in priority order. Credits support two application topologies: invoice-time (credits reduce the amount due on a finalized invoice) and real-time (credits are deducted at the moment of consumption). The billing account's platform_billing_mode and customer_billing_mode fields determine which topology applies.
| Field | Type | Purpose |
|---|---|---|
grant_id |
UUID | Primary key |
billing_account_id |
UUID | FK -> billing_accounts. Who holds these credits. |
name |
VARCHAR(200) | Human-readable description (e.g., "January credit purchase", "Welcome bonus"). |
category |
VARCHAR(20) | paid (customer purchased) or promotional (platform granted). Determines accounting treatment. |
currency |
VARCHAR(3) | Currency of the grant. |
initial_amount |
INTEGER | Original credit amount in smallest currency unit. |
balance |
INTEGER | Current available balance. Denormalized from ledger; maintained by credit transactions. |
priority |
SMALLINT | Application priority. 0 is highest. Lower numbers consumed first. Default: 50. |
applies_to_products |
UUID[] | Product restrictions. NULL means unrestricted. |
effective_at |
TIMESTAMPTZ | When credits become usable. |
expires_at |
TIMESTAMPTZ | When unused credits expire. NULL means no expiration. |
source_provision_id |
UUID | FK -> entitlements.pool_provisions. The provision that triggered this grant via credit rule materialization. NULL for manually created grants. Provides lifecycle coupling and provenance. |
funded_by_invoice_id |
UUID | FK -> invoices. Purchase invoice for paid grants. NULL for promotional. |
created_by_person_id |
UUID | FK -> identity.persons. Who created this grant. |
status |
VARCHAR(20) | pending, active, exhausted, expired, voided. |
voided_at |
TIMESTAMPTZ | When voided. |
voided_by_person_id |
UUID | FK -> identity.persons. Who voided. |
metadata |
JSONB | Additional attributes. |
created_at |
TIMESTAMPTZ | |
updated_at |
TIMESTAMPTZ |
Constraints:
- Index on
(billing_account_id, status). - CHECK:
priority BETWEEN 0 AND 100. - CHECK:
initial_amount > 0. - CHECK:
balance >= 0.
Status lifecycle:
pending:effective_atin the future. Not yet usable.active: Effective, positive balance, not expired.exhausted: Balance fully consumed. Terminal.expired:expires_atpassed with remaining balance. Terminal.voided: Administratively cancelled. Terminal.
Relationships:
credit_grants(0..*) -> (1)billing_accounts: Grants belong to a billing account.credit_grants(0..*) -> (0..1)pool_provisions(viasource_provision_id): FK -> entitlements.pool_provisions. Lifecycle coupling for grants created by credit rule materialization.credit_grants(0..*) -> (0..1)invoices(viafunded_by_invoice_id): Paid grants may reference the purchase invoice.credit_grants(0..*) -> (0..1)persons(viacreated_by_person_id): FK -> identity.persons. Audit attribution.credit_grants(0..*) -> (0..1)persons(viavoided_by_person_id): FK -> identity.persons. Void attribution.credit_grants(1) -> (0..*)credit_transactions: Grants have ledger entries.
credit_transactions
A credit transaction is an immutable ledger entry recording a credit (funds in) or debit (funds out) against a specific grant. The grant's balance field is the running total. The ledger is append-only: corrections are made by adding compensating transactions, never by modifying existing ones.
| Field | Type | Purpose |
|---|---|---|
transaction_id |
UUID | Primary key |
grant_id |
UUID | FK -> credit_grants. Which grant this transaction affects. |
billing_account_id |
UUID | FK -> billing_accounts. Denormalized for account-level ledger queries. |
type |
VARCHAR(20) | credit (funds added) or debit (funds consumed). |
amount |
INTEGER | Transaction amount (smallest currency unit). Always positive; type determines direction. |
balance_after |
INTEGER | Grant balance after this transaction. |
source_type |
VARCHAR(30) | What triggered this: initial_funding, top_up, invoice_application, usage_deduction, expiration, void, reinstatement, adjustment. |
invoice_id |
UUID | FK -> invoices. If applied to an invoice. |
invoice_line_item_id |
UUID | FK -> invoice_line_items. If applied to a specific line item. |
usage_event_id |
UUID | FK -> entitlements.usage_events. If for a real-time usage deduction. |
description |
TEXT | Human-readable description. |
effective_at |
TIMESTAMPTZ | When this transaction takes effect. |
created_by_person_id |
UUID | FK -> identity.persons. For manual adjustments. NULL for system-generated. |
metadata |
JSONB | Additional attributes. |
created_at |
TIMESTAMPTZ | Immutable. |
Constraints:
- Index on
grant_idfor per-grant ledger queries. - Index on
(billing_account_id, effective_at)for account-level ledger queries. - Index on
invoice_idfor "which credits were applied to this invoice?" queries. - Immutability enforced by application logic.
Ledger invariant: For any grant, balance = initial_amount + SUM(credits) - SUM(debits).
Relationships:
credit_transactions(0..*) -> (1)credit_grants: Transactions belong to a grant.credit_transactions(0..*) -> (0..1)invoices: Debits may reference the invoice.credit_transactions(0..*) -> (0..1)invoice_line_items: Debits may reference the line item.credit_transactions(0..*) -> (0..1)usage_events: FK -> entitlements.usage_events. Real-time debits may reference the usage event.credit_transactions(0..*) -> (0..1)persons: FK -> identity.persons (viacreated_by_person_id). Manual adjustment attribution.
Note: The
patronsandpatronage_eventstables have been moved to the cooperative module (Decision 108). Seemodules/cooperative/model.mdfor their current definitions.