activemerchant 1.119.0 → 1.124.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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +216 -1
  3. data/README.md +4 -2
  4. data/lib/active_merchant/billing/check.rb +19 -12
  5. data/lib/active_merchant/billing/credit_card.rb +3 -0
  6. data/lib/active_merchant/billing/credit_card_formatting.rb +1 -0
  7. data/lib/active_merchant/billing/credit_card_methods.rb +32 -14
  8. data/lib/active_merchant/billing/gateways/adyen.rb +94 -25
  9. data/lib/active_merchant/billing/gateways/authorize_net.rb +19 -11
  10. data/lib/active_merchant/billing/gateways/authorize_net_cim.rb +3 -0
  11. data/lib/active_merchant/billing/gateways/blue_pay.rb +29 -0
  12. data/lib/active_merchant/billing/gateways/blue_snap.rb +2 -2
  13. data/lib/active_merchant/billing/gateways/braintree_blue.rb +52 -8
  14. data/lib/active_merchant/billing/gateways/card_stream.rb +17 -13
  15. data/lib/active_merchant/billing/gateways/cashnet.rb +7 -2
  16. data/lib/active_merchant/billing/gateways/checkout_v2.rb +31 -0
  17. data/lib/active_merchant/billing/gateways/credorax.rb +15 -9
  18. data/lib/active_merchant/billing/gateways/cyber_source.rb +53 -6
  19. data/lib/active_merchant/billing/gateways/d_local.rb +9 -2
  20. data/lib/active_merchant/billing/gateways/decidir.rb +7 -1
  21. data/lib/active_merchant/billing/gateways/elavon.rb +70 -28
  22. data/lib/active_merchant/billing/gateways/element.rb +2 -0
  23. data/lib/active_merchant/billing/gateways/forte.rb +12 -0
  24. data/lib/active_merchant/billing/gateways/global_collect.rb +24 -10
  25. data/lib/active_merchant/billing/gateways/hps.rb +55 -1
  26. data/lib/active_merchant/billing/gateways/kushki.rb +23 -0
  27. data/lib/active_merchant/billing/gateways/litle.rb +1 -1
  28. data/lib/active_merchant/billing/gateways/mercado_pago.rb +5 -4
  29. data/lib/active_merchant/billing/gateways/merchant_warrior.rb +2 -0
  30. data/lib/active_merchant/billing/gateways/mit.rb +260 -0
  31. data/lib/active_merchant/billing/gateways/moka.rb +290 -0
  32. data/lib/active_merchant/billing/gateways/monei.rb +228 -144
  33. data/lib/active_merchant/billing/gateways/mundipagg.rb +14 -5
  34. data/lib/active_merchant/billing/gateways/netbanx.rb +26 -2
  35. data/lib/active_merchant/billing/gateways/nmi.rb +27 -9
  36. data/lib/active_merchant/billing/gateways/orbital.rb +99 -59
  37. data/lib/active_merchant/billing/gateways/pay_arc.rb +392 -0
  38. data/lib/active_merchant/billing/gateways/pay_conex.rb +3 -1
  39. data/lib/active_merchant/billing/gateways/pay_trace.rb +404 -0
  40. data/lib/active_merchant/billing/gateways/payeezy.rb +34 -6
  41. data/lib/active_merchant/billing/gateways/payflow/payflow_common_api.rb +1 -0
  42. data/lib/active_merchant/billing/gateways/payflow.rb +21 -4
  43. data/lib/active_merchant/billing/gateways/payment_express.rb +5 -5
  44. data/lib/active_merchant/billing/gateways/paymentez.rb +5 -0
  45. data/lib/active_merchant/billing/gateways/paypal_express.rb +1 -0
  46. data/lib/active_merchant/billing/gateways/paysafe.rb +376 -0
  47. data/lib/active_merchant/billing/gateways/payu_latam.rb +3 -3
  48. data/lib/active_merchant/billing/gateways/payway_dot_com.rb +253 -0
  49. data/lib/active_merchant/billing/gateways/qvalent.rb +23 -9
  50. data/lib/active_merchant/billing/gateways/realex.rb +18 -0
  51. data/lib/active_merchant/billing/gateways/redsys.rb +42 -24
  52. data/lib/active_merchant/billing/gateways/safe_charge.rb +25 -13
  53. data/lib/active_merchant/billing/gateways/spreedly_core.rb +13 -4
  54. data/lib/active_merchant/billing/gateways/stripe.rb +18 -8
  55. data/lib/active_merchant/billing/gateways/stripe_payment_intents.rb +126 -48
  56. data/lib/active_merchant/billing/gateways/trans_first_transaction_express.rb +2 -1
  57. data/lib/active_merchant/billing/gateways/trust_commerce.rb +2 -1
  58. data/lib/active_merchant/billing/gateways/usa_epay_transaction.rb +1 -1
  59. data/lib/active_merchant/billing/gateways/vpos.rb +220 -0
  60. data/lib/active_merchant/billing/gateways/worldpay.rb +78 -18
  61. data/lib/active_merchant/billing/response.rb +4 -0
  62. data/lib/active_merchant/billing/three_d_secure_eci_mapper.rb +27 -0
  63. data/lib/active_merchant/billing.rb +1 -0
  64. data/lib/active_merchant/version.rb +1 -1
  65. data/lib/certs/cacert.pem +1582 -2431
  66. metadata +11 -3
