activemerchant 1.44.1 → 1.45.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +1 -3
  3. data.tar.gz.sig +0 -0
  4. data/CHANGELOG +48 -0
  5. data/CONTRIBUTORS +12 -0
  6. data/README.md +15 -5
  7. data/lib/active_merchant/billing.rb +2 -0
  8. data/lib/active_merchant/billing/apple_pay_payment_token.rb +22 -0
  9. data/lib/active_merchant/billing/gateway.rb +36 -4
  10. data/lib/active_merchant/billing/gateways/adyen.rb +6 -2
  11. data/lib/active_merchant/billing/gateways/authorize_net.rb +332 -255
  12. data/lib/active_merchant/billing/gateways/bank_frick.rb +225 -0
  13. data/lib/active_merchant/billing/gateways/bogus.rb +9 -9
  14. data/lib/active_merchant/billing/gateways/borgun.rb +0 -1
  15. data/lib/active_merchant/billing/gateways/braintree_blue.rb +8 -0
  16. data/lib/active_merchant/billing/gateways/cashnet.rb +17 -10
  17. data/lib/active_merchant/billing/gateways/checkout.rb +213 -0
  18. data/lib/active_merchant/billing/gateways/conekta.rb +1 -1
  19. data/lib/active_merchant/billing/gateways/cyber_source.rb +1 -1
  20. data/lib/active_merchant/billing/gateways/elavon.rb +3 -3
  21. data/lib/active_merchant/billing/gateways/eway_rapid.rb +114 -13
  22. data/lib/active_merchant/billing/gateways/finansbank.rb +1 -1
  23. data/lib/active_merchant/billing/gateways/global_transport.rb +183 -0
  24. data/lib/active_merchant/billing/gateways/hps.rb +27 -20
  25. data/lib/active_merchant/billing/gateways/iats_payments.rb +68 -35
  26. data/lib/active_merchant/billing/gateways/litle.rb +36 -1
  27. data/lib/active_merchant/billing/gateways/merchant_one.rb +0 -1
  28. data/lib/active_merchant/billing/gateways/merchant_ware.rb +8 -4
  29. data/lib/active_merchant/billing/gateways/mercury.rb +17 -10
  30. data/lib/active_merchant/billing/gateways/moneris.rb +11 -6
  31. data/lib/active_merchant/billing/gateways/moneris_us.rb +126 -33
  32. data/lib/active_merchant/billing/gateways/money_movers.rb +0 -1
  33. data/lib/active_merchant/billing/gateways/net_registry.rb +6 -1
  34. data/lib/active_merchant/billing/gateways/network_merchants.rb +5 -5
  35. data/lib/active_merchant/billing/gateways/nmi.rb +241 -5
  36. data/lib/active_merchant/billing/gateways/openpay.rb +1 -0
  37. data/lib/active_merchant/billing/gateways/optimal_payment.rb +6 -1
  38. data/lib/active_merchant/billing/gateways/orbital.rb +6 -4
  39. data/lib/active_merchant/billing/gateways/pay_junction.rb +9 -5
  40. data/lib/active_merchant/billing/gateways/payex.rb +19 -9
  41. data/lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb +2 -2
  42. data/lib/active_merchant/billing/gateways/paypal/paypal_express_response.rb +4 -0
  43. data/lib/active_merchant/billing/gateways/payscout.rb +0 -2
  44. data/lib/active_merchant/billing/gateways/pin.rb +1 -1
  45. data/lib/active_merchant/billing/gateways/psigate.rb +1 -2
  46. data/lib/active_merchant/billing/gateways/redsys.rb +37 -40
  47. data/lib/active_merchant/billing/gateways/secure_pay.rb +181 -9
  48. data/lib/active_merchant/billing/gateways/stripe.rb +106 -31
  49. data/lib/active_merchant/billing/gateways/tns.rb +227 -0
  50. data/lib/active_merchant/billing/gateways/usa_epay_transaction.rb +38 -10
  51. data/lib/active_merchant/billing/gateways/webpay.rb +14 -0
  52. data/lib/active_merchant/billing/payment_token.rb +21 -0
  53. data/lib/active_merchant/billing/response.rb +2 -1
  54. data/lib/active_merchant/country.rb +6 -1
  55. data/lib/active_merchant/version.rb +1 -1
  56. metadata +8 -3
  57. metadata.gz.sig +0 -0
  58. data/lib/active_merchant/billing/gateways/samurai.rb +0 -130
