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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10b566d3f08b9618f3d388ca8a9cc3c65b9f33284cc2f3b33ffc78594e36e5a1
4
- data.tar.gz: 19593bf9c7e4be5d20337ffeeba40e0dc4be4d5e43373f30a360e57ccc1b7013
3
+ metadata.gz: af3a2d531e5ddb1183fdad238a7087a6d77c68e0a3882ccf72c755143c29ca32
4
+ data.tar.gz: d923b080a1268a0e8143bfb5e81b927e651fdfd2d131db3559348adba04b4ae0
5
5
  SHA512:
6
- metadata.gz: a2192ddf6220e465fb44881042a0aa91a56d6b194680ed93147672777ee8f1a7701aa0d3f9d0b0d975a2b497ff85bcd756d0d67cb66f4603b85605fc2c696b48
7
- data.tar.gz: d8ec8c557433f4d9b0acf5f7e6de78c278c06953e0cc8ba7841aafb40723fed4581c58534dfcd3373303b5021418a958dc9eac7a7a15b56321ac8b59bcb4d231
6
+ metadata.gz: 3c2629ceac79372e2722b599a8ba02e3f0670d8eebd1c4405e703284d5db4d2cca654d78e89ea1c1b199366d6ddb5ea6cbd44792b83d66f0e6e51cad41bc4f1e
7
+ data.tar.gz: 74311b6b4d445056f69e18e965bb6cdb337c63ff61c3230d36c45bc419b744646b4167947c326a1f648b85b9b7cfb4667b57bab84eb3cb0ca4f63444b90ecf7c
@@ -1,87 +1,312 @@
1
- # lib/swiss/gateways/mollie_gateway.rb
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ class MollieGateway < Gateway
4
+ include Empty
2
5
 
3
- require 'mollie'
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
- module ActiveMerchant
6
- module Billing
7
- class MollieGateway < Gateway
8
- self.supported_countries = %w[NL DE BE FR AT FI UK US]
9
- self.supported_cardtypes = %i[visa master american_express]
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
- Mollie::Client.configure do |config|
18
- config.api_key = options[:api_key]
19
- end
29
+ @api_key = options[:api_key]
20
30
  end
21
31
 
22
- def purchase(amount, _payment_source, options = {})
23
- payload = build_payment_payload(amount, options)
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
- Response.new(
27
- true,
28
- mollie_payment.status,
29
- mollie_payment.to_h,
30
- test: test?,
31
- authorization: mollie_payment.id,
32
- redirect_url: mollie_payment.checkout_url,
33
- params: {
34
- mollie_checkout_url: mollie_payment.checkout_url,
35
- payment_method: mollie_payment.method,
36
- status: mollie_payment.status
37
- }
38
- )
39
- rescue Mollie::Exception => e
40
- Response.new(false, e.message, {}, test: test?)
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
- payment = Mollie::Payment.get(authorization)
45
- refund = payment.refunds.create(
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
- Response.new(true, 'Refund initiated', refund.to_h, test: test?, authorization: refund.id)
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 fetch_payment_status(authorization)
58
- payment = Mollie::Payment.get(authorization)
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 build_payment_payload(amount, options)
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
- amount: {
69
- value: sprintf('%.2f', amount.to_f / 100),
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 test?
84
- ENV['MOLLIE_ENV'] == 'test'
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
@@ -1,3 +1,3 @@
1
1
  module ActiveMerchant
2
- VERSION = '1.0.22'
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.22
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-08 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