swiss-crm-activemerchant-v2 1.0.22 → 1.0.23
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/lib/active_merchant/billing/gateways/mollie.rb +283 -58
- data/lib/active_merchant/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: af3a2d531e5ddb1183fdad238a7087a6d77c68e0a3882ccf72c755143c29ca32
|
4
|
+
data.tar.gz: d923b080a1268a0e8143bfb5e81b927e651fdfd2d131db3559348adba04b4ae0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3c2629ceac79372e2722b599a8ba02e3f0670d8eebd1c4405e703284d5db4d2cca654d78e89ea1c1b199366d6ddb5ea6cbd44792b83d66f0e6e51cad41bc4f1e
|
7
|
+
data.tar.gz: 74311b6b4d445056f69e18e965bb6cdb337c63ff61c3230d36c45bc419b744646b4167947c326a1f648b85b9b7cfb4667b57bab84eb3cb0ca4f63444b90ecf7c
|
@@ -1,87 +1,312 @@
|
|
1
|
-
|
1
|
+
module ActiveMerchant #:nodoc:
|
2
|
+
module Billing #:nodoc:
|
3
|
+
class MollieGateway < Gateway
|
4
|
+
include Empty
|
2
5
|
|
3
|
-
|
6
|
+
SOFT_DECLINE_REASONS = %w[
|
7
|
+
insufficient_funds
|
8
|
+
card_expired
|
9
|
+
card_declined
|
10
|
+
temporary_failure
|
11
|
+
verification_required
|
12
|
+
].freeze
|
4
13
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
self.
|
9
|
-
self.
|
14
|
+
AUTHORIZATION_PREFIX = 'Bearer '.freeze
|
15
|
+
CONTENT_TYPE = 'application/json'.freeze
|
16
|
+
|
17
|
+
self.test_url = 'https://api.mollie.com/v2'
|
18
|
+
self.live_url = 'https://api.mollie.com/v2'
|
19
|
+
self.supported_countries = %w[NL BE DE FR CH US GB]
|
10
20
|
self.default_currency = 'EUR'
|
21
|
+
self.supported_cardtypes = %i[visa master american_express]
|
11
22
|
self.money_format = :cents
|
23
|
+
self.homepage_url = 'https://www.mollie.com'
|
12
24
|
self.display_name = 'Mollie'
|
13
25
|
|
14
26
|
def initialize(options = {})
|
15
27
|
requires!(options, :api_key)
|
16
28
|
super
|
17
|
-
|
18
|
-
config.api_key = options[:api_key]
|
19
|
-
end
|
29
|
+
@api_key = options[:api_key]
|
20
30
|
end
|
21
31
|
|
22
|
-
def purchase(amount,
|
23
|
-
|
24
|
-
mollie_payment = Mollie::Payment.create(payload)
|
32
|
+
def purchase(amount, payment_method, options = {})
|
33
|
+
return recurring(amount, payment_method, options) if recurring_payment?(payment_method, options)
|
25
34
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
35
|
+
result = add_customer_to_payment(payment_method, options)
|
36
|
+
return result if result.is_a?(Response) && !result.success?
|
37
|
+
|
38
|
+
post = {}
|
39
|
+
add_purchase_data(post, amount, payment_method, options)
|
40
|
+
add_customer_data(post, result, options)
|
41
|
+
add_payment_token(post, payment_method, options)
|
42
|
+
add_addresses(post, options)
|
43
|
+
|
44
|
+
commit('payments', post, options)
|
45
|
+
end
|
46
|
+
|
47
|
+
def recurring(amount, payment_method, options = {})
|
48
|
+
post = {}
|
49
|
+
add_recurring_data(post, amount, payment_method, options)
|
50
|
+
|
51
|
+
commit('payments', post, options)
|
41
52
|
end
|
42
53
|
|
43
54
|
def refund(amount, authorization, options = {})
|
44
|
-
|
45
|
-
|
46
|
-
amount: {
|
47
|
-
value: sprintf('%.2f', amount / 100.0),
|
48
|
-
currency: payment.amount['currency']
|
49
|
-
}
|
50
|
-
)
|
55
|
+
post = {}
|
56
|
+
add_refund_data(post, amount, authorization, options)
|
51
57
|
|
52
|
-
|
53
|
-
rescue Mollie::Exception => e
|
54
|
-
Response.new(false, e.message, {}, test: test?)
|
58
|
+
commit("payments/#{authorization}/refunds", post, options)
|
55
59
|
end
|
56
60
|
|
57
|
-
def
|
58
|
-
|
59
|
-
Response.new(payment.paid?, payment.status, payment.to_h, test: test?)
|
60
|
-
rescue Mollie::Exception => e
|
61
|
-
Response.new(false, e.message, {}, test: test?)
|
61
|
+
def void(authorization, options = {})
|
62
|
+
commit("payments/#{authorization}/cancel", {}, options)
|
62
63
|
end
|
63
64
|
|
64
65
|
private
|
65
66
|
|
66
|
-
def
|
67
|
+
def recurring_payment?(payment_method, options)
|
68
|
+
options[:mollie_customer_id].present? && payment_method.mollie_mandate_id.present?
|
69
|
+
end
|
70
|
+
|
71
|
+
def add_customer_to_payment(payment_method, options)
|
72
|
+
return options[:mollie_customer_id] if existing_customer?(options)
|
73
|
+
|
74
|
+
customer_response = create_customer(options)
|
75
|
+
return customer_response unless customer_response.success?
|
76
|
+
|
77
|
+
customer_response.params['id']
|
78
|
+
end
|
79
|
+
|
80
|
+
def existing_customer?(options)
|
81
|
+
options[:mollie_customer_id].present?
|
82
|
+
end
|
83
|
+
|
84
|
+
def create_customer(options)
|
85
|
+
post = {}
|
86
|
+
add_customer_creation_data(post, options)
|
87
|
+
|
88
|
+
commit('customers', post, options)
|
89
|
+
end
|
90
|
+
|
91
|
+
def add_customer_creation_data(post, options)
|
92
|
+
billing = options[:billing_address] || {}
|
93
|
+
|
94
|
+
post[:name] = billing[:name]
|
95
|
+
post[:email] = options[:email]
|
96
|
+
post[:locale] = options[:locale]
|
97
|
+
end
|
98
|
+
|
99
|
+
def add_purchase_data(post, amount, payment_method, options)
|
100
|
+
post[:amount] = format_amount(amount, options[:currency])
|
101
|
+
post[:description] = "Order ##{options[:order_id]}"
|
102
|
+
post[:method] = payment_method.mollie_payment_method
|
103
|
+
post[:sequenceType] = 'first'
|
104
|
+
post[:locale] = options[:locale]
|
105
|
+
|
106
|
+
add_urls(post, options)
|
107
|
+
end
|
108
|
+
|
109
|
+
def add_recurring_data(post, amount, payment_method, options)
|
110
|
+
post[:amount] = format_amount(amount, options[:currency])
|
111
|
+
post[:description] = "Order ##{options[:order_id]}"
|
112
|
+
post[:sequenceType] = 'recurring'
|
113
|
+
post[:customerId] = options[:mollie_customer_id]
|
114
|
+
post[:mandateId] = payment_method.mollie_mandate_id
|
115
|
+
|
116
|
+
add_urls(post, options)
|
117
|
+
end
|
118
|
+
|
119
|
+
def add_refund_data(post, amount, authorization, options)
|
120
|
+
post[:amount] = format_amount(amount, options[:currency])
|
121
|
+
post[:description] = "Order ##{options[:order_id]} Refund at #{Time.current.to_i}"
|
122
|
+
|
123
|
+
post[:metadata] = {
|
124
|
+
refund_reference: "refund-#{options[:order_id]}-#{SecureRandom.hex(4)}"
|
125
|
+
}
|
126
|
+
end
|
127
|
+
|
128
|
+
def add_customer_data(post, customer_result, options)
|
129
|
+
customer_id = customer_result.is_a?(String) ? customer_result : customer_result.params['id']
|
130
|
+
post[:customerId] = customer_id
|
131
|
+
end
|
132
|
+
|
133
|
+
def add_urls(post, options)
|
134
|
+
post[:redirectUrl] = options.dig(:redirect_links, :success_url)
|
135
|
+
post[:webhookUrl] = options[:mollie_webhook_url]
|
136
|
+
post[:cancelUrl] = options.dig(:redirect_links, :failure_url)
|
137
|
+
end
|
138
|
+
|
139
|
+
def add_payment_token(post, payment_method, options)
|
140
|
+
method = post[:method].to_s.downcase
|
141
|
+
|
142
|
+
return unless %w[creditcard applepay googlepay].include?(method)
|
143
|
+
|
144
|
+
token = payment_method.mollie_payment_token
|
145
|
+
return unless token
|
146
|
+
|
147
|
+
case method
|
148
|
+
when 'creditcard'
|
149
|
+
post[:cardToken] = token
|
150
|
+
when 'applepay'
|
151
|
+
post[:applePayPaymentToken] = token
|
152
|
+
when 'googlepay'
|
153
|
+
post[:googlePayPaymentToken] = token
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def add_addresses(post, options)
|
158
|
+
post[:billingAddress] = build_address(options[:billing_address])
|
159
|
+
post[:shippingAddress] = build_address(options[:shipping_address])
|
160
|
+
end
|
161
|
+
|
162
|
+
def build_address(data)
|
163
|
+
return if data.blank?
|
164
|
+
|
165
|
+
first_name, last_name = split_names(data[:name])
|
166
|
+
|
167
|
+
{
|
168
|
+
title: data[:title],
|
169
|
+
givenName: first_name,
|
170
|
+
familyName: last_name,
|
171
|
+
organizationName: data[:company],
|
172
|
+
streetAndNumber: data[:address1],
|
173
|
+
streetAdditional: data[:address2],
|
174
|
+
postalCode: data[:zip],
|
175
|
+
city: data[:city],
|
176
|
+
region: data[:state],
|
177
|
+
country: data[:country],
|
178
|
+
phone: data[:phone]
|
179
|
+
}.compact
|
180
|
+
end
|
181
|
+
|
182
|
+
def split_names(name)
|
183
|
+
return [nil, nil] if name.blank?
|
184
|
+
|
185
|
+
parts = name.split
|
186
|
+
[parts.first, parts[1..].join(' ')].compact
|
187
|
+
end
|
188
|
+
|
189
|
+
def format_amount(amount, currency = nil)
|
190
|
+
{
|
191
|
+
currency: currency || default_currency,
|
192
|
+
value: sprintf('%.2f', amount.to_f / 100)
|
193
|
+
}
|
194
|
+
end
|
195
|
+
|
196
|
+
def commit(endpoint, post = {}, _options = {})
|
197
|
+
request_url = build_request_url(endpoint)
|
198
|
+
payload = build_payload(post)
|
199
|
+
|
200
|
+
begin
|
201
|
+
raw_response = perform_request(:post, request_url, payload)
|
202
|
+
response = parse(raw_response)
|
203
|
+
succeeded = success_from(response)
|
204
|
+
|
205
|
+
Response.new(
|
206
|
+
succeeded,
|
207
|
+
message_from(succeeded, response),
|
208
|
+
response,
|
209
|
+
authorization: authorization_from(response),
|
210
|
+
test: test_from(response),
|
211
|
+
error_code: error_code_from(succeeded, response),
|
212
|
+
response_type: response_type_from(response),
|
213
|
+
response_http_code: @response_http_code,
|
214
|
+
request_endpoint: request_url,
|
215
|
+
request_method: :post,
|
216
|
+
request_body: payload
|
217
|
+
)
|
218
|
+
rescue ResponseError => e
|
219
|
+
handle_error_response(e)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def build_request_url(endpoint)
|
224
|
+
"#{url}/#{endpoint}"
|
225
|
+
end
|
226
|
+
|
227
|
+
def build_payload(post)
|
228
|
+
post.compact
|
229
|
+
end
|
230
|
+
|
231
|
+
def perform_request(method, url, payload)
|
232
|
+
ssl_post(url, payload.to_json, headers)
|
233
|
+
end
|
234
|
+
|
235
|
+
def url
|
236
|
+
test? ? test_url : live_url
|
237
|
+
end
|
238
|
+
|
239
|
+
def headers
|
67
240
|
{
|
68
|
-
|
69
|
-
|
70
|
-
currency: options[:currency] || default_currency
|
71
|
-
},
|
72
|
-
method: options[:payment_method],
|
73
|
-
description: options[:description] || "Order ##{options[:order_id]}",
|
74
|
-
redirect_url: options[:redirect_url],
|
75
|
-
webhook_url: options[:webhook_url],
|
76
|
-
metadata: {
|
77
|
-
order_id: options[:order_id],
|
78
|
-
customer_id: options[:customer_id]
|
79
|
-
}.compact
|
241
|
+
'Authorization' => "#{AUTHORIZATION_PREFIX}#{@api_key}",
|
242
|
+
'Content-Type' => CONTENT_TYPE
|
80
243
|
}
|
81
244
|
end
|
82
245
|
|
83
|
-
def
|
84
|
-
|
246
|
+
def parse(response)
|
247
|
+
@response_http_code = response.respond_to?(:code) ? response.code.to_i : nil
|
248
|
+
JSON.parse(response)
|
249
|
+
end
|
250
|
+
|
251
|
+
def success_from(response)
|
252
|
+
resource_type = response['resource']
|
253
|
+
status = response['status']
|
254
|
+
|
255
|
+
resource_type == 'customer' || %w[paid authorized].include?(status)
|
256
|
+
end
|
257
|
+
|
258
|
+
def message_from(succeeded, response)
|
259
|
+
resource_type = response['resource']
|
260
|
+
status = response['status']
|
261
|
+
|
262
|
+
return 'Customer created' if resource_type == 'customer'
|
263
|
+
return 'Pending' if status == 'open' || (resource_type == 'refund' && status == 'pending')
|
264
|
+
return 'Success' if %w[paid authorized].include?(status)
|
265
|
+
|
266
|
+
status || 'failed'
|
267
|
+
end
|
268
|
+
|
269
|
+
def error_code_from(succeeded, response)
|
270
|
+
return nil if succeeded
|
271
|
+
|
272
|
+
response['status'] || response['type']
|
273
|
+
end
|
274
|
+
|
275
|
+
def authorization_from(response)
|
276
|
+
response['id']
|
277
|
+
end
|
278
|
+
|
279
|
+
def test_from(response)
|
280
|
+
response['mode'] == 'test'
|
281
|
+
end
|
282
|
+
|
283
|
+
def response_type_from(response)
|
284
|
+
return 'success' if response['sequenceType'] == 'recurring' && response['status'] == 'paid'
|
285
|
+
|
286
|
+
nil
|
287
|
+
end
|
288
|
+
|
289
|
+
def handle_error_response(error)
|
290
|
+
parsed = JSON.parse(error.response.body) rescue {}
|
291
|
+
error_code = error.response.code
|
292
|
+
detail = parsed['detail']
|
293
|
+
field = parsed['field']
|
294
|
+
message = parsed['title'] || parsed['message'] || error.message
|
295
|
+
|
296
|
+
full_message = build_error_message(error_code, message, detail, field)
|
297
|
+
|
298
|
+
Response.new(
|
299
|
+
false,
|
300
|
+
full_message,
|
301
|
+
parsed,
|
302
|
+
error_code: error_code
|
303
|
+
)
|
304
|
+
end
|
305
|
+
|
306
|
+
def build_error_message(error_code, message, detail, field)
|
307
|
+
components = ["Failed with #{error_code}", message, detail]
|
308
|
+
components << "(field: #{field})" if field
|
309
|
+
components.compact.join(': ')
|
85
310
|
end
|
86
311
|
end
|
87
312
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: swiss-crm-activemerchant-v2
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.23
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tobias Luetke
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-07-
|
11
|
+
date: 2025-07-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|