@@ -91,6 +91,7 @@ module ActiveMerchant #:nodoc:
91
91
  post[:method] = 'card'
92
92
  post[:description] = options[:description]
93
93
  post[:order_id] = options[:order_id]
94
+ post[:device_session_id] = options[:device_session_id]
94
95
  add_creditcard(post, creditcard, options)
95
96
  post
96
97
  end
@@ -96,7 +96,7 @@ module ActiveMerchant #:nodoc:
96
96
  else
97
97
  raise 'Unknown Action'
98
98
  end
99
- txnRequest = URI.encode(xml)
99
+ txnRequest = escape_uri(xml)
100
100
  response = parse(ssl_post(test? ? self.test_url : self.live_url, "txnMode=#{action}&txnRequest=#{txnRequest}"))
101
101
 
102
102
  Response.new(successful?(response), message_from(response), hash_from_xml(response),
@@ -107,6 +107,11 @@ module ActiveMerchant #:nodoc:
107
107
  )
108
108
  end
109
109
 
110
+ # The upstream is picky and so we can't use CGI.escape like we want to
111
+ def escape_uri(uri)
112
+ URI::DEFAULT_PARSER.escape(uri)
113
+ end
114
+
110
115
  def successful?(response)
111
116
  REXML::XPath.first(response, '//decision').text == 'ACCEPTED' rescue false
112
117
  end
@@ -189,7 +189,7 @@ module ActiveMerchant #:nodoc:
189
189
  add_creditcard(xml, creditcard, options[:currency])
190
190
  add_address(xml, creditcard, options)
191
191
  if @options[:customer_profiles]
192
- add_customer_data(xml, options)
192
+ add_customer_data(xml, creditcard, options)
193
193
  add_managed_billing(xml, options)
194
194
  end
195
195
  end
@@ -202,7 +202,7 @@ module ActiveMerchant #:nodoc:
202
202
  add_creditcard(xml, creditcard, options[:currency])
203
203
  add_address(xml, creditcard, options)
204
204
  if @options[:customer_profiles]
205
- add_customer_data(xml, options)
205
+ add_customer_data(xml, creditcard, options)
206
206
  add_managed_billing(xml, options)
207
207
  end
208
208
  end
@@ -294,12 +294,14 @@ module ActiveMerchant #:nodoc:
294
294
  authorization.split(';')
295
295
  end
296
296
 
297
- def add_customer_data(xml, options)
297
+ def add_customer_data(xml, creditcard, options)
298
298
  if options[:profile_txn]
299
299
  xml.tag! :CustomerRefNum, options[:customer_ref_num]
300
300
  else
301
301
  if options[:customer_ref_num]
302
- xml.tag! :CustomerProfileFromOrderInd, USE_CUSTOMER_REF_NUM
302
+ if creditcard
303
+ xml.tag! :CustomerProfileFromOrderInd, USE_CUSTOMER_REF_NUM
304
+ end
303
305
  xml.tag! :CustomerRefNum, options[:customer_ref_num]
304
306
  else
305
307
  xml.tag! :CustomerProfileFromOrderInd, AUTO_GENERATE
@@ -297,11 +297,15 @@ module ActiveMerchant #:nodoc:
297
297
 
298
298
  # add fields for credit card
299
299
  def add_creditcard(params, creditcard)
300
- params[:name] = creditcard.name
301
- params[:number] = creditcard.number
302
- params[:expiration_month] = creditcard.month
303
- params[:expiration_year] = creditcard.year
304
- params[:verification_number] = creditcard.verification_value if creditcard.verification_value?
300
+ if creditcard.respond_to?(:track_data) && creditcard.track_data.present?
301
+ params[:track] = creditcard.track_data
302
+ else
303
+ params[:name] = creditcard.name
304
+ params[:number] = creditcard.number
305
+ params[:expiration_month] = creditcard.month
306
+ params[:expiration_year] = creditcard.year
307
+ params[:verification_number] = creditcard.verification_value if creditcard.verification_value?
308
+ end
305
309
  end
