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

Primary context module for Accrue billing operations.

All write operations for billable entities live here, following
conventional Phoenix context boundaries. Host schemas gain access to
these operations via `use Accrue.Billable`, which injects a
convenience `customer/1` that delegates here.

## Customer lifecycle

  * `customer/1` — lazy fetch-or-create. First call
    auto-creates an `accrue_customers` row via the configured
    processor; subsequent calls return the cached row.
  * `create_customer/1` — explicit create, always hits the processor.
  * `customer!/1` and `create_customer!/1` — raising variants following
    the same `{:ok, _} | {:error, _}` vs `!` naming convention as the rest
    of Accrue.

All writes use `Ecto.Multi` to ensure the customer row and the
corresponding `accrue_events` entry are committed atomically.

# `add_item`

# `add_item!`

# `apply_promotion_code`

# `apply_promotion_code!`

# `attach_payment_method`

# `attach_payment_method!`

# `cancel`

# `cancel!`

# `cancel_at_period_end`

# `cancel_at_period_end!`

# `cancel_schedule`

# `cancel_schedule!`

# `charge`

# `charge!`

# `comp_subscription`

# `comp_subscription!`

# `create_billing_portal_session`

```elixir
@spec create_billing_portal_session(Accrue.Billing.Customer.t(), keyword() | map()) ::
  {:ok, Accrue.BillingPortal.Session.t()} | {:error, term()}
```

Creates a Customer Billing Portal session for `customer` through the
configured processor.

`attrs` is a keyword list or map of options aligned with
`Accrue.BillingPortal.Session.create/1`, **except** `:customer` (supplied as
the first argument): `:return_url`, `:configuration`, `:flow_data`,
`:locale`, `:on_behalf_of`, `:operation_id`.

Invalid keys or types raise `NimbleOptions.ValidationError`.

The portal session **URL** is a short-lived bearer credential. Do **not**
log raw session structs, processor payloads, or URLs in production telemetry
or support tickets. For `:configuration`, see
`guides/portal_configuration_checklist.md`.

Emits `[:accrue, :billing, :billing_portal, :create]` (OpenTelemetry name
`accrue.billing.billing_portal.create`).

# `create_billing_portal_session!`

```elixir
@spec create_billing_portal_session!(Accrue.Billing.Customer.t(), keyword() | map()) ::
  Accrue.BillingPortal.Session.t()
```

Bang variant of `create_billing_portal_session/2` — returns
`%Accrue.BillingPortal.Session{}` or raises.

Raises `NimbleOptions.ValidationError` when `attrs` fail validation.

On `{:error, reason}` from the underlying `Session.create/1`, re-raises when
`reason` implements `Exception`; otherwise raises the same message shape as
`Accrue.BillingPortal.Session.create!/1`.

# `create_checkout_session`

```elixir
@spec create_checkout_session(Accrue.Billing.Customer.t(), keyword() | map()) ::
  {:ok, Accrue.Checkout.Session.t()} | {:error, term()}
```

Creates a Stripe Checkout Session for `customer` through the configured
processor.

`attrs` is a keyword list or map of options aligned with
`Accrue.Checkout.Session.create/1`, **except** `:customer` (supplied as the
first argument): `:mode`, `:ui_mode`, `:line_items`, `:success_url`,
`:cancel_url`, `:return_url`, `:metadata`, `:client_reference_id`,
`:automatic_tax`, `:operation_id`.

Invalid keys or types raise `NimbleOptions.ValidationError`.

The Checkout **redirect URL** (hosted mode) and **`client_secret`** (embedded
mode) are bearer credentials. Do **not** log raw session structs, processor
payloads, or URLs in production telemetry or support tickets.

Emits `[:accrue, :billing, :checkout_session, :create]` (OpenTelemetry-style
name `accrue.billing.checkout_session.create`). See `m:Accrue.Checkout.Session`
for field semantics and the underlying `@create_schema`.

# `create_checkout_session!`

```elixir
@spec create_checkout_session!(Accrue.Billing.Customer.t(), keyword() | map()) ::
  Accrue.Checkout.Session.t()
```

Bang variant of `create_checkout_session/2` — returns
`%Accrue.Checkout.Session{}` or raises.

Raises `NimbleOptions.ValidationError` when `attrs` fail validation.

On `{:error, reason}` from the underlying `CheckoutSession.create/1`,
re-raises when `reason` implements `Exception`; otherwise raises with prefix
`Accrue.Checkout.Session.create/1 failed:`.

# `create_coupon`

# `create_coupon!`

# `create_customer`

```elixir
@spec create_customer(struct()) ::
  {:ok, Accrue.Billing.Customer.t()} | {:error, term()}
```

Explicitly creates a `Customer` for the given billable struct.

Uses `Ecto.Multi` to atomically:

  1. Create the customer on the processor side (Fake or Stripe)
  2. Insert the `accrue_customers` row with the processor-assigned ID
  3. Record a `"customer.created"` event

