# `Accrue.Billing.Invoice`
[🔗](https://github.com/szTheory/accrue/blob/accrue-v1.0.0/lib/accrue/billing/invoice.ex#L1)

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.

## Legal user-path transitions

    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.

# `t`

```elixir
@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()
}
```

# `changeset`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

Canonical list of invoice statuses.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