306
310
 
307
311
  # add field for "instant" transaction, using previous transaction id
@@ -1,9 +1,15 @@
1
1
  require "nokogiri"
2
+
2
3
  module ActiveMerchant #:nodoc:
3
4
  module Billing #:nodoc:
4
5
  class PayexGateway < Gateway
5
- self.live_url = 'https://external.payex.com/'
6
- self.test_url = 'https://test-external.payex.com/'
6
+ class_attribute :live_external_url, :test_external_url, :live_confined_url, :test_confined_url
7
+
8
+ self.live_external_url = 'https://external.payex.com/'
9
+ self.test_external_url = 'https://test-external.payex.com/'
10
+
11
+ self.live_confined_url = 'https://confined.payex.com/'
12
+ self.test_confined_url = 'https://test-confined.payex.com/'
7
13
 
8
14
  self.money_format = :cents
9
15
  self.supported_countries = ['DK', 'FI', 'NO', 'SE']
@@ -12,9 +18,6 @@ module ActiveMerchant #:nodoc:
12
18
  self.display_name = 'Payex'
13
19
  self.default_currency = "EUR"
14
20
 
15
- # NOTE: the PurchaseCC uses a different url for test transactions
16
- TEST_CONFINED_URL = 'https://test-confined.payex.com/'
17
-
18
21
  TRANSACTION_STATUS = {
19
22
  sale: '0',
20
23
  initialize: '1',
@@ -206,8 +209,8 @@ module ActiveMerchant #:nodoc:
206
209
  orderRef: order_ref,
207
210
  transactionType: 1, # online payment
208
211
  cardNumber: payment_method.number,
209
- cardNumberExpireMonth: "%02d" % payment_method.month,
210
- cardNumberExpireYear: "%02d" % payment_method.year,
212
+ cardNumberExpireMonth: format(payment_method.month, :two_digits),
213
+ cardNumberExpireYear: format(payment_method.year, :two_digits),
211
214
  cardHolderName: payment_method.name,
212
215
  cardNumberCVC: payment_method.verification_value
213
216
  }
@@ -319,8 +322,15 @@ module ActiveMerchant #:nodoc:
319
322
  end
320
323
 
321
324
  def url_for(soap_action)
322
- base_url = test? ? (soap_action[:confined] ? TEST_CONFINED_URL : test_url) : live_url
323
- File.join(base_url, soap_action[:url])
325
+ File.join(base_url(soap_action), soap_action[:url])
326
+ end
327
+
328
+ def base_url(soap_action)
329
+ if soap_action[:confined]
330
+ test? ? test_confined_url : live_confined_url
331
+ else
332
+ test? ? test_external_url : live_external_url
333
+ end
324
334
  end
325
335
 
326
336
  # this will add a hash to the passed in properties as required by Payex requests
@@ -7,7 +7,7 @@ module ActiveMerchant #:nodoc:
7
7
  URLS = {
8
8
  :test => { :certificate => 'https://api.sandbox.paypal.com/2.0/',
9
9
  :signature => 'https://api-3t.sandbox.paypal.com/2.0/' },
10
- :live => { :certificate => 'https://api-aa.paypal.com/2.0/',
10
+ :live => { :certificate => 'https://api.paypal.com/2.0/',
11
11
  :signature => 'https://api-3t.paypal.com/2.0/' }
12
12
  }
13
13
 
@@ -659,7 +659,7 @@ module ActiveMerchant #:nodoc:
659
659
  end
660
660
 
661
661
  def item_amount(amount, currency_code)
662
- if amount.to_i < 0 && non_fractional_currency?(currency_code)
662
+ if amount.to_i < 0 && Gateway.non_fractional_currency?(currency_code)
663
663
  amount(amount).to_f.floor
664
664
  else
665
665
  localized_amount(amount, currency_code)
@@ -56,6 +56,10 @@ module ActiveMerchant #:nodoc:
56
56
  'name' => shipping['ShippingOptionName']
57
57
  }
