solidus_mollie 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +52 -9
- data/app/controllers/solidus_mollie/callbacks_controller.rb +19 -10
- data/app/decorators/solidus_mollie/checkout_controller_decorator.rb +71 -0
- data/app/models/solidus_mollie/client.rb +4 -4
- data/app/models/solidus_mollie/mollie_source.rb +2 -0
- data/app/models/solidus_mollie/payment_method.rb +7 -3
- data/app/services/solidus_mollie/create_order_payment.rb +2 -2
- data/app/views/checkouts/payment/_mollie.html.erb +5 -0
- data/app/views/orders/payment_info/_mollie.html.erb +5 -0
- data/app/views/spree/admin/payments/source_forms/_mollie.html.erb +3 -0
- data/app/views/spree/admin/payments/source_views/_mollie.html.erb +28 -0
- data/app/views/spree/orders/payment_info/_mollie.html.erb +5 -0
- data/config/locales/en.yml +7 -0
- data/config/routes.rb +6 -2
- data/lib/generators/solidus_mollie/install/install_generator.rb +1 -1
- data/lib/solidus_mollie/configuration.rb +31 -0
- data/lib/solidus_mollie/engine.rb +12 -8
- data/lib/solidus_mollie/frontend.rb +63 -0
- data/lib/solidus_mollie/{factories.rb → testing_support/factories.rb} +1 -1
- data/lib/solidus_mollie/version.rb +1 -1
- data/lib/solidus_mollie.rb +2 -0
- metadata +12 -8
- data/app/decorators/solidus_mollie/spree/checkout_controller_decorator.rb +0 -60
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f592e8878266fb1b4b38e6f42092fcb187672afff238eb443a9bab7c848070f1
|
|
4
|
+
data.tar.gz: 58736fe2073d3299bb7c3e2c5ad129b56b09cf1f884ab6bf72a9553a4c2f0241
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 717c362a01ab15f28e0688aaef56afd03a5843dc0550820af0c8efd55e3095c10818279f5abd5e38fd72f483e6c52c26c453bf6ef5254368e5966f1e602cc113
|
|
7
|
+
data.tar.gz: b2d900cf170ba23ee52ffda9af13a407fecf9eb5dd508186720048578781c515194ef1cd79e0de16476809f624aa7ced8b12f13950286791d5b224d1d49f8883
|
data/README.md
CHANGED
|
@@ -59,23 +59,53 @@ The key prefix (`test_`/`live_`) determines test vs live mode automatically.
|
|
|
59
59
|
|
|
60
60
|
## Frontend
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
redirect. This works with `solidus_starter_frontend` out of the box.
|
|
62
|
+
Solidus stores come in three shapes and the gem detects which one it is running in
|
|
63
|
+
(see `SolidusMollie::Frontend`):
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
| Store | Checkout hook | Buyer returns to |
|
|
66
|
+
|--------------------------------------------------|--------------------------------------|-------------------------------------------|
|
|
67
|
+
| **Starter frontend** (copied into your app) | `CheckoutsController#update` | `token_order_url(order, token)` |
|
|
68
|
+
| **Legacy `solidus_frontend`** | `Spree::CheckoutController#update` | `spree.order_url(order, token:)` |
|
|
69
|
+
| **Headless / API** | none - you call the service yourself | wherever your `order_url` lambda says |
|
|
70
|
+
|
|
71
|
+
Payment-step and order-page partials are shipped for both storefronts
|
|
72
|
+
(`checkouts/payment/_mollie`, `orders/payment_info/_mollie` and the `spree/...`
|
|
73
|
+
equivalents), plus the admin `source_views`/`source_forms` partials so an order's
|
|
74
|
+
Mollie payment is viewable in the backend.
|
|
75
|
+
|
|
76
|
+
Anything the auto-detection gets wrong can be overridden in an initializer:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
SolidusMollie.configure do |config|
|
|
80
|
+
config.checkout_controller = 'MyCheckoutController' # or false to disable the hook
|
|
81
|
+
config.order_url = ->(controller, order) { controller.main_app.my_order_url(order) }
|
|
82
|
+
config.payment_step_path = ->(controller) { controller.main_app.my_payment_path }
|
|
83
|
+
config.root_path = ->(controller) { '/' }
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Headless / API stores
|
|
88
|
+
|
|
89
|
+
There is no checkout controller to hook, so trigger the redirect yourself once
|
|
90
|
+
the order reaches `confirm`:
|
|
68
91
|
|
|
69
92
|
```ruby
|
|
70
93
|
result = SolidusMollie::CreateOrderPayment.call(
|
|
71
94
|
order: order,
|
|
72
95
|
payment: order.payments.detect { |p| p.checkout? && p.payment_method.is_a?(SolidusMollie::PaymentMethod) },
|
|
73
|
-
redirect_url: mollie_return_url(order_number: order.number, token: order.
|
|
74
|
-
webhook_url: mollie_webhook_url
|
|
96
|
+
redirect_url: spree.mollie_return_url(order_number: order.number, token: order.guest_token),
|
|
97
|
+
webhook_url: spree.mollie_webhook_url
|
|
75
98
|
)
|
|
76
99
|
# => redirect the buyer to result.checkout_url
|
|
77
100
|
```
|
|
78
101
|
|
|
102
|
+
### Abandoned payments
|
|
103
|
+
|
|
104
|
+
If a buyer comes back from Mollie without paying and presses "Place order" again,
|
|
105
|
+
the stale `pending` payment is voided (and cancelled at Mollie when still
|
|
106
|
+
cancelable) and a fresh Mollie payment is started, so an order can never complete
|
|
107
|
+
against an unpaid Mollie payment.
|
|
108
|
+
|
|
79
109
|
## Refunds & cancellation
|
|
80
110
|
|
|
81
111
|
Refunds from the Solidus admin call `PaymentMethod#credit`, which creates a Mollie
|
|
@@ -101,9 +131,21 @@ Bundled specs to build on:
|
|
|
101
131
|
failed / idempotent / unknown-id). This is the behaviour most worth covering.
|
|
102
132
|
- `spec/requests/solidus_mollie/callbacks_spec.rb` — webhook + return endpoints.
|
|
103
133
|
- `spec/models/solidus_mollie/payment_method_spec.rb` — test-mode detection, refunds.
|
|
134
|
+
- `spec/decorators/solidus_mollie/checkout_controller_decorator_spec.rb` — the
|
|
135
|
+
confirm-step redirect, run against a stand-in checkout controller.
|
|
136
|
+
- `spec/solidus_mollie/frontend_spec.rb` — storefront detection and overrides.
|
|
104
137
|
- `spec/support/mollie_stubs.rb` — helpers to fake the Mollie API so specs never
|
|
105
138
|
hit the network (`stub_mollie_get`, `stub_mollie_create`).
|
|
106
139
|
|
|
140
|
+
The dummy app is generated against the Solidus branch in `SOLIDUS_BRANCH`, and
|
|
141
|
+
`bundle` re-reads that variable every time, so export it once per shell:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
export SOLIDUS_BRANCH=v4.6
|
|
145
|
+
bin/setup
|
|
146
|
+
bundle exec rspec
|
|
147
|
+
```
|
|
148
|
+
|
|
107
149
|
### Testing against Mollie for real
|
|
108
150
|
|
|
109
151
|
Mollie has no separate sandbox URL — use a **test API key** (`test_…`) on the
|
|
@@ -117,8 +159,9 @@ run a tunnel (`ngrok http 3000`) and use the public host.
|
|
|
117
159
|
|
|
118
160
|
- No one-click / reusable mandates (sources are non-reusable in this version).
|
|
119
161
|
- No partial-payment handling beyond a single payment per order.
|
|
120
|
-
-
|
|
121
|
-
|
|
162
|
+
- Mollie's webhook is unauthenticated by design (it only carries an id and the
|
|
163
|
+
gem re-fetches the payment from Mollie), so it must stay that way; do not put
|
|
164
|
+
it behind auth.
|
|
122
165
|
|
|
123
166
|
## License
|
|
124
167
|
|
|
@@ -4,7 +4,10 @@ module SolidusMollie
|
|
|
4
4
|
# Handles the two requests Mollie sends us:
|
|
5
5
|
# * #webhook - server-to-server POST with the payment id (source of truth)
|
|
6
6
|
# * #return - the buyer's browser coming back from Mollie's hosted page
|
|
7
|
-
|
|
7
|
+
#
|
|
8
|
+
# Inherits from Spree::BaseController (solidus_core) rather than a storefront
|
|
9
|
+
# controller so it loads regardless of which frontend the store uses.
|
|
10
|
+
class CallbacksController < ::Spree::BaseController
|
|
8
11
|
skip_before_action :verify_authenticity_token, only: :webhook
|
|
9
12
|
|
|
10
13
|
# POST /mollie/webhook (body: id=tr_xxxxx)
|
|
@@ -15,7 +18,7 @@ module SolidusMollie
|
|
|
15
18
|
# 422 so Mollie retries (covers the rare webhook-before-persist race).
|
|
16
19
|
Rails.logger.warn("[solidus_mollie] webhook payment not found: #{e.message}")
|
|
17
20
|
head :unprocessable_entity
|
|
18
|
-
rescue
|
|
21
|
+
rescue => e
|
|
19
22
|
Rails.logger.error("[solidus_mollie] webhook error: #{e.message}")
|
|
20
23
|
head :internal_server_error
|
|
21
24
|
end
|
|
@@ -23,21 +26,27 @@ module SolidusMollie
|
|
|
23
26
|
# GET /mollie/return?order_number=R123&token=abc
|
|
24
27
|
def return
|
|
25
28
|
order = ::Spree::Order.find_by!(number: params[:order_number])
|
|
26
|
-
|
|
27
|
-
if params[:token].present? && order.token.present? &&
|
|
28
|
-
!ActiveSupport::SecurityUtils.secure_compare(params[:token].to_s, order.token.to_s)
|
|
29
|
-
raise ActiveRecord::RecordNotFound
|
|
30
|
-
end
|
|
29
|
+
raise ActiveRecord::RecordNotFound unless token_matches?(order)
|
|
31
30
|
|
|
32
31
|
unless order.completed?
|
|
33
32
|
flash[:notice] = I18n.t('solidus_mollie.confirming_payment',
|
|
34
|
-
|
|
33
|
+
default: "Thanks! We're confirming your payment with Mollie.")
|
|
35
34
|
end
|
|
36
35
|
|
|
37
|
-
redirect_to
|
|
36
|
+
redirect_to SolidusMollie::Frontend.order_url(self, order)
|
|
38
37
|
rescue ActiveRecord::RecordNotFound
|
|
39
38
|
flash[:error] = I18n.t('solidus_mollie.order_not_found', default: 'Order not found.')
|
|
40
|
-
redirect_to
|
|
39
|
+
redirect_to SolidusMollie::Frontend.root_path(self)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# The guest token is what lets the buyer see their order page, so the
|
|
45
|
+
# return URL must carry the right one. Never fall through on a blank token.
|
|
46
|
+
def token_matches?(order)
|
|
47
|
+
token = params[:token].to_s
|
|
48
|
+
token.present? && order.guest_token.present? &&
|
|
49
|
+
ActiveSupport::SecurityUtils.secure_compare(token, order.guest_token.to_s)
|
|
41
50
|
end
|
|
42
51
|
end
|
|
43
52
|
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidusMollie
|
|
4
|
+
# Prepended to the storefront checkout controller (see SolidusMollie::Frontend
|
|
5
|
+
# for which one). When the buyer places an order that is paying with Mollie,
|
|
6
|
+
# they are redirected to Mollie's hosted checkout instead of the order being
|
|
7
|
+
# completed synchronously; the webhook finishes the order later
|
|
8
|
+
# (SolidusMollie::ProcessWebhook).
|
|
9
|
+
#
|
|
10
|
+
# Works with both the starter frontend (CheckoutsController#update) and the
|
|
11
|
+
# legacy solidus_frontend (Spree::CheckoutController#update): both expose
|
|
12
|
+
# +@order+, +params[:state]+ and the +spree+ route proxy.
|
|
13
|
+
module CheckoutControllerDecorator
|
|
14
|
+
def update
|
|
15
|
+
return redirect_to_mollie if divert_to_mollie?
|
|
16
|
+
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# Both frontends route "Place order" to update with params[:state] ==
|
|
23
|
+
# 'confirm' (and set @order.state from it), so the param is the reliable
|
|
24
|
+
# signal; an order that merely *is* in confirm must not be diverted while
|
|
25
|
+
# the buyer is editing an earlier step.
|
|
26
|
+
def divert_to_mollie?
|
|
27
|
+
return false unless @order
|
|
28
|
+
return false unless params[:state].to_s == 'confirm'
|
|
29
|
+
|
|
30
|
+
mollie_payment.present?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The Mollie payment to send the buyer off with. Normally the payment the
|
|
34
|
+
# buyer created at the payment step (state +checkout+). If an earlier
|
|
35
|
+
# attempt is still +pending+ (the buyer came back from Mollie without
|
|
36
|
+
# paying and pressed "Place order" again) it is voided and replaced, so the
|
|
37
|
+
# order can never complete against an unpaid Mollie payment.
|
|
38
|
+
def mollie_payment
|
|
39
|
+
return @mollie_payment if defined?(@mollie_payment)
|
|
40
|
+
|
|
41
|
+
payments = @order.payments.select { |p| p.payment_method.is_a?(SolidusMollie::PaymentMethod) }
|
|
42
|
+
@mollie_payment = payments.find(&:checkout?) || replace_pending_mollie_payment(payments.find(&:pending?))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def replace_pending_mollie_payment(pending)
|
|
46
|
+
return nil unless pending
|
|
47
|
+
|
|
48
|
+
pending.payment_method.try_void(pending) if pending.response_code.present?
|
|
49
|
+
pending.void!
|
|
50
|
+
@order.payments.create!(payment_method: pending.payment_method, amount: @order.total)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def redirect_to_mollie
|
|
54
|
+
result = SolidusMollie::CreateOrderPayment.call(
|
|
55
|
+
order: @order,
|
|
56
|
+
payment: mollie_payment,
|
|
57
|
+
redirect_url: spree.mollie_return_url(order_number: @order.number, token: @order.guest_token),
|
|
58
|
+
webhook_url: spree.mollie_webhook_url
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if result.success?
|
|
62
|
+
redirect_to result.checkout_url, allow_other_host: true
|
|
63
|
+
else
|
|
64
|
+
flash[:error] = I18n.t('solidus_mollie.payment_error',
|
|
65
|
+
message: result.error,
|
|
66
|
+
default: "We couldn't start your Mollie payment.")
|
|
67
|
+
redirect_to SolidusMollie::Frontend.payment_step_path(self)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -15,9 +15,9 @@ module SolidusMollie
|
|
|
15
15
|
|
|
16
16
|
# amount: a BigDecimal/Numeric in MAJOR units (e.g. 10.00 for €10).
|
|
17
17
|
def create_payment(amount:, currency:, description:, redirect_url:, webhook_url:,
|
|
18
|
-
|
|
18
|
+
method: nil, metadata: {})
|
|
19
19
|
Mollie::Payment.create(
|
|
20
|
-
amount: {
|
|
20
|
+
amount: {value: self.class.format_amount(amount, currency), currency: currency},
|
|
21
21
|
description: description,
|
|
22
22
|
redirect_url: redirect_url,
|
|
23
23
|
webhook_url: webhook_url,
|
|
@@ -38,7 +38,7 @@ module SolidusMollie
|
|
|
38
38
|
def create_refund(payment_id:, amount:, currency:)
|
|
39
39
|
Mollie::Payment::Refund.create(
|
|
40
40
|
payment_id: payment_id,
|
|
41
|
-
amount: {
|
|
41
|
+
amount: {value: self.class.format_amount(amount, currency), currency: currency},
|
|
42
42
|
api_key: @api_key
|
|
43
43
|
)
|
|
44
44
|
end
|
|
@@ -59,7 +59,7 @@ module SolidusMollie
|
|
|
59
59
|
|
|
60
60
|
def self.currency_exponent(currency)
|
|
61
61
|
::Money::Currency.new(currency).exponent
|
|
62
|
-
rescue
|
|
62
|
+
rescue
|
|
63
63
|
2
|
|
64
64
|
end
|
|
65
65
|
end
|
|
@@ -7,6 +7,8 @@ module SolidusMollie
|
|
|
7
7
|
class MollieSource < ::Spree::PaymentSource
|
|
8
8
|
self.table_name = 'solidus_mollie_sources'
|
|
9
9
|
|
|
10
|
+
belongs_to :user, class_name: ::Spree::UserClassHandle.new, optional: true
|
|
11
|
+
|
|
10
12
|
# Off-site payments are not reusable for one-click in this version.
|
|
11
13
|
def reusable?
|
|
12
14
|
false
|
|
@@ -23,8 +23,12 @@ module SolidusMollie
|
|
|
23
23
|
'mollie'
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# No card data is collected in the store: the MollieSource is created when
|
|
27
|
+
# the buyer is redirected (CreateOrderPayment), not at the payment step.
|
|
28
|
+
# Returning false lets every frontend (starter, legacy, API) create the
|
|
29
|
+
# payment with just a payment_method_id.
|
|
26
30
|
def source_required?
|
|
27
|
-
|
|
31
|
+
false
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
def payment_profiles_supported?
|
|
@@ -75,7 +79,7 @@ module SolidusMollie
|
|
|
75
79
|
currency: currency
|
|
76
80
|
)
|
|
77
81
|
SolidusMollie::Response.success('Mollie refund created', authorization: response_code)
|
|
78
|
-
rescue
|
|
82
|
+
rescue => e
|
|
79
83
|
Rails.logger.error("[solidus_mollie] refund failed: #{e.message}")
|
|
80
84
|
SolidusMollie::Response.failure(e.message)
|
|
81
85
|
end
|
|
@@ -89,7 +93,7 @@ module SolidusMollie
|
|
|
89
93
|
|
|
90
94
|
client.cancel_payment(payment.response_code)
|
|
91
95
|
SolidusMollie::Response.success('Mollie payment canceled', authorization: payment.response_code)
|
|
92
|
-
rescue
|
|
96
|
+
rescue => e
|
|
93
97
|
Rails.logger.error("[solidus_mollie] void failed: #{e.message}")
|
|
94
98
|
false
|
|
95
99
|
end
|
|
@@ -31,12 +31,12 @@ module SolidusMollie
|
|
|
31
31
|
redirect_url: @redirect_url,
|
|
32
32
|
webhook_url: @webhook_url,
|
|
33
33
|
method: payment_method.preferred_mollie_method,
|
|
34
|
-
metadata: {
|
|
34
|
+
metadata: {order_number: @order.number, payment_number: @payment.number}
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
persist(mollie)
|
|
38
38
|
Result.new(checkout_url: mollie.checkout_url)
|
|
39
|
-
rescue
|
|
39
|
+
rescue => e
|
|
40
40
|
Rails.logger.error("[solidus_mollie] failed to create payment for #{@order.number}: #{e.message}")
|
|
41
41
|
Result.new(error: e.message)
|
|
42
42
|
end
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<%# Starter frontend: rendered inside the payment-method card at the payment step.
|
|
2
|
+
Buyers enter nothing here - they are redirected to Mollie when they place the order. %>
|
|
3
|
+
<div class="mollie-payment-method" data-payment-method-type="mollie">
|
|
4
|
+
<p><%= t('solidus_mollie.redirect_notice') %></p>
|
|
5
|
+
</div>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<%# Starter frontend: order page / confirm step summary of a Mollie payment. %>
|
|
2
|
+
<%= payment.payment_method.name %>
|
|
3
|
+
<% if payment.source.respond_to?(:mollie_method) && payment.source.mollie_method.present? %>
|
|
4
|
+
(<%= payment.source.mollie_method.to_s.titleize %>)
|
|
5
|
+
<% end %>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<%# Admin: Orders -> Payments -> a Mollie payment. %>
|
|
2
|
+
<fieldset class="no-border-bottom">
|
|
3
|
+
<legend align="center"><%= t('solidus_mollie.admin.legend') %></legend>
|
|
4
|
+
<table>
|
|
5
|
+
<tbody>
|
|
6
|
+
<tr>
|
|
7
|
+
<th><%= t('solidus_mollie.admin.mollie_payment_id') %></th>
|
|
8
|
+
<td>
|
|
9
|
+
<% if payment.source&.mollie_payment_id.present? %>
|
|
10
|
+
<%= link_to payment.source.mollie_payment_id,
|
|
11
|
+
"https://my.mollie.com/dashboard/payments/#{payment.source.mollie_payment_id}",
|
|
12
|
+
target: '_blank', rel: 'noopener' %>
|
|
13
|
+
<% else %>
|
|
14
|
+
<%= t('solidus_mollie.admin.not_started') %>
|
|
15
|
+
<% end %>
|
|
16
|
+
</td>
|
|
17
|
+
</tr>
|
|
18
|
+
<tr>
|
|
19
|
+
<th><%= t('solidus_mollie.admin.mollie_method') %></th>
|
|
20
|
+
<td><%= payment.source&.mollie_method.presence || '-' %></td>
|
|
21
|
+
</tr>
|
|
22
|
+
<tr>
|
|
23
|
+
<th><%= t('solidus_mollie.admin.mollie_status') %></th>
|
|
24
|
+
<td><%= payment.source&.status.presence || '-' %></td>
|
|
25
|
+
</tr>
|
|
26
|
+
</tbody>
|
|
27
|
+
</table>
|
|
28
|
+
</fieldset>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<%# Legacy solidus_frontend: order page / confirm step summary of a Mollie payment. %>
|
|
2
|
+
<%= payment.payment_method.name %>
|
|
3
|
+
<% if payment.source.respond_to?(:mollie_method) && payment.source.mollie_method.present? %>
|
|
4
|
+
(<%= payment.source.mollie_method.to_s.titleize %>)
|
|
5
|
+
<% end %>
|
data/config/locales/en.yml
CHANGED
|
@@ -4,3 +4,10 @@ en:
|
|
|
4
4
|
confirming_payment: "Thanks! We're confirming your payment with Mollie."
|
|
5
5
|
order_not_found: "Order not found."
|
|
6
6
|
redirect_notice: "You'll be redirected to Mollie to complete your payment securely."
|
|
7
|
+
admin:
|
|
8
|
+
legend: "Mollie"
|
|
9
|
+
mollie_payment_id: "Mollie payment"
|
|
10
|
+
mollie_method: "Method"
|
|
11
|
+
mollie_status: "Mollie status"
|
|
12
|
+
not_started: "Not started (buyer has not been redirected to Mollie yet)"
|
|
13
|
+
cannot_create: "Mollie payments are started by the customer during checkout and cannot be created from the admin."
|
data/config/routes.rb
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
# Mount onto Spree's engine so the helpers are available as
|
|
4
4
|
# spree.mollie_webhook_url / spree.mollie_return_url.
|
|
5
|
+
#
|
|
6
|
+
# The leading slash on the controller is essential: Spree::Core::Engine is an
|
|
7
|
+
# isolated engine, so a bare 'solidus_mollie/callbacks' would resolve to
|
|
8
|
+
# Spree::SolidusMollie::CallbacksController.
|
|
5
9
|
::Spree::Core::Engine.routes.draw do
|
|
6
|
-
post '/mollie/webhook', to: 'solidus_mollie/callbacks#webhook', as: :mollie_webhook
|
|
7
|
-
get
|
|
10
|
+
post '/mollie/webhook', to: '/solidus_mollie/callbacks#webhook', as: :mollie_webhook
|
|
11
|
+
get '/mollie/return', to: '/solidus_mollie/callbacks#return', as: :mollie_return
|
|
8
12
|
end
|
|
@@ -19,7 +19,7 @@ module SolidusMollie
|
|
|
19
19
|
|
|
20
20
|
def run_migrations
|
|
21
21
|
run_migrations = options[:auto_run_migrations] ||
|
|
22
|
-
|
|
22
|
+
['', 'y', 'Y'].include?(ask('Run migrations now? [Yn]'))
|
|
23
23
|
if run_migrations
|
|
24
24
|
rake 'db:migrate'
|
|
25
25
|
else
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidusMollie
|
|
4
|
+
# Global settings. Everything here has a sensible default that is detected
|
|
5
|
+
# from the host application, so most stores never need to touch it:
|
|
6
|
+
#
|
|
7
|
+
# SolidusMollie.configure do |config|
|
|
8
|
+
# config.checkout_controller = 'MyCheckoutController'
|
|
9
|
+
# config.order_url = ->(controller, order) { controller.main_app.my_order_url(order) }
|
|
10
|
+
# end
|
|
11
|
+
class Configuration
|
|
12
|
+
# Name of the storefront controller whose #update action places the order
|
|
13
|
+
# at the confirm step. nil = auto-detect (see SolidusMollie::Frontend).
|
|
14
|
+
# Set to false to disable the override entirely (headless stores).
|
|
15
|
+
attr_accessor :checkout_controller
|
|
16
|
+
|
|
17
|
+
# Optional lambdas, each called with (controller, order) / (controller),
|
|
18
|
+
# returning where the buyer should be sent. nil = auto-detect.
|
|
19
|
+
attr_accessor :order_url, :payment_step_path, :root_path
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def config
|
|
24
|
+
@config ||= Configuration.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def configure
|
|
28
|
+
yield config
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -18,19 +18,23 @@ module SolidusMollie
|
|
|
18
18
|
app.config.spree.payment_methods << 'SolidusMollie::PaymentMethod'
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
#
|
|
22
|
-
# redirected to Mollie's hosted checkout instead of
|
|
21
|
+
# Hook the storefront's checkout controller so confirm-step orders paying
|
|
22
|
+
# with Mollie are redirected to Mollie's hosted checkout instead of being
|
|
23
|
+
# completed synchronously. Re-runs on every code reload in development.
|
|
23
24
|
config.to_prepare do
|
|
24
25
|
SolidusMollie::Engine.prepare_decorators
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def self.prepare_decorators
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
controller = SolidusMollie::Frontend.checkout_controller
|
|
30
|
+
if controller
|
|
31
|
+
controller.prepend(SolidusMollie::CheckoutControllerDecorator)
|
|
32
|
+
else
|
|
33
|
+
Rails.logger.info(
|
|
34
|
+
'[solidus_mollie] no storefront checkout controller found; skipping checkout override. ' \
|
|
35
|
+
'For an API/headless frontend, call SolidusMollie::CreateOrderPayment yourself.'
|
|
36
|
+
)
|
|
37
|
+
end
|
|
34
38
|
end
|
|
35
39
|
end
|
|
36
40
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidusMollie
|
|
4
|
+
# Resolves the storefront-specific pieces of the Mollie flow. Solidus stores
|
|
5
|
+
# come in three shapes and this module papers over the differences:
|
|
6
|
+
#
|
|
7
|
+
# * the starter frontend copied into the host app (Solidus 3.2+): checkout
|
|
8
|
+
# lives in +CheckoutsController+ and order/checkout routes are main-app
|
|
9
|
+
# routes (+token_order_url+, +checkout_state_path+);
|
|
10
|
+
# * the legacy +solidus_frontend+ engine: +Spree::CheckoutController+ and
|
|
11
|
+
# +spree.order_url+ / +spree.checkout_state_path+;
|
|
12
|
+
# * headless / API stores: no checkout controller at all - the host app
|
|
13
|
+
# drives SolidusMollie::CreateOrderPayment itself.
|
|
14
|
+
#
|
|
15
|
+
# Every lookup can be overridden through SolidusMollie.config.
|
|
16
|
+
module Frontend
|
|
17
|
+
CHECKOUT_CONTROLLERS = %w[CheckoutsController Spree::CheckoutController].freeze
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# The controller class to hook, or nil when there is none (headless).
|
|
22
|
+
def checkout_controller
|
|
23
|
+
configured = SolidusMollie.config.checkout_controller
|
|
24
|
+
return nil if configured == false
|
|
25
|
+
return configured.to_s.constantize if configured.present?
|
|
26
|
+
|
|
27
|
+
CHECKOUT_CONTROLLERS.lazy.map(&:safe_constantize).find(&:itself)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Where to send the buyer once they come back from Mollie.
|
|
31
|
+
def order_url(controller, order)
|
|
32
|
+
custom = SolidusMollie.config.order_url
|
|
33
|
+
return custom.call(controller, order) if custom
|
|
34
|
+
|
|
35
|
+
main = controller.main_app
|
|
36
|
+
spree = controller.spree
|
|
37
|
+
if main.respond_to?(:token_order_url)
|
|
38
|
+
main.token_order_url(order, order.guest_token)
|
|
39
|
+
elsif spree.respond_to?(:order_url)
|
|
40
|
+
spree.order_url(order, token: order.guest_token)
|
|
41
|
+
else
|
|
42
|
+
root_path(controller)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Where to send the buyer if the Mollie payment could not be created.
|
|
47
|
+
def payment_step_path(controller)
|
|
48
|
+
custom = SolidusMollie.config.payment_step_path
|
|
49
|
+
return custom.call(controller) if custom
|
|
50
|
+
|
|
51
|
+
routes = [controller.main_app, controller.spree].find { |r| r.respond_to?(:checkout_state_path) }
|
|
52
|
+
routes ? routes.checkout_state_path(:payment) : root_path(controller)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def root_path(controller)
|
|
56
|
+
custom = SolidusMollie.config.root_path
|
|
57
|
+
return custom.call(controller) if custom
|
|
58
|
+
|
|
59
|
+
routes = [controller.main_app, controller.spree].find { |r| r.respond_to?(:root_path) }
|
|
60
|
+
routes ? routes.root_path : '/'
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -5,7 +5,7 @@ FactoryBot.define do
|
|
|
5
5
|
name { 'Mollie' }
|
|
6
6
|
available_to_admin { true }
|
|
7
7
|
available_to_users { true }
|
|
8
|
-
preferences { {
|
|
8
|
+
preferences { {api_key: 'test_fakekey', mollie_method: ''} }
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
factory :mollie_source, class: 'SolidusMollie::MollieSource' do
|
data/lib/solidus_mollie.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solidus_mollie
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kalopa Robotics
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: mollie-api-ruby
|
|
@@ -83,29 +82,35 @@ files:
|
|
|
83
82
|
- LICENSE
|
|
84
83
|
- README.md
|
|
85
84
|
- app/controllers/solidus_mollie/callbacks_controller.rb
|
|
86
|
-
- app/decorators/solidus_mollie/
|
|
85
|
+
- app/decorators/solidus_mollie/checkout_controller_decorator.rb
|
|
87
86
|
- app/models/solidus_mollie/client.rb
|
|
88
87
|
- app/models/solidus_mollie/mollie_source.rb
|
|
89
88
|
- app/models/solidus_mollie/payment_method.rb
|
|
90
89
|
- app/models/solidus_mollie/response.rb
|
|
91
90
|
- app/services/solidus_mollie/create_order_payment.rb
|
|
92
91
|
- app/services/solidus_mollie/process_webhook.rb
|
|
92
|
+
- app/views/checkouts/payment/_mollie.html.erb
|
|
93
|
+
- app/views/orders/payment_info/_mollie.html.erb
|
|
94
|
+
- app/views/spree/admin/payments/source_forms/_mollie.html.erb
|
|
95
|
+
- app/views/spree/admin/payments/source_views/_mollie.html.erb
|
|
93
96
|
- app/views/spree/checkout/payment/_mollie.html.erb
|
|
97
|
+
- app/views/spree/orders/payment_info/_mollie.html.erb
|
|
94
98
|
- config/locales/en.yml
|
|
95
99
|
- config/routes.rb
|
|
96
100
|
- db/migrate/20260620143922_create_solidus_mollie_sources.rb
|
|
97
101
|
- lib/generators/solidus_mollie/install/install_generator.rb
|
|
98
102
|
- lib/generators/solidus_mollie/install/templates/initializer.rb
|
|
99
103
|
- lib/solidus_mollie.rb
|
|
104
|
+
- lib/solidus_mollie/configuration.rb
|
|
100
105
|
- lib/solidus_mollie/engine.rb
|
|
101
|
-
- lib/solidus_mollie/
|
|
106
|
+
- lib/solidus_mollie/frontend.rb
|
|
107
|
+
- lib/solidus_mollie/testing_support/factories.rb
|
|
102
108
|
- lib/solidus_mollie/version.rb
|
|
103
109
|
homepage: https://github.com/kalopa/solidus_mollie
|
|
104
110
|
licenses:
|
|
105
111
|
- BSD-2-Clause
|
|
106
112
|
metadata:
|
|
107
113
|
rubygems_mfa_required: 'true'
|
|
108
|
-
post_install_message:
|
|
109
114
|
rdoc_options: []
|
|
110
115
|
require_paths:
|
|
111
116
|
- lib
|
|
@@ -120,8 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
120
125
|
- !ruby/object:Gem::Version
|
|
121
126
|
version: '0'
|
|
122
127
|
requirements: []
|
|
123
|
-
rubygems_version:
|
|
124
|
-
signing_key:
|
|
128
|
+
rubygems_version: 4.0.21
|
|
125
129
|
specification_version: 4
|
|
126
130
|
summary: Mollie payments for Solidus
|
|
127
131
|
test_files: []
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SolidusMollie
|
|
4
|
-
module Spree
|
|
5
|
-
# Intercepts the checkout confirm step. When the order is paying with a
|
|
6
|
-
# Mollie payment method, the buyer is redirected to Mollie's hosted checkout
|
|
7
|
-
# instead of the order being completed synchronously. The order is finished
|
|
8
|
-
# later by the webhook (see SolidusMollie::ProcessWebhook).
|
|
9
|
-
module CheckoutControllerDecorator
|
|
10
|
-
def update
|
|
11
|
-
return redirect_to_mollie if divert_to_mollie?
|
|
12
|
-
|
|
13
|
-
super
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
private
|
|
17
|
-
|
|
18
|
-
def divert_to_mollie?
|
|
19
|
-
return false unless params[:state].to_s == 'confirm' || @order&.confirm?
|
|
20
|
-
|
|
21
|
-
mollie_payment.present?
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def mollie_payment
|
|
25
|
-
@mollie_payment ||= @order&.payments&.detect do |payment|
|
|
26
|
-
payment.checkout? && payment.payment_method.is_a?(SolidusMollie::PaymentMethod)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def redirect_to_mollie
|
|
31
|
-
result = SolidusMollie::CreateOrderPayment.call(
|
|
32
|
-
order: @order,
|
|
33
|
-
payment: mollie_payment,
|
|
34
|
-
redirect_url: spree.mollie_return_url(mollie_url_options.merge(
|
|
35
|
-
order_number: @order.number,
|
|
36
|
-
token: @order.token
|
|
37
|
-
)),
|
|
38
|
-
webhook_url: spree.mollie_webhook_url(mollie_url_options)
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
if result.success?
|
|
42
|
-
redirect_to result.checkout_url, allow_other_host: true
|
|
43
|
-
else
|
|
44
|
-
flash[:error] = I18n.t('solidus_mollie.payment_error',
|
|
45
|
-
message: result.error,
|
|
46
|
-
default: "We couldn't start your Mollie payment.")
|
|
47
|
-
redirect_to spree.checkout_state_path(:payment)
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def mollie_url_options
|
|
52
|
-
{
|
|
53
|
-
host: request.host,
|
|
54
|
-
port: request.optional_port,
|
|
55
|
-
protocol: request.ssl? ? 'https' : 'http'
|
|
56
|
-
}.compact
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|