laravel-billing-mollie maintained by cboxdk
Cbox Billing — Mollie
cboxdk/laravel-billing-mollie — a Mollie payment-gateway adapter for
cboxdk/laravel-billing. It implements
billing's PaymentGateway contract backed by Mollie.
Install
composer require cboxdk/laravel-billing-mollie
// .env
MOLLIE_KEY=live_...
MOLLIE_REDIRECT_URL=https://your-app.test/billing/return
MOLLIE_WEBHOOK_URL=https://your-app.test/webhooks/mollie
With a key set, the provider binds Cbox\Billing\Payment\Contracts\PaymentGateway
to the Mollie gateway. Mollie is redirect-based, so a payment starts open
(pending) until the customer completes it and Mollie confirms paid.
Design
- SDK isolated behind a seam (
MollieIntentCreator): the realMollieApiIntentCreatorwraps the Mollie SDK; aFakeMollieIntentCreatordrives the tests — so status-mapping and error handling are unit-tested without the network. - Never throws. An API failure becomes a failed
PaymentResult; Mollie statuses map tosucceeded(paid) /pending(open) /requires_action(authorized) /failed. - Customers and mandates.
createCustomer()mints a Mollie customer (cst_…), stamping the host's account key into its metadata so it reconciles from the Mollie dashboard; a customer that never reached Mollie is never returned. In Mollie a saved recurring "payment method" is a mandate, sodetachPaymentMethod($account, $paymentMethodId)treats$paymentMethodIdas a mandate id and$accountas the owning customer reference, and revokes that mandate. The revoke is idempotent — an already-revoked or unknown mandate (Mollie 410 / 404) is a no-op. - Saved methods are mandates. A card mandate lists under its card label; a SEPA
Direct Debit mandate lists as
sepa_debitwith the last four of the IBAN. Aninvalidmandate is left out, because it cannot be charged. - Idempotent webhooks on the shared seam.
MollieApiWebhookVerifierimplements billing's canonicalCbox\Billing\Payment\Contracts\WebhookVerifier. A classic Mollie webhook (id=tr_…, unsigned) is proved by fetching the payment with the account's key: a payment the account does not own is not found and refused. A signed next-gen delivery is also checked againstMOLLIE_WEBHOOK_SECRETvia the SDK. The event is built only from the fetched payment (MollieWebhookEvent, carrying the refunded total and any chargebacks), never from the body.MollieWebhookHandlerthen hands that event to the engine's ownWebhookIngest, which applies the paid effect to the invoice exactly once per reference — collapsing gateway re-deliveries and crash-retries. The adapter only overrides the shared dedup/settle stores with durable database implementations; it owns no webhook contracts of its own. Charges carry a scoped external idempotency key so a crash-and-retry never duplicates a payment. SetMOLLIE_WEBHOOK_URLso Mollie calls back at all;MOLLIE_WEBHOOK_SECRETis only for signed next-gen subscriptions. See docs/core-concepts/webhooks.md.
The SDK wrapper implements Mollie's documented API shape — verify against the live Mollie API before relying on it in production.
Running the live integration tests
The default suite proves the gateway's mapping and error handling against the
FakeMollieIntentCreator — no network. A separate suite in
tests/Integration/MollieLiveTest.php
(Pest group integration) exercises the real Mollie API path
(MollieApiIntentCreator + MolliePaymentGateway) against Mollie test mode.
It is gated on a test_… key in MOLLIE_TEST_KEY — deliberately distinct from a
production MOLLIE_KEY so the two can never collide. Without the key the suite skips
cleanly, so composer qa / vendor/bin/pest stay green (skips reported) and the suite
is excluded from CI. No secret lives in the repo.
Run it explicitly:
MOLLIE_TEST_KEY=test_… vendor/bin/pest --group=integration
It hits Mollie test mode only (a test_… key forces test mode on every request, so
no live money moves), and it creates then revokes throwaway test objects — a stored
customer (cst_…) and a directdebit mandate (mdt_…) established headlessly with
Mollie's documented test IBAN — deleting the customer on teardown. What it covers
end-to-end, unattended:
createCustomer()→ acst_…id with the host account stamped intometadata.account.- a
directdebitmandate created directly via the customer-mandates API (no redirect). detachPaymentMethod()revokes the mandate (it becomesinvalid/gone), and calling it again is an idempotent no-op (Mollie 410/404) — also proven against a never-created mandate id.
Manual step (not covered). The first-sequence setup-payment path
(createSetupIntent()) returns a Mollie hosted-checkout URL a human must complete in a
browser to establish the mandate, so it cannot run unattended and is not exercised
here. The headless directdebit-mandate route above stands in for it to prove the
revoke/detach lifecycle; the redirect-completed setup flow must be verified manually.
Requirements
PHP ^8.4; Laravel ^12 || ^13; mollie/mollie-api-php ^3.13; cboxdk/laravel-billing.
License
MIT.