Accrue.Billing (accrue v0.3.0)

Copy Markdown View Source

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 an accrue_customers row via the configured processor; subsequent calls return the cached row.
  • create_customer/1 — explicit create, always hits the processor.
  • customer!/1 and create_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

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.

Shallow-merges partial_data into the existing data column (D2-08).

Fully replaces the data jsonb column on a billing record (D2-08).

Updates a Customer with the given attributes.

Updates a processor-backed customer's tax location with immediate validation.

Functions

add_item(sub, price_id, opts \\ [])

add_item!(sub, price_id, opts \\ [])

apply_promotion_code(sub, code, opts \\ [])

apply_promotion_code!(sub, code, opts \\ [])

attach_payment_method(customer, pm_id_or_opts, opts \\ [])

attach_payment_method!(customer, pm_id_or_opts, opts \\ [])

cancel(sub, opts \\ [])

cancel!(sub, opts \\ [])

cancel_at_period_end(sub, opts \\ [])

cancel_at_period_end!(sub, opts \\ [])

cancel_schedule(sched, opts \\ [])

cancel_schedule!(sched, opts \\ [])

charge(customer, amount_or_opts, opts \\ [])

charge!(customer, amount_or_opts, opts \\ [])

comp_subscription(billable, price_spec, opts \\ [])

comp_subscription!(billable, price_spec, opts \\ [])

create_coupon(params, opts \\ [])

create_coupon!(params, opts \\ [])

create_customer(billable)

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

  1. Create the customer on the processor side (Fake or Stripe)
  2. Insert the accrue_customers row with the processor-assigned ID
  3. Record a "customer.created" event (EVT-04)

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"

create_customer!(billable)

@spec create_customer!(struct()) :: Accrue.Billing.Customer.t()

Raising variant of create_customer/1. Returns the Customer directly or raises on error.

create_payment_intent(customer, opts \\ [])

create_payment_intent!(customer, opts \\ [])

create_promotion_code(params, opts \\ [])

create_promotion_code!(params, opts \\ [])

create_refund(charge, opts \\ [])

create_refund!(charge, opts \\ [])

create_setup_intent(customer, opts \\ [])

create_setup_intent!(customer, opts \\ [])

customer(billable)

@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

customer!(billable)

@spec customer!(struct()) :: Accrue.Billing.Customer.t()

Raising variant of customer/1. Returns the Customer directly or raises on error.

detach_payment_method(payment_method, opts \\ [])

detach_payment_method!(payment_method, opts \\ [])

fetch_invoice_pdf(invoice_or_id)

finalize_invoice(invoice, opts \\ [])

finalize_invoice!(invoice, opts \\ [])

get_subscription(id, opts \\ [])

get_subscription!(id, opts \\ [])

mark_uncollectible(invoice, opts \\ [])

mark_uncollectible!(invoice, opts \\ [])

patch_data(record, partial_data)

@spec patch_data(Ecto.Schema.t(), map()) :: {:ok, Ecto.Schema.t()} | {:error, term()}

Shallow-merges partial_data into the existing data column (D2-08).

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

pause(sub, opts \\ [])

pause!(sub, opts \\ [])

pay_invoice(invoice, opts \\ [])

pay_invoice!(invoice, opts \\ [])

preview_upcoming_invoice(sub_or_customer, opts \\ [])

preview_upcoming_invoice!(sub_or_customer, opts \\ [])

put_data(record, new_data)

@spec put_data(Ecto.Schema.t(), map()) :: {:ok, Ecto.Schema.t()} | {:error, term()}

Fully replaces the data jsonb column on a billing record (D2-08).

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

release_schedule(sched, opts \\ [])

release_schedule!(sched, opts \\ [])

remove_item(item, opts \\ [])

remove_item!(item, opts \\ [])

render_invoice_pdf(invoice_or_id, opts \\ [])

report_usage(customer, event_name, opts \\ [])

report_usage!(customer, event_name, opts \\ [])

resume(sub, opts \\ [])

resume!(sub, opts \\ [])

send_invoice(invoice, opts \\ [])

send_invoice!(invoice, opts \\ [])

set_default_payment_method(customer, pm_id, opts \\ [])

set_default_payment_method!(customer, pm_id, opts \\ [])

store_invoice_pdf(invoice_or_id, opts \\ [])

subscribe(user, price_id_or_opts \\ [], opts \\ [])

subscribe!(user, price_id_or_opts \\ [], opts \\ [])

subscribe_via_schedule(billable, phases, opts \\ [])

subscribe_via_schedule!(billable, phases, opts \\ [])

swap_plan(sub, new_price_id, opts)

swap_plan!(sub, new_price_id, opts)

unpause(sub, opts \\ [])

unpause!(sub, opts \\ [])

update_customer(customer, attrs)

@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 (EVT-04). Metadata is validated per D2-07 (flat string map, max 50 keys, etc.). Optimistic locking via lock_version prevents torn writes (D2-09).

Examples

{:ok, customer} = Accrue.Billing.update_customer(customer, %{metadata: %{"tier" => "pro"}})

update_customer_tax_location(customer, attrs)

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

update_customer_tax_location!(customer, attrs)

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

update_item_quantity(item, quantity, opts \\ [])

update_item_quantity!(item, quantity, opts \\ [])

update_quantity(sub, quantity, opts \\ [])

update_quantity!(sub, quantity, opts \\ [])

update_schedule(sched, params, opts \\ [])

update_schedule!(sched, params, opts \\ [])

void_invoice(invoice, opts \\ [])

void_invoice!(invoice, opts \\ [])