Accrue.Billing.Subscription (accrue v0.3.0)

Copy Markdown View Source

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.

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 D4-02 dunning sweeper is allowed to ask the processor facade to move it to a terminal action.

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.

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

Canonical list of subscription statuses (D3-03, 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 per D3-03.

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 (BILL-15, D4-02) 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 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(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 (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?(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 versions) and the modern pause_collection map (D3-03).

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

statuses()

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

Canonical list of subscription statuses (D3-03, 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.