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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +206 -0
  3. data/lib/active_merchant/billing/check.rb +5 -8
  4. data/lib/active_merchant/billing/credit_card.rb +10 -0
  5. data/lib/active_merchant/billing/credit_card_methods.rb +18 -3
  6. data/lib/active_merchant/billing/gateway.rb +3 -2
  7. data/lib/active_merchant/billing/gateways/adyen.rb +66 -11
  8. data/lib/active_merchant/billing/gateways/airwallex.rb +341 -0
  9. data/lib/active_merchant/billing/gateways/barclaycard_smartpay.rb +2 -1
  10. data/lib/active_merchant/billing/gateways/blue_pay.rb +1 -1
  11. data/lib/active_merchant/billing/gateways/blue_snap.rb +31 -21
  12. data/lib/active_merchant/billing/gateways/braintree/braintree_common.rb +6 -1
  13. data/lib/active_merchant/billing/gateways/braintree/token_nonce.rb +113 -0
  14. data/lib/active_merchant/billing/gateways/braintree_blue.rb +87 -15
  15. data/lib/active_merchant/billing/gateways/card_connect.rb +1 -1
  16. data/lib/active_merchant/billing/gateways/card_stream.rb +17 -13
  17. data/lib/active_merchant/billing/gateways/cashnet.rb +15 -5
  18. data/lib/active_merchant/billing/gateways/checkout_v2.rb +34 -5
  19. data/lib/active_merchant/billing/gateways/credorax.rb +10 -0
  20. data/lib/active_merchant/billing/gateways/cyber_source.rb +24 -36
  21. data/lib/active_merchant/billing/gateways/d_local.rb +61 -6
  22. data/lib/active_merchant/billing/gateways/decidir.rb +17 -1
  23. data/lib/active_merchant/billing/gateways/decidir_plus.rb +344 -0
  24. data/lib/active_merchant/billing/gateways/ebanx.rb +19 -3
  25. data/lib/active_merchant/billing/gateways/elavon.rb +6 -3
  26. data/lib/active_merchant/billing/gateways/element.rb +20 -2
  27. data/lib/active_merchant/billing/gateways/global_collect.rb +137 -32
  28. data/lib/active_merchant/billing/gateways/ipg.rb +415 -0
  29. data/lib/active_merchant/billing/gateways/kushki.rb +7 -0
  30. data/lib/active_merchant/billing/gateways/litle.rb +93 -1
  31. data/lib/active_merchant/billing/gateways/mercado_pago.rb +3 -1
  32. data/lib/active_merchant/billing/gateways/mit.rb +260 -0
  33. data/lib/active_merchant/billing/gateways/moka.rb +24 -11
  34. data/lib/active_merchant/billing/gateways/moneris.rb +35 -8
  35. data/lib/active_merchant/billing/gateways/mundipagg.rb +8 -6
  36. data/lib/active_merchant/billing/gateways/nmi.rb +27 -8
  37. data/lib/active_merchant/billing/gateways/orbital.rb +357 -319
  38. data/lib/active_merchant/billing/gateways/pay_arc.rb +9 -7
  39. data/lib/active_merchant/billing/gateways/pay_conex.rb +3 -1
  40. data/lib/active_merchant/billing/gateways/pay_trace.rb +1 -1
  41. data/lib/active_merchant/billing/gateways/payflow.rb +76 -6
  42. data/lib/active_merchant/billing/gateways/paymentez.rb +35 -9
  43. data/lib/active_merchant/billing/gateways/paysafe.rb +155 -34
  44. data/lib/active_merchant/billing/gateways/payu_latam.rb +31 -16
  45. data/lib/active_merchant/billing/gateways/payway_dot_com.rb +3 -3
  46. data/lib/active_merchant/billing/gateways/pin.rb +31 -4
  47. data/lib/active_merchant/billing/gateways/priority.rb +369 -0
  48. data/lib/active_merchant/billing/gateways/rapyd.rb +258 -0
  49. data/lib/active_merchant/billing/gateways/realex.rb +18 -0
  50. data/lib/active_merchant/billing/gateways/safe_charge.rb +7 -6
  51. data/lib/active_merchant/billing/gateways/simetrik.rb +362 -0
  52. data/lib/active_merchant/billing/gateways/stripe.rb +30 -8
  53. data/lib/active_merchant/billing/gateways/stripe_payment_intents.rb +175 -72
  54. data/lib/active_merchant/billing/gateways/trans_first_transaction_express.rb +2 -1
  55. data/lib/active_merchant/billing/gateways/trust_commerce.rb +2 -1
  56. data/lib/active_merchant/billing/gateways/usa_epay_transaction.rb +20 -6
  57. data/lib/active_merchant/billing/gateways/visanet_peru.rb +6 -2
  58. data/lib/active_merchant/billing/gateways/wompi.rb +193 -0
  59. data/lib/active_merchant/billing/gateways/worldpay.rb +196 -64
  60. data/lib/active_merchant/billing/network_tokenization_credit_card.rb +1 -1
  61. data/lib/active_merchant/billing/response.rb +4 -0
  62. data/lib/active_merchant/version.rb +1 -1
  63. metadata +11 -2