@@ -20,16 +20,20 @@ module ActiveMerchant #:nodoc:
20
20
  add_capture_method(post, options)
21
21
  add_confirmation_method(post, options)
22
22
  add_customer(post, options)
23
- payment_method = add_payment_method_token(post, payment_method, options)
24
- return payment_method if payment_method.is_a?(ActiveMerchant::Billing::Response)
23
+ result = add_payment_method_token(post, payment_method, options)
24
+ return result if result.is_a?(ActiveMerchant::Billing::Response)
25
25
 
26
26
  add_external_three_d_secure_auth_data(post, options)
27
27
  add_metadata(post, options)
28
28
  add_return_url(post, options)
29
29
  add_connected_account(post, options)
30
+ add_radar_data(post, options)
30
31
  add_shipping_address(post, options)
31
32
  setup_future_usage(post, options)
32
33
  add_exemption(post, options)
34
+ add_stored_credentials(post, options)
35
+ add_ntid(post, options)
36
+ add_claim_without_transaction_id(post, options)
33
37
  add_error_on_requires_action(post, options)
34
38
  request_three_d_secure(post, options)
35
39
 
@@ -46,8 +50,8 @@ module ActiveMerchant #:nodoc:
46
50
 
47
51
  def confirm_intent(intent_id, payment_method, options = {})
48
52
  post = {}
49
- payment_method = add_payment_method_token(post, payment_method, options)
50
- return payment_method if payment_method.is_a?(ActiveMerchant::Billing::Response)
53
+ result = add_payment_method_token(post, payment_method, options)
54
+ return result if result.is_a?(ActiveMerchant::Billing::Response)
51
55
 
52
56
  CONFIRM_INTENT_ATTRIBUTES.each do |attribute|
53
57
  add_whitelisted_attribute(post, options, attribute)
@@ -56,24 +60,31 @@ module ActiveMerchant #:nodoc:
56
60
  end
57
61
 
58
62
  def create_payment_method(payment_method, options = {})
59
- post = {}
60
- post[:type] = 'card'
61
- post[:card] = {}
62
- post[:card][:number] = payment_method.number
63
- post[:card][:exp_month] = payment_method.month
64
- post[:card][:exp_year] = payment_method.year
65
- post[:card][:cvc] = payment_method.verification_value if payment_method.verification_value
66
- add_billing_address(post, options)
63
+ post_data = add_payment_method_data(payment_method, options)
64
+
67
65
  options = format_idempotency_key(options, 'pm')
