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 uses the customer's preferred locale, or the site's content language for a guest payer or an account without a preference. Billing explicitly installs that language for its own copy, including when the sender has set a Gettext backend locale. Dates use core's translated month names and locale-aware ordering, in both sends and previews. This works from the supported core floor of 2.44.0.

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. Both HTML and plain text omit missing document links, empty bank names and SWIFT codes, and absent company details. An invoice without a due date omits its payment deadline.

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.