activemerchant 1.90.0 → 1.91.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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +38 -0
  3. data/README.md +3 -2
  4. data/lib/active_merchant/billing/credit_card_methods.rb +1 -1
  5. data/lib/active_merchant/billing/gateways/adyen.rb +26 -21
  6. data/lib/active_merchant/billing/gateways/bambora_apac.rb +186 -0
  7. data/lib/active_merchant/billing/gateways/blue_snap.rb +189 -34
  8. data/lib/active_merchant/billing/gateways/braintree_blue.rb +11 -9
  9. data/lib/active_merchant/billing/gateways/card_connect.rb +3 -0
  10. data/lib/active_merchant/billing/gateways/cecabank.rb +13 -2
  11. data/lib/active_merchant/billing/gateways/fat_zebra.rb +20 -7
  12. data/lib/active_merchant/billing/gateways/ipp.rb +1 -0
  13. data/lib/active_merchant/billing/gateways/moneris.rb +3 -4
  14. data/lib/active_merchant/billing/gateways/netbanx.rb +4 -0
  15. data/lib/active_merchant/billing/gateways/nmi.rb +6 -4
  16. data/lib/active_merchant/billing/gateways/openpay.rb +1 -1
  17. data/lib/active_merchant/billing/gateways/orbital.rb +8 -1
  18. data/lib/active_merchant/billing/gateways/payment_express.rb +4 -1
  19. data/lib/active_merchant/billing/gateways/paymentez.rb +4 -9
  20. data/lib/active_merchant/billing/gateways/pin.rb +19 -6
  21. data/lib/active_merchant/billing/gateways/pro_pay.rb +1 -1
  22. data/lib/active_merchant/billing/gateways/qvalent.rb +11 -0
  23. data/lib/active_merchant/billing/gateways/trans_first_transaction_express.rb +3 -3
  24. data/lib/active_merchant/billing/gateways/trust_commerce.rb +1 -0
  25. data/lib/active_merchant/billing/gateways/usa_epay_transaction.rb +11 -3
  26. data/lib/active_merchant/billing/gateways/worldpay.rb +51 -2
  27. data/lib/active_merchant/country.rb +1 -1
  28. data/lib/active_merchant/version.rb +1 -1
  29. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0966806ab488c5e7ce61def32caf6d697a360f41e8044398309b58e3318396b0'
4
- data.tar.gz: 9793482a76e42cfab91c5918153f629c66fdfccbaeff49489eb06a9d97d892bb
3
+ metadata.gz: c313ba80bb5c2c04320b78d8b7248c86cb569869f9b9b25b450de62bec97dfde
4
+ data.tar.gz: 8fb0d7c5d581d5407cb68d7a545e8c8a52d377a232cb919804e9218fd4363d48
5
5
  SHA512:
6
- metadata.gz: 7a852cfc08faedf04bac009e0facd471e8387882b2e340ba9428bb71c67b8480cedd626ca6a663262f0269d7c42923caec65c8e0e23ad6980346763a4739a234
7
- data.tar.gz: cc5a90ebd00682fda94ab7898529570ca0fce25f23cb1d1049b2055feedbcf1fe172ec0fed2712f9a1b04ee4c18747297f389079fae0ca281f3110aa68bcad0a
6
+ metadata.gz: d583c8da5c326c3c3e12589f4fa37ed10243d3be91098436e085bf1ccba255a6ae8e27cbe657d022d5740bbb03b0097803ae10ec8c85c327b436f7c3e33fccb4
7
+ data.tar.gz: cac4ea1716084ca36cd0e2a66f3940e34728c0e4731c85b5c569bf7b0aa0e56097c2b71b56e3c0fef5c1a004e8a014abdc1ed448e0ab22201c3aaca80c39cec1
data/CHANGELOG CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  == HEAD
4
4
 
