activemerchant 1.123.0 → 1.126.0
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/CHANGELOG +206 -0
- data/lib/active_merchant/billing/check.rb +5 -8
- data/lib/active_merchant/billing/credit_card.rb +10 -0
- data/lib/active_merchant/billing/credit_card_methods.rb +18 -3
- data/lib/active_merchant/billing/gateway.rb +3 -2
- data/lib/active_merchant/billing/gateways/adyen.rb +66 -11
- data/lib/active_merchant/billing/gateways/airwallex.rb +341 -0
- data/lib/active_merchant/billing/gateways/barclaycard_smartpay.rb +2 -1
- data/lib/active_merchant/billing/gateways/blue_pay.rb +1 -1
- data/lib/active_merchant/billing/gateways/blue_snap.rb +31 -21
- data/lib/active_merchant/billing/gateways/braintree/braintree_common.rb +6 -1
- data/lib/active_merchant/billing/gateways/braintree/token_nonce.rb +113 -0
- data/lib/active_merchant/billing/gateways/braintree_blue.rb +87 -15
- data/lib/active_merchant/billing/gateways/card_connect.rb +1 -1
- data/lib/active_merchant/billing/gateways/card_stream.rb +17 -13
- data/lib/active_merchant/billing/gateways/cashnet.rb +15 -5
- data/lib/active_merchant/billing/gateways/checkout_v2.rb +34 -5
- data/lib/active_merchant/billing/gateways/credorax.rb +10 -0
- data/lib/active_merchant/billing/gateways/cyber_source.rb +24 -36
- data/lib/active_merchant/billing/gateways/d_local.rb +61 -6
- data/lib/active_merchant/billing/gateways/decidir.rb +17 -1
- data/lib/active_merchant/billing/gateways/decidir_plus.rb +344 -0
- data/lib/active_merchant/billing/gateways/ebanx.rb +19 -3
- data/lib/active_merchant/billing/gateways/elavon.rb +6 -3
- data/lib/active_merchant/billing/gateways/element.rb +20 -2
- data/lib/active_merchant/billing/gateways/global_collect.rb +137 -32
- data/lib/active_merchant/billing/gateways/ipg.rb +415 -0
- data/lib/active_merchant/billing/gateways/kushki.rb +7 -0
- data/lib/active_merchant/billing/gateways/litle.rb +93 -1
- data/lib/active_merchant/billing/gateways/mercado_pago.rb +3 -1
- data/lib/active_merchant/billing/gateways/mit.rb +260 -0
- data/lib/active_merchant/billing/gateways/moka.rb +24 -11
- data/lib/active_merchant/billing/gateways/moneris.rb +35 -8
- data/lib/active_merchant/billing/gateways/mundipagg.rb +8 -6
- data/lib/active_merchant/billing/gateways/nmi.rb +27 -8
- data/lib/active_merchant/billing/gateways/orbital.rb +357 -319
- data/lib/active_merchant/billing/gateways/pay_arc.rb +9 -7
- data/lib/active_merchant/billing/gateways/pay_conex.rb +3 -1
- data/lib/active_merchant/billing/gateways/pay_trace.rb +1 -1
- data/lib/active_merchant/billing/gateways/payflow.rb +76 -6
- data/lib/active_merchant/billing/gateways/paymentez.rb +35 -9
- data/lib/active_merchant/billing/gateways/paysafe.rb +155 -34
- data/lib/active_merchant/billing/gateways/payu_latam.rb +31 -16
- data/lib/active_merchant/billing/gateways/payway_dot_com.rb +3 -3
- data/lib/active_merchant/billing/gateways/pin.rb +31 -4
- data/lib/active_merchant/billing/gateways/priority.rb +369 -0
- data/lib/active_merchant/billing/gateways/rapyd.rb +258 -0
- data/lib/active_merchant/billing/gateways/realex.rb +18 -0
- data/lib/active_merchant/billing/gateways/safe_charge.rb +7 -6
- data/lib/active_merchant/billing/gateways/simetrik.rb +362 -0
- data/lib/active_merchant/billing/gateways/stripe.rb +30 -8
- data/lib/active_merchant/billing/gateways/stripe_payment_intents.rb +175 -72
- data/lib/active_merchant/billing/gateways/trans_first_transaction_express.rb +2 -1
- data/lib/active_merchant/billing/gateways/trust_commerce.rb +2 -1
- data/lib/active_merchant/billing/gateways/usa_epay_transaction.rb +20 -6
- data/lib/active_merchant/billing/gateways/visanet_peru.rb +6 -2
- data/lib/active_merchant/billing/gateways/wompi.rb +193 -0
- data/lib/active_merchant/billing/gateways/worldpay.rb +196 -64
- data/lib/active_merchant/billing/network_tokenization_credit_card.rb +1 -1
- data/lib/active_merchant/billing/response.rb +4 -0
- data/lib/active_merchant/version.rb +1 -1
- metadata +11 -2
@@ -0,0 +1,341 @@
|
|
1
|
+
module ActiveMerchant #:nodoc:
|
2
|
+
module Billing #:nodoc:
|
3
|
+
class AirwallexGateway < Gateway
|
4
|
+
self.test_url = 'https://api-demo.airwallex.com/api/v1'
|
5
|
+
self.live_url = 'https://pci-api.airwallex.com/api/v1'
|
6
|
+
|
7
|
+
# per https://www.airwallex.com/docs/online-payments__overview, cards are accepted in all EU countries
|
8
|
+
self.supported_countries = %w[AT AU BE BG CY CZ DE DK EE GR ES FI FR GB HK HR HU IE IT LT LU LV MT NL PL PT RO SE SG SI SK]
|
9
|
+
self.default_currency = 'AUD'
|
10
|
+
self.supported_cardtypes = %i[visa master]
|
11
|
+
|
12
|
+
self.homepage_url = 'https://airwallex.com/'
|
13
|
+
self.display_name = 'Airwallex'
|
14
|
+
|
15
|
+
ENDPOINTS = {
|
16
|
+
login: '/authentication/login',
|
17
|
+
setup: '/pa/payment_intents/create',
|
18
|
+
sale: '/pa/payment_intents/%{id}/confirm',
|
19
|
+
capture: '/pa/payment_intents/%{id}/capture',
|
20
|
+
refund: '/pa/refunds/create',
|
21
|
+
void: '/pa/payment_intents/%{id}/cancel'
|
22
|
+
}
|
23
|
+
|
24
|
+
def initialize(options = {})
|
25
|
+
requires!(options, :client_id, :client_api_key)
|
26
|
+
@client_id = options[:client_id]
|
27
|
+
@client_api_key = options[:client_api_key]
|
28
|
+
super
|
29
|
+
@access_token = setup_access_token
|
30
|
+
end
|
31
|
+
|
32
|
+
def purchase(money, card, options = {})
|
33
|
+
requires!(options, :return_url)
|
34
|
+
|
35
|
+
payment_intent_id = create_payment_intent(money, options)
|
36
|
+
post = {
|
37
|
+
'request_id' => request_id(options),
|
38
|
+
'merchant_order_id' => merchant_order_id(options),
|
39
|
+
'return_url' => options[:return_url]
|
40
|
+
}
|
41
|
+
add_card(post, card, options)
|
42
|
+
add_descriptor(post, options)
|
43
|
+
add_stored_credential(post, options)
|
44
|
+
post['payment_method_options'] = { 'card' => { 'auto_capture' => false } } if authorization_only?(options)
|
45
|
+
|
46
|
+
add_three_ds(post, options)
|
47
|
+
commit(:sale, post, payment_intent_id)
|
48
|
+
end
|
49
|
+
|
50
|
+
def authorize(money, payment, options = {})
|
51
|
+
# authorize is just a purchase w/o an auto capture
|
52
|
+
purchase(money, payment, options.merge({ auto_capture: false }))
|
53
|
+
end
|
54
|
+
|
55
|
+
def capture(money, authorization, options = {})
|
56
|
+
raise ArgumentError, 'An authorization value must be provided.' if authorization.blank?
|
57
|
+
|
58
|
+
post = {
|
59
|
+
'request_id' => request_id(options),
|
60
|
+
'merchant_order_id' => merchant_order_id(options),
|
61
|
+
'amount' => amount(money)
|
62
|
+
}
|
63
|
+
add_descriptor(post, options)
|
64
|
+
|
65
|
+
commit(:capture, post, authorization)
|
66
|
+
end
|
67
|
+
|
68
|
+
def refund(money, authorization, options = {})
|
69
|
+
raise ArgumentError, 'An authorization value must be provided.' if authorization.blank?
|
70
|
+
|
71
|
+
post = {}
|
72
|
+
post[:amount] = amount(money)
|
73
|
+
post[:payment_intent_id] = authorization
|
74
|
+
post[:request_id] = request_id(options)
|
75
|
+
post[:merchant_order_id] = merchant_order_id(options)
|
76
|
+
|
77
|
+
commit(:refund, post)
|
78
|
+
end
|
79
|
+
|
80
|
+
def void(authorization, options = {})
|
81
|
+
raise ArgumentError, 'An authorization value must be provided.' if authorization.blank?
|
82
|
+
|
83
|
+
post = {}
|
84
|
+
post[:request_id] = request_id(options)
|
85
|
+
post[:merchant_order_id] = merchant_order_id(options)
|
86
|
+
add_descriptor(post, options)
|
87
|
+
|
88
|
+
commit(:void, post, authorization)
|
89
|
+
end
|
90
|
+
|
91
|
+
def verify(credit_card, options = {})
|
92
|
+
MultiResponse.run(:use_first_response) do |r|
|
93
|
+
r.process { authorize(100, credit_card, options) }
|
94
|
+
r.process(:ignore_result) { void(r.authorization, options) }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def supports_scrubbing?
|
99
|
+
true
|
100
|
+
end
|
101
|
+
|
102
|
+
def scrub(transcript)
|
103
|
+
transcript.
|
104
|
+
gsub(/(\\\"number\\\":\\\")\d+/, '\1[REDACTED]').
|
105
|
+
gsub(/(\\\"cvc\\\":\\\")\d+/, '\1[REDACTED]')
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def request_id(options)
|
111
|
+
options[:request_id] || generate_timestamp
|
112
|
+
end
|
113
|
+
|
114
|
+
def merchant_order_id(options)
|
115
|
+
options[:merchant_order_id] || options[:order_id] || generate_timestamp
|
116
|
+
end
|
117
|
+
|
118
|
+
def generate_timestamp
|
119
|
+
(Time.now.to_f.round(2) * 100).to_i.to_s
|
120
|
+
end
|
121
|
+
|
122
|
+
def setup_access_token
|
123
|
+
token_headers = {
|
124
|
+
'Content-Type' => 'application/json',
|
125
|
+
'x-client-id' => @client_id,
|
126
|
+
'x-api-key' => @client_api_key
|
127
|
+
}
|
128
|
+
response = ssl_post(build_request_url(:login), nil, token_headers)
|
129
|
+
JSON.parse(response)['token']
|
130
|
+
end
|
131
|
+
|
132
|
+
def build_request_url(action, id = nil)
|
133
|
+
base_url = (test? ? test_url : live_url)
|
134
|
+
base_url + ENDPOINTS[action].to_s % { id: id }
|
135
|
+
end
|
136
|
+
|
137
|
+
def create_payment_intent(money, options = {})
|
138
|
+
post = {}
|
139
|
+
add_invoice(post, money, options)
|
140
|
+
add_order(post, options)
|
141
|
+
post[:request_id] = "#{request_id(options)}_setup"
|
142
|
+
post[:merchant_order_id] = "#{merchant_order_id(options)}_setup"
|
143
|
+
add_descriptor(post, options)
|
144
|
+
|
145
|
+
response = commit(:setup, post)
|
146
|
+
raise ArgumentError.new(response.message) unless response.success?
|
147
|
+
|
148
|
+
response.params['id']
|
149
|
+
end
|
150
|
+
|
151
|
+
def add_billing(post, card, options = {})
|
152
|
+
return unless has_name_info?(card)
|
153
|
+
|
154
|
+
billing = post['payment_method']['card']['billing'] || {}
|
155
|
+
billing['email'] = options[:email] if options[:email]
|
156
|
+
billing['phone'] = options[:phone] if options[:phone]
|
157
|
+
billing['first_name'] = card.first_name
|
158
|
+
billing['last_name'] = card.last_name
|
159
|
+
billing_address = options[:billing_address]
|
160
|
+
billing['address'] = build_address(billing_address) if has_required_address_info?(billing_address)
|
161
|
+
|
162
|
+
post['payment_method']['card']['billing'] = billing
|
163
|
+
end
|
164
|
+
|
165
|
+
def has_name_info?(card)
|
166
|
+
# These fields are required if billing data is sent.
|
167
|
+
card.first_name && card.last_name
|
168
|
+
end
|
169
|
+
|
170
|
+
def has_required_address_info?(address)
|
171
|
+
# These fields are required if address data is sent.
|
172
|
+
return unless address
|
173
|
+
|
174
|
+
address[:address1] && address[:country]
|
175
|
+
end
|
176
|
+
|
177
|
+
def build_address(address)
|
178
|
+
return unless address
|
179
|
+
|
180
|
+
address_data = {} # names r hard
|
181
|
+
address_data[:country_code] = address[:country]
|
182
|
+
address_data[:street] = address[:address1]
|
183
|
+
address_data[:city] = address[:city] if address[:city] # required per doc, not in practice
|
184
|
+
address_data[:postcode] = address[:zip] if address[:zip]
|
185
|
+
address_data[:state] = address[:state] if address[:state]
|
186
|
+
address_data
|
187
|
+
end
|
188
|
+
|
189
|
+
def add_invoice(post, money, options)
|
190
|
+
post[:amount] = amount(money)
|
191
|
+
post[:currency] = (options[:currency] || currency(money))
|
192
|
+
end
|
193
|
+
|
194
|
+
def add_card(post, card, options = {})
|
195
|
+
post['payment_method'] = {
|
196
|
+
'type' => 'card',
|
197
|
+
'card' => {
|
198
|
+
'expiry_month' => format(card.month, :two_digits),
|
199
|
+
'expiry_year' => card.year.to_s,
|
200
|
+
'number' => card.number.to_s,
|
201
|
+
'name' => card.name,
|
202
|
+
'cvc' => card.verification_value
|
203
|
+
}
|
204
|
+
}
|
205
|
+
add_billing(post, card, options)
|
206
|
+
end
|
207
|
+
|
208
|
+
def add_order(post, options)
|
209
|
+
return unless shipping_address = options[:shipping_address]
|
210
|
+
|
211
|
+
physical_address = build_shipping_address(shipping_address)
|
212
|
+
first_name, last_name = split_names(shipping_address[:name])
|
213
|
+
shipping = {}
|
214
|
+
shipping[:first_name] = first_name if first_name
|
215
|
+
shipping[:last_name] = last_name if last_name
|
216
|
+
shipping[:phone_number] = shipping_address[:phone_number] if shipping_address[:phone_number]
|
217
|
+
shipping[:address] = physical_address
|
218
|
+
post[:order] = { shipping: shipping }
|
219
|
+
end
|
220
|
+
|
221
|
+
def build_shipping_address(shipping_address)
|
222
|
+
address = {}
|
223
|
+
address[:city] = shipping_address[:city]
|
224
|
+
address[:country_code] = shipping_address[:country]
|
225
|
+
address[:postcode] = shipping_address[:zip]
|
226
|
+
address[:state] = shipping_address[:state]
|
227
|
+
address[:street] = shipping_address[:address1]
|
228
|
+
address
|
229
|
+
end
|
230
|
+
|
231
|
+
def add_stored_credential(post, options)
|
232
|
+
return unless stored_credential = options[:stored_credential]
|
233
|
+
|
234
|
+
external_recurring_data = post[:external_recurring_data] = {}
|
235
|
+
|
236
|
+
case stored_credential.dig(:reason_type)
|
237
|
+
when 'recurring', 'installment'
|
238
|
+
external_recurring_data[:merchant_trigger_reason] = 'scheduled'
|
239
|
+
when 'unscheduled'
|
240
|
+
external_recurring_data[:merchant_trigger_reason] = 'unscheduled'
|
241
|
+
end
|
242
|
+
|
243
|
+
external_recurring_data[:original_transaction_id] = stored_credential.dig(:network_transaction_id)
|
244
|
+
external_recurring_data[:triggered_by] = stored_credential.dig(:initiator) == 'cardholder' ? 'customer' : 'merchant'
|
245
|
+
end
|
246
|
+
|
247
|
+
def add_three_ds(post, options)
|
248
|
+
return unless three_d_secure = options[:three_d_secure]
|
249
|
+
|
250
|
+
pm_options = post.dig('payment_method_options', 'card')
|
251
|
+
|
252
|
+
external_three_ds = {
|
253
|
+
'version': format_three_ds_version(three_d_secure),
|
254
|
+
'eci': three_d_secure[:eci]
|
255
|
+
}.merge(three_ds_version_specific_fields(three_d_secure))
|
256
|
+
|
257
|
+
pm_options ? pm_options.merge!('external_three_ds': external_three_ds) : post['payment_method_options'] = { 'card': { 'external_three_ds': external_three_ds } }
|
258
|
+
end
|
259
|
+
|
260
|
+
def format_three_ds_version(three_d_secure)
|
261
|
+
version = three_d_secure[:version].split('.')
|
262
|
+
|
263
|
+
version.push('0') until version.length == 3
|
264
|
+
version.join('.')
|
265
|
+
end
|
266
|
+
|
267
|
+
def three_ds_version_specific_fields(three_d_secure)
|
268
|
+
if three_d_secure[:version].to_f >= 2
|
269
|
+
{
|
270
|
+
'authentication_value': three_d_secure[:cavv],
|
271
|
+
'ds_transaction_id': three_d_secure[:ds_transaction_id],
|
272
|
+
'three_ds_server_transaction_id': three_d_secure[:three_ds_server_trans_id]
|
273
|
+
}
|
274
|
+
else
|
275
|
+
{
|
276
|
+
'cavv': three_d_secure[:cavv],
|
277
|
+
'xid': three_d_secure[:xid]
|
278
|
+
}
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def authorization_only?(options = {})
|
283
|
+
options.include?(:auto_capture) && options[:auto_capture] == false
|
284
|
+
end
|
285
|
+
|
286
|
+
def add_descriptor(post, options)
|
287
|
+
post[:descriptor] = options[:description] if options[:description]
|
288
|
+
end
|
289
|
+
|
290
|
+
def parse(body)
|
291
|
+
JSON.parse(body)
|
292
|
+
end
|
293
|
+
|
294
|
+
def commit(action, post, id = nil)
|
295
|
+
url = build_request_url(action, id)
|
296
|
+
post_headers = { 'Authorization' => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
|
297
|
+
response = parse(ssl_post(url, post_data(post), post_headers))
|
298
|
+
|
299
|
+
Response.new(
|
300
|
+
success_from(response),
|
301
|
+
message_from(response),
|
302
|
+
response,
|
303
|
+
authorization: authorization_from(response),
|
304
|
+
avs_result: AVSResult.new(code: response.dig('latest_payment_attempt', 'authentication_data', 'avs_result')),
|
305
|
+
cvv_result: CVVResult.new(response.dig('latest_payment_attempt', 'authentication_data', 'cvc_code')),
|
306
|
+
test: test?,
|
307
|
+
error_code: error_code_from(response)
|
308
|
+
)
|
309
|
+
end
|
310
|
+
|
311
|
+
def handle_response(response)
|
312
|
+
case response.code.to_i
|
313
|
+
when 200...300, 400, 404
|
314
|
+
response.body
|
315
|
+
else
|
316
|
+
raise ResponseError.new(response)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def post_data(post)
|
321
|
+
post.to_json
|
322
|
+
end
|
323
|
+
|
324
|
+
def success_from(response)
|
325
|
+
%w(REQUIRES_PAYMENT_METHOD SUCCEEDED RECEIVED REQUIRES_CAPTURE CANCELLED).include?(response['status'])
|
326
|
+
end
|
327
|
+
|
328
|
+
def message_from(response)
|
329
|
+
response.dig('latest_payment_attempt', 'status') || response['status'] || response['message']
|
330
|
+
end
|
331
|
+
|
332
|
+
def authorization_from(response)
|
333
|
+
response.dig('latest_payment_attempt', 'payment_intent_id')
|
334
|
+
end
|
335
|
+
|
336
|
+
def error_code_from(response)
|
337
|
+
response['provider_original_response_code'] || response['code'] unless success_from(response)
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
@@ -6,9 +6,10 @@ module ActiveMerchant #:nodoc:
|
|
6
6
|
|
7
7
|
self.supported_countries = %w[AL AD AM AT AZ BY BE BA BG HR CY CZ DK EE FI FR DE GR HU IS IE IT KZ LV LI LT LU MK MT MD MC ME NL NO PL PT RO RU SM RS SK SI ES SE CH TR UA GB VA]
|
8
8
|
self.default_currency = 'EUR'
|
9
|
-
self.currencies_with_three_decimal_places = %w(BHD KWD OMR RSD TND)
|
9
|
+
self.currencies_with_three_decimal_places = %w(BHD KWD OMR RSD TND IQD JOD LYD)
|
10
10
|
self.money_format = :cents
|
11
11
|
self.supported_cardtypes = %i[visa master american_express discover diners_club jcb dankort maestro]
|
12
|
+
self.currencies_without_fractions = %w(CVE DJF GNF IDR JPY KMF KRW PYG RWF UGX VND VUV XAF XOF XPF)
|
12
13
|
|
13
14
|
self.homepage_url = 'https://www.barclaycardsmartpay.com/'
|
14
15
|
self.display_name = 'Barclaycard Smartpay'
|
@@ -78,7 +78,7 @@ module ActiveMerchant
|
|
78
78
|
def purchase(money, payment_method, options = {})
|
79
79
|
payment_method_details = PaymentMethodDetails.new(payment_method)
|
80
80
|
|
81
|
-
commit(:purchase, :post, payment_method_details) do |doc|
|
81
|
+
commit(:purchase, options, :post, payment_method_details) do |doc|
|
82
82
|
if payment_method_details.alt_transaction?
|
83
83
|
add_alt_transaction_purchase(doc, money, payment_method_details, options)
|
84
84
|
else
|
@@ -88,13 +88,13 @@ module ActiveMerchant
|
|
88
88
|
end
|
89
89
|
|
90
90
|
def authorize(money, payment_method, options = {})
|
91
|
-
commit(:authorize) do |doc|
|
91
|
+
commit(:authorize, options) do |doc|
|
92
92
|
add_auth_purchase(doc, money, payment_method, options)
|
93
93
|
end
|
94
94
|
end
|
95
95
|
|
96
96
|
def capture(money, authorization, options = {})
|
97
|
-
commit(:capture, :put) do |doc|
|
97
|
+
commit(:capture, options, :put) do |doc|
|
98
98
|
add_authorization(doc, authorization)
|
99
99
|
add_order(doc, options)
|
100
100
|
add_amount(doc, money, options) if options[:include_capture_amount] == true
|
@@ -102,15 +102,16 @@ module ActiveMerchant
|
|
102
102
|
end
|
103
103
|
|
104
104
|
def refund(money, authorization, options = {})
|
105
|
-
|
106
|
-
|
107
|
-
add_amount(doc, money, options)
|
108
|
-
|
105
|
+
options[:endpoint] = options[:merchant_transaction_id] ? "/refund/merchant/#{options[:merchant_transaction_id]}" : "/refund/#{authorization}"
|
106
|
+
commit(:refund, options, :post) do |doc|
|
107
|
+
add_amount(doc, money, options) if money
|
108
|
+
%i[reason cancel_subscription tax_amount].each { |field| send_when_present(doc, field, options) }
|
109
|
+
add_metadata(doc, options)
|
109
110
|
end
|
110
111
|
end
|
111
112
|
|
112
113
|
def void(authorization, options = {})
|
113
|
-
commit(:void, :put) do |doc|
|
114
|
+
commit(:void, options, :put) do |doc|
|
114
115
|
add_authorization(doc, authorization)
|
115
116
|
add_order(doc, options)
|
116
117
|
end
|
@@ -123,7 +124,7 @@ module ActiveMerchant
|
|
123
124
|
def store(payment_method, options = {})
|
124
125
|
payment_method_details = PaymentMethodDetails.new(payment_method)
|
125
126
|
|
126
|
-
commit(:store, :post, payment_method_details) do |doc|
|
127
|
+
commit(:store, options, :post, payment_method_details) do |doc|
|
127
128
|
add_personal_info(doc, payment_method, options)
|
128
129
|
add_echeck_company(doc, payment_method) if payment_method_details.check?
|
129
130
|
doc.send('payment-sources') do
|
@@ -149,7 +150,7 @@ module ActiveMerchant
|
|
149
150
|
|
150
151
|
def verify_credentials
|
151
152
|
begin
|
152
|
-
ssl_get(url.to_s, headers)
|
153
|
+
ssl_get(url.to_s, headers(options))
|
153
154
|
rescue ResponseError => e
|
154
155
|
return false if e.response.code.to_i == 401
|
155
156
|
end
|
@@ -234,6 +235,7 @@ module ActiveMerchant
|
|
234
235
|
doc.send('meta-key', truncate(entry[:meta_key], 40))
|
235
236
|
doc.send('meta-value', truncate(entry[:meta_value], 500))
|
236
237
|
doc.send('meta-description', truncate(entry[:meta_description], 40))
|
238
|
+
doc.send('is-visible', truncate(entry[:meta_is_visible], 5))
|
237
239
|
end
|
238
240
|
end
|
239
241
|
end
|
@@ -386,13 +388,12 @@ module ActiveMerchant
|
|
386
388
|
|
387
389
|
def parse(response)
|
388
390
|
return bad_authentication_response if response.code.to_i == 401
|
389
|
-
return generic_error_response(response.body) if [403, 429].include?(response.code.to_i)
|
391
|
+
return generic_error_response(response.body) if [403, 405, 429].include?(response.code.to_i)
|
390
392
|
|
391
393
|
parsed = {}
|
392
394
|
doc = Nokogiri::XML(response.body)
|
393
395
|
doc.root.xpath('*').each do |node|
|
394
396
|
name = node.name.downcase
|
395
|
-
|
396
397
|
if node.elements.empty?
|
397
398
|
parsed[name] = node.text
|
398
399
|
elsif name == 'transaction-meta-data'
|
@@ -433,15 +434,15 @@ module ActiveMerchant
|
|
433
434
|
end
|
434
435
|
end
|
435
436
|
|
436
|
-
def api_request(action, request, verb, payment_method_details)
|
437
|
-
ssl_request(verb, url(action, payment_method_details), request, headers)
|
437
|
+
def api_request(action, request, verb, payment_method_details, options)
|
438
|
+
ssl_request(verb, url(action, options, payment_method_details), request, headers(options))
|
438
439
|
rescue ResponseError => e
|
439
440
|
e.response
|
440
441
|
end
|
441
442
|
|
442
|
-
def commit(action, verb = :post, payment_method_details = PaymentMethodDetails.new())
|
443
|
+
def commit(action, options, verb = :post, payment_method_details = PaymentMethodDetails.new())
|
443
444
|
request = build_xml_request(action, payment_method_details) { |doc| yield(doc) }
|
444
|
-
response = api_request(action, request, verb, payment_method_details)
|
445
|
+
response = api_request(action, request, verb, payment_method_details, options)
|
445
446
|
parsed = parse(response)
|
446
447
|
|
447
448
|
succeeded = success_from(action, response)
|
@@ -457,9 +458,10 @@ module ActiveMerchant
|
|
457
458
|
)
|
458
459
|
end
|
459
460
|
|
460
|
-
def url(action = nil, payment_method_details = PaymentMethodDetails.new())
|
461
|
+
def url(action = nil, options = {}, payment_method_details = PaymentMethodDetails.new())
|
461
462
|
base = test? ? test_url : live_url
|
462
463
|
resource = action == :store ? 'vaulted-shoppers' : payment_method_details.resource_url
|
464
|
+
resource += options[:endpoint] if action == :refund
|
463
465
|
"#{base}/#{resource}"
|
464
466
|
end
|
465
467
|
|
@@ -532,20 +534,28 @@ module ActiveMerchant
|
|
532
534
|
end
|
533
535
|
|
534
536
|
def root_element(action, payment_method_details)
|
535
|
-
|
537
|
+
return 'refund' if action == :refund
|
538
|
+
return 'vaulted-shopper' if action == :store
|
539
|
+
|
540
|
+
payment_method_details.root_element
|
536
541
|
end
|
537
542
|
|
538
|
-
def headers
|
539
|
-
|
543
|
+
def headers(options)
|
544
|
+
idempotency_key = options[:idempotency_key] if options[:idempotency_key]
|
545
|
+
|
546
|
+
headers = {
|
540
547
|
'Content-Type' => 'application/xml',
|
541
548
|
'Authorization' => ('Basic ' + Base64.strict_encode64("#{@options[:api_username]}:#{@options[:api_password]}").strip)
|
542
549
|
}
|
550
|
+
|
551
|
+
headers['Idempotency-Key'] = idempotency_key if idempotency_key
|
552
|
+
headers
|
543
553
|
end
|
544
554
|
|
545
555
|
def build_xml_request(action, payment_method_details)
|
546
556
|
builder = Nokogiri::XML::Builder.new
|
547
557
|
builder.__send__(root_element(action, payment_method_details), root_attributes) do |doc|
|
548
|
-
doc.send('card-transaction-type', TRANSACTIONS[action]) if TRANSACTIONS[action] && !payment_method_details.alt_transaction?
|
558
|
+
doc.send('card-transaction-type', TRANSACTIONS[action]) if TRANSACTIONS[action] && !payment_method_details.alt_transaction? && action != :refund
|
549
559
|
yield(doc)
|
550
560
|
end
|
551
561
|
builder.doc.root.to_xml
|
@@ -18,6 +18,11 @@ module BraintreeCommon
|
|
18
18
|
transcript.
|
19
19
|
gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
|
20
20
|
gsub(%r((&?ccnumber=)\d*(&?)), '\1[FILTERED]\2').
|
21
|
-
gsub(%r((&?cvv=)\d*(&?)), '\1[FILTERED]\2')
|
21
|
+
gsub(%r((&?cvv=)\d*(&?)), '\1[FILTERED]\2').
|
22
|
+
gsub(%r((<account-number>)\d+(</account-number>)), '\1[FILTERED]\2').
|
23
|
+
gsub(%r((<payment-method-nonce>)[^<]+(</payment-method-nonce>)), '\1[FILTERED]\2').
|
24
|
+
gsub(%r((<payment-method-token>)[^<]+(</payment-method-token>)), '\1[FILTERED]\2').
|
25
|
+
gsub(%r((<value>)[^<]{100,}(</value>)), '\1[FILTERED]\2').
|
26
|
+
gsub(%r((<token>)[^<]+(</token>)), '\1[FILTERED]\2')
|
22
27
|
end
|
23
28
|
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module ActiveMerchant #:nodoc:
|
2
|
+
module Billing #:nodoc:
|
3
|
+
class TokenNonce #:nodoc:
|
4
|
+
include PostsData
|
5
|
+
# This class emulates the behavior of the front-end js library to
|
6
|
+
# create token nonce for a bank account base on the docs:
|
7
|
+
# https://developer.paypal.com/braintree/docs/guides/ach/client-side
|
8
|
+
|
9
|
+
attr_reader :braintree_gateway, :options
|
10
|
+
|
11
|
+
def initialize(gateway, options = {})
|
12
|
+
@braintree_gateway = gateway
|
13
|
+
@options = options
|
14
|
+
end
|
15
|
+
|
16
|
+
def url
|
17
|
+
sandbox = @braintree_gateway.config.environment == :sandbox
|
18
|
+
"https://payments#{'.sandbox' if sandbox}.braintree-api.com/graphql"
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_token_nonce_for_payment_method(payment_method)
|
22
|
+
headers = {
|
23
|
+
'Accept' => 'application/json',
|
24
|
+
'Authorization' => "Bearer #{client_token}",
|
25
|
+
'Content-Type' => 'application/json',
|
26
|
+
'Braintree-Version' => '2018-05-10'
|
27
|
+
}
|
28
|
+
resp = ssl_post(url, build_nonce_request(payment_method), headers)
|
29
|
+
json_response = JSON.parse(resp)
|
30
|
+
|
31
|
+
message = json_response['errors'].map { |err| err['message'] }.join("\n") if json_response['errors'].present?
|
32
|
+
token = json_response.dig('data', 'tokenizeUsBankAccount', 'paymentMethod', 'id')
|
33
|
+
|
34
|
+
return token, message
|
35
|
+
end
|
36
|
+
|
37
|
+
def client_token
|
38
|
+
base64_token = @braintree_gateway.client_token.generate
|
39
|
+
JSON.parse(Base64.decode64(base64_token))['authorizationFingerprint']
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def graphql_query
|
45
|
+
<<-GRAPHQL
|
46
|
+
mutation TokenizeUsBankAccount($input: TokenizeUsBankAccountInput!) {
|
47
|
+
tokenizeUsBankAccount(input: $input) {
|
48
|
+
paymentMethod {
|
49
|
+
id
|
50
|
+
details {
|
51
|
+
... on UsBankAccountDetails {
|
52
|
+
last4
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}
|
56
|
+
}
|
57
|
+
}
|
58
|
+
GRAPHQL
|
59
|
+
end
|
60
|
+
|
61
|
+
def billing_address_from_options
|
62
|
+
return nil if options[:billing_address].blank?
|
63
|
+
|
64
|
+
address = options[:billing_address]
|
65
|
+
|
66
|
+
{
|
67
|
+
streetAddress: address[:address1],
|
68
|
+
extendedAddress: address[:address2],
|
69
|
+
city: address[:city],
|
70
|
+
state: address[:state],
|
71
|
+
zipCode: address[:zip]
|
72
|
+
}.compact
|
73
|
+
end
|
74
|
+
|
75
|
+
def build_nonce_request(payment_method)
|
76
|
+
input = {
|
77
|
+
usBankAccount: {
|
78
|
+
achMandate: options[:ach_mandate],
|
79
|
+
routingNumber: payment_method.routing_number,
|
80
|
+
accountNumber: payment_method.account_number,
|
81
|
+
accountType: payment_method.account_type.upcase,
|
82
|
+
billingAddress: billing_address_from_options
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
if payment_method.account_holder_type == 'personal'
|
87
|
+
input[:usBankAccount][:individualOwner] = {
|
88
|
+
firstName: payment_method.first_name,
|
89
|
+
lastName: payment_method.last_name
|
90
|
+
}
|
91
|
+
else
|
92
|
+
input[:usBankAccount][:businessOwner] = {
|
93
|
+
businessName: payment_method.name
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
{
|
98
|
+
clientSdkMetadata: {
|
99
|
+
platform: 'web',
|
100
|
+
source: 'client',
|
101
|
+
integration: 'custom',
|
102
|
+
sessionId: SecureRandom.uuid,
|
103
|
+
version: '3.83.0'
|
104
|
+
},
|
105
|
+
query: graphql_query,
|
106
|
+
variables: {
|
107
|
+
input: input
|
108
|
+
}
|
109
|
+
}.to_json
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|