0.0
The project is in a healthy, maintained state
A Solidus extension that lets your store accept payments through Mollie using their hosted (redirect) checkout and webhooks.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

Runtime

 Project Readme

solidus_mollie

Gem Version

Accept Mollie payments in your Solidus store using Mollie's hosted checkout (the buyer is redirected to Mollie to pay) and webhooks for settlement.

Tested against the stack it was written for: Solidus 4.6 / Rails 7.1 / Ruby 3.2, with mollie-api-ruby ~> 4.

How it works (read this first)

Mollie is not a synchronous credit-card gateway, so this is not a normal ActiveMerchant-style authorize/capture extension. The flow is:

  1. Buyer selects Mollie at the checkout payment step. A Spree::Payment is created with a SolidusMollie::MollieSource (no card data is collected).
  2. At the confirm step, instead of completing the order synchronously, the buyer is redirected to Mollie's hosted page. The payment is moved to pending. (See SolidusMollie::CreateOrderPayment and the Spree::CheckoutController override.)
  3. The buyer pays on Mollie and is sent back to /mollie/return, which forwards them to their order page.
  4. Mollie also POSTs to /mollie/webhook — this is the source of truth. SolidusMollie::ProcessWebhook re-fetches the authoritative status, marks the Spree::Payment complete/failed, and completes the order. It is idempotent because Mollie may call the webhook more than once.

Because the webhook is what completes the order, Mollie must be able to reach your app over a public HTTPS URL, including in development. Use a tunnel such as ngrok or cloudflared locally — Mollie will not call localhost.

Installation

Add to your store's Gemfile:

gem 'solidus_mollie'

Then:

bundle install
bin/rails generate solidus_mollie:install
bin/rails db:migrate

Configuration

  1. In the admin, go to Configuration → Payment Methods → New Payment Method.
  2. Choose SolidusMollie::PaymentMethod as the provider.
  3. Paste your Mollie API key (test_… for testing, live_… for production).
  4. Optionally set Mollie method to force a single method (e.g. ideal, creditcard, bancontact). Leave blank to let the buyer choose on Mollie's page.

The key prefix (test_/live_) determines test vs live mode automatically.

Frontend

Solidus stores come in three shapes and the gem detects which one it is running in (see SolidusMollie::Frontend):

Store Checkout hook Buyer returns to
Starter frontend (copied into your app) CheckoutsController#update token_order_url(order, token)
Legacy solidus_frontend Spree::CheckoutController#update spree.order_url(order, token:)
Headless / API none - you call the service yourself wherever your order_url lambda says

Payment-step and order-page partials are shipped for both storefronts (checkouts/payment/_mollie, orders/payment_info/_mollie and the spree/... equivalents), plus the admin source_views/source_forms partials so an order's Mollie payment is viewable in the backend.

Anything the auto-detection gets wrong can be overridden in an initializer:

SolidusMollie.configure do |config|
  config.checkout_controller = 'MyCheckoutController' # or false to disable the hook
  config.order_url = ->(controller, order) { controller.main_app.my_order_url(order) }
  config.payment_step_path = ->(controller) { controller.main_app.my_payment_path }
  config.root_path = ->(controller) { '/' }
end

Headless / API stores

There is no checkout controller to hook, so trigger the redirect yourself once the order reaches confirm:

result = SolidusMollie::CreateOrderPayment.call(
  order: order,
  payment: order.payments.detect { |p| p.checkout? && p.payment_method.is_a?(SolidusMollie::PaymentMethod) },
  redirect_url: spree.mollie_return_url(order_number: order.number, token: order.guest_token),
  webhook_url: spree.mollie_webhook_url
)
# => redirect the buyer to result.checkout_url

Abandoned payments

If a buyer comes back from Mollie without paying and presses "Place order" again, the stale pending payment is voided (and cancelled at Mollie when still cancelable) and a fresh Mollie payment is started, so an order can never complete against an unpaid Mollie payment.

Refunds & cancellation

Refunds from the Solidus admin call PaymentMethod#credit, which creates a Mollie refund. Cancelling a still-cancelable Mollie payment is attempted via try_void; otherwise Solidus falls back to a refund.

Development & tests

The extension is scaffolded with solidus_dev_support, which generates a dummy Solidus app under spec/dummy to test against.

bin/setup                      # bundle + generate the dummy app
bin/rake                       # run the full spec suite (extension:specs)
bundle exec rspec spec/services/solidus_mollie/process_webhook_spec.rb  # one file
SOLIDUS_BRANCH=v4.6 bin/setup  # pin a specific Solidus version
bin/sandbox                    # build a runnable sandbox store under ./sandbox

Bundled specs to build on:

  • spec/services/solidus_mollie/process_webhook_spec.rb — settlement (paid / failed / idempotent / unknown-id). This is the behaviour most worth covering.
  • spec/requests/solidus_mollie/callbacks_spec.rb — webhook + return endpoints.
  • spec/models/solidus_mollie/payment_method_spec.rb — test-mode detection, refunds.
  • spec/decorators/solidus_mollie/checkout_controller_decorator_spec.rb — the confirm-step redirect, run against a stand-in checkout controller.
  • spec/solidus_mollie/frontend_spec.rb — storefront detection and overrides.
  • spec/support/mollie_stubs.rb — helpers to fake the Mollie API so specs never hit the network (stub_mollie_get, stub_mollie_create).

The dummy app is generated against the Solidus branch in SOLIDUS_BRANCH, and bundle re-reads that variable every time, so export it once per shell:

export SOLIDUS_BRANCH=v4.6
bin/setup
bundle exec rspec

Testing against Mollie for real

Mollie has no separate sandbox URL — use a test API key (test_…) on the payment method. Test payments are fully isolated from live data, and Mollie's hosted page is replaced by a screen where you choose the resulting status (Paid / Failed / Expired / Cancelled). Because settlement is webhook-driven, Mollie must reach /mollie/webhook over a public HTTPS URL even in test mode — run a tunnel (ngrok http 3000) and use the public host.

Known limitations / TODO

  • No one-click / reusable mandates (sources are non-reusable in this version).
  • No partial-payment handling beyond a single payment per order.
  • Mollie's webhook is unauthenticated by design (it only carries an id and the gem re-fetches the payment from Mollie), so it must stay that way; do not put it behind auth.

License

BSD-2-Clause.