5
+ == Version 1.91.0 (February 22, 2019)
6
+ * WorldPay: Pull CVC and AVS Result from Response [nfarve] #3106
7
+ * Worldpay: Add AVS and CVC Mapping [nfarve] #3107
8
+ * Paymentez: Fixes extra_params field [molbrown] #3108
9
+ * Improved support for account_type using Check class's account_type instead [lancecarlson] #3097
10
+ * USA Epay: Allow quantity to be passed and check custom fields [lancecarlson] #3090
11
+ * Fix usaepay transaction invoice [lancecarlson] #3093
12
+ * Adyen: Handles blank state address field [molbrown] #3113
13
+ * Braintree: Send all country fields [curiousepic] #3112
14
+ * Braintree: Account for empty string countries [curiousepic] #3115
15
+ * Orbital: Support for stored credentials framework [jknipp] #3117
16
+ * Openpay: Fix for marking successful transaction(s) as failed [jknipp] #3121
17
+ * Braintree: Adds support for transaction_source [molbrown] #3120
18
+ * Moneris: Remove redundant card on file guard clause [davidsantoso] #3123
19
+ * Switch order of Romania country codes [molbrown] #3125
20
+ * Blue Snap: Supports Level 2/3 data [molbrown] #3126
21
+ * Blue Snap: Support personal_identification_number [jknipp] #3128
22
+ * ProPay: Send 9 digit zip code without dash [molbrown] #3129
23
+ * Adyen: Extend AVS code mappings [therufs] #3119
24
+ * NMI: Add customer id to authorization on store [curiousepic] #3130
25
+ * Trans First Express: Don't pass blank name field [curiousepic] #3133
26
+ * TrustCommerce: Send full name on ACH transactions [jknipp] #3132
27
+ * Qvalent: Map CVV Result to responses [curiousepic] #3135
28
+ * Card Connect: Handle 401s as responses [curiousepic] #3137
29
+ * Worldpay: Introduce normalized stored credential options [davidsantoso] #3134
30
+ * Worldpay: Adjust use of normalized stored credentials hash [davidsantoso] #3139
31
+ * Adyen: Enable Dynamic 3DS [molbrown] #3138
32
+ * Fat Zebra: Support voids [curiousepic] #3142
33
+ * Blue Snap: Support ACH/ECP payments [jknipp] #3143
34
+ * Blue Snap: Fix Card-on-File field typo [jknipp] #3143
35
+ * Add Bambora gateway [InfraRuby] #3145
36
+ * Bambora Asia-Pacific: Updates Gateway [molbrown] #3145
37
+ * PaymentExpress: Support ClientInfo field [jknipp] #3131
38
+ * Pin Payments: Concatenate card and customer tokens when storing card [therufs] #3144
39
+ * Update Discover regex to allow card numbers longer than 16 digits [prashcr] #3146
40
+ * Merrco partial refunds fix [payfirma1] #3141
41
+
5
42
  == Version 1.90.0 (January 8, 2019)
6
43
  * Mercado Pago: Support "gateway" processing mode [curiousepic] #3087
7
44
  * Braintree: Update gem to latest version [curiousepic] #3091
@@ -12,6 +49,7 @@
12
49
  * Braintree Blue: Refactor line_items field [curiousepic] #3100
13
50
  * TrustCommerce: Use `application_id` [nfarve] #3103
14
51
  * Stripe: Add 3DS Support [nfarve] #3086
52
+ * Cecabank: Append error text to message [therufs] #3127
15
53
 
16
54
  == Version 1.89.0 (December 17, 2018)
17
55
  * Worldpay: handle Visa and MasterCard payouts differently [bpollack] #3068