68
- commit(:post, 'payment_methods', post, options)
66
+ commit(:post, 'payment_methods', post_data, options)
67
+ end
68
+
69
+ def add_payment_method_data(payment_method, options = {})
70
+ post_data = {}
71
+ post_data[:type] = 'card'
72
+ post_data[:card] = {}
73
+ post_data[:card][:number] = payment_method.number
74
+ post_data[:card][:exp_month] = payment_method.month
75
+ post_data[:card][:exp_year] = payment_method.year
76
+ post_data[:card][:cvc] = payment_method.verification_value if payment_method.verification_value
77
+ add_billing_address(post_data, options)
78
+ add_name_only(post_data, payment_method) if post_data[:billing_details].nil?
79
+ post_data
69
80
  end
70
81
 
71
82
  def update_intent(money, intent_id, payment_method, options = {})
72
83
  post = {}
73
84
  add_amount(post, money, options)
74
85
 
75
- payment_method = add_payment_method_token(post, payment_method, options)
76
- return payment_method if payment_method.is_a?(ActiveMerchant::Billing::Response)
86
+ result = add_payment_method_token(post, payment_method, options)
87
+ return result if result.is_a?(ActiveMerchant::Billing::Response)
77
88
 
78
89
  add_payment_method_types(post, options)
79
90
  add_customer(post, options)
@@ -90,8 +101,8 @@ module ActiveMerchant #:nodoc:
90
101
  def create_setup_intent(payment_method, options = {})
91
102
  post = {}
92
103
  add_customer(post, options)
93
- payment_method = add_payment_method_token(post, payment_method, options)
94
- return payment_method if payment_method.is_a?(ActiveMerchant::Billing::Response)
104
+ result = add_payment_method_token(post, payment_method, options)
105
+ return result if result.is_a?(ActiveMerchant::Billing::Response)
95
106
 
96
107
  add_metadata(post, options)
97
108
  add_return_url(post, options)
@@ -102,6 +113,17 @@ module ActiveMerchant #:nodoc:
102
113
  commit(:post, 'setup_intents', post, options)
103
114
  end
104
115
 
116
+ def retrieve_setup_intent(setup_intent_id)
117
+ # Retrieving a setup_intent passing 'expand[]=latest_attempt' allows the caller to
118
+ # check for a network_transaction_id and ds_transaction_id
119
+ # eg (latest_attempt -> payment_method_details -> card -> network_transaction_id)
120
+ #
121
+ # Being able to retrieve these fields enables payment flows that rely on MIT exemptions, e.g: off_session
122
+ commit(:post, "setup_intents/#{setup_intent_id}", {
123
+ 'expand[]': 'latest_attempt'
124
+ }, {})
125
+ end
126
+
105
127
  def authorize(money, payment_method, options = {})
106
128
  create_intent(money, payment_method, options.merge!(confirm: true, capture_method: 'manual'))
107
129
  end
@@ -159,13 +181,12 @@ module ActiveMerchant #:nodoc:
159
181
  # If customer option is provided, create a payment method and attach to customer id
160
182
  # Otherwise, create a customer, then attach
161
183
  if payment_method.is_a?(StripePaymentToken) || payment_method.is_a?(ActiveMerchant::Billing::CreditCard)
162
- payment_method = add_payment_method_token(params, payment_method, options)
163
- return payment_method if payment_method.is_a?(ActiveMerchant::Billing::Response)
184
+ result = add_payment_method_token(params, payment_method, options)
185
+ return result if result.is_a?(ActiveMerchant::Billing::Response)
164
186
 
165
187
  if options[:customer]
166
188
  customer_id = options[:customer]
167
189
  else
168
- post[:validate] = options[:validate] unless options[:validate].nil?
169
190
  post[:description] = options[:description] if options[:description]
170
191
  post[:email] = options[:email] if options[:email]
171
192
  options = format_idempotency_key(options, 'customer')
@@ -173,7 +194,9 @@ module ActiveMerchant #:nodoc:
173
194
  customer_id = customer.params['id']
174
195
  end
175
196
  options = format_idempotency_key(options, 'attach')
176
- commit(:post, "payment_methods/#{params[:payment_method]}/attach", { customer: customer_id }, options)
197
+ attach_parameters = { customer: customer_id }
198
+ attach_parameters[:validate] = options[:validate] unless options[:validate].nil?
199
+ commit(:post, "payment_methods/#{params[:payment_method]}/attach", attach_parameters, options)
177
200
  else
