atannex/billing
Atannex Billing is a Laravel/PHP package that provides billing support for your application. It gives you a reusable foundation for billing-related workflows, reducing the need to build and maintain custom billing logic from scratch. Composer-installable for Laravel projects.
composer require atannex/billing (PHP 8.2+, Laravel 11 or 12 only — this will not install on L10 or earlier, so check composer.json constraints before planning an upgrade).config/billing.php, plus tables for payments, refunds, idempotency keys, webhooks and the audit ledger). Do this before touching any gateway credentials — the migrations define the state machine's persistence layer and the webhook dedupe store, and nothing works correctly without them.Billable trait to your User (or tenant) model and the Payable trait to whatever represents the thing being paid for — an Order, Invoice, Subscription charge, etc. These traits are where the fluent entry points and refund helpers live..env (Stripe, PayPal Orders v2, Flutterwave, MTN, Orange). Keep them in env, not in the published config, so config:cache in deploy stays safe.Where to look first, in order:
config/billing.php — the gateway/currency/method/operator routing table and failover settings. This file is the mental model of the whole package; read it top to bottom before writing a controller.php artisan billing:gateways — confirms which drivers are configured and reachable before you debug your own code.Billable/Payable trait source — the actual method names and return types matter more than the README examples.Keep gateway selection out of your controllers. The selling point here is routing by currency, method and phone operator. Encode that policy once in the config routing table (e.g. XAF + mobile money + MTN prefix → MTN driver) and let the package resolve it. Controllers should express intent (charge this payable for this amount in this currency via this method), never if ($currency === 'XAF').
Treat the fluent call as "initiate", not "complete". Every flow converges on the same shape: initiate → customer redirect or USSD prompt → webhook → guarded transition → reconciliation. Write your controller to start the payment and redirect/poll; write a separate webhook handler and let the package's verified, deduplicated webhook processing drive the state. Never set a status column yourself — the state machine is guarded and amount-verified, so direct writes will either be rejected or silently diverge from the ledger.
Async flows (MTN Request to Pay, Orange Cameroon USSD push) need the polling route. Return the JSON status URL to the frontend and poll it rather than inventing your own status endpoint. Pair it with billing:reconcile on a short schedule (every minute or two) so payments that never produce a webhook still settle; without the scheduler entry, async payments sit pending forever and it looks like a package bug when it's a missing cron.
Use idempotency keys on anything retryable. Generate a stable key per logical attempt (e.g. order:123:attempt:1) and reuse it across retries and across failover. Failover to a secondary gateway is only safe if the key travels with the request; otherwise a retry after a timeout can double-charge.
Refunds: model partial refunds as data, not as a special case. Full and partial refunds both go through the Payable/Billable helpers; store the refund record and let the ledger be your source of truth for "how much has been returned" rather than a column you maintain by hand.
Testing with Billing::fake(). Use it for your happy-path feature tests and to assert state transitions and refunds. It will not exercise real operator-prefix routing or failover, so test routing separately by asserting your config resolves to the expected driver for each (currency, method, phone) tuple — those tests are cheap and catch the highest-consequence misconfiguration.
Operational commands to wire into your scheduler/deploy:
billing:reconcile — frequent (async settlement).billing:prune — daily, with deliberate retention.billing:gateways — in your deploy smoke check or a health endpoint.billing:mtn-sandbox-user — local/CI setup for MTN sandbox testing.billing:payment — ad-hoc inspection when a support ticket arrives.Use the audit ledger for support, not your own logs. When a customer says "I paid and it didn't go through", the ledger plus the payment record should answer it; build any internal admin view on top of those rather than on application log lines.
+2376XXXXXXXX) with spaces and dashes stripped. A user-entered 6 7X XX XX XX will not match the prefix table and you'll get a "no gateway" resolution error that looks like a config problem.billing:prune deletes real records. Set retention longer than your gateway's dispute/chargeback window before enabling it in production. Pruning idempotency keys too aggressively reopens the double-charge risk.config:cache and gateway credentials. If you cache config in deploy, credentials must be resolved from env at runtime or read after cache clear — a stale cached config after rotating a key is a classic 3am failure.^1.0) and keep your integration code behind one adapter class so a 1.1 with signature changes touches one file.How can I help you explore Laravel packages today?