swiss-crm-activemerchant-v2 1.0.26 → 1.0.28

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: f047038c38a8e7697181cfacf67ed5e78d25d6183ad600b62847e9424c81c62c
4
- data.tar.gz: f4c1fbfedadf6a59e131e3fcc15ba6db6eac232c2254c9644d8068edd5a68b0c
3
+ metadata.gz: 395642de699b76ea08960c76960c856dbc3c493e39c95a3755cb9503517ed980
4
+ data.tar.gz: 83f22a8df4ba195ecb8485fc857af605d6abae4505a5bdff13cd35806f83f38f
5
5
  SHA512:
6
- metadata.gz: 5f03d59d1f9d3bd67b3844e6ed58cc63fd889b3008401e8cf2431b7ead8cf9805db4f4a0d0286b4cb7060b03eb0454d5a437bc00035a4c151ba3dea6f1ad64c0
7
- data.tar.gz: e3b549c33adf95ecdb4db05dbdff4079672f6cdd721490840ce3026043169c523aff4c2c29902e8ebffb84a5db7f769b1299b6dc2ae88b917142e69f752e2df8
6
+ metadata.gz: 86734ccd901d11b2743de57dd7a7440ce4e716eb29528ec5388aeca631549293a1a796e8f4797649afb4f40ff31ffff6ebdd67b0c49f341b493efa701e3f2f74
7
+ data.tar.gz: 831f5049e863030c2e200d7fc03bb7fdf67d71c90ede532b5fa7a67b78667693660fddda1a62d12d54e1f687b3ff5b487bd066b9add0e4fc0fa825bbc9424390
@@ -0,0 +1,382 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ class FlexchargeGateway < Gateway
4
+ include Empty
5
+
6
+ self.test_url = 'https://api-sandbox.flexfactor.io'
7
+ self.live_url = 'https://api.flexfactor.io'
8
+ self.homepage_url = 'https://www.flexcharge.com'
9
+ self.supported_countries = %w[US CA]
10
+ self.supported_cardtypes = %i[visa master american_express discover]
11
+ self.default_currency = 'USD'
12
+ self.money_format = :cents
13
+ self.display_name = 'Flexcharge'
14
+
15
+ def initialize(options = {})
16
+ requires!(options, :api_key, :api_secret, :merchant_id, :token)
17
+ super
18
+ @api_key = options[:api_key]
19
+ @api_secret = options[:api_secret]
20
+ @mid = options[:merchant_id]
21
+ @tokenization_key = options[:token]
22
+ @site_id = options[:site_id]
23
+ @response_http_code = nil
24
+ end
25
+
26
+ def purchase(amount, payment_source, options = {})
27
+ token_response = get_access_token
28
+ return token_response unless token_response.success?
29
+
30
+ tokenize_response = tokenize_card(payment_source, options)
31
+ return tokenize_response unless tokenize_response.success?
32
+
33
+ post = {}
34
+ add_invoice(post, amount, options)
35
+ add_payment_method(post, tokenize_response.params['paymentMethod'])
36
+ add_customer_data(post, options)
37
+ add_billing_address(post, payment_source, options['billing_address'])
38
+ add_merchant_data(post, options)
39
+ add_idempotency_key(post, options)
40
+ add_subscription_data(post, amount, options) if options[:flex_merchant_initiated]
41
+
42
+ evaluate_response = commit('evaluate', post, token_response.authorization, options)
43
+ handle_evaluate_response(evaluate_response, amount, options)
44
+ end
45
+
46
+ def capture(amount, authorization, options = {})
47
+ token_response = get_access_token
48
+ return token_response unless token_response.success?
49
+
50
+ post = {}
51
+ add_capture_data(post, amount, authorization, options)
52
+ add_idempotency_key(post, options)
53
+
54
+ commit('capture', post, token_response.authorization, options)
55
+ end
56
+
57
+ def refund(amount, authorization, options = {})
58
+ token_response = get_access_token
59
+ return token_response unless token_response.success?
60
+
61
+ post = {}
62
+ add_refund_data(post, amount)
63
+
64
+ commit('refund', post, token_response.authorization, options.merge(order_id: authorization))
65
+ end
66
+
67
+ def verify_credentials
68
+ response = get_access_token
69
+ response.success?
70
+ end
71
+
72
+ private
73
+
74
+ def tokenize_card(payment_source, options)
75
+ uri = build_tokenization_uri
76
+ payload = build_tokenization_payload(payment_source, options)
77
+
78
+ begin
79
+ raw_response = ssl_post(uri, payload.to_json, tokenization_headers)
80
+ parsed = parse(raw_response)
81
+ process_tokenization_response(parsed, payment_source)
82
+ rescue ResponseError => e
83
+ handle_response_error(e)
84
+ end
85
+ end
86
+
87
+ def build_tokenization_uri
88
+ base_url = test? ? test_url : live_url
89
+ "#{base_url}/v1/tokenize?mid=#{@mid}&environment=#{@tokenization_key}"
90
+ end
91
+
92
+ def build_tokenization_payload(payment_source, options)
93
+ {
94
+ payment_method: {
95
+ email: options[:email],
96
+ credit_card: {
97
+ first_name: payment_source.first_name,
98
+ last_name: payment_source.last_name,
99
+ number: payment_source.number,
100
+ verification_value: payment_source.verification_value,
101
+ month: payment_source.month.to_s,
102
+ year: payment_source.year.to_s
103
+ }
104
+ }
105
+ }
106
+ end
107
+
108
+ def process_tokenization_response(parsed, payment_source)
109
+ token = parsed.dig("transaction", "payment_method", "token")
110
+ success = token.present?
111
+ payment_method = build_payment_method_data(payment_source, token)
112
+
113
+ Response.new(
114
+ success,
115
+ success ? 'Tokenization successful' : 'Tokenization failed',
116
+ parsed.merge('paymentMethod' => payment_method),
117
+ test: test?,
118
+ authorization: token
119
+ )
120
+ end
121
+
122
+ def build_payment_method_data(payment_source, token)
123
+ {
124
+ holderName: "#{payment_source.first_name} #{payment_source.last_name}",
125
+ cardType: "CREDIT",
126
+ expirationMonth: payment_source.month.to_i,
127
+ expirationYear: payment_source.year.to_i,
128
+ cardBinNumber: payment_source.bin_card,
129
+ cardLast4Digits: payment_source.last_digits,
130
+ cardNumber: token,
131
+ token: true
132
+ }
133
+ end
134
+
135
+ def handle_evaluate_response(evaluate_response, amount, options)
136
+ return evaluate_response unless evaluate_response.success?
137
+
138
+ status = evaluate_response.params['status']
139
+
140
+ case status
141
+ when 'CAPTUREREQUIRED'
142
+ process_capture_required(evaluate_response, amount, options)
143
+ else
144
+ evaluate_response
145
+ end
146
+ end
147
+
148
+ def process_capture_required(evaluate_response, amount, options)
149
+ capture_response = capture(amount, evaluate_response.authorization, options)
150
+ merged_params = evaluate_response.params.merge('capture' => capture_response.params)
151
+
152
+ Response.new(
153
+ capture_response.success?,
154
+ capture_response.message,
155
+ merged_params,
156
+ test: test?,
157
+ authorization: capture_response.authorization
158
+ )
159
+ end
160
+
161
+ def add_invoice(post, amount, options)
162
+ post[:transaction] = {
163
+ amount: amount,
164
+ currency: options[:currency],
165
+ id: options[:order_id],
166
+ dynamicDescriptor: options[:descriptor],
167
+ transactionType: 'CAPTURE'
168
+ }
169
+ end
170
+
171
+ def add_payment_method(post, payment_method)
172
+ post[:paymentMethod] = payment_method
173
+ end
174
+
175
+ def add_customer_data(post, options)
176
+ post[:payer] = {
177
+ email: options[:email]
178
+ }
179
+ end
180
+
181
+ def add_billing_address(post, payment_source, billing_address)
182
+ post[:billingInformation] = {
183
+ firstName: payment_source.first_name,
184
+ lastName: payment_source.last_name,
185
+ country: billing_address["country"],
186
+ countryCode: billing_address["country"],
187
+ addressLine1: billing_address["address1"],
188
+ city: billing_address["city"],
189
+ state: billing_address["state"],
190
+ zipcode: billing_address["zip"]
191
+ }.compact
192
+ end
193
+
194
+ def add_subscription_data(post, amount, options)
195
+ post[:isRecurring] = true
196
+ post[:isMIT] = true
197
+ post[:expiryDateUtc] = 2.hours.from_now.utc.iso8601
198
+ post[:subscription] = {
199
+ subscriptionId: options['merchant_initiated_data']['subscriptionId'],
200
+ interval: "Monthly",
201
+ price: amount,
202
+ currency: options[:currency]
203
+ }
204
+ end
205
+
206
+ def add_merchant_data(post, options)
207
+ post[:orderId] = options[:order_id]
208
+ post[:mid] = @mid
209
+ post[:isDeclined] = true
210
+ post[:siteId] = @site_id
211
+ end
212
+
213
+ def add_capture_data(post, amount, authorization, options)
214
+ post[:idempotencyKey] = options[:idempotency_id]
215
+ post[:orderId] = authorization
216
+ post[:amount] = amount
217
+ post[:currency] = options[:currency]
218
+ end
219
+
220
+ def add_refund_data(post, amount)
221
+ post[:amountToRefund] = amount.to_f / 100
222
+ end
223
+
224
+ def add_idempotency_key(post, options)
225
+ post[:idempotencyKey] = options[:idempotency_id]
226
+ end
227
+
228
+ def commit(action, params, access_token, options = {})
229
+ request_url = build_request_url(action, options)
230
+ raw_response = ssl_post(request_url, params.to_json, headers(access_token))
231
+ response = parse(raw_response)
232
+ succeeded = success_from(action, response)
233
+
234
+ Response.new(
235
+ succeeded,
236
+ message_from(action, response),
237
+ response,
238
+ error_code: error_code_from(succeeded, response),
239
+ authorization: authorization_from(response),
240
+ test: test?,
241
+ request_method: :post,
242
+ request_endpoint: request_url,
243
+ request_body: params,
244
+ response_type: response_type(response['status']),
245
+ response_http_code: @response_http_code
246
+ )
247
+ rescue ResponseError => e
248
+ handle_response_error(e)
249
+ end
250
+
251
+ def build_request_url(action, options)
252
+ case action
253
+ when 'evaluate'
254
+ "#{url}/v1/evaluate"
255
+ when 'capture'
256
+ "#{url}/v1/capture"
257
+ when 'refund'
258
+ "#{url}/v1/orders/#{options[:order_id]}/refund"
259
+ end
260
+ end
261
+
262
+ def headers(access_token)
263
+ {
264
+ 'Authorization' => "Bearer #{access_token}",
265
+ 'accept' => 'application/json',
266
+ 'content-type' => 'application/*+json'
267
+ }
268
+ end
269
+
270
+ def tokenization_headers
271
+ {
272
+ 'Accept' => 'application/json',
273
+ 'Content-Type' => 'application/json'
274
+ }
275
+ end
276
+
277
+ def get_access_token
278
+ payload = build_token_payload
279
+ raw_response = ssl_post("#{url}/v1/oauth2/token", payload.to_json, tokenization_headers)
280
+ process_token_response(raw_response)
281
+ rescue ResponseError => e
282
+ handle_response_error(e)
283
+ end
284
+
285
+ def build_token_payload
286
+ {
287
+ AppKey: @api_key,
288
+ AppSecret: @api_secret
289
+ }
290
+ end
291
+
292
+ def process_token_response(raw_response)
293
+ parsed = parse(raw_response)
294
+ token = parsed['accessToken']
295
+ success = token.present?
296
+
297
+ Response.new(
298
+ success,
299
+ success ? 'Token Retrieved' : 'Token Request Failed',
300
+ parsed,
301
+ authorization: token,
302
+ test: test?
303
+ )
304
+ end
305
+
306
+ def url
307
+ test? ? test_url : live_url
308
+ end
309
+
310
+ def parse(body)
311
+ JSON.parse(body)
312
+ end
313
+
314
+ def success_from(action, response)
315
+ case action
316
+ when 'evaluate'
317
+ %w[APPROVED CAPTUREREQUIRED].include?(response['status'])
318
+ when 'capture'
319
+ response['status'] == 'SUCCESS'
320
+ when 'refund'
321
+ response['status'] == 'SUCCESS'
322
+ else
323
+ false
324
+ end
325
+ end
326
+
327
+ def message_from(action, response)
328
+ status = response['status']
329
+
330
+ case action
331
+ when 'evaluate'
332
+ return 'Pending' if %w[SUBMITTED CHALLENGE].include?(status)
333
+ status == 'APPROVED' ? 'Succeeded' : status
334
+ when 'capture'
335
+ status == 'SUCCESS' ? status : Array(response['errors']).join(', ').presence
336
+ when 'refund'
337
+ status == 'SUCCESS' ? status : response['responseMessage']
338
+ end
339
+ end
340
+
341
+ def authorization_from(response)
342
+ response['orderId'] || response['transactionId']
343
+ end
344
+
345
+ def handle_response_error(error)
346
+ @response_http_code = error.response&.code&.to_i
347
+ body = error.response.body
348
+ parsed = parse(body) rescue { 'error' => body }
349
+ Response.new(
350
+ false,
351
+ parsed['error'],
352
+ parsed,
353
+ test: test?,
354
+ response_http_code: @response_http_code
355
+ )
356
+ end
357
+
358
+ def error_code_from(succeeded, response)
359
+ return nil if succeeded
360
+
361
+ errors = response['errors']
362
+ return response['title'] if errors.blank?
363
+
364
+ errors.map { |_, messages| Array(messages).join(', ') }.join('; ')
365
+ end
366
+
367
+ def handle_response(response)
368
+ @response_http_code = response.code.to_i
369
+ response.body
370
+ end
371
+
372
+ def response_type(status)
373
+ case status
374
+ when 'APPROVED', 'SUCCESS', 'SUBMITTED' then 0
375
+ when 'CAPTUREREQUIRED', 'CHALLENGE' then 1
376
+ when 'FAILED' then 2
377
+ else 1
378
+ end
379
+ end
380
+ end
381
+ end
382
+ end
@@ -1,3 +1,3 @@
1
1
  module ActiveMerchant
2
- VERSION = '1.0.26'
2
+ VERSION = '1.0.28'
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.26
4
+ version: 1.0.28
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-25 00:00:00.000000000 Z
11
+ date: 2025-08-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -289,6 +289,7 @@ files:
289
289
  - lib/active_merchant/billing/gateways/first_pay.rb
290
290
  - lib/active_merchant/billing/gateways/firstdata_e4.rb
291
291
  - lib/active_merchant/billing/gateways/firstdata_e4_v27.rb
292
+ - lib/active_merchant/billing/gateways/flexcharge.rb
292
293
  - lib/active_merchant/billing/gateways/flo2cash.rb
293
294
  - lib/active_merchant/billing/gateways/flo2cash_simple.rb
294
295
  - lib/active_merchant/billing/gateways/fluidpay.rb