178
201
  super(payment_method, options)
179
202
  end
@@ -194,6 +217,10 @@ module ActiveMerchant #:nodoc:
194
217
 
195
218
  private
196
219
 
220
+ def off_session_request?(options = {})
221
+ (options[:off_session] || options[:setup_future_usage]) && options[:confirm] == true
222
+ end
223
+
197
224
  def add_connected_account(post, options = {})
198
225
  super(post, options)
199
226
  post[:application_fee_amount] = options[:application_fee] if options[:application_fee]
@@ -201,25 +228,21 @@ module ActiveMerchant #:nodoc:
201
228
 
202
229
  def add_whitelisted_attribute(post, options, attribute)
203
230
  post[attribute] = options[attribute] if options[attribute]
204
- post
205
231
  end
206
232
 
207
233
  def add_capture_method(post, options)
208
234
  capture_method = options[:capture_method].to_s
209
235
  post[:capture_method] = capture_method if ALLOWED_METHOD_STATES.include?(capture_method)
210
- post
211
236
  end
212
237
 
213
238
  def add_confirmation_method(post, options)
214
239
  confirmation_method = options[:confirmation_method].to_s
215
240
  post[:confirmation_method] = confirmation_method if ALLOWED_METHOD_STATES.include?(confirmation_method)
216
- post
217
241
  end
218
242
 
219
243
  def add_customer(post, options)
220
244
  customer = options[:customer].to_s
221
245
  post[:customer] = customer if customer.start_with?('cus_')
222
- post
223
246
  end
224
247
 
225
248
  def add_return_url(post, options)
@@ -227,31 +250,39 @@ module ActiveMerchant #:nodoc:
227
250
 
228
251
  post[:confirm] = options[:confirm]
229
252
  post[:return_url] = options[:return_url] if options[:return_url]
230
- post
231
253
  end
232
254
 
233
255
  def add_payment_method_token(post, payment_method, options)
234
- return if payment_method.nil?
235
-
236
- if payment_method.is_a?(ActiveMerchant::Billing::CreditCard)
237
- p = create_payment_method(payment_method, options)
238
- return p unless p.success?
239
-
240
- payment_method = p.params['id']
256
+ case payment_method
257
+ when StripePaymentToken
258
+ post[:payment_method] = payment_method.payment_data['id']
259
+ when String
260
+ extract_token_from_string_and_maybe_add_customer_id(post, payment_method)
261
+ when ActiveMerchant::Billing::CreditCard
262
+ get_payment_method_data_from_card(post, payment_method, options)
241
263
  end
264
+ end
242
265
 
243
- if payment_method.is_a?(StripePaymentToken)
244
- post[:payment_method] = payment_method.payment_data['id']
245
- elsif payment_method.is_a?(String)
246
- if payment_method.include?('|')
247
- customer_id, payment_method_id = payment_method.split('|')
248
- token = payment_method_id
249
- post[:customer] = customer_id
250
- else
251
- token = payment_method
252
- end
253
- post[:payment_method] = token
266
+ def extract_token_from_string_and_maybe_add_customer_id(post, payment_method)
267
+ if payment_method.include?('|')
268
+ customer_id, payment_method = payment_method.split('|')
269
+ post[:customer] = customer_id
254
270
  end
271
+
272
+ post[:payment_method] = payment_method
273
+ end
274
+
275
+ def get_payment_method_data_from_card(post, payment_method, options)
276
+ return create_payment_method_and_extract_token(post, payment_method, options) unless off_session_request?(options)
277
+
278
+ post[:payment_method_data] = add_payment_method_data(payment_method, options)
279
+ end
280
+
281
+ def create_payment_method_and_extract_token(post, payment_method, options)
282
+ payment_method_response = create_payment_method(payment_method, options)
283
+ return payment_method_response if payment_method_response.failure?
284
+
285
+ add_payment_method_token(post, payment_method_response.params['id'], options)
255
286
  end
256
287
 
257
288
  def add_payment_method_types(post, options)
