Files
member-console/design/billing/model.md

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_sets
  • products (0..) -> (0..) plan_ladders (via plan_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:

  • code has 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: For once and repeating -- all applicable cycles used.
  • expired: ends_at timestamp 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 (via applied_by_person_id). Who applied.
  • discounts (0..*) -> (0..1) persons: FK -> identity.persons (via removed_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_id for "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_at in the future. Not yet usable.
  • active: Effective, positive balance, not expired.
  • exhausted: Balance fully consumed. Terminal.
  • expired: expires_at passed 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 (via source_provision_id): FK -> entitlements.pool_provisions. Lifecycle coupling for grants created by credit rule materialization.
  • credit_grants (0..*) -> (0..1) invoices (via funded_by_invoice_id): Paid grants may reference the purchase invoice.
  • credit_grants (0..*) -> (0..1) persons (via created_by_person_id): FK -> identity.persons. Audit attribution.
  • credit_grants (0..*) -> (0..1) persons (via voided_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_id for per-grant ledger queries.
  • Index on (billing_account_id, effective_at) for account-level ledger queries.
  • Index on invoice_id for "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 (via created_by_person_id). Manual adjustment attribution.

Note: The patrons and patronage_events tables have been moved to the cooperative module (Decision 108). See modules/cooperative/model.md for their current definitions.