Main context for PhoenixKit Billing system.
Provides comprehensive billing functionality including currencies, billing profiles, orders, and invoices with manual bank transfer payments (Phase 1).
Features
- Currencies: Multi-currency support with exchange rates
- Billing Profiles: User billing information (individuals & companies)
- Orders: Order management with line items and status tracking
- Invoices: Invoice generation with receipt functionality
- Bank Payments: Manual bank transfer workflow
System Enable/Disable
# Check if billing is enabled
PhoenixKitBilling.enabled?()
# Enable/disable billing system
PhoenixKitBilling.enable_system()
PhoenixKitBilling.disable_system()Order Workflow
# Create order
{:ok, order} = Billing.create_order(user, %{...})
# Confirm order
{:ok, order} = Billing.confirm_order(order)
# Generate invoice
{:ok, invoice} = Billing.create_invoice_from_order(order)
# Send invoice
{:ok, invoice, _email_result} = Billing.send_invoice(invoice)
# Mark as paid (generates receipt)
{:ok, invoice} = Billing.mark_invoice_paid(invoice)
Summary
Functions
Returns list of available payment methods for manual recording. Bank transfer is always available, plus any enabled providers (Stripe/PayPal/Razorpay).
Calculates the total paid amount for an invoice from all transactions.
Calculates the current receipt status based on invoice state and transactions.
Cancels an order.
Cancels a subscription.
Returns a changeset for billing profile form.
Returns an order changeset for form building.
Returns a changeset for tracking payment option changes.
Changes a subscription's type.
Confirms an order.
Creates a billing profile.
Creates a checkout session for paying an invoice.
Creates a currency.
Creates a standalone invoice (without order).
Creates an invoice from an order.
Creates an order from attributes (user_uuid included in attrs).
Creates an order for a user.
Creates a payment method record.
Creates a new payment option.
Creates a setup session for saving a payment method.
Creates a new subscription for a user.
Creates a subscription type.
Deletes a billing profile.
Deletes a currency.
Deletes an order (only drafts).
Deletes a payment option.
Deletes a subscription type.
Disables the billing system.
Enables the billing system.
Checks if the billing system is enabled.
Formats company address from a company_info map for document printing.
Generates a receipt for an invoice.
Generates a unique transaction number.
The shop's base currency — the is_default row.
Gets a billing profile by ID or UUID, returns nil if not found.
Gets a billing profile by ID or UUID, raises if not found.
Returns the company info map used by the printable document views (invoice, receipt, credit note, payment confirmation).
Returns the current billing configuration.
Gets a currency by ID or UUID.
Gets a currency by ID or UUID, raises if not found.
Gets a currency by code.
Returns dashboard statistics.
Gets the default billing profile for a user.
Gets the default currency.
Gets the default payment method for a user.
The currency to SHOW and CHARGE for the current request.
Gets an invoice by ID or UUID with optional preloads.
Gets an invoice by ID or UUID.
Gets an invoice by invoice number.
Gets the remaining amount for an invoice.
Gets an order by ID or UUID with optional preloads.
Gets an order by ID or UUID.
Gets an order by order number.
Gets an order by UUID with optional preloads. Used for public-facing URLs to prevent ID enumeration.
Gets a payment method by ID or UUID.
Gets a payment option by ID.
Gets a payment option by code.
Gets a subscription by ID or UUID.
Gets a subscription by ID or UUID, raises if not found.
Gets a subscription type by ID or UUID.
Gets a subscription type by slug.
Returns the default tax rate as a Decimal (e.g., Decimal.new("0.20") for 20%).
Returns the default tax rate as integer percentage (e.g., 20 for 20%).
Gets a transaction by ID or UUID.
Gets a transaction by ID or UUID, raises if not found.
Gets a transaction by number.
Lists active payment options for checkout.
Lists billing profiles with optional filters.
Lists billing profiles with count for pagination.
Lists all currencies with optional filters.
Lists enabled currencies.
Gets transactions for a specific invoice.
Lists all invoices with optional filters.
Lists invoices for a specific order.
Lists invoices with count for pagination.
Lists all orders with optional filters.
Lists orders with count for pagination.
Lists saved payment methods for a user.
Lists all payment options.
Lists all subscription types.
Lists all subscriptions for a user.
Lists all transactions with optional filters.
Lists transactions with count for pagination.
Lists billing profiles for a user (shorthand).
Lists invoices for a specific user.
Lists orders for a specific user.
Lists all subscriptions for a specific user.
Marks an invoice as paid (generates receipt).
Marks an order as paid.
Marks an order as refunded.
Marks overdue invoices.
Returns stats for the module card on the admin Modules page.
Notification types this module contributes, collected by core's
Notifications.Types via function_exported?/3.
Pauses a subscription.
Checks if a payment option requires a billing profile.
Records a payment for an invoice.
Records a refund for an invoice.
Removes a payment method.
Resolves a display-currency CODE to a %Currency{}, fail-safe per §6.3.
Resumes a paused subscription.
Sends a credit note email for a refund transaction.
Sends credit note email to the customer.
Sends an invoice (marks as sent and sends email).
Sends invoice email to the customer.
Sends a payment confirmation email for an individual payment transaction.
Sends payment confirmation email to the customer.
Sends receipt for a paid invoice.
Sends receipt email to the customer.
Sets a billing profile as default.
Sets a currency as default.
Sets a payment method as the default for a user.
The configured snapshot-refresh policy: pending_only | never | always.
Whether an order's billing snapshot may still be refreshed from the live profile.
Returns whether tax is enabled in billing settings.
Toggles the active status of a payment option.
Updates a billing profile.
Updates a currency.
Updates an invoice.
Updates an invoice's paid_amount based on its transactions.
Updates an order.
Updates a payment option.
Updates the receipt status based on current invoice state. Call this after refunds to update the receipt status.
Updates a subscription with the given attributes.
Updates a subscription type.
Voids an invoice.
Functions
Returns list of available payment methods for manual recording. Bank transfer is always available, plus any enabled providers (Stripe/PayPal/Razorpay).
Examples
iex> Billing.available_payment_methods()
["bank"] # Only bank if no providers enabled
iex> Billing.available_payment_methods()
["bank", "stripe", "paypal"] # Bank + enabled providers
Calculates the total paid amount for an invoice from all transactions.
Calculates the current receipt status based on invoice state and transactions.
Cancels an order.
Cancels a subscription.
Options
immediately: true- Cancel immediately instead of at period end
Examples
Billing.cancel_subscription(subscription)
Billing.cancel_subscription(subscription, immediately: true)
Returns a changeset for billing profile form.
Returns an order changeset for form building.
Returns a changeset for tracking payment option changes.
Changes a subscription's type.
By default, the new type takes effect at the next billing cycle.
Confirms an order.
Creates a billing profile.
Creates a checkout session for paying an invoice.
Returns the checkout URL to redirect the user to.
Parameters
invoice- The invoice to payprovider- Payment provider atom (:stripe, :paypal, :razorpay)opts- Options forwarded to the provider::success_url- URL to redirect after success (required):cancel_url- URL to redirect if cancelled (defaults to:success_url):customer_email,:save_payment_method,:metadata- optional, provider-dependent
Examples
{:ok, url} = Billing.create_checkout_session(invoice, :stripe, success_url: "/success")
Creates a currency.
Creates a standalone invoice (without order).
Creates an invoice from an order.
Creates an order from attributes (user_uuid included in attrs).
Creates an order for a user.
Creates a payment method record.
Usually called after a successful setup session webhook.
Creates a new payment option.
Creates a setup session for saving a payment method.
Returns the setup URL to redirect the user to.
Parameters
user_uuid- The user saving the payment methodprovider- Payment provider atomopts- Options (success_url required)
Creates a new subscription for a user.
This creates the master subscription record. The first payment should be processed separately via checkout session.
Parameters
user_uuid- The user creating the subscription (UUID)attrs- Subscription attributes::subscription_type_uuid- Required: subscription type UUID:billing_profile_uuid- Optional: billing profile UUID to use:payment_method_uuid- Optional: saved payment method UUID for renewals:trial_days- Optional: override type's trial days:plan_uuid- Alternative: can use:plan_uuidinstead of:subscription_type_uuid
Examples
Billing.create_subscription(user.uuid, %{subscription_type_uuid: type.uuid})
Billing.create_subscription(user.uuid, %{subscription_type_uuid: type.uuid, trial_days: 14})
# Using plan_uuid parameter
Billing.create_subscription(user.uuid, %{plan_uuid: type.uuid})
Creates a subscription type.
Deletes a billing profile.
Deletes a currency.
The default currency and currencies referenced by orders cannot be deleted.
Deletes an order (only drafts).
Deletes a payment option.
Deletes a subscription type.
Types with active subscriptions cannot be deleted.
Disables the billing system.
Enables the billing system.
Checks if the billing system is enabled.
Formats company address from a company_info map for document printing.
The map is required (callers pass the result of
Organization.get_company_info/0), keeping this function pure.
Generates a receipt for an invoice.
Receipts can be generated:
- When invoice is fully paid (status: "paid")
- When invoice has any payment (paid_amount > 0) - partial receipt
Receipt status:
- "paid" - fully paid
- "partially_paid" - partial payment received
- "refunded" - fully refunded after payment
Generates a unique transaction number.
@spec get_base_currency() :: PhoenixKitBilling.Currency.t() | nil
The shop's base currency — the is_default row.
Authoring and admin paths (a product's stored price, an order's own
currency before display, anywhere the number being handled is the
base amount rather than something being shown to a shopper) call this
directly. Unlike get_display_currency/0, it never depends on the
request: the base currency is a property of the shop, not of who is
looking at it right now (§4.2).
Cached (§13): Currency.present/3 calls this — through
resolve_display_currency/1 — on every single price it converts, and
a catalog page converts dozens of prices per render. get_default_currency/0
itself stays a direct, uncached query (unchanged, on purpose — some
callers legitimately want the guarantee of a fresh read); this function
reuses it as the query underneath its own cache rather than duplicating
the Ecto query. See invalidate_currency_cache/0 for what clears the
cache and when.
Gets a billing profile by ID or UUID, returns nil if not found.
Gets a billing profile by ID or UUID, raises if not found.
Returns the company info map used by the printable document views (invoice, receipt, credit note, payment confirmation).
Combines organization company details and bank details into a single map of formatted, print-ready strings.
Returns the current billing configuration.
Gets a currency by ID or UUID.
Gets a currency by ID or UUID, raises if not found.
Gets a currency by code.
Cached (§13) alongside get_base_currency/0, under the same
:billing_currencies namespace — resolve_display_currency/1
calls this for every non-base code Currency.present/3 resolves.
Returns dashboard statistics.
Gets the default billing profile for a user.
Gets the default currency.
Gets the default payment method for a user.
@spec get_display_currency() :: PhoenixKitBilling.Currency.t() | nil
The currency to SHOW and CHARGE for the current request.
Resolves the request-scoped code set by the host app (a Plug/on_mount
hook calling PhoenixKitBilling.Currency.put_request_currency/1 — see
its docs) through resolve_display_currency/1, falling back to the
base currency per §6.3. Call this wherever a price is about to be
shown to (or charged from) the current shopper; call get_base_currency/0
instead for anything that reads or writes the stored, base-currency
amount.
Gets an invoice by ID or UUID with optional preloads.
Gets an invoice by ID or UUID.
Gets an invoice by invoice number.
Gets the remaining amount for an invoice.
Gets an order by ID or UUID with optional preloads.
Gets an order by ID or UUID.
Gets an order by order number.
Gets an order by UUID with optional preloads. Used for public-facing URLs to prevent ID enumeration.
Gets a payment method by ID or UUID.
Gets a payment option by ID.
Gets a payment option by code.
Gets a subscription by ID or UUID.
Options
:preload- list of associations to preload (default: [])
Gets a subscription by ID or UUID, raises if not found.
Gets a subscription type by ID or UUID.
Gets a subscription type by slug.
Returns the default tax rate as a Decimal (e.g., Decimal.new("0.20") for 20%).
Uses the billing settings value. When company country is configured, the suggested rate from BeamLabCountries can be applied via billing settings UI.
Returns the default tax rate as integer percentage (e.g., 20 for 20%).
Gets a transaction by ID or UUID.
Gets a transaction by ID or UUID, raises if not found.
Gets a transaction by number.
Lists active payment options for checkout.
Lists billing profiles with optional filters.
Options
:user_uuid- Filter by user UUID:type- Filter by type ("individual" or "company"):search- Search in name/email/company fields:page- Page number:per_page- Items per page:preload- Associations to preload
Lists billing profiles with count for pagination.
Lists all currencies with optional filters.
Options
:enabled- Filter by enabled status:order_by- Custom ordering
Lists enabled currencies.
Gets transactions for a specific invoice.
Lists all invoices with optional filters.
Lists invoices for a specific order.
Lists invoices with count for pagination.
Lists all orders with optional filters.
Lists orders with count for pagination.
Lists saved payment methods for a user.
Lists all payment options.
Lists all subscription types.
Options
:active_only- Only return active types (default: true)
Lists all subscriptions for a user.
Options
:status- Filter by status (e.g., "active", "cancelled"):preload- Associations to preload (default: [:subscription_type])
Examples
Billing.list_subscriptions(user_uuid)
Billing.list_subscriptions(user_uuid, status: "active")
Lists all transactions with optional filters.
Options
:invoice_uuid- Filter by invoice UUID:user_uuid- Filter by user who created the transaction:payment_method- Filter by payment method:type- Filter by type: "payment" (amount > 0) or "refund" (amount < 0):search- Search by transaction number:limit- Limit results:offset- Offset for pagination:preload- Associations to preload
Examples
Billing.list_transactions(invoice_uuid: "some-uuid")
Billing.list_transactions(type: "payment", limit: 10)
Lists transactions with count for pagination.
Lists billing profiles for a user (shorthand).
Lists invoices for a specific user.
Accepts the shared invoice filters, including :limit for callers that
only need the most recent few (rollup panels) and must not pull a
long-standing customer's entire invoice history into memory.
Lists orders for a specific user.
Lists all subscriptions for a specific user.
Options
:status- filter by status (e.g., "active", "cancelled"):preload- list of associations to preload (default: [:subscription_type])
Examples
Billing.list_user_subscriptions(user.uuid)
Billing.list_user_subscriptions(user.uuid, status: "active")
Marks an invoice as paid (generates receipt).
Marks an order as paid.
Options
:payment_method- The payment method used (e.g., "bank", "stripe", "paypal")
Marks an order as refunded.
Marks overdue invoices.
Returns stats for the module card on the admin Modules page.
Runs get_config/0, which issues three count queries (orders,
invoices, currencies). This is invoked once per render of the admin
Modules card, so the cost is bounded; it is not cached.
Notification types this module contributes, collected by core's
Notifications.Types via function_exported?/3.
Admin and customer audiences are separate sub-types on purpose: an operator muting the invoice firehose must not also silence their own receipts, and a customer's invoice notice must not be governed by an admin-facing preference.
⚠️ These are the NOTIFY actions. Audit rows use DIFFERENT action strings
(billing.invoice_issued_audit and friends): Activity.log/1
auto-derives notifications from registered actions, so an audit row
written with a notify action delivers a duplicate on top of the explicit
fan-out.
Pauses a subscription.
Paused subscriptions don't renew until resumed.
Checks if a payment option requires a billing profile.
Records a payment for an invoice.
Creates a transaction with positive amount and updates invoice's paid_amount. If paid_amount >= total, marks invoice as paid and generates receipt.
Parameters
invoice- The invoice to payattrs- Transaction attributes including :amount, :payment_method, :descriptionadmin_user- The admin user recording the payment
Examples
{:ok, transaction} = Billing.record_payment(invoice, %{amount: "100.00", payment_method: "bank"}, admin)
Records a refund for an invoice.
Creates a transaction with negative amount and updates invoice's paid_amount.
Parameters
invoice- The invoice to refundattrs- Transaction attributes including :amount (positive value), :description (reason)admin_user- The admin user recording the refund
Examples
{:ok, transaction} = Billing.record_refund(invoice, %{amount: "50.00", description: "Partial refund"}, admin)
Removes a payment method.
Marks as removed in database. Should also delete from provider.
@spec resolve_display_currency(String.t() | nil) :: PhoenixKitBilling.Currency.t() | nil
Resolves a display-currency CODE to a %Currency{}, fail-safe per §6.3.
nil (no request override) or the base currency's own code resolve to
the base, silently — that is the expected, unremarkable case, not a
failure worth a log line. Any other code resolves to itself only if it
is a currency this shop actually knows about, has enabled, and carries
a usable (positive) exchange rate; anything short of that — unknown
code, disabled currency, missing or non-positive rate — falls back to
the base currency rather than raising or showing a broken price, and
logs exactly ONE warning per process per offending code (tracked via
the process dictionary key {:phoenix_kit_billing_fx_warned, code}),
so a page that calls this many times over one request does not flood
the log for the same unresolvable code.
Resumes a paused subscription.
Sends a credit note email for a refund transaction.
Parameters
invoice- The invoice associated with the refundtransaction- The refund transactionopts- Options::to_email- Override recipient email:credit_note_url- URL to view credit note online
Returns {:ok, transaction, email_result} — see send_invoice/2's own
docs for what email_result means.
Examples
{:ok, transaction, _email_result} =
Billing.send_credit_note(invoice, transaction, credit_note_url: "https://...")
Sends credit note email to the customer.
Sends an invoice (marks as sent and sends email).
Options:
:send_email- Whether to send email (default: true):invoice_url- URL to view invoice online (optional)
Returns {:ok, invoice, email_result} on success — the status/history
update always commits independently of the email, so it's never folded
into {:error, _}. email_result is :skipped when :send_email is
false, otherwise whatever send_invoice_email/2 returned — check it
if the caller cares whether the customer was actually emailed, not
just whether the invoice record itself updated.
Sends invoice email to the customer.
Sends a payment confirmation email for an individual payment transaction.
Parameters
invoice- The invoice associated with the paymenttransaction- The payment transactionopts- Options including::to_email- Override recipient email address:payment_url- URL to view payment confirmation online:send_email- Whether to send email (default: true)
Returns {:ok, transaction, email_result} — see send_invoice/2's own
docs for what email_result means.
Sends payment confirmation email to the customer.
Sends receipt for a paid invoice.
Options:
:send_email- Whether to send email (default: true):to_email- Override recipient email address:receipt_url- URL to view receipt online (optional)
Returns {:ok, invoice, email_result} — see send_invoice/2's own
docs for what email_result means.
Sends receipt email to the customer.
Sets a billing profile as default.
Sets a currency as default.
Promoting a currency renormalizes every currency's exchange_rate
against it as the new base FIRST, via a raw update_all that bypasses
Currency.changeset/2 entirely (no changeset touches this step at
all) — only THEN is the promoted row itself updated through the
changeset, in step 3 below, pinning its own rate to exactly 1.0. That
order is the invariant the currency design spec fixes in §3.2 ("the
base currency's rate must be 1.0"). Renormalizing is a division by
every rate's ratio to the new base, so no conversion result changes:
only the displayed numbers become honest about which currency is the
base.
Refuses a nil or non-positive exchange_rate on currency with
{:error, :invalid_base_rate} instead of dividing every other rate by
zero.
NOTE: Currency.changeset/2 deliberately does NOT validate "the
default currency's rate is 1.0". set_default_currency/1 promotes
through that same changeset (update!/1 inside the transaction below),
so a changeset-level check would also block promoting a currency whose
own stored rate happens to be wrong — exactly the operation a host
needs to fix that. The invariant is enforced here, procedurally,
instead: renormalize, then promote at 1.0.
The struct the caller passes may be STALE by the time step 3 runs —
most importantly when the caller is re-promoting the currency that is
ALREADY the default (fixing its rate without switching currencies,
e.g. a one-off renormalization task): step 2's update_all has just
set that very row's is_default to false in the database, but the
passed-in struct's own is_default field still reads true from
before the call. Ecto.Changeset.cast/3 compares a field's new value
against the struct's OWN current value, not a fresh read of the row —
sees true -> true, decides nothing changed, and never puts
:is_default in the changeset's changes, so the UPDATE it emits
never touches that column. Left uncorrected, the row stays false
from step 2 and nothing is left default at all. Step 3 therefore
RELOADS the row before building the promote changeset, and forces both
:is_default and :exchange_rate into the changeset regardless of
what the reloaded row already says — belt-and-braces, since step 1's
renormalization also just changed this same row's rate out from under
any struct captured before the transaction.
Sets a payment method as the default for a user.
Unsets any existing default.
The configured snapshot-refresh policy: pending_only | never | always.
Whether an order's billing snapshot may still be refreshed from the live profile.
The snapshot is the record of who an order was billed to, so rewriting it after the fact makes history mutable. The line is drawn at money changing hands:
"draft"/"pending"— refreshable"confirmed"/"paid"/"refunded"/"cancelled"— frozen
Operators who want the stricter or looser rule set
billing_snapshot_policy to "never" (freeze from creation) or
"always" (the pre-2026-08 behaviour, refresh whenever the order's profile
changes). Fails closed to the default on a settings-layer error.
Scope
This governs a profile SWITCH — the order pointing at a different
billing profile than before. Editing the fields of the profile an order
already points at never refreshes the snapshot, at any status or policy;
maybe_update_billing_snapshot/2 only reconsiders when the incoming
billing_profile_uuid differs (or the snapshot is empty). Refreshing on
in-place profile edits would need the snapshot compared field by field,
which is not implemented.
Returns whether tax is enabled in billing settings.
Toggles the active status of a payment option.
Updates a billing profile.
Updates a currency.
Updates an invoice.
Updates an invoice's paid_amount based on its transactions.
Updates an order.
Updates a payment option.
Updates the receipt status based on current invoice state. Call this after refunds to update the receipt status.
Updates a subscription with the given attributes.
Useful for administrative adjustments such as extending the billing
period or correcting plan details. Status/lifecycle fields are not
updatable here — use the dedicated cancel_subscription/1,
pause_subscription/1, and resume_subscription/1 functions instead,
so their broadcasts and bookkeeping always fire.
Updates a subscription type.
Voids an invoice.