@@ -259,7 +290,6 @@ module ActiveMerchant #:nodoc:
259
290
  return if payment_method_types.nil?
260
291
 
261
292
  post[:payment_method_types] = Array(payment_method_types)
262
- post
263
293
  end
264
294
 
265
295
  def add_exemption(post, options = {})
@@ -270,6 +300,49 @@ module ActiveMerchant #:nodoc:
270
300
  post[:payment_method_options][:card][:moto] = true if options[:moto]
271
301
  end
272
302
 
303
+ # Stripe Payment Intents does not pass any parameters for cardholder/merchant initiated
304
+ # it also does not support installments for any country other than Mexico (reason for this is unknown)
305
+ # The only thing that Stripe PI requires for stored credentials to work currently is the network_transaction_id
306
+ # network_transaction_id is created when the card is authenticated using the field `setup_for_future_usage` with the value `off_session` see def setup_future_usage below
307
+
308
+ def add_stored_credentials(post, options = {})
309
+ return unless options[:stored_credential] && !options[:stored_credential].values.all?(&:nil?)
310
+
311
+ stored_credential = options[:stored_credential]
312
+ post[:payment_method_options] ||= {}
313
+ post[:payment_method_options][:card] ||= {}
314
+ post[:payment_method_options][:card][:mit_exemption] = {}
315
+
316
+ # Stripe PI accepts network_transaction_id and ds_transaction_id via mit field under card.
317
+ # The network_transaction_id can be sent in nested under stored credentials OR as its own field (add_ntid handles when it is sent in on its own)
318
+ # If it is sent is as its own field AND under stored credentials, the value sent under its own field is what will send.
319
+ post[:payment_method_options][:card][:mit_exemption][:ds_transaction_id] = stored_credential[:ds_transaction_id] if stored_credential[:ds_transaction_id]
320
+ post[:payment_method_options][:card][:mit_exemption][:network_transaction_id] = stored_credential[:network_transaction_id] if stored_credential[:network_transaction_id]
321
+ end
322
+
323
+ def add_ntid(post, options = {})
324
+ return unless options[:network_transaction_id]
325
+
326
+ post[:payment_method_options] ||= {}
327
+ post[:payment_method_options][:card] ||= {}
328
+ post[:payment_method_options][:card][:mit_exemption] = {}
329
+
330
+ post[:payment_method_options][:card][:mit_exemption][:network_transaction_id] = options[:network_transaction_id] if options[:network_transaction_id]
331
+ end
332
+
333
+ def add_claim_without_transaction_id(post, options = {})
334
+ return if options[:stored_credential] || options[:network_transaction_id] || options[:ds_transaction_id]
335
+ return unless options[:claim_without_transaction_id]
336
+
337
+ post[:payment_method_options] ||= {}
338
+ post[:payment_method_options][:card] ||= {}
339
+ post[:payment_method_options][:card][:mit_exemption] = {}
340
+
341
+ # Stripe PI accepts claim_without_transaction_id for transactions without transaction ids.
342
+ # Gateway validation for this field occurs through a different service, before the transaction request is sent to the gateway.
343
+ post[:payment_method_options][:card][:mit_exemption][:claim_without_transaction_id] = options[:claim_without_transaction_id]
344
+ end
345
+
273
346
  def add_error_on_requires_action(post, options = {})
274
347
  return unless options[:confirm]
275
348
 
@@ -299,7 +372,7 @@ module ActiveMerchant #:nodoc:
299
372
 
300
373
  def setup_future_usage(post, options = {})
301
374
  post[:setup_future_usage] = options[:setup_future_usage] if %w(on_session off_session).include?(options[:setup_future_usage])
302
- post[:off_session] = options[:off_session] if options[:off_session] && options[:confirm] == true
375
+ post[:off_session] = options[:off_session] if off_session_request?(options)
303
376
  post
304
377
  end
305
378
 
@@ -317,7 +390,13 @@ module ActiveMerchant #:nodoc:
317
390
  post[:billing_details][:email] = billing[:email] if billing[:email]
318
391
  post[:billing_details][:name] = billing[:name] if billing[:name]
