cboxdk / laravel-billing-mollie
Cbox Billing — Mollie payment-gateway adapter: a PaymentGateway implementation backed by Mollie, for cboxdk/laravel-billing.
Requires
- php: ^8.4
- cboxdk/laravel-billing: >=0.4 <1.0
- illuminate/contracts: ^13.0
- illuminate/database: ^13.0
- illuminate/support: ^13.0
- mollie/mollie-api-php: ^3.13
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^11.0
- pestphp/pest: ^4.0
README
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
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. - Idempotent webhooks on the shared seam.
MollieApiWebhookVerifierimplements billing's canonicalCbox\Billing\Payment\Contracts\WebhookVerifier: it proves the Mollie signature via the SDK (deny-by-default), fetches the payment's authoritative state through thePaymentFetcherseam (a Mollie webhook carries only the payment id), and normalises the delivery onto the engine's sharedWebhookEvent.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_SECRETto enable it. 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.