@@ -1,11 +1,14 @@
1
1
  module ActiveMerchant #:nodoc:
2
2
  module Billing #:nodoc:
3
3
  class GlobalCollectGateway < Gateway
4
+ class_attribute :preproduction_url
5
+
4
6
  self.display_name = 'GlobalCollect'
5
7
  self.homepage_url = 'http://www.globalcollect.com/'
6
8
 
7
9
  self.test_url = 'https://eu.sandbox.api-ingenico.com'
8
- self.live_url = 'https://api.globalcollect.com'
10
+ self.preproduction_url = 'https://world.preprod.api-ingenico.com'
11
+ self.live_url = 'https://world.api-ingenico.com'
9
12
 
10
13
  self.supported_countries = %w[AD AE AG AI AL AM AO AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BW BY BZ CA CC CD CF CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HN HR HT HU ID IE IL IM IN IS IT JM JO JP KE KG KH KI KM KN KR KW KY KZ LA LB LC LI LK LR LS LT LU LV MA MC MD ME MF MG MH MK MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PL PN PS PT PW QA RE RO RS RU RW SA SB SC SE SG SH SI SJ SK SL SM SN SR ST SV SZ TC TD TG TH TJ TL TM TN TO TR TT TV TW TZ UA UG US UY UZ VC VE VG VI VN WF WS ZA ZM ZW]
11
14
  self.default_currency = 'USD'
@@ -84,7 +87,9 @@ module ActiveMerchant #:nodoc:
84
87
  'master' => '3',
85
88
  'discover' => '128',
86
89
  'jcb' => '125',
87
- 'diners_club' => '132'
90
+ 'diners_club' => '132',
91
+ 'cabal' => '135',
92
+ 'naranja' => '136'
88
93
  }
89
94
 
90
95
  def add_order(post, money, options, capture: false)
@@ -100,36 +105,126 @@ module ActiveMerchant #:nodoc:
100
105
  post['order']['references']['invoiceData'] = {
101
106
  'invoiceNumber' => options[:invoice]
102
107
  }
103
- add_airline_data(post, options) if options[:airline_data]
108
+ add_airline_data(post, options)
109
+ add_lodging_data(post, options)
104
110
  add_number_of_installments(post, options) if options[:number_of_installments]
105
111
  end
106
112
 
107
113
  def add_airline_data(post, options)
114
+ return unless airline_options = options[:airline_data]
115
+
108
116
  airline_data = {}
109
117
 
110
- flight_date = options[:airline_data][:flight_date]
111
- passenger_name = options[:airline_data][:passenger_name]
112
- code = options[:airline_data][:code]
113
- name = options[:airline_data][:name]
118
+ airline_data['flightDate'] = airline_options[:flight_date] if airline_options[:flight_date]
119
+ airline_data['passengerName'] = airline_options[:passenger_name] if airline_options[:passenger_name]
120
+ airline_data['code'] = airline_options[:code] if airline_options[:code]
121
+ airline_data['name'] = airline_options[:name] if airline_options[:name]
122
+ airline_data['invoiceNumber'] = options[:airline_data][:invoice_number] if options[:airline_data][:invoice_number]
123
+ airline_data['isETicket'] = options[:airline_data][:is_eticket] if options[:airline_data][:is_eticket]
124
+ airline_data['isRestrictedTicket'] = options[:airline_data][:is_restricted_ticket] if options[:airline_data][:is_restricted_ticket]
125
+ airline_data['isThirdParty'] = options[:airline_data][:is_third_party] if options[:airline_data][:is_third_party]
126
+ airline_data['issueDate'] = options[:airline_data][:issue_date] if options[:airline_data][:issue_date]
127
+ airline_data['merchantCustomerId'] = options[:airline_data][:merchant_customer_id] if options[:airline_data][:merchant_customer_id]
128
+ airline_data['flightLegs'] = add_flight_legs(airline_options)
129
+ airline_data['passengers'] = add_passengers(airline_options)
114
130
 
115
- airline_data['flightDate'] = flight_date if flight_date
116
- airline_data['passengerName'] = passenger_name if passenger_name
117
- airline_data['code'] = code if code
118
- airline_data['name'] = name if name
131
+ post['order']['additionalInput']['airlineData'] = airline_data
132
+ end
119
133
 
134
+ def add_flight_legs(airline_options)
120
135
  flight_legs = []
121
- options[:airline_data][:flight_legs]&.each do |fl|
136
+ airline_options[:flight_legs]&.each do |fl|
122
137
  leg = {}
138
+ leg['airlineClass'] = fl[:airline_class] if fl[:airline_class]
123
139
  leg['arrivalAirport'] = fl[:arrival_airport] if fl[:arrival_airport]
124
- leg['originAirport'] = fl[:origin_airport] if fl[:origin_airport]
140
+ leg['arrivalTime'] = fl[:arrival_time] if fl[:arrival_time]
141
+ leg['carrierCode'] = fl[:carrier_code] if fl[:carrier_code]
142
+ leg['conjunctionTicket'] = fl[:conjunction_ticket] if fl[:conjunction_ticket]
143
+ leg['couponNumber'] = fl[:coupon_number] if fl[:coupon_number]
125
144
  leg['date'] = fl[:date] if fl[:date]