319
392
  post[:billing_details][:phone] = billing[:phone] if billing[:phone]
320
- post
393
+ end
394
+
395
+ def add_name_only(post, payment_method)
396
+ post[:billing_details] = {} unless post[:billing_details]
397
+
398
+ name = [payment_method.first_name, payment_method.last_name].compact.join(' ')
399
+ post[:billing_details][:name] = name
321
400
  end
322
401
 
323
402
  def add_shipping_address(post, options = {})
@@ -336,7 +415,6 @@ module ActiveMerchant #:nodoc:
336
415
  post[:shipping][:carrier] = shipping[:carrier] if shipping[:carrier]
337
416
  post[:shipping][:phone] = shipping[:phone] if shipping[:phone]
338
417
  post[:shipping][:tracking_number] = shipping[:tracking_number] if shipping[:tracking_number]
339
- post
340
418
  end
341
419
 
342
420
  def format_idempotency_key(options, suffix)
@@ -317,7 +317,8 @@ module ActiveMerchant #:nodoc:
317
317
  gsub(%r((<[^>]+pan>)[^<]+(<))i, '\1[FILTERED]\2').
318
318
  gsub(%r((<[^>]+sec>)[^<]+(<))i, '\1[FILTERED]\2').
319
319
  gsub(%r((<[^>]+id>)[^<]+(<))i, '\1[FILTERED]\2').
320
- gsub(%r((<[^>]+regKey>)[^<]+(<))i, '\1[FILTERED]\2')
320
+ gsub(%r((<[^>]+regKey>)[^<]+(<))i, '\1[FILTERED]\2').
321
+ gsub(%r((<[^>]+acctNr>)[^<]+(<))i, '\1[FILTERED]\2')
321
322
  end
322
323
 
323
324
  private
@@ -331,7 +331,8 @@ module ActiveMerchant #:nodoc:
331
331
  gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
332
332
  gsub(%r((&?cc=)\d*(&?)), '\1[FILTERED]\2').
333
333
  gsub(%r((&?password=)[^&]+(&?)), '\1[FILTERED]\2').
334
- gsub(%r((&?cvv=)\d*(&?)), '\1[FILTERED]\2')
334
+ gsub(%r((&?cvv=)\d*(&?)), '\1[FILTERED]\2').
335
+ gsub(%r((&?account=)\d*(&?)), '\1[FILTERED]\2')
335
336
  end
336
337
 
337
338
  private
@@ -343,7 +343,7 @@ module ActiveMerchant #:nodoc:
343
343
  parameters[:software] = 'Active Merchant'
344
344
  parameters[:testmode] = (@options[:test] ? 1 : 0) unless parameters.has_key?(:testmode)
345
345
  seed = SecureRandom.hex(32).upcase
346
- hash = Digest::SHA1.hexdigest("#{parameters[:command]}:#{@options[:password]}:#{parameters[:amount]}:#{parameters[:invoice]}:#{seed}")
346
+ hash = Digest::SHA1.hexdigest("#{parameters[:command]}:#{@options[:pin] || @options[:password]}:#{parameters[:amount]}:#{parameters[:invoice]}:#{seed}")
347
347
  parameters[:hash] = "s/#{seed}/#{hash}/n"
348
348
 
349
349
  parameters.collect { |key, value| "UM#{key}=#{CGI.escape(value.to_s)}" }.join('&')
