swiss-crm-activemerchant-v2 1.0.21 → 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
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
|
@@ -44,6 +44,7 @@ module ActiveMerchant #:nodoc:
|
|
44
44
|
return result if result.is_a?(Response) && !result.success?
|
45
45
|
|
46
46
|
add_idempotency_id(post, options)
|
47
|
+
add_statement_descriptor(post, options)
|
47
48
|
|
48
49
|
verification_data = options[:verification_data] || {}
|
49
50
|
commit('transaction', post, verification_data, options)
|
@@ -148,6 +149,10 @@ module ActiveMerchant #:nodoc:
|
|
148
149
|
post[:idempotency_id] = options['idempotency_id']
|
149
150
|
end
|
150
151
|
|
152
|
+
def add_statement_descriptor(post, options)
|
153
|
+
post[:statement_descriptor] = options['descriptor']
|
154
|
+
end
|
155
|
+
|
151
156
|
def add_identity_data(post, options)
|
152
157
|
billing = options[:billing_address] || {}
|
153
158
|
first_name, last_name = split_names(billing[:name])
|
@@ -257,6 +262,7 @@ module ActiveMerchant #:nodoc:
|
|
257
262
|
merchant: @merchant_id,
|
258
263
|
source: params[:instrument_id],
|
259
264
|
idempotency_id: params[:idempotency_id],
|
265
|
+
statement_descriptor: params[:statement_descriptor],
|
260
266
|
tags: params[:tags]
|
261
267
|
}
|
262
268
|
else
|
@@ -0,0 +1,313 @@
|
|
1
|
+
module ActiveMerchant #:nodoc:
|
2
|
+
module Billing #:nodoc:
|
3
|
+
class MollieGateway < Gateway
|
4
|
+
include Empty
|
5
|
+
|
6
|
+
SOFT_DECLINE_REASONS = %w[
|
7
|
+
insufficient_funds
|
8
|
+
card_expired
|
9
|
+
card_declined
|
10
|
+
temporary_failure
|
11
|
+
verification_required
|
12
|
+
].freeze
|
13
|
+
|
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]
|
20
|
+
self.default_currency = 'EUR'
|
21
|
+
self.supported_cardtypes = %i[visa master american_express]
|
22
|
+
self.money_format = :cents
|
23
|
+
self.homepage_url = 'https://www.mollie.com'
|
24
|
+
self.display_name = 'Mollie'
|
25
|
+
|
26
|
+
def initialize(options = {})
|
27
|
+
requires!(options, :api_key)
|
28
|
+
super
|
29
|
+
@api_key = options[:api_key]
|
30
|
+
end
|
31
|
+
|
32
|
+
def purchase(amount, payment_method, options = {})
|
33
|
+
return recurring(amount, payment_method, options) if recurring_payment?(payment_method, options)
|
34
|
+
|
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)
|
52
|
+
end
|
53
|
+
|
54
|
+
def refund(amount, authorization, options = {})
|
55
|
+
post = {}
|
56
|
+
add_refund_data(post, amount, authorization, options)
|
57
|
+
|
58
|
+
commit("payments/#{authorization}/refunds", post, options)
|
59
|
+
end
|
60
|
+
|
61
|
+
def void(authorization, options = {})
|
62
|
+
commit("payments/#{authorization}/cancel", {}, options)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
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
|
240
|
+
{
|
241
|
+
'Authorization' => "#{AUTHORIZATION_PREFIX}#{@api_key}",
|
242
|
+
'Content-Type' => CONTENT_TYPE
|
243
|
+
}
|
244
|
+
end
|
245
|
+
|
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(': ')
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
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-
|
11
|
+
date: 2025-07-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -334,6 +334,7 @@ files:
|
|
334
334
|
- lib/active_merchant/billing/gateways/modern_payments.rb
|
335
335
|
- lib/active_merchant/billing/gateways/modern_payments_cim.rb
|
336
336
|
- lib/active_merchant/billing/gateways/moka.rb
|
337
|
+
- lib/active_merchant/billing/gateways/mollie.rb
|
337
338
|
- lib/active_merchant/billing/gateways/monei.rb
|
338
339
|
- lib/active_merchant/billing/gateways/moneris.rb
|
339
340
|
- lib/active_merchant/billing/gateways/money_movers.rb
|