58
58
  end
59
+
60
+ def note
61
+ @params['note_text']
62
+ end
59
63
  end
60
64
  end
61
65
  end
@@ -9,8 +9,6 @@ module ActiveMerchant #:nodoc:
9
9
  self.homepage_url = 'http://www.payscout.com/'
10
10
  self.display_name = 'Payscout'
11
11
 
12
- self.ssl_version = 'SSLv3'
13
-
14
12
  def initialize(options = {})
15
13
  requires!(options, :username, :password)
16
14
  super
@@ -9,7 +9,7 @@ module ActiveMerchant #:nodoc:
9
9
  self.supported_countries = ['AU']
10
10
  self.supported_cardtypes = [:visa, :master, :american_express]
11
11
  self.homepage_url = 'http://www.pin.net.au/'
12
- self.display_name = 'Pin'
12
+ self.display_name = 'Pin Payments'
13
13
 
14
14
  def initialize(options = {})
15
15
  requires!(options, :api_key)
@@ -36,13 +36,12 @@ module ActiveMerchant #:nodoc:
36
36
  # )
37
37
  class PsigateGateway < Gateway
38
38
  self.test_url = 'https://dev.psigate.com:7989/Messenger/XMLMessenger'
39
- self.live_url = 'https://secure.psigate.com:7934/Messenger/XMLMessenger'
39
+ self.live_url = 'https://secure.psigate.com:17934/Messenger/XMLMessenger'
40
40
 
41
41
  self.supported_cardtypes = [:visa, :master, :american_express]
42
42
  self.supported_countries = ['CA']
43
43
  self.homepage_url = 'http://www.psigate.com/'
44
44
  self.display_name = 'Psigate'
45
- self.ssl_version = :SSLv3
46
45
 
47
46
  SUCCESS_MESSAGE = 'Success'
48
47
  FAILURE_MESSAGE = 'The transaction was declined'
@@ -9,30 +9,14 @@ module ActiveMerchant #:nodoc:
9
9
  # used by many banks in Spain and is particularly well supported by
10
10
  # Catalunya Caixa's ecommerce department.
11
11
  #
12
- # Standard ActiveMerchant methods are supported, with one notable exception:
13
- # :order_id must be provided and must conform to a very specific format.
12
+ # Redsys requires an order_id be provided with each transaction and it must
13
+ # follow a specific format. The rules are as follows:
14
14
  #
15
- # == Example use:
16
- #
17
- # gateway = ActiveMerchant::Billing::RedsysGateway.new(
18
- # :login => "091358382",
19
- # :secret_key => "qwertyasdf0123456789"
20
- # )
21
- #
22
- # # Run a purchase for 10 euros
23
- # response = gateway.purchase(1000, creditcard, :order_id => "123456")
24
- # puts reponse.success? # => true
25
- #
26
- # # Partially refund the purchase
27
- # response = gateway.refund(500, response.authorization)
28
- #
29
- # Redsys requires an order_id be provided with each transaction of a
30
- # specific format. The rules are as follows:
31
- #
32
- # * Minimum length: 4
33
- # * Maximum length: 12
34
15
  # * First 4 digits must be numerical
35
16
  # * Remaining 8 digits may be alphanumeric
17
+ # * Max length: 12
18
+ #
19
+ # If an invalid order_id is provided, we do our best to clean it up.
36
20
  #
37
21
  # Much of the code for this library is based on the active_merchant_sermepa
38
22
  # integration gateway which uses essentially the same API but with the
@@ -44,18 +28,13 @@ module ActiveMerchant #:nodoc:
44
28
  self.live_url = "https://sis.sermepa.es/sis/operaciones"
45
29
  self.test_url = "https://sis-t.sermepa.es:25443/sis/operaciones"
46
30
 
47
- # Sensible region specific defaults.
48
31
  self.supported_countries = ['ES']
49
32
  self.default_currency = 'EUR'
50
33
  self.money_format = :cents
51
34
 
