Accrue.Entitlements (accrue v1.4.0)

Copy Markdown View Source

Public, fail-closed entitlement gate API.

Four boolean/scalar functions answer "what has this billable paid for?" from local subscription state only (via the configured Accrue.Entitlements.Resolver, default Accrue.Entitlements.Resolver.LocalMap):

Fail-closed contract

Every function fails closed: nil/non-billable/no-customer/no-active-sub/ unmapped/raising-resolver all collapse to false / [] / 0. {:ok, true} (a present affirmative match) is the SOLE path to true. Errors, exceptions, throws, and exits are caught and collapse to the fail-closed value — a billing/availability hiccup never grants a paid feature for free.

Multi-active-plan

has_active_plan?/2 tests membership in the resolved active_plans SET (ALL active plan atoms), never the representative :plan — so a billable holding two active subscriptions on two different mapped plans answers true for BOTH, consistent with the UNION semantics of entitled?/2 and features_for/1.

Telemetry (per-check, NOT the audit ledger)

Each check emits [:accrue, :entitlements, :check, :start | :stop | :exception] via Accrue.Telemetry.span/3 with metadata %{feature, result, resolver, reason, surface, subject_type, subject_id}. subject_id is the internal customer/billable id only — never email/name or any PII. Per-check decisions are telemetry only; this module NEVER writes to the accrue_events audit ledger.

surface: :plug | :live is an additive, guard-supplied metadata dimension (D-18): entitled?/3 and has_active_plan?/3 accept an optional opts keyword list whose :surface is merged onto the same :check span, so a deny/allow from the Plug guard is distinguishable from one from the LiveView on_mount guard. It defaults to nil for direct (non-guard) callers and is internal telemetry only — the public Accrue.entitled?/2 / Accrue.has_active_plan?/2 facade delegates stay arity 2.

Summary

Functions

Returns true iff billable's resolved active feature set contains feature. Fail-closed false otherwise.

Returns the seat/quota count for quota_key (min(cap, quantity) where a cap exists, else the raw quantity). Fail-closed 0.

Returns the sorted, deduped list of features granted by billable's active plans. Always a plain [atom], never a MapSet. Fail-closed [].

Returns true iff billable holds plan among its active plans. plan is a plan atom or a price_id string (reverse-indexed to its plan atom). Tests membership in the SET of ALL active plans — multi-active-plan correct. Fail-closed false otherwise.

Functions

entitled?(billable, feature, opts \\ [])

@spec entitled?(term(), atom(), keyword()) :: boolean()

Returns true iff billable's resolved active feature set contains feature. Fail-closed false otherwise.

opts is an additive, internal keyword list; surface: :plug | :live (guard-supplied, D-18) is merged onto the :check span metadata. All existing 2-arity callers (incl. the Accrue.entitled?/2 facade delegate) are unaffected.

entitlement_quantity(billable, quota_key)

@spec entitlement_quantity(term(), atom()) :: non_neg_integer()

Returns the seat/quota count for quota_key (min(cap, quantity) where a cap exists, else the raw quantity). Fail-closed 0.

features_for(billable)

@spec features_for(term()) :: [atom()]

Returns the sorted, deduped list of features granted by billable's active plans. Always a plain [atom], never a MapSet. Fail-closed [].

has_active_plan?(billable, plan, opts \\ [])

@spec has_active_plan?(term(), atom() | String.t(), keyword()) :: boolean()

Returns true iff billable holds plan among its active plans. plan is a plan atom or a price_id string (reverse-indexed to its plan atom). Tests membership in the SET of ALL active plans — multi-active-plan correct. Fail-closed false otherwise.

opts is an additive, internal keyword list; surface: :plug | :live (guard-supplied, D-18) is merged onto the :check span metadata. All existing 2-arity callers (incl. the Accrue.has_active_plan?/2 facade delegate) are unaffected.