data/README.md CHANGED
@@ -86,12 +86,13 @@ For more in-depth documentation and tutorials, see [GettingStarted.md](GettingSt
86
86
 
87
87
  ## Supported Payment Gateways
88
88
 
89
- The [ActiveMerchant Wiki](http://github.com/activemerchant/active_merchant/wikis) contains a [table of features supported by each gateway](http://github.com/activemerchant/active_merchant/wikis/gatewayfeaturematrix).
89
+ The [ActiveMerchant Wiki](https://github.com/activemerchant/active_merchant/wikis) contains a [table of features supported by each gateway](https://github.com/activemerchant/active_merchant/wiki/Gateway-Feature-Matrix).
90
90
 
91
91
  * [Authorize.Net CIM](http://www.authorize.net/) - US
92
92
  * [Authorize.Net](http://www.authorize.net/) - AD, AT, AU, BE, BG, CA, CH, CY, CZ, DE, DK, ES, FI, FR, GB, GB, GI, GR, HU, IE, IT, LI, LU, MC, MT, NL, NO, PL, PT, RO, SE, SI, SK, SM, TR, US, VA
93
93
  * [Axcess MS](http://www.axcessms.com/) - AD, AT, BE, BG, BR, CA, CH, CY, CZ, DE, DK, EE, ES, FI, FO, FR, GB, GI, GR, HR, HU, IE, IL, IM, IS, IT, LI, LT, LU, LV, MC, MT, MX, NL, NO, PL, PT, RO, RU, SE, SI, SK, TR, US, VA
94
94
  * [Balanced](https://www.balancedpayments.com/) - US
95
+ * [Bambora Asia-Pacific](http://www.bambora.com/) - AU, NZ
95
96
  * [Bank Frick](http://www.bankfrickacquiring.com/) - LI, US
96
97
  * [Banwire](http://www.banwire.com/) - MX
97
98
  * [Barclays ePDQ Extra Plus](http://www.barclaycard.co.uk/business/accepting-payments/epdq-ecomm/) - GB
@@ -192,7 +193,7 @@ The [ActiveMerchant Wiki](http://github.com/activemerchant/active_merchant/wikis
192
193
  * [Paystation](http://paystation.co.nz) - NZ
193
194
  * [Pay Way](http://www.payway.com.au) - AU
194
195
  * [PayU India](https://www.payu.in/) - IN
195
- * [Pin Payments](http://www.pin.net.au/) - AU
196
+ * [Pin Payments](http://www.pinpayments.com/) - AU
196
197
  * [Plug'n Pay](http://www.plugnpay.com/) - US
197
198
  * [Psigate](http://www.psigate.com/) - CA
198
199
  * [PSL Payment Solutions](http://www.paymentsolutionsltd.com/) - GB
@@ -5,7 +5,7 @@ module ActiveMerchant #:nodoc:
5
5
  CARD_COMPANY_DETECTORS = {
6
6
  'visa' => ->(num) { num =~ /^4\d{12}(\d{3})?(\d{3})?$/ },
7
7
  'master' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), MASTERCARD_RANGES) },
8
- 'discover' => ->(num) { num =~ /^(6011|65\d{2}|64[4-9]\d)\d{12}|(62\d{14})$/ },
8
+ 'discover' => ->(num) { num =~ /^(6011|65\d{2}|64[4-9]\d)\d{12,15}|(62\d{14,17})$/ },
9
9
  'american_express' => ->(num) { num =~ /^3[47]\d{13}$/ },
10
10
  'diners_club' => ->(num) { num =~ /^3(0[0-5]|[68]\d)\d{11}$/ },
11
11
  'jcb' => ->(num) { num =~ /^35(28|29|[3-8]\d)\d{12}$/ },
@@ -33,7 +33,7 @@ module ActiveMerchant #:nodoc:
33
33
  end
34
34
 
35
35
  def purchase(money, payment, options={})
36
- if options[:execute_threed]
36
+ if options[:execute_threed] || options[:threed_dynamic]
37
37
  authorize(money, payment, options)
38
38
  else
39
39
  MultiResponse.run do |r|
@@ -52,7 +52,7 @@ module ActiveMerchant #:nodoc:
52
52
  add_shopper_interaction(post, payment, options)
53
53
  add_address(post, options)
54
54
  add_installments(post, options) if options[:installments]
55
- add_3ds(post, options) if options[:execute_threed]
55
+ add_3ds(post, options)
56
56
  commit('authorise', post)
57
57
  end
58
58
 
@@ -110,24 +110,28 @@ module ActiveMerchant #:nodoc:
110
110
 
111
111
  AVS_MAPPING = {
112
112
  '0' => 'R', # Unknown
113
- '1' => 'A', # Address matches, postal code doesn't
114
- '2' => 'N', # Neither postal code nor address match
115
- '3' => 'R', # AVS unavailable
116
- '4' => 'E', # AVS not supported for this card type
117
- '5' => 'U', # No AVS data provided
118
- '6' => 'Z', # Postal code matches, address doesn't match
119
- '7' => 'D', # Both postal code and address match
120
- '8' => 'U', # Address not checked, postal code unknown
121
- '9' => 'B', # Address matches, postal code unknown
122
- '10' => 'N', # Address doesn't match, postal code unknown
123
- '11' => 'U', # Postal code not checked, address unknown
124
- '12' => 'B', # Address matches, postal code not checked
125
- '13' => 'U', # Address doesn't match, postal code not checked
126
- '14' => 'P', # Postal code matches, address unknown
127
- '15' => 'P', # Postal code matches, address not checked
128
- '16' => 'N', # Postal code doesn't match, address unknown
113
+ '1' => 'A', # Address matches, postal code doesn't
114
+ '2' => 'N', # Neither postal code nor address match
115
+ '3' => 'R', # AVS unavailable
116
+ '4' => 'E', # AVS not supported for this card type
117
+ '5' => 'U', # No AVS data provided
118
+ '6' => 'Z', # Postal code matches, address doesn't match
119
+ '7' => 'D', # Both postal code and address match
120
+ '8' => 'U', # Address not checked, postal code unknown
121
+ '9' => 'B', # Address matches, postal code unknown
122
+ '10' => 'N', # Address doesn't match, postal code unknown
123
+ '11' => 'U', # Postal code not checked, address unknown
124
+ '12' => 'B', # Address matches, postal code not checked
125
+ '13' => 'U', # Address doesn't match, postal code not checked
126
+ '14' => 'P', # Postal code matches, address unknown
127
+ '15' => 'P', # Postal code matches, address not checked
128
+ '16' => 'N', # Postal code doesn't match, address unknown
129
129
  '17' => 'U', # Postal code doesn't match, address not checked
130
- '18' => 'I' # Neither postal code nor address were checked
130
+ '18' => 'I', # Neither postal code nor address were checked
131
+ '20' => 'V', # Name, address and postal code matches.
132
+ '23' => 'F', # Postal code matches, name doesn't match.
133
+ '24' => 'H', # Both postal code and address matches, name doesn't match.
134
+ '25' => 'T' # Address matches, name doesn't match.
131
135
  }
132
136
 
133
137
  CVC_MAPPING = {
@@ -187,7 +191,7 @@ module ActiveMerchant #:nodoc:
187
191
  post[:card][:billingAddress][:houseNumberOrName] = address[:address2] || 'N/A'
188
192
  post[:card][:billingAddress][:postalCode] = address[:zip] if address[:zip]
189
193
  post[:card][:billingAddress][:city] = address[:city] || 'N/A'
190
- post[:card][:billingAddress][:stateOrProvince] = address[:state] if address[:state]
194
+ post[:card][:billingAddress][:stateOrProvince] = address[:state] || 'N/A'
191
195
  post[:card][:billingAddress][:country] = address[:country] if address[:country]
192
196
  end
193
197
  end
@@ -272,8 +276,9 @@ module ActiveMerchant #:nodoc:
272
276
  end
273
277
 
274
278
  def add_3ds(post, options)
275
- post[:additionalData] = { executeThreeD: 'true' }
279
+ return unless options[:execute_threed] || options[:threed_dynamic]
276
280
  post[:browserInfo] = { userAgent: options[:user_agent], acceptHeader: options[:accept_header] }
281
+ post[:additionalData] = { executeThreeD: 'true' } if options[:execute_threed]
277
282
  end
278
283
 
279
284
  def parse(body)
@@ -0,0 +1,186 @@
1
+ require 'nokogiri'
2
+
3
+ module ActiveMerchant #:nodoc:
4
+ module Billing #:nodoc:
5
+ class BamboraApacGateway < Gateway
6
+ self.live_url = 'https://www.bambora.co.nz/interface/api/dts.asmx'
7
+ self.test_url = 'https://demo.bambora.co.nz/interface/api/dts.asmx'
8
+
9
+ self.supported_countries = ['AU', 'NZ']
10
+ self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :jcb]
11
+
12
+ self.homepage_url = 'http://www.bambora.com/'
13
+ self.display_name = 'Bambora Asia-Pacific'
14
+
15
+ self.money_format = :cents
16
+
17
+ STANDARD_ERROR_CODE_MAPPING = {
18
+ '05' => STANDARD_ERROR_CODE[:card_declined],
19
+ '06' => STANDARD_ERROR_CODE[:processing_error],
20
+ '14' => STANDARD_ERROR_CODE[:invalid_number],
21
+ '54' => STANDARD_ERROR_CODE[:expired_card],
22
+ }
23
+
24
+ def initialize(options={})
25
+ requires!(options, :username, :password)
26
+ super
27
+ end
28
+
29
+ def purchase(money, payment, options={})
30
+ commit('SubmitSinglePayment') do |xml|
31
+ xml.Transaction do
32
+ xml.CustRef options[:order_id]
33
+ add_amount(xml, money)
34
+ xml.TrnType '1'
35
+ add_credit_card(xml, payment)
36
+ add_credentials(xml, options)
37
+ xml.TrnSource options[:ip]
38
+ end
39
+ end
40
+ end
41
+
42
+ def authorize(money, payment, options={})
43
+ commit('SubmitSinglePayment') do |xml|
44
+ xml.Transaction do
45
+ xml.CustRef options[:order_id]
46
+ add_amount(xml, money)
47
+ xml.TrnType '2'
48
+ add_credit_card(xml, payment)
49
+ add_credentials(xml, options)
50
+ xml.TrnSource options[:ip]
51
+ end
52
+ end
53
+ end
54
+
55
+ def capture(money, authorization, options={})
56
+ commit('SubmitSingleCapture') do |xml|
57
+ xml.Capture do
58
+ xml.Receipt authorization
59
+ add_amount(xml, money)
60
+ add_credentials(xml, options)
61
+ end
62
+ end
63
+ end
64
+
65
+ def refund(money, authorization, options={})
66
+ commit('SubmitSingleRefund') do |xml|
67
+ xml.Refund do
68
+ xml.Receipt authorization
69
+ add_amount(xml, money)
70
+ add_credentials(xml, options)
71
+ end
72
+ end
73
+ end
74
+
75
+ def void(money, authorization, options={})
76
+ commit('SubmitSingleVoid') do |xml|
77
+ xml.Void do
78
+ xml.Receipt authorization
79
+ add_amount(xml, money)
80
+ add_credentials(xml, options)
81
+ end
82
+ end
83
+ end
84
+
85
+ def supports_scrubbing?
86
+ true
87
+ end
88
+
89
+ def scrub(transcript)
90
+ transcript.
91
+ gsub(%r((<CardNumber>)[^<]+(<))i, '\1[FILTERED]\2').
92
+ gsub(%r((<CVN>)[^<]+(<))i, '\1[FILTERED]\2').
93
+ gsub(%r((<Password>)[^<]+(<))i, '\1[FILTERED]\2')
94
+ end
95
+
96
+ private
97
+
98
+ def add_credentials(xml, options)
99
+ xml.AccountNumber options[:account_number] if options[:account_number]
100
+ xml.Security do
101
+ xml.UserName @options[:username]
102
+ xml.Password @options[:password]
103
+ end
104
+ end
105
+
106
+ def add_amount(xml, money)
107
+ xml.Amount amount(money)
108
+ end
109
+
110
+ def add_credit_card(xml, payment)
111
+ xml.CreditCard :Registered => 'False' do
112
+ xml.CardNumber payment.number
113
+ xml.ExpM format(payment.month, :two_digits)
114
+ xml.ExpY format(payment.year, :four_digits)
115
+ xml.CVN payment.verification_value
116
+ xml.CardHolderName payment.name
117
+ end
118
+ end
119
+
120
+ def parse(body)
121
+ element = Nokogiri::XML(body).root.first_element_child.first_element_child
122
+
123
+ response = {}
124
+ doc = Nokogiri::XML(element)
125
+ doc.root.elements.each do |e|
126
+ response[e.name.underscore.to_sym] = e.inner_text
127
+ end
128
+ response
129
+ end
130
+
131
+ def commit(action, &block)
132
+ headers = {
133
+ 'Content-Type' => 'text/xml; charset=utf-8',
134
+ 'SOAPAction' => "http://www.ippayments.com.au/interface/api/dts/#{action}",
135
+ }
136
+ response = parse(ssl_post(commit_url, new_submit_xml(action, &block), headers))
137
+
138
+ Response.new(
139
+ success_from(response),
140
+ message_from(response),
141
+ response,
142
+ authorization: authorization_from(response),
143
+ error_code: error_code_from(response),
144
+ test: test?
145
+ )
146
+ end
147
+
148
+ def new_submit_xml(action)
149
+ xml = Builder::XmlMarkup.new(indent: 2)
150
+ xml.instruct!
151
+ xml.soap :Envelope, 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', 'xmlns:soap' => 'http://schemas.xmlsoap.org/soap/envelope/' do
152
+ xml.soap :Body do
153
+ xml.__send__(action, 'xmlns' => 'http://www.ippayments.com.au/interface/api/dts') do
154
+ xml.trnXML do
155
+ inner_xml = Builder::XmlMarkup.new(indent: 2)
156
+ yield(inner_xml)
157
+ xml.cdata!(inner_xml.target!)
158
+ end
159
+ end
160
+ end
161
+ end
162
+ xml.target!
163
+ end
164
+
165
+ def commit_url
166
+ test? ? test_url : live_url
167
+ end
168
+
169
+ def success_from(response)
170
+ response[:response_code] == '0'
171
+ end
172
+
173
+ def error_code_from(response)
174
+ STANDARD_ERROR_CODE_MAPPING[response[:declined_code]]
175
+ end
176
+
177
+ def message_from(response)
178
+ response[:declined_message]
179
+ end
180
+
181
+ def authorization_from(response)
182
+ response[:receipt]
183
+ end
184
+ end
185
+ end
186
+ end
@@ -5,7 +5,7 @@ module ActiveMerchant
5
5
  class BlueSnapGateway < Gateway
6
6
  self.test_url = 'https://sandbox.bluesnap.com/services/2'
7
7
  self.live_url = 'https://ws.bluesnap.com/services/2'
8
- self.supported_countries = %w(US CA GB AT BE BG HR CY CZ DK EE FI FR DE GR HU IE IT LV LT LU MT NL PL PT RO SK SI ES SE)
8
+ self.supported_countries = %w(US CA GB AT BE BG HR CY CZ DK EE FI FR DE GR HU IE IT LV LT LU MT NL PL PT RO SK SI ES SE AR BO BR BZ CL CO CR DO EC GF GP GT HN HT MF MQ MX NI PA PE PR PY SV UY VE)
9
9
 
10
10
  self.default_currency = 'USD'
11
11
  self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :diners_club, :maestro]
@@ -59,14 +59,27 @@ module ActiveMerchant
59
59
  'line1: N, zip: N, name: N' => 'N',
60
60
  }
61
61
 
62
+ BANK_ACCOUNT_TYPE_MAPPING = {
63
+ 'personal_checking' => 'CONSUMER_CHECKING',
64
+ 'personal_savings' => 'CONSUMER_SAVINGS',
65
+ 'business_checking' => 'CORPORATE_CHECKING',
66
+ 'business_savings' => 'CORPORATE_SAVINGS'
67
+ }
68
+
62
69
  def initialize(options={})
63
70
  requires!(options, :api_username, :api_password)
64
71
  super
65
72
  end
66
73
 
67
74
  def purchase(money, payment_method, options={})
68
- commit(:purchase) do |doc|
69
- add_auth_purchase(doc, money, payment_method, options)
75
+ payment_method_details = PaymentMethodDetails.new(payment_method)
76
+
77
+ commit(:purchase, :post, payment_method_details) do |doc|
78
+ if payment_method_details.alt_transaction?
79
+ add_alt_transaction_purchase(doc, money, payment_method_details, options)
80
+ else
81
+ add_auth_purchase(doc, money, payment_method, options)
82
+ end
70
83
  end
71
84
  end
72
85
 
@@ -102,18 +115,33 @@ module ActiveMerchant
102
115
  authorize(0, payment_method, options)
103
116
  end
104
117
 
105
- def store(credit_card, options = {})
106
- commit(:store) do |doc|
107
- add_personal_info(doc, credit_card, options)
118
+ def store(payment_method, options = {})
119
+ payment_method_details = PaymentMethodDetails.new(payment_method)
120
+
121
+ commit(:store, :post, payment_method_details) do |doc|
122
+ add_personal_info(doc, payment_method, options)
123
+ add_echeck_company(doc, payment_method) if payment_method_details.check?
108
124
  doc.send('payment-sources') do
109
- doc.send('credit-card-info') do
110
- add_credit_card(doc, credit_card)
111
- end
125
+ payment_method_details.check? ? store_echeck(doc, payment_method) : store_credit_card(doc, payment_method)
112
126
  end
113
127
  add_order(doc, options)
114
128
  end
115
129
  end
116
130
 
131
+ def store_credit_card(doc, payment_method)
132
+ doc.send('credit-card-info') do
133
+ add_credit_card(doc, payment_method)
134
+ end
135
+ end
136
+
137
+ def store_echeck(doc, payment_method)
138
+ doc.send('ecp-info') do
139
+ doc.send('ecp') do
140
+ add_echeck(doc, payment_method)
141
+ end
142
+ end
143
+ end
144
+
117
145
  def verify_credentials
118
146
  begin
119
147
  ssl_get(url.to_s, headers)
@@ -132,7 +160,9 @@ module ActiveMerchant
132
160
  transcript.
133
161
  gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
134
162
  gsub(%r((<card-number>).+(</card-number>)), '\1[FILTERED]\2').
135
- gsub(%r((<security-code>).+(</security-code>)), '\1[FILTERED]\2')
163
+ gsub(%r((<security-code>).+(</security-code>)), '\1[FILTERED]\2').
164
+ gsub(%r((<(?:public-)?account-number>).+(</(?:public-)?account-number>)), '\1[FILTERED]\2').
165
+ gsub(%r((<(?:public-)?routing-number>).+(</(?:public-)?routing-number>)), '\1[FILTERED]\2')
136
166
  end
137
167
 
138
168
  private
@@ -140,11 +170,9 @@ module ActiveMerchant
140
170
  def add_auth_purchase(doc, money, payment_method, options)
141
171
  doc.send('recurring-transaction', options[:recurring] ? 'RECURRING' : 'ECOMMERCE')
142
172
  add_order(doc, options)
143
- doc.send('storeCard', options[:store_card] || false)
173
+ doc.send('store-card', options[:store_card] || false)
144
174
  add_amount(doc, money, options)
145
- doc.send('transaction-fraud-info') do
146
- doc.send('shopper-ip-address', options[:ip]) if options[:ip]
147
- end
175
+ add_fraud_info(doc, options)
148
176
 
149
177
  if payment_method.is_a?(String)
150
178
  doc.send('vaulted-shopper-id', payment_method)
@@ -161,9 +189,10 @@ module ActiveMerchant
161
189
  doc.currency(options[:currency] || currency(money))
162
190
  end
163
191
 
164
- def add_personal_info(doc, credit_card, options)
165
- doc.send('first-name', credit_card.first_name)
166
- doc.send('last-name', credit_card.last_name)
192
+ def add_personal_info(doc, payment_method, options)
193
+ doc.send('first-name', payment_method.first_name)
194
+ doc.send('last-name', payment_method.last_name)
195
+ doc.send('personal-identification-number', options[:personal_identification_number]) if options[:personal_identification_number]
167
196
  doc.email(options[:email]) if options[:email]
168
197
  add_address(doc, options)
169
198
  end
@@ -191,6 +220,7 @@ module ActiveMerchant
191
220
  doc.send('merchant-transaction-id', truncate(options[:order_id], 50)) if options[:order_id]
192
221
  doc.send('soft-descriptor', options[:soft_descriptor]) if options[:soft_descriptor]
193
222
  add_description(doc, options[:description]) if options[:description]
223
+ add_level_3_data(doc, options)
194
224
  end
195
225
 
196
226
  def add_address(doc, options)
@@ -204,10 +234,92 @@ module ActiveMerchant
204
234
  doc.zip(address[:zip]) if address[:zip]
205
235
  end
206
236
 
237
+ def add_level_3_data(doc, options)
238
+ return unless options[:customer_reference_number]
239
+ doc.send('level-3-data') do
240
+ send_when_present(doc, :customer_reference_number, options)
241
+ send_when_present(doc, :sales_tax_amount, options)
242
+ send_when_present(doc, :freight_amount, options)
243
+ send_when_present(doc, :duty_amount, options)
244
+ send_when_present(doc, :destination_zip_code, options)
245
+ send_when_present(doc, :destination_country_code, options)
246
+ send_when_present(doc, :ship_from_zip_code, options)
247
+ send_when_present(doc, :discount_amount, options)
248
+ send_when_present(doc, :tax_amount, options)
249
+ send_when_present(doc, :tax_rate, options)
250
+ add_level_3_data_items(doc, options[:level_3_data_items]) if options[:level_3_data_items]
251
+ end
252
+ end
253
+
254
+ def send_when_present(doc, options_key, options, xml_element_name = nil)
255
+ return unless options[options_key]
256
+ xml_element_name ||= options_key.to_s
257
+
258
+ doc.send(xml_element_name.dasherize, options[options_key])
259
+ end
260
+
261
+ def add_level_3_data_items(doc, items)
262
+ items.each do |item|
263
+ doc.send('level-3-data-item') do
264
+ item.each do |key, value|
265
+ key = key.to_s.dasherize
266
+ doc.send(key, value)
267
+ end
268
+ end
269
+ end
270
+ end
271
+
207
272
  def add_authorization(doc, authorization)
208
273
  doc.send('transaction-id', authorization)
209
274
  end
210
275
 
276
+ def add_fraud_info(doc, options)
277
+ doc.send('transaction-fraud-info') do
278
+ doc.send('shopper-ip-address', options[:ip]) if options[:ip]
279
+ end
280
+ end
281
+
282
+ def add_alt_transaction_purchase(doc, money, payment_method_details, options)
283
+ doc.send('merchant-transaction-id', truncate(options[:order_id], 50)) if options[:order_id]
284
+ doc.send('soft-descriptor', options[:soft_descriptor]) if options[:soft_descriptor]
285
+ add_amount(doc, money, options)
286
+
287
+ vaulted_shopper_id = payment_method_details.vaulted_shopper_id
288
+ doc.send('vaulted-shopper-id', vaulted_shopper_id) if vaulted_shopper_id
289
+
290
+ if payment_method_details.check?
291
+ add_echeck_transaction(doc, payment_method_details.payment_method, options, vaulted_shopper_id.present?)
292
+ end
293
+
294
+ add_fraud_info(doc, options)
295
+ add_description(doc, options)
296
+ end
297
+
298
+ def add_echeck_transaction(doc, check, options, vaulted_shopper)
299
+ unless vaulted_shopper
300
+ doc.send('payer-info') do
301
+ add_personal_info(doc, check, options)
302
+ add_echeck_company(doc, check)
303
+ end
304
+ end
305
+
306
+ doc.send('ecp-transaction') do
307
+ add_echeck(doc, check) unless vaulted_shopper
308
+ end
309
+
310
+ doc.send('authorized-by-shopper', options[:authorized_by_shopper])
311
+ end
312
+
313
+ def add_echeck_company(doc, check)
314
+ doc.send('company-name', truncate(check.name, 50)) if check.account_holder_type = 'business'
315
+ end
316
+
317
+ def add_echeck(doc, check)
318
+ doc.send('account-number', check.account_number)
319
+ doc.send('routing-number', check.routing_number)
320
+ doc.send('account-type', BANK_ACCOUNT_TYPE_MAPPING["#{check.account_holder_type}_#{check.account_type}"])
321
+ end
322
+
211
323
  def parse(response)
212
324
  return bad_authentication_response if response.code.to_i == 401
213
325
  return forbidden_response(response.body) if response.code.to_i == 403
@@ -236,15 +348,15 @@ module ActiveMerchant
236
348
  end
237
349
  end
238
350
 
239
- def api_request(action, request, verb)
240
- ssl_request(verb, url(action), request, headers)
351
+ def api_request(action, request, verb, payment_method_details)
352
+ ssl_request(verb, url(action, payment_method_details), request, headers)
241
353
  rescue ResponseError => e
242
354
  e.response
243
355
  end
244
356
 
245
- def commit(action, verb = :post)
246
- request = build_xml_request(action) { |doc| yield(doc) }
247
- response = api_request(action, request, verb)
357
+ def commit(action, verb = :post, payment_method_details = PaymentMethodDetails.new())
358
+ request = build_xml_request(action, payment_method_details) { |doc| yield(doc) }
359
+ response = api_request(action, request, verb, payment_method_details)
248
360
  parsed = parse(response)
249
361
 
250
362
  succeeded = success_from(action, response)
@@ -252,7 +364,7 @@ module ActiveMerchant
252
364
  succeeded,
253
365
  message_from(succeeded, parsed),
254
366
  parsed,
255
- authorization: authorization_from(action, parsed),
367
+ authorization: authorization_from(action, parsed, payment_method_details),
256
368
  avs_result: avs_result(parsed),
257
369
  cvv_result: cvv_result(parsed),
258
370
  error_code: error_code_from(parsed),
@@ -260,9 +372,9 @@ module ActiveMerchant
260
372
  )
261
373
  end
262
374
 
263
- def url(action = nil)
375
+ def url(action = nil, payment_method_details = PaymentMethodDetails.new())
264
376
  base = test? ? test_url : live_url
265
- resource = action == :store ? 'vaulted-shoppers' : 'transactions'
377
+ resource = action == :store ? 'vaulted-shoppers' : payment_method_details.resource_url
266
378
  "#{base}/#{resource}"
267
379
  end
268
380
 
@@ -287,13 +399,15 @@ module ActiveMerchant
287
399
  parsed_response['description']
288
400
  end
289
401
 
290
- def authorization_from(action, parsed_response)
291
- action == :store ? vaulted_shopper_id(parsed_response) : parsed_response['transaction-id']
402
+ def authorization_from(action, parsed_response, payment_method_details)
403
+ action == :store ? vaulted_shopper_id(parsed_response, payment_method_details) : parsed_response['transaction-id']
292
404
  end
293
405
 
294
- def vaulted_shopper_id(parsed_response)
406
+ def vaulted_shopper_id(parsed_response, payment_method_details)
295
407
  return nil unless parsed_response['content-location-header']
296
- parsed_response['content-location-header'].split('/').last
408
+ vaulted_shopper_id = parsed_response['content-location-header'].split('/').last
409
+ vaulted_shopper_id += "|#{payment_method_details.payment_method_type}" if payment_method_details.alt_transaction?
410
+ vaulted_shopper_id
297
411
  end
298
412
 
299
413
  def error_code_from(parsed_response)
@@ -306,8 +420,8 @@ module ActiveMerchant
306
420
  }
307
421
  end
308
422
 
309
- def root_element(action)
310
- action == :store ? 'vaulted-shopper' : 'card-transaction'
423
+ def root_element(action, payment_method_details)
424
+ action == :store ? 'vaulted-shopper' : payment_method_details.root_element
311
425
  end
312
426
 
313
427
  def headers
@@ -317,10 +431,10 @@ module ActiveMerchant
317
431
  }
318
432
  end
319
433
 
320
- def build_xml_request(action)
434
+ def build_xml_request(action, payment_method_details)
321
435
  builder = Nokogiri::XML::Builder.new
322
- builder.__send__(root_element(action), root_attributes) do |doc|
323
- doc.send('card-transaction-type', TRANSACTIONS[action]) if TRANSACTIONS[action]
436
+ builder.__send__(root_element(action, payment_method_details), root_attributes) do |doc|
437
+ doc.send('card-transaction-type', TRANSACTIONS[action]) if TRANSACTIONS[action] && !payment_method_details.alt_transaction?
324
438
  yield(doc)
325
439
  end
326
440
  builder.doc.root.to_xml
@@ -343,5 +457,46 @@ module ActiveMerchant
343
457
  { 'description' => body }
344
458
  end
345
459
  end
460
+
461
+ class PaymentMethodDetails
462
+ attr_reader :payment_method, :vaulted_shopper_id, :payment_method_type
463
+
464
+ def initialize(payment_method = nil)
465
+ @payment_method = payment_method
466
+ @payment_method_type = nil
467
+ parse(payment_method)
468
+ end
469
+
470
+ def check?
471
+ @payment_method.is_a?(Check) || @payment_method_type == 'check'
472
+ end
473
+
474
+ def alt_transaction?
475
+ check?
476
+ end
477
+
478
+ def root_element
479
+ alt_transaction? ? 'alt-transaction' : 'card-transaction'
480
+ end
481
+
482
+ def resource_url
483
+ alt_transaction? ? 'alt-transactions' : 'transactions'
484
+ end
485
+
486
+ private
487
+
488
+ def parse(payment_method)
489
+ return unless payment_method
490
+
491
+ if payment_method.is_a?(String)
492
+ @vaulted_shopper_id, payment_method_type = payment_method.split('|')
493
+ @payment_method_type = payment_method_type if payment_method_type.present?
494
+ elsif payment_method.is_a?(Check)
495
+ @payment_method_type = payment_method.type
496
+ else
497
+ @payment_method_type = 'credit_card'
498
+ end
499
+ end
500
+ end
346
501
  end
347
502
  end