Returns `{:ok, %Customer{}}` on success or `{:error, reason}` on
failure. The entire transaction rolls back if any step fails.

## Examples

    {:ok, customer} = Accrue.Billing.create_customer(user)
    customer.processor_id  #=> "cus_fake_00001"

# `create_customer!`

```elixir
@spec create_customer!(struct()) :: Accrue.Billing.Customer.t()
```

Raising variant of `create_customer/1`. Returns the `Customer`
directly or raises on error.

# `create_payment_intent`

# `create_payment_intent!`

# `create_promotion_code`

# `create_promotion_code!`

# `create_refund`

# `create_refund!`

# `create_setup_intent`

# `create_setup_intent!`

# `customer`

```elixir
@spec customer(struct()) :: {:ok, Accrue.Billing.Customer.t()} | {:error, term()}
```

Lazily fetches or creates a `Customer` for the given billable struct.

If a customer row already exists for the billable's `owner_type` and
`owner_id`, returns it. Otherwise, creates one via the configured
processor and persists it atomically with an event record.

## Examples

    {:ok, customer} = Accrue.Billing.customer(user)
    {:ok, ^customer} = Accrue.Billing.customer(user)  # same row

# `customer!`

```elixir
@spec customer!(struct()) :: Accrue.Billing.Customer.t()
```

Raising variant of `customer/1`. Returns the `Customer` directly or
raises on error.

# `detach_payment_method`

# `detach_payment_method!`

# `fetch_invoice_pdf`

# `finalize_invoice`

# `finalize_invoice!`

# `get_subscription`

# `get_subscription!`

# `list_payment_methods`

Lists payment methods for `customer` from the configured processor (Stripe
or Fake). This is **read-through** processor state, not a projection of
local `accrue_payment_methods` rows.

Delegates to `Accrue.Billing.PaymentMethodActions.list_payment_methods/2`.
See that module for supported `opts` filters (`type`, `limit`, pagination
cursors).

# `list_payment_methods!`

Raising variant of `list_payment_methods/2`.

Delegates to `Accrue.Billing.PaymentMethodActions.list_payment_methods!/2`.

# `mark_uncollectible`

# `mark_uncollectible!`

# `patch_data`

```elixir
@spec patch_data(Ecto.Schema.t(), map()) :: {:ok, Ecto.Schema.t()} | {:error, term()}
```

Shallow-merges `partial_data` into the existing `data` column.

Used when a partial event carries only a delta. Applies optimistic
locking via `lock_version`.

## Examples

    {:ok, patched} = Accrue.Billing.patch_data(customer, %{"balance" => 100})

# `pause`

# `pause!`

# `pay_invoice`

# `pay_invoice!`

# `preview_upcoming_invoice`

# `preview_upcoming_invoice!`

# `put_data`

```elixir
@spec put_data(Ecto.Schema.t(), map()) :: {:ok, Ecto.Schema.t()} | {:error, term()}
```

Fully replaces the `data` jsonb column on a billing record.

Used by webhook reconcile paths that receive the whole object (e.g.
`customer.updated`). Applies optimistic locking via `lock_version`.

## Examples

    {:ok, updated} = Accrue.Billing.put_data(customer, %{"balance" => 0})

# `release_schedule`

# `release_schedule!`

# `remove_item`

# `remove_item!`

# `render_invoice_pdf`

# `report_usage`

`report_usage/3` records a metered usage event for `customer` (a `%Accrue.Billing.Customer{}` or Stripe customer id string) and `event_name`, persisting through the transactional outbox before invoking the configured processor.

## Options

Keys mirror `Accrue.Billing.MeterEventActions`'s `@report_usage_schema` (types and defaults stay in sync there):

* `:value` — non-negative integer count; default `1`.
* `:timestamp` — `%DateTime{}`, Unix seconds as integer, or `nil`. When `nil`, normalization uses the current UTC instant; see `Accrue.Billing.MeterEventActions` for the exact normalization pipeline.
* `:identifier` — string or `nil`; default `nil` derives a stable audit-layer identifier from customer, `event_name`, `:value`, the resolved timestamp, and optional `:operation_id` (uniqueness enforced on `accrue_meter_events.identifier`).
* `:operation_id` — string or `nil`; when set, it participates in identifier derivation and supports idempotent replays alongside the other fields above.
* `:payload` — map of extra dimensions or `nil`; default `nil`. Forwarded to the processor as supplemental context (e.g. `%{"dimension" => "seats"}` in tests).

Invalid `opts` raise `NimbleOptions.ValidationError` from `NimbleOptions.validate!/2`.

## Error tuples vs persisted rows

