Accrue.Billing.Subscription (accrue v1.0.0)

Copy Markdown View Source

Ecto schema for the accrue_subscriptions table.

Stores the local projection of a Stripe subscription. :status is an Ecto.Enum over the full Stripe subscription status set, and the schema includes cancel_at_period_end, pause_collection, and other lifecycle fields needed to answer all common billing questions locally.

Use the predicates, not raw .status

Do not gate business logic on direct comparisons to .status. Raw status checks are easy to get wrong — for example, a subscription with cancel_at_period_end: true still has status :active, and an :incomplete_expired subscription is just as terminated as a :canceled one.

The predicates in this module capture those edge cases correctly:

For database queries over multiple subscriptions, the matching fragments are in Accrue.Billing.Query.

Summary

Functions

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

True if the subscription has terminated.

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).

Builds a changeset for creating or updating a Subscription.

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

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

Webhook-path changeset. 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.

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

True if the subscription is paused.

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

Canonical list of subscription statuses (Stripe's 8 values).

True if the subscription is currently in a trial.

Types

t()

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

Functions

active?(arg1)

@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 — a customer in a trial period has full access.

canceled?(arg1)

@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?(arg1)

@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(subscription_or_changeset, attrs \\ %{})

@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(arg1)

@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 to detect terminal transitions without raw .status access.

dunning_sweepable?(arg1)

@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 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.

force_status_changeset(subscription_or_changeset, attrs \\ %{})

@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. 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?(arg1)

@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?(arg1)

@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 API versions) and the modern pause_collection map returned by current Stripe versions.

pending_intent(arg1)

@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 subscribe/3 to surface SCA/3DS action-required to the caller.

statuses()

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

Canonical list of subscription statuses (Stripe's 8 values).

trialing?(arg1)

@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.