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 +4 -4
- data/lib/active_merchant/billing/gateways/flexcharge.rb +382 -0
- data/lib/active_merchant/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 395642de699b76ea08960c76960c856dbc3c493e39c95a3755cb9503517ed980
|
4
|
+
data.tar.gz: 83f22a8df4ba195ecb8485fc857af605d6abae4505a5bdff13cd35806f83f38f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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.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
|
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
|