Accrue.Billing.Invoice (accrue v1.0.0)

Copy Markdown View Source

Ecto schema for the accrue_invoices table.

Stores the local copy of a Stripe invoice, including all rollup columns (subtotal_minor, tax_minor, total_minor, etc.) that the admin UI and application logic need without round-tripping to Stripe. The data jsonb column holds the full Stripe payload for callers that need the raw shape.

For mutations (finalize_invoice, pay_invoice, void_invoice, etc.) see Accrue.Billing.InvoiceActions, which is the correct entry point for any write operation.

draft -> open | void
open  -> paid | uncollectible | void
paid, uncollectible, void  -> (terminal)

Changeset paths

Two changeset functions exist to support the two sources of status changes:

  • changeset/2 — enforces the transition table; use for user-originated writes (finalize_invoice, pay_invoice, void_invoice).
  • force_status_changeset/2 — bypasses transition validation; use only from the webhook reconcile path where Stripe is canonical.

Summary

Functions

Builds a user-path changeset. Enforces the legal transition table — illegal transitions add an error on :status.

Webhook-path discount denormalization. Mirrors Stripe's discount_minor + total_discount_amounts into local columns. Stripe is canonical — no local math, no validate_number guard. Callers pass nil-safe attrs so the cast preserves existing values when the webhook doesn't carry them.

Builds a webhook-path changeset. Stripe is canonical in this path, so the transition table is bypassed. Use only from the webhook reconcile path.

Canonical list of invoice statuses.

Types

t()

@type t() :: %Accrue.Billing.Invoice{
  __meta__: term(),
  amount_due_minor: term(),
  amount_paid_minor: term(),
  amount_remaining_minor: term(),
  automatic_tax: term(),
  automatic_tax_disabled_reason: term(),
  automatic_tax_status: term(),
  billing_reason: term(),
  collection_method: term(),
  currency: term(),
  customer: term(),
  customer_id: term(),
  data: term(),
  discount_minor: term(),
  due_date: term(),
  finalized_at: term(),
  hosted_url: term(),
  id: term(),
  inserted_at: term(),
  items: term(),
  last_finalization_error_code: term(),
  last_stripe_event_id: term(),
  last_stripe_event_ts: term(),
  lock_version: term(),
  metadata: term(),
  number: term(),
  paid_at: term(),
  pdf_url: term(),
  period_end: term(),
  period_start: term(),
  processor: term(),
  processor_id: term(),
  status: term(),
  subscription: term(),
  subscription_id: term(),
  subtotal_minor: term(),
  tax_minor: term(),
  total_cents: term(),
  total_discount_amounts: term(),
  total_minor: term(),
  updated_at: term(),
  voided_at: term()
}

Functions

changeset(invoice_or_changeset, attrs \\ %{})

@spec changeset(
  %Accrue.Billing.Invoice{
    __meta__: term(),
    amount_due_minor: term(),
    amount_paid_minor: term(),
    amount_remaining_minor: term(),
    automatic_tax: term(),
    automatic_tax_disabled_reason: term(),
    automatic_tax_status: term(),
    billing_reason: term(),
    collection_method: term(),
    currency: term(),
    customer: term(),
    customer_id: term(),
    data: term(),
    discount_minor: term(),
    due_date: term(),
    finalized_at: term(),
    hosted_url: term(),
    id: term(),
    inserted_at: term(),
    items: term(),
    last_finalization_error_code: term(),
    last_stripe_event_id: term(),
    last_stripe_event_ts: term(),
    lock_version: term(),
    metadata: term(),
    number: term(),
    paid_at: term(),
    pdf_url: term(),
    period_end: term(),
    period_start: term(),
    processor: term(),
    processor_id: term(),
    status: term(),
    subscription: term(),
    subscription_id: term(),
    subtotal_minor: term(),
    tax_minor: term(),
    total_cents: term(),
    total_discount_amounts: term(),
    total_minor: term(),
    updated_at: term(),
    voided_at: term()
  }
  | Ecto.Changeset.t(),
  map()
) :: Ecto.Changeset.t()

Builds a user-path changeset. Enforces the legal transition table — illegal transitions add an error on :status.

force_discount_changeset(invoice_or_changeset, attrs \\ %{})

@spec force_discount_changeset(
  %Accrue.Billing.Invoice{
    __meta__: term(),
    amount_due_minor: term(),
    amount_paid_minor: term(),
    amount_remaining_minor: term(),
    automatic_tax: term(),
    automatic_tax_disabled_reason: term(),
    automatic_tax_status: term(),
    billing_reason: term(),
    collection_method: term(),
    currency: term(),
    customer: term(),
    customer_id: term(),
    data: term(),
    discount_minor: term(),
    due_date: term(),
    finalized_at: term(),
    hosted_url: term(),
    id: term(),
    inserted_at: term(),
    items: term(),
    last_finalization_error_code: term(),
    last_stripe_event_id: term(),
    last_stripe_event_ts: term(),
    lock_version: term(),
    metadata: term(),
    number: term(),
    paid_at: term(),
    pdf_url: term(),
    period_end: term(),
    period_start: term(),
    processor: term(),
    processor_id: term(),
    status: term(),
    subscription: term(),
    subscription_id: term(),
    subtotal_minor: term(),
    tax_minor: term(),
    total_cents: term(),
    total_discount_amounts: term(),
    total_minor: term(),
    updated_at: term(),
    voided_at: term()
  }
  | Ecto.Changeset.t(),
  map()
) :: Ecto.Changeset.t()

Webhook-path discount denormalization. Mirrors Stripe's discount_minor + total_discount_amounts into local columns. Stripe is canonical — no local math, no validate_number guard. Callers pass nil-safe attrs so the cast preserves existing values when the webhook doesn't carry them.

force_status_changeset(invoice_or_changeset, attrs \\ %{})

@spec force_status_changeset(
  %Accrue.Billing.Invoice{
    __meta__: term(),
    amount_due_minor: term(),
    amount_paid_minor: term(),
    amount_remaining_minor: term(),
    automatic_tax: term(),
    automatic_tax_disabled_reason: term(),
    automatic_tax_status: term(),
    billing_reason: term(),
    collection_method: term(),
    currency: term(),
    customer: term(),
    customer_id: term(),
    data: term(),
    discount_minor: term(),
    due_date: term(),
    finalized_at: term(),
    hosted_url: term(),
    id: term(),
    inserted_at: term(),
    items: term(),
    last_finalization_error_code: term(),
    last_stripe_event_id: term(),
    last_stripe_event_ts: term(),
    lock_version: term(),
    metadata: term(),
    number: term(),
    paid_at: term(),
    pdf_url: term(),
    period_end: term(),
    period_start: term(),
    processor: term(),
    processor_id: term(),
    status: term(),
    subscription: term(),
    subscription_id: term(),
    subtotal_minor: term(),
    tax_minor: term(),
    total_cents: term(),
    total_discount_amounts: term(),
    total_minor: term(),
    updated_at: term(),
    voided_at: term()
  }
  | Ecto.Changeset.t(),
  map()
) :: Ecto.Changeset.t()

Builds a webhook-path changeset. Stripe is canonical in this path, so the transition table is bypassed. Use only from the webhook reconcile path.

statuses()

@spec statuses() :: [atom()]

Canonical list of invoice statuses.