52
35
  # Not all card types may be activated by the bank!
53
36
  self.supported_cardtypes = [:visa, :master, :american_express, :jcb, :diners_club]
54
-
55
- # Homepage URL of the gateway for reference
56
37
  self.homepage_url = "http://www.redsys.es/"
57
-
58
- # What to call this gateway
59
38
  self.display_name = "Redsys"
60
39
 
61
40
  CURRENCY_CODES = {
@@ -75,6 +54,7 @@ module ActiveMerchant #:nodoc:
75
54
  "NZD" => '554',
76
55
  "PEN" => '604',
77
56
  "RUB" => '643',
57
+ "SGD" => '702',
78
58
  "USD" => '840',
79
59
  "UYU" => '858'
80
60
  }
@@ -94,14 +74,12 @@ module ActiveMerchant #:nodoc:
94
74
  # a card has been rejected. Syntax or general request errors
95
75
  # are not covered here.
96
76
  RESPONSE_TEXTS = {
97
- # Accepted Codes
98
77
  0 => "Transaction Approved",
99
78
  400 => "Cancellation Accepted",
100
79
  481 => "Cancellation Accepted",
101
80
  500 => "Reconciliation Accepted",
102
81
  900 => "Refund / Confirmation approved",
103
82
 
104
- # Declined error codes
105
83
  101 => "Card expired",
106
84
  102 => "Card blocked temporarily or under susciption of fraud",
107
85
  104 => "Transaction not permitted",
@@ -121,7 +99,6 @@ module ActiveMerchant #:nodoc:
121
99
  190 => "Refusal with no specific reason",
122
100
  191 => "Expiry date incorrect",
123
101
 
124
- # Declined, and suspected of fraud
125
102
  201 => "Card expired",
126
103
  202 => "Card blocked temporarily or under suscipition of fraud",
127
104
  204 => "Transaction not permitted",
@@ -131,13 +108,11 @@ module ActiveMerchant #:nodoc:
131
108
  280 => "CVV2/CVC2 Error",
132
109
  290 => "Declined with no specific reason",
133
110
 
134
- # More general codes for specific types of transaction
135
111
  480 => "Original transaction not located, or time-out exceeded",
136
112
  501 => "Original transaction not located, or time-out exceeded",
137
113
  502 => "Original transaction not located, or time-out exceeded",
138
114
  503 => "Original transaction not located, or time-out exceeded",
139
115
 
140
- # Declined transactions by the bank
141
116
  904 => "Merchant not registered at FUC",
142
117
  909 => "System error",
143
118
  912 => "Issuer not available",
@@ -192,6 +167,7 @@ module ActiveMerchant #:nodoc:
192
167
  add_amount(data, money, options)
193
168
  add_order(data, options[:order_id])
194
169
  add_creditcard(data, creditcard)
170
+ data[:description] = options[:description]
195
171
 
196
172
  commit data
197
173
  end
@@ -204,6 +180,7 @@ module ActiveMerchant #:nodoc:
204
180
  add_amount(data, money, options)
205
181
  add_order(data, options[:order_id])
206
182
  add_creditcard(data, creditcard)
183
+ data[:description] = options[:description]
207
184
 
208
185
  commit data
209
186
  end
@@ -214,6 +191,7 @@ module ActiveMerchant #:nodoc:
214
191
  add_amount(data, money, options)
215
192
  order_id, _, _ = split_authorization(authorization)
216
193
  add_order(data, order_id)
194
+ data[:description] = options[:description]
217
195
 
218
196
  commit data
219
197
  end
@@ -224,6 +202,7 @@ module ActiveMerchant #:nodoc:
224
202
  order_id, amount, currency = split_authorization(authorization)
225
203
  add_amount(data, amount, :currency => currency)
226
204
  add_order(data, order_id)
205
+ data[:description] = options[:description]
227
206
 
228
207
  commit data
229
208
  end
@@ -234,10 +213,18 @@ module ActiveMerchant #:nodoc:
234
213
  add_amount(data, money, options)
235
214
  order_id, _, _ = split_authorization(authorization)
236
215
  add_order(data, order_id)
216
+ data[:description] = options[:description]
237
217
 
238
218
  commit data
239
219
  end
240
220
 
221
+ def verify(creditcard, options = {})
222
+ MultiResponse.run(:use_first_response) do |r|
223
+ r.process { authorize(100, creditcard, options) }
224
+ r.process(:ignore_result) { void(r.authorization, options) }
225
+ end
226
+ end
227
+
241
228
  private
242
229
 
243
230
  def add_action(data, action)
@@ -250,8 +237,7 @@ module ActiveMerchant #:nodoc:
250
237
  end
251
238
 
252
239
  def add_order(data, order_id)
253
- raise ArgumentError.new("Invalid order_id format") unless(/^\d{4}[\da-zA-Z]{0,8}$/ =~ order_id)
254
- data[:order_id] = order_id
240
+ data[:order_id] = clean_order_id(order_id)
255
241
  end
256
242
 
257
243
  def url
@@ -300,13 +286,14 @@ module ActiveMerchant #:nodoc:
300
286
  xml.DATOSENTRADA do
301
287
  # Basic elements
302
288
  xml.DS_Version 0.1
303
- xml.DS_MERCHANT_CURRENCY data[:currency]
304
- xml.DS_MERCHANT_AMOUNT data[:amount]
305
- xml.DS_MERCHANT_ORDER data[:order_id]
306
- xml.DS_MERCHANT_TRANSACTIONTYPE data[:action]
307
- xml.DS_MERCHANT_TERMINAL @options[:terminal]
308
- xml.DS_MERCHANT_MERCHANTCODE @options[:login]
309
- xml.DS_MERCHANT_MERCHANTSIGNATURE build_signature(data)
289
+ xml.DS_MERCHANT_CURRENCY data[:currency]
290
+ xml.DS_MERCHANT_AMOUNT data[:amount]
291
+ xml.DS_MERCHANT_ORDER data[:order_id]
292
+ xml.DS_MERCHANT_TRANSACTIONTYPE data[:action]
293
+ xml.DS_MERCHANT_PRODUCTDESCRIPTION data[:description]
294
+ xml.DS_MERCHANT_TERMINAL @options[:terminal]
295
+ xml.DS_MERCHANT_MERCHANTCODE @options[:login]
296
+ xml.DS_MERCHANT_MERCHANTSIGNATURE build_signature(data)
310
297
 
311
298
  # Only when card is present
312
299
  if data[:card]
@@ -373,6 +360,7 @@ module ActiveMerchant #:nodoc:
373
360
 
374
361
  def currency_code(currency)
375
362
  return currency if currency =~ /^\d+$/
363
+ raise ArgumentError, "Unknown currency #{currency}" unless CURRENCY_CODES[currency]
376
364
  CURRENCY_CODES[currency]
377
365
  end
378
366
 
@@ -389,6 +377,15 @@ module ActiveMerchant #:nodoc:
389
377
  def is_success_response?(code)
390
378
  (code.to_i < 100) || [400, 481, 500, 900].include?(code.to_i)
391
379
  end
380
+
381
+ def clean_order_id(order_id)
382
+ cleansed = order_id.gsub(/[^\da-zA-Z]/, '')
383
+ if cleansed =~ /^\d{4}/
384
+ cleansed[0..12]
385
+ else
386
+ "%04d%s" % [rand(0..9999), cleansed[0...8]]
387
+ end
388
+ end
392
389
  end
393
390
  end
394
391
  end
@@ -2,24 +2,196 @@ require File.dirname(__FILE__) + '/authorize_net'
2
2
 
3
3
  module ActiveMerchant #:nodoc:
4
4
  module Billing #:nodoc:
5
- class SecurePayGateway < AuthorizeNetGateway
5
+ class SecurePayGateway < Gateway
6
+ API_VERSION = '3.1'
7
+
6
8
  self.live_url = self.test_url = 'https://www.securepay.com/AuthSpayAdapter/process.aspx'
7
9
 
10
+ class_attribute :duplicate_window
11
+
12
+ APPROVED, DECLINED, ERROR, FRAUD_REVIEW = 1, 2, 3, 4
13
+
14
+ RESPONSE_CODE, RESPONSE_REASON_CODE, RESPONSE_REASON_TEXT, AUTHORIZATION_CODE = 0, 2, 3, 4
15
+ AVS_RESULT_CODE, TRANSACTION_ID, CARD_CODE_RESPONSE_CODE, CARDHOLDER_AUTH_CODE = 5, 6, 38, 39
16
+
17
+ self.default_currency = 'USD'
18
+
19
+ self.supported_countries = %w(US CA GB AU)
20
+ self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb]
8
21
  self.homepage_url = 'http://www.securepay.com/'