145
+ leg['departureTime'] = fl[:departure_time] if fl[:departure_time]
146
+ leg['endorsementOrRestriction'] = fl[:endorsement_or_restriction] if fl[:endorsement_or_restriction]
147
+ leg['exchangeTicket'] = fl[:exchange_ticket] if fl[:exchange_ticket]
148
+ leg['fare'] = fl[:fare] if fl[:fare]
149
+ leg['fareBasis'] = fl[:fare_basis] if fl[:fare_basis]
150
+ leg['fee'] = fl[:fee] if fl[:fee]
151
+ leg['flightNumber'] = fl[:flight_number] if fl[:flight_number]
126
152
  leg['number'] = fl[:number] if fl[:number]
127
- leg['carrierCode'] = fl[:carrier_code] if fl[:carrier_code]
128
- leg['airlineClass'] = fl[:carrier_code] if fl[:airline_class]
153
+ leg['originAirport'] = fl[:origin_airport] if fl[:origin_airport]
154
+ leg['passengerClass'] = fl[:passenger_class] if fl[:passenger_class]
155
+ leg['stopoverCode'] = fl[:stopover_code] if fl[:stopover_code]
156
+ leg['taxes'] = fl[:taxes] if fl[:taxes]
129
157
  flight_legs << leg
130
158
  end
131
- airline_data['flightLegs'] = flight_legs
132
- post['order']['additionalInput']['airlineData'] = airline_data
159
+ flight_legs
160
+ end
161
+
162
+ def add_passengers(airline_options)
163
+ passengers = []
164
+ airline_options[:passengers]&.each do |flyer|
165
+ passenger = {}
166
+ passenger['firstName'] = flyer[:first_name] if flyer[:first_name]
167
+ passenger['surname'] = flyer[:surname] if flyer[:surname]
168
+ passenger['surnamePrefix'] = flyer[:surname_prefix] if flyer[:surname_prefix]
169
+ passenger['title'] = flyer[:title] if flyer[:title]
170
+ passengers << passenger
171
+ end
172
+ passengers
173
+ end
174
+
175
+ def add_lodging_data(post, options)
176
+ return unless lodging_options = options[:lodging_data]
177
+
178
+ lodging_data = {}
179
+
180
+ lodging_data['charges'] = add_charges(lodging_options)
181
+ lodging_data['checkInDate'] = lodging_options[:check_in_date] if lodging_options[:check_in_date]
182
+ lodging_data['checkOutDate'] = lodging_options[:check_out_date] if lodging_options[:check_out_date]
183
+ lodging_data['folioNumber'] = lodging_options[:folio_number] if lodging_options[:folio_number]
184
+ lodging_data['isConfirmedReservation'] = lodging_options[:is_confirmed_reservation] if lodging_options[:is_confirmed_reservation]
185
+ lodging_data['isFacilityFireSafetyConform'] = lodging_options[:is_facility_fire_safety_conform] if lodging_options[:is_facility_fire_safety_conform]
186
+ lodging_data['isNoShow'] = lodging_options[:is_no_show] if lodging_options[:is_no_show]
187
+ lodging_data['isPreferenceSmokingRoom'] = lodging_options[:is_preference_smoking_room] if lodging_options[:is_preference_smoking_room]
188
+ lodging_data['numberOfAdults'] = lodging_options[:number_of_adults] if lodging_options[:number_of_adults]
189
+ lodging_data['numberOfNights'] = lodging_options[:number_of_nights] if lodging_options[:number_of_nights]
190
+ lodging_data['numberOfRooms'] = lodging_options[:number_of_rooms] if lodging_options[:number_of_rooms]
191
+ lodging_data['programCode'] = lodging_options[:program_code] if lodging_options[:program_code]
192
+ lodging_data['propertyCustomerServicePhoneNumber'] = lodging_options[:property_customer_service_phone_number] if lodging_options[:property_customer_service_phone_number]
193
+ lodging_data['propertyPhoneNumber'] = lodging_options[:property_phone_number] if lodging_options[:property_phone_number]
194
+ lodging_data['renterName'] = lodging_options[:renter_name] if lodging_options[:renter_name]
195
+ lodging_data['rooms'] = add_rooms(lodging_options)
196
+
197
+ post['order']['additionalInput']['lodgingData'] = lodging_data
198
+ end
199
+
200
+ def add_charges(lodging_options)
201
+ charges = []
202
+ lodging_options[:charges]&.each do |item|
203
+ charge = {}
204
+ charge['chargeAmount'] = item[:charge_amount] if item[:charge_amount]
205
+ charge['chargeAmountCurrencyCode'] = item[:charge_amount_currency_code] if item[:charge_amount_currency_code]
206
+ charge['chargeType'] = item[:charge_type] if item[:charge_type]
207
+ charges << charge
208
+ end
209
+ charges
210
+ end
211
+
212
+ def add_rooms(lodging_options)
213
+ rooms = []
214
+ lodging_options[:rooms]&.each do |item|
215
+ room = {}
216
+ room['dailyRoomRate'] = item[:daily_room_rate] if item[:daily_room_rate]
217
+ room['dailyRoomRateCurrencyCode'] = item[:daily_room_rate_currency_code] if item[:daily_room_rate_currency_code]
218
+ room['dailyRoomTaxAmount'] = item[:daily_room_tax_amount] if item[:daily_room_tax_amount]
219
+ room['dailyRoomTaxAmountCurrencyCode'] = item[:daily_room_tax_amount_currency_code] if item[:daily_room_tax_amount_currency_code]
220
+ room['numberOfNightsAtRoomRate'] = item[:number_of_nights_at_room_rate] if item[:number_of_nights_at_room_rate]
221
+ room['roomLocation'] = item[:room_location] if item[:room_location]
222
+ room['roomNumber'] = item[:room_number] if item[:room_number]
223
+ room['typeOfBed'] = item[:type_of_bed] if item[:type_of_bed]
224
+ room['typeOfRoom'] = item[:type_of_room] if item[:type_of_room]
225
+ rooms << room
226
+ end
227
+ rooms
133
228
  end
