PhoenixKitBilling

Elixir License: MIT

Billing module for PhoenixKit. Drop-in payments, subscriptions, invoices, orders, and multi-currency support with Stripe, PayPal, Razorpay, and EveryPay integration.

Features

Installation

Add phoenix_kit_billing to your dependencies in mix.exs:

def deps do
  [
    {:phoenix_kit_billing, "~> 0.3"}
  ]
end

Then fetch dependencies:

mix deps.get

Note: For development or if not yet published to Hex, you can use:

{:phoenix_kit_billing, github: "BeamLabEU/phoenix_kit_billing"}

PhoenixKit auto-discovers the module at startup — no additional configuration needed.

Quick Start

  1. Add the dependency to mix.exs
  2. Run mix deps.get
  3. Enable the module in admin settings (billing_enabled: true)
  4. Configure at least one payment provider in admin settings
  5. Orders and invoices are available at /admin/billing

Usage

Order-to-Invoice Workflow

alias PhoenixKitBilling

# Create an order
{:ok, order} = Billing.create_order(user, %{
  line_items: [%{description: "Widget", quantity: 1, unit_price: Decimal.new("29.99")}],
  currency: "EUR"
})

# Confirm the order
{:ok, order} = Billing.confirm_order(order)

# Generate an invoice from the order
{:ok, invoice} = Billing.create_invoice_from_order(order)

# Send the invoice to the customer
{:ok, invoice} = Billing.send_invoice(invoice)

# Mark as paid (generates receipt automatically)
{:ok, invoice} = Billing.mark_invoice_paid(invoice)

Subscriptions

# Create a subscription type (plan)
{:ok, type} = Billing.create_subscription_type(%{
  name: "Pro Monthly",
  interval: "month",
  price: Decimal.new("19.99"),
  currency: "EUR"
})

# Create a subscription for a user
{:ok, subscription} = Billing.create_subscription(user, type)

Subscription renewals and failed-payment retries (dunning) are handled automatically by Oban workers.

Payment Providers

Supported payment providers:

Provider Code Notes
Stripe :stripe Cards and wallets; signed webhooks
PayPal :paypal PayPal and cards; signed webhooks
Razorpay :razorpay India (UPI, cards); signed webhooks
EveryPay :everypay EveryPay AS gateway (Baltics), API v4; callbacks verified by server-side status re-fetch

Providers are configured in admin settings. The system uses hosted checkout — users are redirected to the provider's payment page:

# Create a checkout session for an invoice
{:ok, session} = Billing.create_checkout_session(invoice, :stripe, %{
  success_url: "https://example.com/success",
  cancel_url: "https://example.com/cancel"
})

# Redirect user to session.url

Webhooks are handled automatically at /webhooks/billing/:provider.

Host setup — raw body reader. Webhook signature verification needs the raw request body, so your endpoint must wire PhoenixKitBilling.Plugs.CacheBodyReader into Plug.Parsers:

plug Plug.Parsers,
  parsers: [:urlencoded, :multipart, :json],
  body_reader: {PhoenixKitBilling.Plugs.CacheBodyReader, :read_body, []},
  json_decoder: Phoenix.json_library()

mix phoenix_kit_billing.install does this for you. Without it, webhooks return 400 with :no_raw_body before processing.

Testing Stripe locally

  1. Configure test keys at /admin/settings/billing/providers (Stripe card): paste your sk_test_… / pk_test_…, tick Enabled, Save. The UI writes the billing_stripe_secret_key / billing_stripe_publishable_key / billing_stripe_webhook_secret settings; "Active Providers: Stripe" then confirms available?/0 is true.

  2. Forward webhooks to localhost with the Stripe CLI (no stripe login needed — pass the key directly):

    brew install stripe/stripe-cli/stripe   # once
    stripe listen --api-key sk_test_… \
      --forward-to http://localhost:4000/phoenix_kit/webhooks/billing/stripe

    It prints Your webhook signing secret is whsec_…. Paste that value into the Webhook Secret field on the providers page and Save — it's what the controller verifies against (not the dashboard endpoint's secret). Adjust the /phoenix_kit prefix to your host's mount.

  3. Fire a test event:

    stripe trigger checkout.session.completed --api-key sk_test_…

    Success = a [200] in the stripe listen output, a phoenix_kit_webhook_events row with processed=true, and a Webhook processed successfully log line. A 400 with :no_raw_body means the host hasn't wired CacheBodyReader (see above).

Real-Time Events

Subscribe to billing events in your LiveViews:

def mount(_params, _session, socket) do
  PhoenixKitBilling.Events.subscribe_orders()
  {:ok, socket}
end

def handle_info({:order_created, order}, socket) do
  # Update UI
  {:noreply, socket}
end

Settings

Key Type Default Description
billing_enabled boolean false Enable/disable the billing system
billing_default_currency string "EUR" Default currency for new orders
billing_tax_enabled boolean false Enable tax calculations
billing_company_name string — Company name on invoices
billing_company_address string — Company address on invoices

Provider-specific settings (API keys, webhook secrets) are configured per-provider in the admin panel at /admin/settings/billing/providers.

Invoice Status Workflow

Status Description
"draft" Invoice created, not yet sent
"sent" Invoice sent to customer
"paid" Payment received, receipt generated
"overdue" Past due date, not yet paid
"void" Cancelled / voided
draft → sent → paid
            ↘
           overdue → paid
            ↘
            void

