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: 6492d82a00dc0cda7d47420fca25eeb1e114c84f9e096701777fb413da42b666
4
- data.tar.gz: 1b3736c2d480bff6c8e4649cbb9ff1e2da3b9f5b8bdfeb148072d00721e68c4b
3
+ metadata.gz: af3a2d531e5ddb1183fdad238a7087a6d77c68e0a3882ccf72c755143c29ca32
4
+ data.tar.gz: d923b080a1268a0e8143bfb5e81b927e651fdfd2d131db3559348adba04b4ae0
5
5
  SHA512:
6
- metadata.gz: cfcb2bd27067db2d864c7f7fbbef05244bafc82483b6aeff7f062cb7bf6f2b49ec73a03925690b7a2c2afcee7ab415ca1e7c6535df013e750c96c0ab6b374bce
7
- data.tar.gz: c4cc8c4f2fbea5fb3593e06d0aba983866af9577f8f699cb78a0ae0a5a0c217e2394f3367d459ec17560df74fc75e8e854d250add11e6d67dacc477afaa110a2
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
@@ -1,3 +1,3 @@
1
1
  module ActiveMerchant
2
- VERSION = '1.0.21'
2
+ VERSION = '1.0.23'
3
3
  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.21
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-06-20 00:00:00.000000000 Z
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