# `Accrue.Billing.Subscription`
[🔗](https://github.com/szTheory/accrue/blob/accrue-v0.3.0/lib/accrue/billing/subscription.ex#L1)

Ecto schema for the `accrue_subscriptions` table.

Stores the local projection of a processor subscription (e.g. Stripe
`sub_xxx`). Phase 3 upgrades `:status` to an `Ecto.Enum` over the
canonical Stripe subscription status set (BILL-05, D3-03) and adds
the cancel-at-period-end + pause_collection fields needed for the
full lifecycle state machine.

## Predicates (BILL-05)

Never gate on raw `.status` access. The predicates defined in this
module are the canonical way to ask "is this subscription X?" — raw
access is lint-time forbidden by `Accrue.Credo.NoRawStatusAccess`.

  - `trialing?/1`
  - `active?/1` — includes `:trialing`
  - `past_due?/1` — `:past_due` or `:unpaid`
  - `canceled?/1` — `:canceled`, `:incomplete_expired`, or any `ended_at`
  - `canceling?/1` — `:active` + `cancel_at_period_end` + future period end
  - `paused?/1` — legacy `:paused` status OR non-nil `pause_collection`

# `t`

```elixir
@type t() :: %Accrue.Billing.Subscription{
  __meta__: term(),
  automatic_tax: term(),
  automatic_tax_disabled_reason: term(),
  automatic_tax_status: term(),
  cancel_at: term(),
  cancel_at_period_end: term(),
  canceled_at: term(),
  current_period_end: term(),
  current_period_start: term(),
  customer: term(),
  customer_id: term(),
  data: term(),
  discount_id: term(),
  dunning_sweep_attempted_at: term(),
  ended_at: term(),
  id: term(),
  inserted_at: term(),
  last_stripe_event_id: term(),
  last_stripe_event_ts: term(),
  lock_version: term(),
  metadata: term(),
  past_due_since: term(),
  pause_behavior: term(),
  pause_collection: term(),
  paused_at: term(),
  processor: term(),
  processor_id: term(),
  status: term(),
  subscription_items: term(),
  trial_end: term(),
  trial_start: term(),
  updated_at: term()
}
```

# `active?`

```elixir
@spec active?(
  %Accrue.Billing.Subscription{
    __meta__: term(),
    automatic_tax: term(),
    automatic_tax_disabled_reason: term(),
    automatic_tax_status: term(),
    cancel_at: term(),
    cancel_at_period_end: term(),
    canceled_at: term(),
    current_period_end: term(),
    current_period_start: term(),
    customer: term(),
    customer_id: term(),
    data: term(),
    discount_id: term(),
    dunning_sweep_attempted_at: term(),
    ended_at: term(),
    id: term(),
    inserted_at: term(),
    last_stripe_event_id: term(),
    last_stripe_event_ts: term(),
    lock_version: term(),
    metadata: term(),
    past_due_since: term(),
    pause_behavior: term(),
    pause_collection: term(),
    paused_at: term(),
    processor: term(),
    processor_id: term(),
    status: term(),
    subscription_items: term(),
    trial_end: term(),
    trial_start: term(),
    updated_at: term()
  }
  | map()
) :: boolean()
```

True if the subscription counts as "active" for entitlement purposes.

Includes `:trialing` per D3-03.

# `canceled?`

```elixir
@spec canceled?(
  %Accrue.Billing.Subscription{
    __meta__: term(),
    automatic_tax: term(),
    automatic_tax_disabled_reason: term(),
    automatic_tax_status: term(),
    cancel_at: term(),
    cancel_at_period_end: term(),
    canceled_at: term(),
    current_period_end: term(),
    current_period_start: term(),
    customer: term(),
    customer_id: term(),
    data: term(),
    discount_id: term(),
    dunning_sweep_attempted_at: term(),
    ended_at: term(),
    id: term(),
    inserted_at: term(),
    last_stripe_event_id: term(),
    last_stripe_event_ts: term(),
    lock_version: term(),
    metadata: term(),
    past_due_since: term(),
    pause_behavior: term(),
    pause_collection: term(),
    paused_at: term(),
    processor: term(),
    processor_id: term(),
    status: term(),
    subscription_items: term(),
    trial_end: term(),
    trial_start: term(),
    updated_at: term()
  }
  | map()
) :: boolean()
```

True if the subscription has terminated.

`:canceled`, `:incomplete_expired`, or any row with a non-nil `ended_at`.

# `canceling?`

```elixir
@spec canceling?(
  %Accrue.Billing.Subscription{
    __meta__: term(),
    automatic_tax: term(),
    automatic_tax_disabled_reason: term(),
    automatic_tax_status: term(),
    cancel_at: term(),
    cancel_at_period_end: term(),
    canceled_at: term(),
    current_period_end: term(),
    current_period_start: term(),
    customer: term(),
    customer_id: term(),
    data: term(),
    discount_id: term(),
    dunning_sweep_attempted_at: term(),
    ended_at: term(),
    id: term(),
    inserted_at: term(),
    last_stripe_event_id: term(),
    last_stripe_event_ts: term(),
    lock_version: term(),
    metadata: term(),
    past_due_since: term(),
    pause_behavior: term(),
    pause_collection: term(),
    paused_at: term(),
    processor: term(),
    processor_id: term(),
    status: term(),
    subscription_items: term(),
    trial_end: term(),
    trial_start: term(),
    updated_at: term()
  }
  | map()
) :: boolean()
```

True if the subscription is `:active` with `cancel_at_period_end` set and
the current period end is still in the future (cancel_at_period_end cancel
hasn't taken effect yet).

# `changeset`

```elixir
@spec changeset(
  %Accrue.Billing.Subscription{
    __meta__: term(),
    automatic_tax: term(),
    automatic_tax_disabled_reason: term(),
    automatic_tax_status: term(),
    cancel_at: term(),
    cancel_at_period_end: term(),
    canceled_at: term(),
    current_period_end: term(),
    current_period_start: term(),
    customer: term(),
    customer_id: term(),
    data: term(),
    discount_id: term(),
    dunning_sweep_attempted_at: term(),
    ended_at: term(),
    id: term(),
    inserted_at: term(),
    last_stripe_event_id: term(),
    last_stripe_event_ts: term(),
    lock_version: term(),
    metadata: term(),
    past_due_since: term(),
    pause_behavior: term(),
    pause_collection: term(),
    paused_at: term(),
    processor: term(),
    processor_id: term(),
    status: term(),
    subscription_items: term(),
    trial_end: term(),
    trial_start: term(),
    updated_at: term()
  }
  | Ecto.Changeset.t(),
  map()
) :: Ecto.Changeset.t()
```

Builds a changeset for creating or updating a Subscription.

# `dunning_exhausted_status`

```elixir
@spec dunning_exhausted_status(
  %Accrue.Billing.Subscription{
    __meta__: term(),
    automatic_tax: term(),
    automatic_tax_disabled_reason: term(),
    automatic_tax_status: term(),
    cancel_at: term(),
    cancel_at_period_end: term(),
    canceled_at: term(),
    current_period_end: term(),
    current_period_start: term(),
    customer: term(),
    customer_id: term(),
    data: term(),
    discount_id: term(),
    dunning_sweep_attempted_at: term(),
    ended_at: term(),
    id: term(),
    inserted_at: term(),
    last_stripe_event_id: term(),
    last_stripe_event_ts: term(),
    lock_version: term(),
    metadata: term(),
    past_due_since: term(),
    pause_behavior: term(),
    pause_collection: term(),
    paused_at: term(),
    processor: term(),
    processor_id: term(),
    status: term(),
    subscription_items: term(),
    trial_end: term(),
    trial_start: term(),
    updated_at: term()
  }
  | map()
) :: :unpaid | :canceled | nil
```

Returns the dunning-terminal status atom (`:unpaid` or `:canceled`)
if the subscription has reached a dunning-exhaustion state. Returns
`nil` otherwise.

Used by the `customer.subscription.updated` webhook reducer (BILL-15,
D4-02) to detect terminal transitions without raw `.status` access.

# `dunning_sweepable?`

```elixir
@spec dunning_sweepable?(
  %Accrue.Billing.Subscription{
    __meta__: term(),
    automatic_tax: term(),
    automatic_tax_disabled_reason: term(),
    automatic_tax_status: term(),
    cancel_at: term(),
    cancel_at_period_end: term(),
    canceled_at: term(),
    current_period_end: term(),
    current_period_start: term(),
    customer: term(),
    customer_id: term(),
    data: term(),
    discount_id: term(),
    dunning_sweep_attempted_at: term(),
    ended_at: term(),
    id: term(),
    inserted_at: term(),
    last_stripe_event_id: term(),
    last_stripe_event_ts: term(),
    lock_version: term(),
    metadata: term(),
    past_due_since: term(),
    pause_behavior: term(),
    pause_collection: term(),
    paused_at: term(),
    processor: term(),
    processor_id: term(),
    status: term(),
    subscription_items: term(),
    trial_end: term(),
    trial_start: term(),
    updated_at: term()
  }
  | map()
) :: boolean()
```

True if the subscription is in the narrow `:past_due` retry window
where the D4-02 dunning sweeper is allowed to ask the processor
facade to move it to a terminal action.

Strictly `:past_due` — does NOT include `:unpaid`. An `:unpaid`
subscription has already reached its terminal state (whether via
Stripe-native termination or a prior sweep) and must not be swept
again (BILL-15).

# `force_status_changeset`

```elixir
@spec force_status_changeset(
  %Accrue.Billing.Subscription{
    __meta__: term(),
    automatic_tax: term(),
    automatic_tax_disabled_reason: term(),
    automatic_tax_status: term(),
    cancel_at: term(),
    cancel_at_period_end: term(),
    canceled_at: term(),
    current_period_end: term(),
    current_period_start: term(),
    customer: term(),
    customer_id: term(),
    data: term(),
    discount_id: term(),
    dunning_sweep_attempted_at: term(),
    ended_at: term(),
    id: term(),
    inserted_at: term(),
    last_stripe_event_id: term(),
    last_stripe_event_ts: term(),
    lock_version: term(),
    metadata: term(),
    past_due_since: term(),
    pause_behavior: term(),
    pause_collection: term(),
    paused_at: term(),
    processor: term(),
    processor_id: term(),
    status: term(),
    subscription_items: term(),
    trial_end: term(),
    trial_start: term(),
    updated_at: term()
  }
  | Ecto.Changeset.t(),
  map()
) :: Ecto.Changeset.t()
```

Webhook-path changeset (D3-17). Skips user-path validation guards so
out-of-order webhook events can settle arbitrary state without the
state-machine check failing on an otherwise-valid transition.

# `past_due?`

```elixir
@spec past_due?(
  %Accrue.Billing.Subscription{
    __meta__: term(),
    automatic_tax: term(),
    automatic_tax_disabled_reason: term(),
    automatic_tax_status: term(),
    cancel_at: term(),
    cancel_at_period_end: term(),
    canceled_at: term(),
    current_period_end: term(),
    current_period_start: term(),
    customer: term(),
    customer_id: term(),
    data: term(),
    discount_id: term(),
    dunning_sweep_attempted_at: term(),
    ended_at: term(),
    id: term(),
    inserted_at: term(),
    last_stripe_event_id: term(),
    last_stripe_event_ts: term(),
    lock_version: term(),
    metadata: term(),
    past_due_since: term(),
    pause_behavior: term(),
    pause_collection: term(),
    paused_at: term(),
    processor: term(),
    processor_id: term(),
    status: term(),
    subscription_items: term(),
    trial_end: term(),
    trial_start: term(),
    updated_at: term()
  }
  | map()
) :: boolean()
```

True if the subscription is past due or unpaid (dunning territory).

# `paused?`

```elixir
@spec paused?(
  %Accrue.Billing.Subscription{
    __meta__: term(),
    automatic_tax: term(),
    automatic_tax_disabled_reason: term(),
    automatic_tax_status: term(),
    cancel_at: term(),
    cancel_at_period_end: term(),
    canceled_at: term(),
    current_period_end: term(),
    current_period_start: term(),
    customer: term(),
    customer_id: term(),
    data: term(),
    discount_id: term(),
    dunning_sweep_attempted_at: term(),
    ended_at: term(),
    id: term(),
    inserted_at: term(),
    last_stripe_event_id: term(),
    last_stripe_event_ts: term(),
    lock_version: term(),
    metadata: term(),
    past_due_since: term(),
    pause_behavior: term(),
    pause_collection: term(),
    paused_at: term(),
    processor: term(),
    processor_id: term(),
    status: term(),
    subscription_items: term(),
    trial_end: term(),
    trial_start: term(),
    updated_at: term()
  }
  | map()
) :: boolean()
```

True if the subscription is paused.

Covers both the legacy `:paused` status (used by earlier Stripe versions)
and the modern `pause_collection` map (D3-03).

# `pending_intent`

```elixir
@spec pending_intent(
  %Accrue.Billing.Subscription{
    __meta__: term(),
    automatic_tax: term(),
    automatic_tax_disabled_reason: term(),
    automatic_tax_status: term(),
    cancel_at: term(),
    cancel_at_period_end: term(),
    canceled_at: term(),
    current_period_end: term(),
    current_period_start: term(),
    customer: term(),
    customer_id: term(),
    data: term(),
    discount_id: term(),
    dunning_sweep_attempted_at: term(),
    ended_at: term(),
    id: term(),
    inserted_at: term(),
    last_stripe_event_id: term(),
    last_stripe_event_ts: term(),
    lock_version: term(),
    metadata: term(),
    past_due_since: term(),
    pause_behavior: term(),
    pause_collection: term(),
    paused_at: term(),
    processor: term(),
    processor_id: term(),
    status: term(),
    subscription_items: term(),
    trial_end: term(),
    trial_start: term(),
    updated_at: term()
  }
  | map()
) :: map() | nil
```

Extracts a pre-hydrated PaymentIntent from `data.latest_invoice.payment_intent`,
used by Plan 04 `subscribe/3` to surface SCA/3DS action-required to the caller.

# `statuses`

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

Canonical list of subscription statuses (D3-03, Stripe's 8 values).

# `trialing?`

```elixir
@spec trialing?(
  %Accrue.Billing.Subscription{
    __meta__: term(),
    automatic_tax: term(),
    automatic_tax_disabled_reason: term(),
    automatic_tax_status: term(),
    cancel_at: term(),
    cancel_at_period_end: term(),
    canceled_at: term(),
    current_period_end: term(),
    current_period_start: term(),
    customer: term(),
    customer_id: term(),
    data: term(),
    discount_id: term(),
    dunning_sweep_attempted_at: term(),
    ended_at: term(),
    id: term(),
    inserted_at: term(),
    last_stripe_event_id: term(),
    last_stripe_event_ts: term(),
    lock_version: term(),
    metadata: term(),
    past_due_since: term(),
    pause_behavior: term(),
    pause_collection: term(),
    paused_at: term(),
    processor: term(),
    processor_id: term(),
    status: term(),
    subscription_items: term(),
    trial_end: term(),
    trial_start: term(),
    updated_at: term()
  }
  | map()
) :: boolean()
```

True if the subscription is currently in a trial.

---

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