Emails

Billing sends four emails to the customer: billing_invoice, billing_receipt, billing_credit_note and billing_payment_confirmation. They go out through phoenix_kit_emails (required_modules/0) and appear in core's email preview (/admin/settings/email-sending/preview) with sample data.

Language. A send passes the customer's preferred locale when their account has one (a guest payer gets the site's language). Billing's own copy follows it on a core whose RecipientLocale.in_locale/2 also sets the locale for a module's Gettext backend (BeamLabEU/phoenix_kit#892). On an older core billing's copy is translated into the sending process's locale instead — the admin's language for a send from the admin, whatever the global locale is for a background job — while host override files are still picked by the recipient's locale.

Each email's built-in copy (PhoenixKitBilling.EmailDefaults) has three parts:

Part Builds Line items as
subject the subject —
markdown the HTML version, inside core's layout {{{line_items_table_html}}} — a ready-made table
text the plain-text version {{line_items_text}} — one line per item

The link to the document online ({{invoice_url}}, {{receipt_url}}, {{credit_note_url}}, {{payment_url}}) is a button. The company's details close the body; a blank one (an empty VAT number, say) is left out, and so is the invoice's bank transfer section when there is no IBAN.

Overriding. Put files under priv/phoenix_kit_templates/<email name>/ in the host app, as for any email (subject.txt, markdown.md, html.html, text.txt, each optionally per locale: markdown.et.md). A part you don't override keeps billing's default. To keep the line-items table in the HTML version, place {{{line_items_table_html}}} — three braces, because the value is HTML; billing escapes and styles the table inline — in one of:

A host that overrides only text.txt changes the plain-text version, and, on a core that builds the HTML from a host's text, the HTML version too — then without the table. {{{line_items_html}}} is the older form: bare <tr> rows for a template that wraps them in its own <table>.

Layout group. Billing emails are sent with layout: "billing", so a host can give them their own chrome — company details in the footer, say — with _layout-billing, _header-billing or _footer-billing, each falling back to the shared _layout, _header or _footer.

An active database template from phoenix_kit_emails under the same name still wins over all of the above.

Permissions

The module declares permissions via permission_metadata/0:

Use Scope.has_module_access?/2 to check permissions in your application.

CSS Requirements

This module implements css_sources/0 returning [:phoenix_kit_billing], so PhoenixKit's installer automatically adds the correct @source directive to your app.css for Tailwind scanning. No manual configuration needed.

Architecture

lib/
├── phoenix_kit_billing.ex                    # Main module (context + PhoenixKit.Module behaviour)
└── phoenix_kit_billing/
    ├── mix_tasks/
    │   └── phoenix_kit_billing.install.ex    # Install mix task
    ├── events.ex                     # PubSub event broadcasts
    ├── paths.ex                      # Centralized URL path helpers
    ├── supervisor.ex                 # OTP Supervisor
    ├── providers/
    │   ├── provider.ex               # Provider behaviour (9 callbacks)
    │   ├── providers.ex              # Provider registry and routing
    │   ├── stripe.ex                 # Stripe implementation
    │   ├── paypal.ex                 # PayPal implementation
    │   ├── razorpay.ex               # Razorpay implementation
    │   └── everypay.ex               # EveryPay implementation
    ├── schemas/
    │   ├── billing_profile.ex        # User billing information
    │   ├── currency.ex               # Currency definitions
    │   ├── invoice.ex                # Invoice with receipt
    │   ├── order.ex                  # Order with line items
    │   ├── payment_method.ex         # Saved payment methods
    │   ├── subscription.ex           # Subscription records
    │   ├── subscription_type.ex      # Subscription plan definitions
    │   ├── transaction.ex            # Payment/refund transactions
    │   └── webhook_event.ex          # Provider webhook log
    ├── workers/
    │   ├── subscription_renewal_worker.ex   # Oban: subscription renewals
    │   └── subscription_dunning_worker.ex   # Oban: failed payment retries
    └── web/                          # Admin LiveViews, controllers, components

Database Tables

Table Description
phoenix_kit_billing_profiles User billing information (UUIDv7 PK)
phoenix_kit_currencies Currency definitions
phoenix_kit_orders Orders with line items
phoenix_kit_invoices Invoices with receipt data
phoenix_kit_transactions Payment/refund transaction ledger
phoenix_kit_subscriptions Active subscriptions
phoenix_kit_subscription_types Subscription plan definitions
phoenix_kit_payment_methods Saved payment methods from providers
phoenix_kit_payment_options Available payment options
phoenix_kit_webhook_events Provider webhook event log

Development

mix deps.get       # Install dependencies
mix test           # Run tests
mix format         # Format code
mix credo --strict # Static analysis (strict mode)
mix dialyzer       # Type checking
mix docs           # Generate documentation
mix precommit      # Compile + format + credo + dialyzer
mix quality        # Format + credo + dialyzer

Testing

The module ships its own test harness — a DataCase/LiveCase, a test Endpoint + Router, and core's versioned migrations run via PhoenixKit.Migration.ensure_current/2 (no parent app required):

createdb phoenix_kit_billing_test   # one-time: create the test database
mix test                            # run the suite

Use PhoenixKitBilling.DataCase for schema/context tests and PhoenixKitBilling.LiveCase for admin LiveView tests.

Troubleshooting

Billing not appearing in admin

Webhooks not processing

Subscription renewals not running

License

MIT — see LICENSE for details.