`{:error, _}` means this **call** could not advance durable meter state as
requested (for example the processor rejected the usage report). After
retries with the same idempotency inputs, `{:ok, %Accrue.Billing.MeterEvent{}}`
may be returned when the row already reflects a terminal outcome — inspect
`stripe_status` and `stripe_error` on the persisted row for the canonical
failure details. See `guides/metering.md` for how public calls, internal rows,
and the processor seam relate.

## Fake / test mode

Host apps can configure `Accrue.Processor.Fake` (for example via `Accrue.Test.setup_fake_processor/1`) to exercise this path without outbound network calls.

# `report_usage!`

Bang variant of `report_usage/3` — returns `%Accrue.Billing.MeterEvent{}` or raises on error.

See `report_usage/3` for the full options reference (`## Options`).

Raises `NimbleOptions.ValidationError` when `opts` fail validation.

Raises on `{:error, _}` from the underlying implementation when that tuple
indicates a true failure for this invocation (for example a missing customer
or a processor error that performed the failing attempt). When the non-bang
`report_usage/3` would return `{:ok, row}` on an idempotent replay (including
a row already in `failed`), this function returns that row without raising.
`Accrue.APIError` (including `resource_missing` / HTTP 404) is re-raised when
it implements `Exception`; other error tuples become a `RuntimeError` raised by
`Accrue.Billing.MeterEventActions.report_usage!/3`.

# `resume`

# `resume!`

# `send_invoice`

# `send_invoice!`

# `set_default_payment_method`

# `set_default_payment_method!`

# `store_invoice_pdf`

# `subscribe`

# `subscribe!`

# `subscribe_via_schedule`

# `subscribe_via_schedule!`

# `swap_plan`

# `swap_plan!`

# `unpause`

# `unpause!`

# `update_customer`

```elixir
@spec update_customer(
  %Accrue.Billing.Customer{
    __meta__: term(),
    charges: term(),
    data: term(),
    default_payment_method: term(),
    default_payment_method_id: term(),
    email: term(),
    id: term(),
    inserted_at: term(),
    invoices: term(),
    last_stripe_event_id: term(),
    last_stripe_event_ts: term(),
    lock_version: term(),
    metadata: term(),
    name: term(),
    owner_id: term(),
    owner_type: term(),
    payment_methods: term(),
    preferred_locale: term(),
    preferred_timezone: term(),
    processor: term(),
    processor_id: term(),
    subscriptions: term(),
    updated_at: term()
  },
  map()
) :: {:ok, Accrue.Billing.Customer.t()} | {:error, term()}
```

Updates a `Customer` with the given attributes.

Uses `Ecto.Multi` to atomically update the customer and record a
`"customer.updated"` event. Metadata is validated as a flat string
map (max 50 keys, etc.). Optimistic locking via `lock_version`
prevents torn writes.

## Examples

    {:ok, customer} = Accrue.Billing.update_customer(customer, %{metadata: %{"tier" => "pro"}})

# `update_customer_tax_location`

```elixir
@spec update_customer_tax_location(
  %Accrue.Billing.Customer{
    __meta__: term(),
    charges: term(),
    data: term(),
    default_payment_method: term(),
    default_payment_method_id: term(),
    email: term(),
    id: term(),
    inserted_at: term(),
    invoices: term(),
    last_stripe_event_id: term(),
    last_stripe_event_ts: term(),
    lock_version: term(),
    metadata: term(),
    name: term(),
    owner_id: term(),
    owner_type: term(),
    payment_methods: term(),
    preferred_locale: term(),
    preferred_timezone: term(),
    processor: term(),
    processor_id: term(),
    subscriptions: term(),
    updated_at: term()
  },
  map()
) :: {:ok, Accrue.Billing.Customer.t()} | {:error, term()}
```

Updates a processor-backed customer's tax location with immediate validation.

This public path is distinct from `update_customer/2`, which remains a
local-only row update for non-processor customer maintenance.

# `update_customer_tax_location!`

```elixir
@spec update_customer_tax_location!(
  %Accrue.Billing.Customer{
    __meta__: term(),
    charges: term(),
    data: term(),
    default_payment_method: term(),
    default_payment_method_id: term(),
    email: term(),
    id: term(),
    inserted_at: term(),
    invoices: term(),
    last_stripe_event_id: term(),
    last_stripe_event_ts: term(),
    lock_version: term(),
    metadata: term(),
    name: term(),
    owner_id: term(),
    owner_type: term(),
    payment_methods: term(),
    preferred_locale: term(),
    preferred_timezone: term(),
    processor: term(),
    processor_id: term(),
    subscriptions: term(),
    updated_at: term()
  },
  map()
) :: Accrue.Billing.Customer.t()
```

Raising variant of `update_customer_tax_location/2`.

# `update_item_quantity`

# `update_item_quantity!`

# `update_quantity`

# `update_quantity!`

# `update_schedule`

# `update_schedule!`

# `void_invoice`

# `void_invoice!`

---

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