134
229
 
135
230
  def add_creator_info(post, options)
@@ -155,7 +250,6 @@ module ActiveMerchant #:nodoc:
155
250
  month = format(payment.month, :two_digits)
156
251
  expirydate = "#{month}#{year}"
157
252
  pre_authorization = options[:pre_authorization] ? 'PRE_AUTHORIZATION' : 'FINAL_AUTHORIZATION'
158
-
159
253
  post['cardPaymentMethodSpecificInput'] = {
160
254
  'paymentProductId' => BRAND_MAP[payment.brand],
161
255
  'skipAuthentication' => 'true', # refers to 3DSecure
@@ -260,6 +354,8 @@ module ActiveMerchant #:nodoc:
260
354
  end
261
355
 
262
356
  def url(action, authorization)
357
+ return preproduction_url + uri(action, authorization) if @options[:url_override].to_s == 'preproduction'
358
+
263
359
  (test? ? test_url : live_url) + uri(action, authorization)
264
360
  end
265
361
 
@@ -291,12 +387,12 @@ module ActiveMerchant #:nodoc:
291
387
  response = json_error(raw_response)
292
388
  end
293
389
 
294
- succeeded = success_from(response)
390
+ succeeded = success_from(action, response)
295
391
  Response.new(
296
392
  succeeded,
297
393
  message_from(succeeded, response),
298
394
  response,
299
- authorization: authorization_from(succeeded, response),
395
+ authorization: authorization_from(response),
300
396
  error_code: error_code_from(succeeded, response),
301
397
  test: test?
302
398
  )
@@ -305,8 +401,7 @@ module ActiveMerchant #:nodoc:
305
401
  def json_error(raw_response)
306
402
  {
307
403
  'error_message' => 'Invalid response received from the Ingenico ePayments (formerly GlobalCollect) API. Please contact Ingenico ePayments if you continue to receive this message.' \
308
- " (The raw response returned by the API was #{raw_response.inspect})",
309
- 'status' => 'REJECTED'
404
+ " (The raw response returned by the API was #{raw_response.inspect})"
310
405
  }
311
406
  end
312
407
 
@@ -343,8 +438,24 @@ module ActiveMerchant #:nodoc:
343
438
  'application/json'
344
439
  end
345
440
 
346
- def success_from(response)
347
- !response['errorId'] && response['status'] != 'REJECTED'
441
+ def success_from(action, response)
442
+ return false if response['errorId'] || response['error_message']
443
+
444
+ case action
445
+ when :authorize
446
+ response.dig('payment', 'statusOutput', 'isAuthorized')
447
+ when :capture
448
+ capture_status = response.dig('status') || response.dig('payment', 'status')
449
+ %w(CAPTURED CAPTURE_REQUESTED).include?(capture_status)
450
+ when :void
451
+ void_response_id = response.dig('cardPaymentMethodSpecificOutput', 'voidResponseId') || response.dig('mobilePaymentMethodSpecificOutput', 'voidResponseId')
452
+ %w(00 0 8 11).include?(void_response_id) || response.dig('payment', 'status') == 'CANCELLED'
453
+ when :refund
454
+ refund_status = response.dig('status') || response.dig('payment', 'status')
455
+ %w(REFUNDED REFUND_REQUESTED).include?(refund_status)
456
+ else
457
+ response['status'] != 'REJECTED'
458
+ end
348
459
  end
349
460
 
350
461
  def message_from(succeeded, response)
@@ -361,14 +472,8 @@ module ActiveMerchant #:nodoc:
361
472
  end
362
473
  end
363
474
 
364
- def authorization_from(succeeded, response)
365
- if succeeded
366
- response['id'] || response['payment']['id'] || response['paymentResult']['payment']['id']
367
- elsif response['errorId']
368
- response['errorId']
369
- else
370
- 'GATEWAY ERROR'
371
- end
475
+ def authorization_from(response)
476
+ response.dig('id') || response.dig('payment', 'id') || response.dig('paymentResult', 'payment', 'id')
372
477
  end
373
478
 
374
479
  def error_code_from(succeeded, response)
@@ -0,0 +1,415 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ class IpgGateway < Gateway
4
+ self.test_url = 'https://test.ipg-online.com/ipgapi/services'
5
+ self.live_url = 'https://www5.ipg-online.com'
6
+
7
+ self.supported_countries = %w(AR)
8
+ self.default_currency = 'ARS'
9
+ self.supported_cardtypes = %i[visa master american_express discover]
10
+
11
+ self.homepage_url = 'https://www.ipg-online.com'
12
+ self.display_name = 'IPG'
13
+
14
+ CURRENCY_CODES = {
15
+ 'ARS' => '032'
16
+ }
17
+
18
+ ACTION_REQUEST_ITEMS = %w(vault unstore)
19
+
20
+ def initialize(options = {})
21
+ requires!(options, :store_id, :user_id, :password, :pem, :pem_password)
22
+ @credentials = options
23
+ @hosted_data_id = nil
24
+ super
25
+ end
26
+
27
+ def purchase(money, payment, options = {})
28
+ xml = build_purchase_and_authorize_request(money, payment, options)
29
+
30
+ commit('sale', xml)
31
+ end
32
+
33
+ def authorize(money, payment, options = {})
34
+ xml = build_purchase_and_authorize_request(money, payment, options)
35
+
36
+ commit('preAuth', xml)
37
+ end
38
+
39
+ def capture(money, authorization, options = {})
40
+ xml = build_capture_and_refund_request(money, authorization, options)
41
+
42
+ commit('postAuth', xml)
43
+ end
44
+
45
+ def refund(money, authorization, options = {})
46
+ xml = build_capture_and_refund_request(money, authorization, options)
47
+
48
+ commit('return', xml)
49
+ end
50
+
51
+ def void(authorization, options = {})
52
+ xml = Builder::XmlMarkup.new(indent: 2)
53
+ add_transaction_details(xml, options.merge!({ order_id: authorization }))
54
+
55
+ commit('void', xml)
56
+ end
57
+
58
+ def store(credit_card, options = {})
59
+ @hosted_data_id = options[:hosted_data_id] || generate_unique_id
60
+ xml = Builder::XmlMarkup.new(indent: 2)
61
+ add_storage_item(xml, credit_card, options)
62
+
63
+ commit('vault', xml)
64
+ end
65
+
66
+ def unstore(hosted_data_id)
67
+ xml = Builder::XmlMarkup.new(indent: 2)
68
+ add_unstore_item(xml, hosted_data_id)
69
+
70
+ commit('unstore', xml)
71
+ end
72
+
73
+ def verify(credit_card, options = {})
74
+ options[:currency] = self.default_currency unless options[:currency] && !options[:currency].empty?
75
+ MultiResponse.run(:use_first_response) do |r|
76
+ r.process { authorize(100, credit_card, options) }
77
+ r.process(:ignore_result) { void(r.authorization, options) }
78
+ end
79
+ end
80
+
81
+ def supports_scrubbing?
82
+ true
83
+ end
84
+
85
+ def scrub(transcript)
86
+ transcript.
87
+ gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
88
+ gsub(%r((<v1:CardNumber>).+(</v1:CardNumber>)), '\1[FILTERED]\2').
89
+ gsub(%r((<v1:CardCodeValue>).+(</v1:CardCodeValue>)), '\1[FILTERED]\2').
90
+ gsub(%r((<v1:StoreId>).+(</v1:StoreId>)), '\1[FILTERED]\2')
91
+ end
92
+
93
+ private
94
+
95
+ NAMESPACE_BASE_URL = 'http://ipg-online.com'
96
+
97
+ def build_purchase_and_authorize_request(money, payment, options)
98
+ xml = Builder::XmlMarkup.new(indent: 2)
99
+
100
+ add_credit_card(xml, payment, options)
101
+ add_sub_merchant(xml, options[:submerchant]) if options[:submerchant]
102
+ add_three_d_secure(xml, options[:three_d_secure]) if options[:three_d_secure]
103
+ add_stored_credentials(xml, options) if options[:stored_credential] || options[:recurring_type]
104
+ add_payment(xml, money, payment, options)
105
+ add_transaction_details(xml, options)
106
+ add_billing(xml, options[:billing]) if options[:billing]
107
+ add_shipping(xml, options[:shipping]) if options[:shipping]
108
+ xml
109
+ end
110
+
111
+ def build_capture_and_refund_request(money, authorization, options)
112
+ xml = Builder::XmlMarkup.new(indent: 2)
113
+ add_payment(xml, money, nil, options)
114
+ add_transaction_details(xml, options.merge!({ order_id: authorization }), true)
115
+ xml
116
+ end
117
+
118
+ def build_order_request(xml, action, body)
119
+ xml.tag!('ipg:IPGApiOrderRequest') do
120
+ xml.tag!('v1:Transaction') do
121
+ add_transaction_type(xml, action)
122
+ xml << body.target!
123
+ end
124
+ end
125
+ end
126
+
127
+ def build_action_request(xml, action, body)
128
+ xml.tag!('ns4:IPGApiActionRequest', ipg_action_namespaces) do
129
+ xml.tag!('ns2:Action') do
130
+ xml << body.target!
131
+ end
132
+ end
133
+ end
134
+
135
+ def build_soap_request(action, body)
136
+ xml = Builder::XmlMarkup.new(indent: 2)
137
+ xml.tag!('soapenv:Envelope', envelope_namespaces) do
138
+ xml.tag!('soapenv:Header')
139
+ xml.tag!('soapenv:Body') do
140
+ build_order_request(xml, action, body) unless ACTION_REQUEST_ITEMS.include?(action)
141
+ build_action_request(xml, action, body) if ACTION_REQUEST_ITEMS.include?(action)
142
+ end
143
+ end
144
+ xml.target!
145
+ end
146
+
147
+ def add_stored_credentials(xml, params)
148
+ recurring_type = params[:stored_credential][:initial_transaction] ? 'FIRST' : 'REPEAT' if params[:stored_credential]
149
+ recurring_type = params[:recurring_type] if params[:recurring_type]
150
+ xml.tag!('v1:recurringType', recurring_type)
151
+ end
152
+
153
+ def add_storage_item(xml, credit_card, options)
154
+ requires!(options.merge!({ credit_card: credit_card, hosted_data_id: @hosted_data_id }), :credit_card, :hosted_data_id)
155
+ xml.tag!('ns2:StoreHostedData') do
156
+ xml.tag!('ns2:DataStorageItem') do
157
+ add_credit_card(xml, credit_card, {}, 'ns2')
158
+ add_three_d_secure(xml, options[:three_d_secure]) if options[:three_d_secure]
159
+ xml.tag!('ns2:HostedDataID', @hosted_data_id) if @hosted_data_id
160
+ end
161
+ end
162
+ end
163
+
164
+ def add_unstore_item(xml, hosted_data_id)
165
+ requires!({}.merge!({ hosted_data_id: hosted_data_id }), :hosted_data_id)
166
+ xml.tag!('ns2:StoreHostedData') do
167
+ xml.tag!('ns2:DataStorageItem') do
168
+ xml.tag!('ns2:Function', 'delete')
169
+ xml.tag!('ns2:HostedDataID', hosted_data_id)
170
+ end
171
+ end
172
+ end
173
+
174
+ def add_transaction_type(xml, type)
175
+ xml.tag!('v1:CreditCardTxType') do
176
+ xml.tag!('v1:StoreId', @credentials[:store_id])
177
+ xml.tag!('v1:Type', type)
178
+ end
179
+ end
180
+
181
+ def add_credit_card(xml, payment, options = {}, credit_envelope = 'v1')
182
+ if payment&.is_a?(CreditCard)
183
+ requires!(options.merge!({ card_number: payment.number, month: payment.month, year: payment.year }), :card_number, :month, :year)
184
+
185
+ xml.tag!("#{credit_envelope}:CreditCardData") do
186
+ xml.tag!('v1:CardNumber', payment.number) if payment.number
187
+ xml.tag!('v1:ExpMonth', format(payment.month, :two_digits)) if payment.month
188
+ xml.tag!('v1:ExpYear', format(payment.year, :two_digits)) if payment.year
189
+ xml.tag!('v1:CardCodeValue', payment.verification_value) if payment.verification_value
190
+ xml.tag!('v1:Brand', options[:brand]) if options[:brand]
191
+ end
192
+ end
193
+
194
+ if options[:card_function_type]
195
+ xml.tag!('v1:cardFunction') do
196
+ xml.tag!('v1:Type', options[:card_function_type])
197
+ end
198
+ end
199
+
200
+ if options[:track_data]
201
+ xml.tag!("#{credit_envelope}:CreditCardData") do
202
+ xml.tag!('v1:TrackData', options[:track_data])
203
+ end
204
+ end
205
+ end
206
+
207
+ def add_sub_merchant(xml, submerchant)
208
+ xml.tag!('v1:SubMerchant') do
209
+ xml.tag!('v1:Mcc', submerchant[:mcc]) if submerchant[:mcc]
210
+ xml.tag!('v1:LegalName', submerchant[:legal_name]) if submerchant[:legal_name]
211
+ add_address(xml, submerchant[:address]) if submerchant[:address]
212
+ add_document(xml, submerchant[:document]) if submerchant[:document]
213
+ xml.tag!('v1:MerchantID', submerchant[:merchant_id]) if submerchant[:merchant_id]
214
+ end
215
+ end
216
+
217
+ def add_address(xml, address)
218
+ xml.tag!('v1:Address') do
219
+ xml.tag!('v1:Address1', address[:address1]) if address[:address1]
220
+ xml.tag!('v1:Address2', address[:address2]) if address[:address2]
221
+ xml.tag!('v1:Zip', address[:zip]) if address[:zip]
222
+ xml.tag!('v1:City', address[:city]) if address[:city]
223
+ xml.tag!('v1:State', address[:state]) if address[:state]
224
+ xml.tag!('v1:Country', address[:country]) if address[:country]
225
+ end
226
+ end
227
+
228
+ def add_document(xml, document)
229
+ xml.tag!('v1:Document') do
230
+ xml.tag!('v1:Type', document[:type]) if document[:type]
231
+ xml.tag!('v1:Number', document[:number]) if document[:number]
232
+ end
233
+ end
234
+
235
+ def add_three_d_secure(xml, three_d_secure)
236
+ xml.tag!('v1:CreditCard3DSecure') do
237
+ xml.tag!('v1:AuthenticationValue', three_d_secure[:cavv]) if three_d_secure[:cavv]
238
+ xml.tag!('v1:XID', three_d_secure[:xid]) if three_d_secure[:xid]
239
+ xml.tag!('v1:Secure3D2TransactionStatus', three_d_secure[:directory_response_status]) if three_d_secure[:directory_response_status]
240
+ xml.tag!('v1:Secure3D2AuthenticationResponse', three_d_secure[:authentication_response_status]) if three_d_secure[:authentication_response_status]
241
+ xml.tag!('v1:Secure3DProtocolVersion', three_d_secure[:version]) if three_d_secure[:version]
242
+ xml.tag!('v1:DirectoryServerTransactionId', three_d_secure[:ds_transaction_id]) if three_d_secure[:ds_transaction_id]
243
+ end
244
+ end
245
+
246
+ def add_transaction_details(xml, options, pre_order = false)
247
+ requires!(options, :order_id) if pre_order
248
+ xml.tag!('v1:TransactionDetails') do
249
+ xml.tag!('v1:OrderId', options[:order_id]) if options[:order_id]
250
+ xml.tag!('v1:MerchantTransactionId', options[:merchant_transaction_id]) if options[:merchant_transaction_id]
251
+ xml.tag!('v1:Ip', options[:ip]) if options[:ip]
252
+ xml.tag!('v1:Tdate', options[:t_date]) if options[:t_date]
253
+ xml.tag!('v1:IpgTransactionId', options[:ipg_transaction_id]) if options[:ipg_transaction_id]
254
+ xml.tag!('v1:ReferencedMerchantTransactionId', options[:referenced_merchant_transaction_id]) if options[:referenced_merchant_transaction_id]
255
+ xml.tag!('v1:TransactionOrigin', options[:transaction_origin]) if options[:transaction_origin]
256
+ xml.tag!('v1:InvoiceNumber', options[:invoice_number]) if options[:invoice_number]
257
+ xml.tag!('v1:DynamicMerchantName', options[:dynamic_merchant_name]) if options[:dynamic_merchant_name]
258
+ xml.tag!('v1:Comments', options[:comments]) if options[:comments]
259
+ if options[:terminal_id]
260
+ xml.tag!('v1:Terminal') do
261
+ xml.tag!('v1:TerminalID', options[:terminal_id]) if options[:terminal_id]
262
+ end
263
+ end
264
+ end
265
+ end
266
+
267
+ def add_payment(xml, money, payment, options)
268
+ requires!(options.merge!({ money: money }), :currency, :money)
269
+ xml.tag!('v1:Payment') do
270
+ xml.tag!('v1:HostedDataID', payment) if payment&.is_a?(String)
271
+ xml.tag!('v1:HostedDataStoreID', options[:hosted_data_store_id]) if options[:hosted_data_store_id]
272
+ xml.tag!('v1:DeclineHostedDataDuplicates', options[:decline_hosted_data_duplicates]) if options[:decline_hosted_data_duplicates]
273
+ xml.tag!('v1:SubTotal', options[:sub_total]) if options[:sub_total]
274
+ xml.tag!('v1:ValueAddedTax', options[:value_added_tax]) if options[:value_added_tax]
275
+ xml.tag!('v1:DeliveryAmount', options[:delivery_amount]) if options[:delivery_amount]
276
+ xml.tag!('v1:ChargeTotal', money)
277
+ xml.tag!('v1:Currency', CURRENCY_CODES[options[:currency]])
278
+ xml.tag!('v1:numberOfInstallments', options[:number_of_installments]) if options[:number_of_installments]
279
+ end
280
+ end
281
+
282
+ def add_billing(xml, billing)
283
+ xml.tag!('v1:Billing') do
284
+ xml.tag!('v1:CustomerID', billing[:customer_id]) if billing[:customer_id]
285
+ xml.tag!('v1:Name', billing[:name]) if billing[:name]
286
+ xml.tag!('v1:Company', billing[:company]) if billing[:company]
287
+ xml.tag!('v1:Address1', billing[:address_1]) if billing[:address_1]
288
+ xml.tag!('v1:Address2', billing[:address_2]) if billing[:address_2]
289
+ xml.tag!('v1:City', billing[:city]) if billing[:city]
290
+ xml.tag!('v1:State', billing[:state]) if billing[:state]
291
+ xml.tag!('v1:Zip', billing[:zip]) if billing[:zip]
292
+ xml.tag!('v1:Country', billing[:country]) if billing[:country]
293
+ xml.tag!('v1:Phone', billing[:phone]) if billing[:phone]
294
+ xml.tag!('v1:Fax', billing[:fax]) if billing[:fax]
295
+ xml.tag!('v1:Email', billing[:email]) if billing[:email]
296
+ end
297
+ end
298
+
299
+ def add_shipping(xml, shipping)
300
+ xml.tag!('v1:Shipping') do
301
+ xml.tag!('v1:Type', shipping[:type]) if shipping[:type]
302
+ xml.tag!('v1:Name', shipping[:name]) if shipping[:name]
303
+ xml.tag!('v1:Address1', shipping[:address_1]) if shipping[:address_1]
304
+ xml.tag!('v1:Address2', shipping[:address_2]) if shipping[:address_2]
305
+ xml.tag!('v1:City', shipping[:city]) if shipping[:city]
306
+ xml.tag!('v1:State', shipping[:state]) if shipping[:state]
307
+ xml.tag!('v1:Zip', shipping[:zip]) if shipping[:zip]
308
+ xml.tag!('v1:Country', shipping[:country]) if shipping[:country]
309
+ end
310
+ end
311
+
312
+ def build_header
313
+ {
314
+ 'Content-Type' => 'text/xml; charset=utf-8',
315
+ 'Authorization' => "Basic #{encoded_credentials}"
316
+ }
317
+ end
318
+
319
+ def encoded_credentials
320
+ Base64.encode64("WS#{@credentials[:store_id]}._.#{@credentials[:user_id]}:#{@credentials[:password]}").delete("\n")
321
+ end
322
+
323
+ def envelope_namespaces
324
+ {
325
+ 'xmlns:soapenv' => 'http://schemas.xmlsoap.org/soap/envelope/',
326
+ 'xmlns:ipg' => "#{NAMESPACE_BASE_URL}/ipgapi/schemas/ipgapi",
327
+ 'xmlns:v1' => "#{NAMESPACE_BASE_URL}/ipgapi/schemas/v1"
328
+ }
329
+ end
330
+
331
+ def ipg_order_namespaces
332
+ {
333
+ 'xmlns:v1' => "#{NAMESPACE_BASE_URL}/ipgapi/schemas/v1",
334
+ 'xmlns:ipgapi' => "#{NAMESPACE_BASE_URL}/ipgapi/schemas/ipgapi"
335
+ }
336
+ end
337
+
338
+ def ipg_action_namespaces
339
+ {
340
+ 'xmlns:ns4' => "#{NAMESPACE_BASE_URL}/ipgapi/schemas/ipgapi",
341
+ 'xmlns:ns2' => "#{NAMESPACE_BASE_URL}/ipgapi/schemas/a1",
342
+ 'xmlns:ns3' => "#{NAMESPACE_BASE_URL}/ipgapi/schemas/v1"
343
+ }
344
+ end
345
+
346
+ def commit(action, request)
347
+ url = (test? ? test_url : live_url)
348
+ soap_request = build_soap_request(action, request)
349
+ response = parse(ssl_post(url, soap_request, build_header))
350
+ Response.new(
351
+ response[:success],
352
+ message_from(response),
353
+ response,
354
+ authorization: authorization_from(action, response),
355
+ avs_result: AVSResult.new(code: response[:AVSResponse]),
356
+ cvv_result: CVVResult.new(response[:ProcessorCCVResponse]),
357
+ test: test?,
358
+ error_code: error_code_from(response)
359
+ )
360
+ end
361
+
362
+ def parse(xml)
363
+ reply = {}
364
+ xml = REXML::Document.new(xml)
365
+ root = REXML::XPath.first(xml, '//ipgapi:IPGApiOrderResponse') || REXML::XPath.first(xml, '//ipgapi:IPGApiActionResponse') || REXML::XPath.first(xml, '//SOAP-ENV:Fault') || REXML::XPath.first(xml, '//ns4:IPGApiActionResponse')
366
+ reply[:success] = REXML::XPath.first(xml, '//faultcode') ? false : true
367
+ if REXML::XPath.first(xml, '//ns4:IPGApiActionResponse')
368
+ reply[:tpv_error_code] = REXML::XPath.first(root, '//ns2:Error').attributes['Code']
369
+ reply[:tpv_error_msg] = REXML::XPath.first(root, '//ns2:ErrorMessage').text
370
+ reply[:success] = false
371
+ end
372
+ root.elements.to_a.each do |node|
373
+ parse_element(reply, node)
374
+ end
375
+ reply[:hosted_data_id] = @hosted_data_id if @hosted_data_id
376
+ return reply
377
+ end
378
+
379
+ def parse_element(reply, node)
380
+ if node.has_elements?
381
+ node.elements.each { |e| parse_element(reply, e) }
382
+ else
383
+ if /item/.match?(node.parent.name)
384
+ parent = node.parent.name
385
+ parent += '_' + node.parent.attributes['id'] if node.parent.attributes['id']
386
+ parent += '_'
387
+ end
388
+ reply["#{parent}#{node.name}".to_sym] ||= node.text
389
+ end
390
+ return reply
391
+ end
392
+
393
+ def message_from(response)
394
+ response[:TransactionResult]
395
+ end
396
+
397
+ def authorization_from(action, response)
398
+ return (action == 'vault' ? response[:hosted_data_id] : response[:OrderId])
399
+ end
400
+
401
+ def error_code_from(response)
402
+ response[:ErrorMessage]&.split(':')&.first unless response[:success]
403
+ end
404
+
405
+ def handle_response(response)
406
+ case response.code.to_i
407
+ when 200...300, 500
408
+ response.body
409
+ else
410
+ raise ResponseError.new(response)
411
+ end
412
+ end
413
+ end
414
+ end
415
+ end