@@ -0,0 +1,220 @@
1
+ require 'digest'
2
+ require 'jwe'
3
+
4
+ module ActiveMerchant #:nodoc:
5
+ module Billing #:nodoc:
6
+ class VposGateway < Gateway
7
+ self.test_url = 'https://vpos.infonet.com.py:8888'
8
+ self.live_url = 'https://vpos.infonet.com.py'
9
+
10
+ self.supported_countries = ['PY']
11
+ self.default_currency = 'PYG'
12
+ self.supported_cardtypes = %i[visa master]
13
+
14
+ self.homepage_url = 'https://comercios.bancard.com.py'
15
+ self.display_name = 'vPOS'
16
+
17
+ self.money_format = :dollars
18
+
19
+ ENDPOINTS = {
20
+ pci_encryption_key: '/vpos/api/0.3/application/encryption-key',
21
+ pay_pci_buy_encrypted: '/vpos/api/0.3/pci/encrypted',
22
+ pci_buy_rollback: '/vpos/api/0.3/pci_buy/rollback',
23
+ refund: '/vpos/api/0.3/refunds'
24
+ }
25
+
26
+ def initialize(options = {})
27
+ requires!(options, :private_key, :public_key)
28
+ @private_key = options[:private_key]
29
+ @public_key = options[:public_key]
30
+ @shop_process_id = options[:shop_process_id] || SecureRandom.random_number(10**15)
31
+ super
32
+ end
33
+
34
+ def purchase(money, payment, options = {})
35
+ commerce = options[:commerce] || @options[:commerce]
36
+ commerce_branch = options[:commerce_branch] || @options[:commerce_branch]
37
+ shop_process_id = options[:shop_process_id] || @shop_process_id
38
+
39
+ token = generate_token(shop_process_id, 'pay_pci', commerce, commerce_branch, amount(money), currency(money))
40
+
41
+ post = {}
42
+ post[:token] = token
43
+ post[:commerce] = commerce.to_s
44
+ post[:commerce_branch] = commerce_branch.to_s
45
+ post[:shop_process_id] = shop_process_id
46
+ post[:number_of_payments] = options[:number_of_payments] || 1
47
+ post[:recursive] = options[:recursive] || false
48
+
49
+ add_invoice(post, money, options)
50
+ add_card_data(post, payment)
51
+ add_customer_data(post, options)
52
+
53
+ commit(:pay_pci_buy_encrypted, post)
54
+ end
55
+
56
+ def void(authorization, options = {})
57
+ _, shop_process_id = authorization.to_s.split('#')
58
+ token = generate_token(shop_process_id, 'rollback', '0.00')
59
+ post = {
60
+ token: token,
61
+ shop_process_id: shop_process_id
62
+ }
63
+ commit(:pci_buy_rollback, post)
64
+ end
65
+
66
+ def credit(money, payment, options = {})
67
+ # Not permitted for foreign cards.
68
+ commerce = options[:commerce] || @options[:commerce]
69
+ commerce_branch = options[:commerce_branch] || @options[:commerce_branch]
70
+
71
+ token = generate_token(@shop_process_id, 'refund', commerce, commerce_branch, amount(money), currency(money))
72
+ post = {}
73
+ post[:token] = token
74
+ post[:commerce] = commerce.to_i
75
+ post[:commerce_branch] = commerce_branch.to_i
76
+ post[:shop_process_id] = @shop_process_id
77
+ add_invoice(post, money, options)
78
+ add_card_data(post, payment)
79
+ add_customer_data(post, options)
80
+ post[:origin_shop_process_id] = options[:original_shop_process_id] if options[:original_shop_process_id]
81
+ commit(:refund, post)
82
+ end
83
+
84
+ def refund(money, authorization, options = {})
85
+ commerce = options[:commerce] || @options[:commerce]
86
+ commerce_branch = options[:commerce_branch] || @options[:commerce_branch]
87
+ shop_process_id = options[:shop_process_id] || @shop_process_id
88
+ _, original_shop_process_id = authorization.to_s.split('#')
89
+
90
+ token = generate_token(shop_process_id, 'refund', commerce, commerce_branch, amount(money), currency(money))
91
+ post = {}
92
+ post[:token] = token
93
+ post[:commerce] = commerce.to_i
94
+ post[:commerce_branch] = commerce_branch.to_i
95
+ post[:shop_process_id] = shop_process_id
96
+ add_invoice(post, money, options)
97
+ add_customer_data(post, options)
98
+ post[:origin_shop_process_id] = original_shop_process_id || options[:original_shop_process_id]
99
+ commit(:refund, post)
100
+ end
101
+
102
+ def supports_scrubbing?
103
+ true
104
+ end
105
+
106
+ def scrub(transcript)
107
+ clean_transcript = remove_invalid_utf_8_byte_sequences(transcript)
108
+ clean_transcript.
109
+ gsub(/(token\\":\\")[.\-\w]+/, '\1[FILTERED]').
110
+ gsub(/(card_encrypted_data\\":\\")[.\-\w]+/, '\1[FILTERED]')
111
+ end
112
+
113
+ def remove_invalid_utf_8_byte_sequences(transcript)
114
+ transcript.encode('UTF-8', 'binary', undef: :replace, replace: '')
115
+ end
116
+
117
+ private
118
+
119
+ # Required to encrypt PAN data.
120
+ def one_time_public_key
121
+ token = generate_token('get_encription_public_key', @public_key)
122
+ response = commit(:pci_encryption_key, token: token)
123
+ OpenSSL::PKey::RSA.new(response.params['encryption_key'])
124
+ end
125
+
126
+ def generate_token(*elements)
127
+ Digest::MD5.hexdigest(@private_key + elements.join)
128
+ end
129
+
130
+ def add_invoice(post, money, options)
131
+ post[:amount] = amount(money)
132
+ post[:currency] = options[:currency] || currency(money)
133
+ end
134
+
135
+ def add_card_data(post, payment)
136
+ card_number = payment.number
137
+ cvv = payment.verification_value
138
+
139
+ payload = { card_number: card_number, 'cvv': cvv }.to_json
140
+
141
+ post[:card_encrypted_data] = JWE.encrypt(payload, one_time_public_key)
142
+ post[:card_month_expiration] = format(payment.month, :two_digits)
143
+ post[:card_year_expiration] = format(payment.year, :two_digits)
144
+ end
145
+
146
+ def add_customer_data(post, options)
147
+ post[:additional_data] = options[:additional_data] || '' # must be passed even if empty
148
+ end
149
+
150
+ def parse(body)
151
+ JSON.parse(body)
152
+ end
153
+
154
+ def commit(action, parameters)
155
+ url = build_request_url(action)
156
+ begin
157
+ response = parse(ssl_post(url, post_data(parameters)))
158
+ rescue ResponseError => response
159
+ # Errors are returned with helpful data,
160
+ # but get filtered out by `ssl_post` because of their HTTP status.
161
+ response = parse(response.response.body)
162
+ end
163
+
164
+ Response.new(
165
+ success_from(response),
166
+ message_from(response),
167
+ response,
168
+ authorization: authorization_from(response),
169
+ avs_result: nil,
170
+ cvv_result: nil,
171
+ test: test?,
172
+ error_code: error_code_from(response)
173
+ )
174
+ end
175
+
176
+ def success_from(response)
177
+ if code = response.dig('confirmation', 'response_code')
178
+ code == '00'
179
+ else
180
+ response['status'] == 'success'
181
+ end
182
+ end
183
+
184
+ def message_from(response)
185
+ %w(confirmation refund).each do |m|
186
+ message =
187
+ response.dig(m, 'extended_response_description') ||
188
+ response.dig(m, 'response_description') ||
189
+ response.dig(m, 'response_details')
190
+ return message if message
191
+ end
192
+ [response.dig('messages', 0, 'key'), response.dig('messages', 0, 'dsc')].join(':')
193
+ end
194
+
195
+ def authorization_from(response)
196
+ response_body = response.dig('confirmation') || response.dig('refund')
197
+ return unless response_body
198
+
199
+ authorization_number = response_body.dig('authorization_number') || response_body.dig('authorization_code')
200
+ shop_process_id = response_body.dig('shop_process_id')
201
+
202
+ "#{authorization_number}##{shop_process_id}"
203
+ end
204
+
205
+ def error_code_from(response)
206
+ response.dig('confirmation', 'response_code') unless success_from(response)
207
+ end
208
+
209
+ def build_request_url(action)
210
+ base_url = (test? ? test_url : live_url)
211
+ base_url + ENDPOINTS[action]
212
+ end
213
+
214
+ def post_data(data)
215
+ { public_key: @public_key,
216
+ operation: data }.compact.to_json
217
+ end
218
+ end
219
+ end
220
+ end