Accrue.Billing.Query (accrue v1.4.0)

Copy Markdown View Source

Composable Ecto.Query fragments mirroring the Accrue.Billing.Subscription predicates.

Every predicate in Accrue.Billing.Subscription has a matching query fragment here so you can filter subscriptions in the database with the same semantics as the in-memory predicates. Prefer these fragments over direct .status comparisons in where clauses — the predicates on Accrue.Billing.Subscription are the correct way to check subscription state, as direct comparisons miss edge cases like cancel_at_period_end and ended_at that the predicates cover.

All functions accept an optional queryable (default Accrue.Billing.Subscription) and compose via |>:

import Ecto.Query

from(s in Subscription, where: s.customer_id == ^id)
|> Accrue.Billing.Query.active()
|> Repo.all()

Summary

Functions

Subscriptions counted as active (includes :trialing).

Subscriptions that are terminated (:canceled, :incomplete_expired, or any ended_at).

Subscriptions that are :active with cancel_at_period_end set and a period end still in the future — i.e. the cancel hasn't landed yet.

Subscriptions eligible for a dunning sweep tick: strictly :past_due, with past_due_since older than the grace window, and with no prior dunning_sweep_attempted_at stamp.

Subscriptions whose lifecycle grants entitlement: active/trialing, not paused, not ended.

Entitlement candidates including :past_due rows for the grace overlay.

Subscriptions currently in an active dunning campaign (anchor column is non-nil). Composable — pipe after any Subscription query to add the campaign-active predicate.

Subscriptions that are past due or unpaid (dunning territory).

Subscriptions that are paused (legacy :paused status or non-nil pause_collection).

Subscriptions currently in trial.

Functions

active(query \\ Subscription)

@spec active(Ecto.Queryable.t()) :: Ecto.Query.t()

Subscriptions counted as active (includes :trialing).

canceled(query \\ Subscription)

@spec canceled(Ecto.Queryable.t()) :: Ecto.Query.t()

Subscriptions that are terminated (:canceled, :incomplete_expired, or any ended_at).

canceling(query \\ Subscription)

@spec canceling(Ecto.Queryable.t()) :: Ecto.Query.t()

Subscriptions that are :active with cancel_at_period_end set and a period end still in the future — i.e. the cancel hasn't landed yet.

dunning_sweep_candidates(grace_days, query \\ Subscription)

@spec dunning_sweep_candidates(pos_integer(), Ecto.Queryable.t()) :: Ecto.Query.t()

Subscriptions eligible for a dunning sweep tick: strictly :past_due, with past_due_since older than the grace window, and with no prior dunning_sweep_attempted_at stamp.

entitling(query \\ Subscription)

@spec entitling(Ecto.Queryable.t()) :: Ecto.Query.t()

Subscriptions whose lifecycle grants entitlement: active/trialing, not paused, not ended.

The database twin of Accrue.Billing.Subscription.entitling?/1 — the rows this fragment returns are exactly those for which entitling?/1 is true. Beyond active/1's status set it adds is_nil(s.pause_collection) (the SQL twin of paused?/1's non-nil pause_collection head, closing the status: :active + pause_collection fail-open gap) and is_nil(s.ended_at) (the SQL twin of canceled?/1's terminal ended_at override). It deliberately does NOT add the legacy :paused status OR-clause that paused/1 carries, because active/1's status set already excludes :paused.

Distinct from active/1, which keeps its status-only semantics for other callers (e.g. the dunning sweeper and projections).

entitling_with_grace_candidates(query \\ Subscription)

@spec entitling_with_grace_candidates(Ecto.Queryable.t()) :: Ecto.Query.t()

Entitlement candidates including :past_due rows for the grace overlay.

The grace-widen twin of entitling/1: it adds :past_due (and ONLY :past_due — never :unpaid, which is dunning-terminal) to the status set while keeping both is_nil(s.pause_collection) and is_nil(s.ended_at) guards. This widens the read just enough to surface candidate :past_due rows; the per-row grace-window cutoff check stays in Elixir (Accrue.Entitlements.PastDueGrace.within_grace?/2) because the clock comparison must be test-driven, so this fragment deliberately does NOT do any cutoff math.

Used only by Accrue.Entitlements.Resolver.LocalMap.fold_active/1 when Accrue.Config.past_due_grace/0 is enabled; the :none default path uses the leaner entitling/1 instead (zero query change).

in_active_dunning_campaign(query \\ Subscription)

@spec in_active_dunning_campaign(Ecto.Queryable.t()) :: Ecto.Query.t()

Subscriptions currently in an active dunning campaign (anchor column is non-nil). Composable — pipe after any Subscription query to add the campaign-active predicate.

past_due(query \\ Subscription)

@spec past_due(Ecto.Queryable.t()) :: Ecto.Query.t()

Subscriptions that are past due or unpaid (dunning territory).

paused(query \\ Subscription)

@spec paused(Ecto.Queryable.t()) :: Ecto.Query.t()

Subscriptions that are paused (legacy :paused status or non-nil pause_collection).

trialing(query \\ Subscription)

@spec trialing(Ecto.Queryable.t()) :: Ecto.Query.t()

Subscriptions currently in trial.