9
22
  self.display_name = 'SecurePay'
10
- self.supported_countries = %w(US CA GB AU)
11
23
 
12
- # Limit support to purchase() for the time being
13
- # JRuby chokes here
14
- # undef_method :authorize, :capture, :void, :credit
24
+ CARD_CODE_ERRORS = %w( N S )
25
+ AVS_ERRORS = %w( A E N R W Z )
26
+ AVS_REASON_CODES = %w(27 45)
27
+ TRANSACTION_ALREADY_ACTIONED = %w(310 311)
15
28
 
16
- undef_method :authorize
17
- undef_method :capture
18
- undef_method :void
19
- undef_method :credit
29
+ def initialize(options = {})
30
+ requires!(options, :login, :password)
31
+ super
32
+ end
33
+
34
+ def purchase(money, paysource, options = {})
35
+ post = {}
36
+ add_currency_code(post, money, options)
37
+ add_invoice(post, options)
38
+ add_payment_source(post, paysource, options)
39
+ add_address(post, options)
40
+ add_customer_data(post, options)
41
+ add_duplicate_window(post)
42
+
43
+ commit('AUTH_CAPTURE', money, post)
44
+ end
20
45
 
21
46
  private
22
47
 
48
+ def commit(action, money, parameters)
49
+ parameters[:amount] = amount(money) unless action == 'VOID'
50
+
51
+ url = (test? ? self.test_url : self.live_url)
52
+ data = ssl_post(url, post_data(action, parameters))
53
+
54
+ response = parse(data)
55
+ response[:action] = action
56
+
57
+ message = message_from(response)
58
+
59
+ Response.new(success?(response), message, response,
60
+ :test => test?,
61
+ :authorization => response[:transaction_id],
62
+ :fraud_review => fraud_review?(response),
63
+ :avs_result => { :code => response[:avs_result_code] },
64
+ :cvv_result => response[:card_code]
65
+ )
66
+ end
67
+
68
+ def success?(response)
69
+ response[:response_code] == APPROVED && TRANSACTION_ALREADY_ACTIONED.exclude?(response[:response_reason_code])
70
+ end
71
+
72
+ def fraud_review?(response)
73
+ response[:response_code] == FRAUD_REVIEW
74
+ end
75
+
76
+ def parse(body)
77
+ fields = split(body)
78
+
79
+ results = {
80
+ :response_code => fields[RESPONSE_CODE].to_i,
81
+ :response_reason_code => fields[RESPONSE_REASON_CODE],
82
+ :response_reason_text => fields[RESPONSE_REASON_TEXT],
83
+ :avs_result_code => fields[AVS_RESULT_CODE],
84
+ :transaction_id => fields[TRANSACTION_ID],
85
+ :card_code => fields[CARD_CODE_RESPONSE_CODE],
86
+ :authorization_code => fields[AUTHORIZATION_CODE],
87
+ :cardholder_authentication_code => fields[CARDHOLDER_AUTH_CODE]
88
+ }
89
+ results
90
+ end
91
+
92
+ def post_data(action, parameters = {})
93
+ post = {}
94
+
95
+ post[:version] = API_VERSION
96
+ post[:login] = @options[:login]
97
+ post[:tran_key] = @options[:password]
98
+ post[:relay_response] = "FALSE"
99
+ post[:type] = action
100
+ post[:delim_data] = "TRUE"
101
+ post[:delim_char] = ","
102
+ post[:encap_char] = "$"
103
+ post[:solution_ID] = application_id if application_id.present? && application_id != "ActiveMerchant"
104
+
105
+ request = post.merge(parameters).collect { |key, value| "x_#{key}=#{CGI.escape(value.to_s)}" }.join("&")
106
+ request
107
+ end
108
+
109
+ def add_currency_code(post, money, options)
110
+ post[:currency_code] = options[:currency] || currency(money)
111
+ end
112
+
113
+ def add_invoice(post, options)
114
+ post[:invoice_num] = options[:order_id]
115
+ post[:description] = options[:description]
116
+ end
117
+
118
+ def add_creditcard(post, creditcard, options={})
119
+ post[:card_num] = creditcard.number
120
+ post[:card_code] = creditcard.verification_value if creditcard.verification_value?
121
+ post[:exp_date] = expdate(creditcard)
122
+ post[:first_name] = creditcard.first_name
123
+ post[:last_name] = creditcard.last_name
124
+ end
125
+
126
+ def add_payment_source(params, source, options={})
127
+ add_creditcard(params, source, options)
128
+ end
129
+
130
+ def add_customer_data(post, options)
131
+ if options.has_key? :email
132
+ post[:email] = options[:email]
133
+ post[:email_customer] = false
134
+ end
135
+
136
+ if options.has_key? :customer
137
+ post[:cust_id] = options[:customer] if Float(options[:customer]) rescue nil
138
+ end
139
+
140
+ if options.has_key? :ip
141
+ post[:customer_ip] = options[:ip]
142
+ end
143
+
144
+ if options.has_key? :cardholder_authentication_value
145
+ post[:cardholder_authentication_value] = options[:cardholder_authentication_value]
146
+ end
147
+
148
+ if options.has_key? :authentication_indicator
149
+ post[:authentication_indicator] = options[:authentication_indicator]
150
+ end
151
+ end
152
+
153
+ # x_duplicate_window won't be sent by default, because sending it changes the response.
154
+ # "If this field is present in the request with or without a value, an enhanced duplicate transaction response will be sent."
155
+ # (as of 2008-12-30) http://www.authorize.net/support/AIM_guide_SCC.pdf
156
+ def add_duplicate_window(post)
157
+ post[:duplicate_window] = duplicate_window if duplicate_window
158
+ end
159
+
160
+ def add_address(post, options)
161
+ if address = options[:billing_address] || options[:address]
162
+ post[:address] = address[:address1].to_s
163
+ post[:company] = address[:company].to_s
164
+ post[:phone] = address[:phone].to_s
165
+ post[:zip] = address[:zip].to_s
166
+ post[:city] = address[:city].to_s
167
+ post[:country] = address[:country].to_s
168
+ post[:state] = address[:state].blank? ? 'n/a' : address[:state]
169
+ end
170
+
171
+ if address = options[:shipping_address]
172
+ post[:ship_to_first_name] = address[:first_name].to_s
173
+ post[:ship_to_last_name] = address[:last_name].to_s
174
+ post[:ship_to_address] = address[:address1].to_s
175
+ post[:ship_to_company] = address[:company].to_s
176
+ post[:ship_to_phone] = address[:phone].to_s
177
+ post[:ship_to_zip] = address[:zip].to_s
178
+ post[:ship_to_city] = address[:city].to_s
179
+ post[:ship_to_country] = address[:country].to_s
180
+ post[:ship_to_state] = address[:state].blank? ? 'n/a' : address[:state]
181
+ end
182
+ end
183
+
184
+ def message_from(results)
185
+ if results[:response_code] == DECLINED
186
+ return CVVResult.messages[ results[:card_code] ] if CARD_CODE_ERRORS.include?(results[:card_code])
187
+ if AVS_REASON_CODES.include?(results[:response_reason_code]) && AVS_ERRORS.include?(results[:avs_result_code])
188
+ return AVSResult.messages[ results[:avs_result_code] ]
189
+ end
190
+ end
191
+
192
+ (results[:response_reason_text] ? results[:response_reason_text].chomp('.') : '')
193
+ end
194
+
23
195
  def split(response)
24
196
  response.split(',')
25
197
  end