Primary context module for Accrue billing operations.
All write operations for billable entities live here, following
conventional Phoenix context boundaries. Host schemas gain access to
these operations via use Accrue.Billable, which injects a
convenience customer/1 that delegates here.
Customer lifecycle
customer/1— lazy fetch-or-create. First call auto-creates anaccrue_customersrow via the configured processor; subsequent calls return the cached row.create_customer/1— explicit create, always hits the processor.customer!/1andcreate_customer!/1— raising variants following the same{:ok, _} | {:error, _}vs!naming convention as the rest of Accrue.
All writes use Ecto.Multi to ensure the customer row and the
corresponding accrue_events entry are committed atomically.
Summary
Functions
Creates a Customer Billing Portal session for customer through the
configured processor.
Bang variant of create_billing_portal_session/2 — returns
%Accrue.BillingPortal.Session{} or raises.
Creates a Stripe Checkout Session for customer through the configured
processor.
Bang variant of create_checkout_session/2 — returns
%Accrue.Checkout.Session{} or raises.
Explicitly creates a Customer for the given billable struct.
Raising variant of create_customer/1. Returns the Customer
directly or raises on error.
Lazily fetches or creates a Customer for the given billable struct.
Raising variant of customer/1. Returns the Customer directly or
raises on error.
Lists payment methods for customer from the configured processor (Stripe
or Fake). This is read-through processor state, not a projection of
local accrue_payment_methods rows.
Raising variant of list_payment_methods/2.
Shallow-merges partial_data into the existing data column.
Fully replaces the data jsonb column on a billing record.
report_usage/3 records a metered usage event for customer (a %Accrue.Billing.Customer{} or Stripe customer id string) and event_name, persisting through the transactional outbox before invoking the configured processor.
Bang variant of report_usage/3 — returns %Accrue.Billing.MeterEvent{} or raises on error.
Updates a Customer with the given attributes.
Updates a processor-backed customer's tax location with immediate validation.
Raising variant of update_customer_tax_location/2.
Functions
@spec create_billing_portal_session(Accrue.Billing.Customer.t(), keyword() | map()) :: {:ok, Accrue.BillingPortal.Session.t()} | {:error, term()}
Creates a Customer Billing Portal session for customer through the
configured processor.
attrs is a keyword list or map of options aligned with
Accrue.BillingPortal.Session.create/1, except :customer (supplied as
the first argument): :return_url, :configuration, :flow_data,
:locale, :on_behalf_of, :operation_id.
Invalid keys or types raise NimbleOptions.ValidationError.
The portal session URL is a short-lived bearer credential. Do not
log raw session structs, processor payloads, or URLs in production telemetry
or support tickets. For :configuration, see
guides/portal_configuration_checklist.md.
Emits [:accrue, :billing, :billing_portal, :create] (OpenTelemetry name
accrue.billing.billing_portal.create).
@spec create_billing_portal_session!(Accrue.Billing.Customer.t(), keyword() | map()) :: Accrue.BillingPortal.Session.t()
Bang variant of create_billing_portal_session/2 — returns
%Accrue.BillingPortal.Session{} or raises.
Raises NimbleOptions.ValidationError when attrs fail validation.
On {:error, reason} from the underlying Session.create/1, re-raises when
reason implements Exception; otherwise raises the same message shape as
Accrue.BillingPortal.Session.create!/1.
@spec create_checkout_session(Accrue.Billing.Customer.t(), keyword() | map()) :: {:ok, Accrue.Checkout.Session.t()} | {:error, term()}
Creates a Stripe Checkout Session for customer through the configured
processor.
attrs is a keyword list or map of options aligned with
Accrue.Checkout.Session.create/1, except :customer (supplied as the
first argument): :mode, :ui_mode, :line_items, :success_url,
:cancel_url, :return_url, :metadata, :client_reference_id,
:automatic_tax, :operation_id.
Invalid keys or types raise NimbleOptions.ValidationError.
The Checkout redirect URL (hosted mode) and client_secret (embedded
mode) are bearer credentials. Do not log raw session structs, processor
payloads, or URLs in production telemetry or support tickets.
Emits [:accrue, :billing, :checkout_session, :create] (OpenTelemetry-style
name accrue.billing.checkout_session.create). See Accrue.Checkout.Session
for field semantics and the underlying @create_schema.
@spec create_checkout_session!(Accrue.Billing.Customer.t(), keyword() | map()) :: Accrue.Checkout.Session.t()
Bang variant of create_checkout_session/2 — returns
%Accrue.Checkout.Session{} or raises.
Raises NimbleOptions.ValidationError when attrs fail validation.
On {:error, reason} from the underlying CheckoutSession.create/1,
re-raises when reason implements Exception; otherwise raises with prefix
Accrue.Checkout.Session.create/1 failed:.
@spec create_customer(struct()) :: {:ok, Accrue.Billing.Customer.t()} | {:error, term()}
Explicitly creates a Customer for the given billable struct.
Uses Ecto.Multi to atomically:
- Create the customer on the processor side (Fake or Stripe)
- Insert the
accrue_customersrow with the processor-assigned ID - Record a
"customer.created"event
Returns {:ok, %Customer{}} on success or {:error, reason} on
failure. The entire transaction rolls back if any step fails.
Examples
{:ok, customer} = Accrue.Billing.create_customer(user)
customer.processor_id #=> "cus_fake_00001"
@spec create_customer!(struct()) :: Accrue.Billing.Customer.t()
Raising variant of create_customer/1. Returns the Customer
directly or raises on error.
@spec customer(struct()) :: {:ok, Accrue.Billing.Customer.t()} | {:error, term()}
Lazily fetches or creates a Customer for the given billable struct.
If a customer row already exists for the billable's owner_type and
owner_id, returns it. Otherwise, creates one via the configured
processor and persists it atomically with an event record.
Examples
{:ok, customer} = Accrue.Billing.customer(user)
{:ok, ^customer} = Accrue.Billing.customer(user) # same row
@spec customer!(struct()) :: Accrue.Billing.Customer.t()
Raising variant of customer/1. Returns the Customer directly or
raises on error.
Lists payment methods for customer from the configured processor (Stripe
or Fake). This is read-through processor state, not a projection of
local accrue_payment_methods rows.
Delegates to Accrue.Billing.PaymentMethodActions.list_payment_methods/2.
See that module for supported opts filters (type, limit, pagination
cursors).
Raising variant of list_payment_methods/2.
Delegates to Accrue.Billing.PaymentMethodActions.list_payment_methods!/2.
@spec patch_data(Ecto.Schema.t(), map()) :: {:ok, Ecto.Schema.t()} | {:error, term()}
Shallow-merges partial_data into the existing data column.
Used when a partial event carries only a delta. Applies optimistic
locking via lock_version.
Examples
{:ok, patched} = Accrue.Billing.patch_data(customer, %{"balance" => 100})
@spec put_data(Ecto.Schema.t(), map()) :: {:ok, Ecto.Schema.t()} | {:error, term()}
Fully replaces the data jsonb column on a billing record.
Used by webhook reconcile paths that receive the whole object (e.g.
customer.updated). Applies optimistic locking via lock_version.
Examples
{:ok, updated} = Accrue.Billing.put_data(customer, %{"balance" => 0})
report_usage/3 records a metered usage event for customer (a %Accrue.Billing.Customer{} or Stripe customer id string) and event_name, persisting through the transactional outbox before invoking the configured processor.
Options
Keys mirror Accrue.Billing.MeterEventActions's @report_usage_schema (types and defaults stay in sync there):
:value— non-negative integer count; default1.:timestamp—%DateTime{}, Unix seconds as integer, ornil. Whennil, normalization uses the current UTC instant; seeAccrue.Billing.MeterEventActionsfor the exact normalization pipeline.:identifier— string ornil; defaultnilderives a stable audit-layer identifier from customer,event_name,:value, the resolved timestamp, and optional:operation_id(uniqueness enforced onaccrue_meter_events.identifier).:operation_id— string ornil; when set, it participates in identifier derivation and supports idempotent replays alongside the other fields above.:payload— map of extra dimensions ornil; defaultnil. Forwarded to the processor as supplemental context (e.g.%{"dimension" => "seats"}in tests).
Invalid opts raise NimbleOptions.ValidationError from NimbleOptions.validate!/2.
Error tuples vs persisted rows
{:error, _} means this call could not advance durable meter state as
requested (for example the processor rejected the usage report). After
retries with the same idempotency inputs, {:ok, %Accrue.Billing.MeterEvent{}}
may be returned when the row already reflects a terminal outcome — inspect
stripe_status and stripe_error on the persisted row for the canonical
failure details. See guides/metering.md for how public calls, internal rows,
and the processor seam relate.
Fake / test mode
Host apps can configure Accrue.Processor.Fake (for example via Accrue.Test.setup_fake_processor/1) to exercise this path without outbound network calls.
Bang variant of report_usage/3 — returns %Accrue.Billing.MeterEvent{} or raises on error.
See report_usage/3 for the full options reference (## Options).
Raises NimbleOptions.ValidationError when opts fail validation.
Raises on {:error, _} from the underlying implementation when that tuple
indicates a true failure for this invocation (for example a missing customer
or a processor error that performed the failing attempt). When the non-bang
report_usage/3 would return {:ok, row} on an idempotent replay (including
a row already in failed), this function returns that row without raising.
Accrue.APIError (including resource_missing / HTTP 404) is re-raised when
it implements Exception; other error tuples become a RuntimeError raised by
Accrue.Billing.MeterEventActions.report_usage!/3.
@spec update_customer( %Accrue.Billing.Customer{ __meta__: term(), charges: term(), data: term(), default_payment_method: term(), default_payment_method_id: term(), email: term(), id: term(), inserted_at: term(), invoices: term(), last_stripe_event_id: term(), last_stripe_event_ts: term(), lock_version: term(), metadata: term(), name: term(), owner_id: term(), owner_type: term(), payment_methods: term(), preferred_locale: term(), preferred_timezone: term(), processor: term(), processor_id: term(), subscriptions: term(), updated_at: term() }, map() ) :: {:ok, Accrue.Billing.Customer.t()} | {:error, term()}
Updates a Customer with the given attributes.
Uses Ecto.Multi to atomically update the customer and record a
"customer.updated" event. Metadata is validated as a flat string
map (max 50 keys, etc.). Optimistic locking via lock_version
prevents torn writes.
Examples
{:ok, customer} = Accrue.Billing.update_customer(customer, %{metadata: %{"tier" => "pro"}})
@spec update_customer_tax_location( %Accrue.Billing.Customer{ __meta__: term(), charges: term(), data: term(), default_payment_method: term(), default_payment_method_id: term(), email: term(), id: term(), inserted_at: term(), invoices: term(), last_stripe_event_id: term(), last_stripe_event_ts: term(), lock_version: term(), metadata: term(), name: term(), owner_id: term(), owner_type: term(), payment_methods: term(), preferred_locale: term(), preferred_timezone: term(), processor: term(), processor_id: term(), subscriptions: term(), updated_at: term() }, map() ) :: {:ok, Accrue.Billing.Customer.t()} | {:error, term()}
Updates a processor-backed customer's tax location with immediate validation.
This public path is distinct from update_customer/2, which remains a
local-only row update for non-processor customer maintenance.
@spec update_customer_tax_location!( %Accrue.Billing.Customer{ __meta__: term(), charges: term(), data: term(), default_payment_method: term(), default_payment_method_id: term(), email: term(), id: term(), inserted_at: term(), invoices: term(), last_stripe_event_id: term(), last_stripe_event_ts: term(), lock_version: term(), metadata: term(), name: term(), owner_id: term(), owner_type: term(), payment_methods: term(), preferred_locale: term(), preferred_timezone: term(), processor: term(), processor_id: term(), subscriptions: term(), updated_at: term() }, map() ) :: Accrue.Billing.Customer.t()
Raising variant of update_customer_tax_location/2.