activemerchant 1.45.0 → 1.46.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. checksums.yaml.gz.sig +3 -1
  3. data.tar.gz.sig +0 -0
  4. data/CHANGELOG +25 -0
  5. data/CONTRIBUTORS +9 -0
  6. data/README.md +6 -20
  7. data/lib/active_merchant.rb +9 -50
  8. data/lib/active_merchant/billing.rb +1 -0
  9. data/lib/active_merchant/billing/gateway.rb +10 -1
  10. data/lib/active_merchant/billing/gateways.rb +6 -11
  11. data/lib/active_merchant/billing/gateways/authorize_net.rb +58 -47
  12. data/lib/active_merchant/billing/gateways/beanstream.rb +1 -1
  13. data/lib/active_merchant/billing/gateways/beanstream_interac.rb +8 -8
  14. data/lib/active_merchant/billing/gateways/braintree.rb +2 -2
  15. data/lib/active_merchant/billing/gateways/braintree_blue.rb +69 -22
  16. data/lib/active_merchant/billing/gateways/braintree_orange.rb +2 -2
  17. data/lib/active_merchant/billing/gateways/checkout.rb +7 -2
  18. data/lib/active_merchant/billing/gateways/cyber_source.rb +25 -9
  19. data/lib/active_merchant/billing/gateways/elavon.rb +1 -1
  20. data/lib/active_merchant/billing/gateways/eway_rapid.rb +6 -3
  21. data/lib/active_merchant/billing/gateways/finansbank.rb +1 -1
  22. data/lib/active_merchant/billing/gateways/hps.rb +0 -1
  23. data/lib/active_merchant/billing/gateways/ideal/ideal_base.rb +1 -1
  24. data/lib/active_merchant/billing/gateways/ideal/ideal_rabobank.pem +0 -0
  25. data/lib/active_merchant/billing/gateways/ideal_rabobank.rb +1 -1
  26. data/lib/active_merchant/billing/gateways/instapay.rb +0 -0
  27. data/lib/active_merchant/billing/gateways/ipp.rb +175 -0
  28. data/lib/active_merchant/billing/gateways/mercury.rb +4 -11
  29. data/lib/active_merchant/billing/gateways/migs.rb +1 -1
  30. data/lib/active_merchant/billing/gateways/modern_payments.rb +1 -1
  31. data/lib/active_merchant/billing/gateways/orbital.rb +2 -2
  32. data/lib/active_merchant/billing/gateways/pay_gate_xml.rb +26 -0
  33. data/lib/active_merchant/billing/gateways/payflow.rb +3 -3
  34. data/lib/active_merchant/billing/gateways/payflow/payflow_common_api.rb +16 -5
  35. data/lib/active_merchant/billing/gateways/payflow_express.rb +3 -3
  36. data/lib/active_merchant/billing/gateways/payflow_express_uk.rb +2 -2
  37. data/lib/active_merchant/billing/gateways/payflow_uk.rb +4 -4
  38. data/lib/active_merchant/billing/gateways/paypal.rb +15 -3
  39. data/lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb +10 -1
  40. data/lib/active_merchant/billing/gateways/paypal/paypal_recurring_api.rb +1 -1
  41. data/lib/active_merchant/billing/gateways/paypal_ca.rb +1 -1
  42. data/lib/active_merchant/billing/gateways/paypal_digital_goods.rb +3 -3
  43. data/lib/active_merchant/billing/gateways/paypal_express.rb +4 -4
  44. data/lib/active_merchant/billing/gateways/pin.rb +10 -1
  45. data/lib/active_merchant/billing/gateways/quickbooks.rb +278 -0
  46. data/lib/active_merchant/billing/gateways/redsys.rb +2 -2
  47. data/lib/active_merchant/billing/gateways/sage.rb +3 -3
  48. data/lib/active_merchant/billing/gateways/sage/sage_bankcard.rb +1 -1
  49. data/lib/active_merchant/billing/gateways/sage/sage_virtual_check.rb +1 -1
  50. data/lib/active_merchant/billing/gateways/secure_pay.rb +1 -1
  51. data/lib/active_merchant/billing/gateways/skip_jack.rb +0 -1
  52. data/lib/active_merchant/billing/gateways/stripe.rb +13 -2
  53. data/lib/active_merchant/billing/gateways/wirecard.rb +0 -1
  54. data/lib/active_merchant/billing/payment_token.rb +1 -1
  55. data/lib/active_merchant/connection.rb +169 -0
  56. data/lib/active_merchant/network_connection_retries.rb +78 -0
  57. data/lib/active_merchant/post_data.rb +24 -0
  58. data/lib/active_merchant/posts_data.rb +78 -0
  59. data/lib/active_merchant/version.rb +1 -1
  60. data/lib/certs/cacert.pem +3866 -0
  61. metadata +22 -44
  62. metadata.gz.sig +0 -0
  63. data/lib/active_merchant/offsite_payments_shim.rb +0 -19
@@ -18,7 +18,7 @@ module ActiveMerchant #:nodoc:
18
18
 
19
19
  self.homepage_url = 'http://www.mercurypay.com'
20
20
  self.display_name = 'Mercury'
21
- self.supported_countries = ['US']
21
+ self.supported_countries = ['US','CA']
22
22
  self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb]
23
23
  self.default_currency = 'USD'
24
24
 
@@ -72,13 +72,6 @@ module ActiveMerchant #:nodoc:
72
72
  def void(authorization, options={})
73
73
  requires!(options, :credit_card) unless @use_tokenization
74
74
 
75
- if options[:try_reversal]
76
- request = build_authorized_request('VoidSale', nil, authorization, options[:credit_card], options.merge(:reversal => true))
77
- response = commit('VoidSale', request)
78
-
79
- return response if response.success?
80
- end
81
-
82
75
  request = build_authorized_request('VoidSale', nil, authorization, options[:credit_card], options)
83
76
  commit('VoidSale', request)
84
77
  end
@@ -115,7 +108,7 @@ module ActiveMerchant #:nodoc:
115
108
  xml = Builder::XmlMarkup.new
116
109
 
117
110
  invoice_no, ref_no, auth_code, acq_ref_data, process_data, record_no, amount = split_authorization(authorization)
118
- ref_no = "1" if options[:reversal] #filler value for preauth voids -- not used by mercury but will reject if missing or not numeric
111
+ ref_no = "1" if ref_no.blank?
119
112
 
120
113
  xml.tag! "TStream" do
121
114
  xml.tag! "Transaction" do
@@ -132,8 +125,8 @@ module ActiveMerchant #:nodoc:
132
125
  add_address(xml, options)
133
126
  xml.tag! 'TranInfo' do
134
127
  xml.tag! "AuthCode", auth_code
135
- xml.tag! "AcqRefData", acq_ref_data if options[:reversal]
136
- xml.tag! "ProcessData", process_data if options[:reversal]
128
+ xml.tag! "AcqRefData", acq_ref_data
129
+ xml.tag! "ProcessData", process_data
137
130
  end
138
131
  end
139
132
  end
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/migs/migs_codes'
1
+ require 'active_merchant/billing/gateways/migs/migs_codes'
2
2
 
3
3
  require 'digest/md5' # Used in add_secure_hash
4
4
 
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/modern_payments_cim'
1
+ require 'active_merchant/billing/gateways/modern_payments_cim'
2
2
 
3
3
  module ActiveMerchant #:nodoc:
4
4
  module Billing #:nodoc:
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/orbital/orbital_soft_descriptors'
1
+ require 'active_merchant/billing/gateways/orbital/orbital_soft_descriptors'
2
2
  require "rexml/document"
3
3
 
4
4
  module ActiveMerchant #:nodoc:
@@ -240,7 +240,7 @@ module ActiveMerchant #:nodoc:
240
240
 
241
241
 
242
242
  # ==== Customer Profiles
243
- # :customer_ref_num should be set unless your happy with Orbital providing one
243
+ # :customer_ref_num should be set unless you're happy with Orbital providing one
244
244
  #
245
245
  # :customer_profile_order_override_ind can be set to map
246
246
  # the CustomerRefNum to OrderID or Comments. Defaults to 'NO' - no mapping
@@ -177,6 +177,13 @@ module ActiveMerchant #:nodoc:
177
177
  def capture(money, authorization, options = {})
178
178
  action = 'settletx'
179
179
 
180
+ options.merge!(:money => money, :authorization => authorization)
181
+ commit_capture(action, authorization, build_request(action, options))
182
+ end
183
+
184
+ def refund(money, authorization, options={})
185
+ action = 'refundtx'
186
+
180
187
  options.merge!(:money => money, :authorization => authorization)
181
188
  commit(action, build_request(action, options))
182
189
  end
@@ -201,6 +208,10 @@ module ActiveMerchant #:nodoc:
201
208
  money = options.delete(:money)
202
209
  authorization = options.delete(:authorization)
203
210
  build_capture(protocol, money, authorization, options)
211
+ when 'refundtx'
212
+ money = options.delete(:money)
213
+ authorization = options.delete(:authorization)
214
+ build_refund(protocol, money, authorization, options)
204
215
  else
205
216
  raise "no action specified for build_request"
206
217
  end
@@ -228,6 +239,13 @@ module ActiveMerchant #:nodoc:
228
239
  }
229
240
  end
230
241
 
242
+ def build_refund(xml, money, authorization, options={})
243
+ xml.tag! 'refundtx', {
244
+ :tid => authorization,
245
+ :amt => amount(money)
246
+ }
247
+ end
248
+
231
249
  def parse(action, body)
232
250
  hash = {}
233
251
  xml = REXML::Document.new(body)
@@ -252,6 +270,14 @@ module ActiveMerchant #:nodoc:
252
270
  )
253
271
  end
254
272
 
273
+ def commit_capture(action, authorization, request)
274
+ response = parse(action, ssl_post(self.live_url, request))
275
+ Response.new(successful?(response), message_from(response), response,
276
+ :test => test?,
277
+ :authorization => authorization
278
+ )
279
+ end
280
+
255
281
  def message_from(response)
256
282
  (response[:rdesc] || response[:edesc])
257
283
  end
@@ -1,6 +1,6 @@
1
- require File.dirname(__FILE__) + '/payflow/payflow_common_api'
2
- require File.dirname(__FILE__) + '/payflow/payflow_response'
3
- require File.dirname(__FILE__) + '/payflow_express'
1
+ require 'active_merchant/billing/gateways/payflow/payflow_common_api'
2
+ require 'active_merchant/billing/gateways/payflow/payflow_response'
3
+ require 'active_merchant/billing/gateways/payflow_express'
4
4
 
5
5
  module ActiveMerchant #:nodoc:
6
6
  module Billing #:nodoc:
@@ -197,13 +197,24 @@ module ActiveMerchant #:nodoc:
197
197
 
198
198
  response = parse(ssl_post(test? ? self.test_url : self.live_url, request, headers))
199
199
 
200
- build_response(response[:result] == "0", response[:message], response,
201
- :test => test?,
202
- :authorization => response[:pn_ref] || response[:rp_ref],
203
- :cvv_result => CVV_CODE[response[:cv_result]],
204
- :avs_result => { :code => response[:avs_result] }
200
+ build_response(
201
+ success_for(response),
202
+ response[:message], response,
203
+ test: test?,
204
+ authorization: response[:pn_ref] || response[:rp_ref],
205
+ cvv_result: CVV_CODE[response[:cv_result]],
206
+ avs_result: { code: response[:avs_result] },
207
+ fraud_review: under_fraud_review?(response)
205
208
  )
206
209
  end
210
+
211
+ def success_for(response)
212
+ %w(0 126).include?(response[:result])
213
+ end
214
+
215
+ def under_fraud_review?(response)
216
+ (response[:result] == "126")
217
+ end
207
218
  end
208
219
  end
209
220
  end
@@ -1,6 +1,6 @@
1
- require File.dirname(__FILE__) + '/payflow/payflow_common_api'
2
- require File.dirname(__FILE__) + '/payflow/payflow_express_response'
3
- require File.dirname(__FILE__) + '/paypal_express_common'
1
+ require 'active_merchant/billing/gateways/payflow/payflow_common_api'
2
+ require 'active_merchant/billing/gateways/payflow/payflow_express_response'
3
+ require 'active_merchant/billing/gateways/paypal_express_common'
4
4
 
5
5
  module ActiveMerchant #:nodoc:
6
6
  module Billing #:nodoc:
@@ -1,11 +1,11 @@
1
- require File.dirname(__FILE__) + '/payflow_express'
1
+ require 'active_merchant/billing/gateways/payflow_express'
2
2
 
3
3
  module ActiveMerchant #:nodoc:
4
4
  module Billing #:nodoc:
5
5
  class PayflowExpressUkGateway < PayflowExpressGateway
6
6
  self.default_currency = 'GBP'
7
7
  self.partner = 'PayPalUk'
8
-
8
+
9
9
  self.supported_countries = ['GB']
10
10
  self.homepage_url = 'https://www.paypal.com/uk/cgi-bin/webscr?cmd=_additional-payment-overview-outside'
11
11
  self.display_name = 'PayPal Express Checkout (UK)'
@@ -1,16 +1,16 @@
1
- require File.dirname(__FILE__) + '/payflow'
2
- require File.dirname(__FILE__) + '/payflow_express_uk'
1
+ require 'active_merchant/billing/gateways/payflow'
2
+ require 'active_merchant/billing/gateways/payflow_express_uk'
3
3
 
4
4
  module ActiveMerchant #:nodoc:
5
5
  module Billing #:nodoc:
6
6
  class PayflowUkGateway < PayflowGateway
7
7
  self.default_currency = 'GBP'
8
8
  self.partner = 'PayPalUk'
9
-
9
+
10
10
  def express
11
11
  @express ||= PayflowExpressUkGateway.new(@options)
12
12
  end
13
-
13
+
14
14
  self.supported_cardtypes = [:visa, :master, :american_express, :discover, :solo, :switch]
15
15
  self.supported_countries = ['GB']
16
16
  self.homepage_url = 'https://www.paypal.com/uk/webapps/mpp/pro'
@@ -1,6 +1,6 @@
1
- require File.dirname(__FILE__) + '/paypal/paypal_common_api'
2
- require File.dirname(__FILE__) + '/paypal/paypal_recurring_api'
3
- require File.dirname(__FILE__) + '/paypal_express'
1
+ require 'active_merchant/billing/gateways/paypal/paypal_common_api'
2
+ require 'active_merchant/billing/gateways/paypal/paypal_recurring_api'
3
+ require 'active_merchant/billing/gateways/paypal_express'
4
4
 
5
5
  module ActiveMerchant #:nodoc:
6
6
  module Billing #:nodoc:
@@ -38,6 +38,18 @@ module ActiveMerchant #:nodoc:
38
38
  @express ||= PaypalExpressGateway.new(@options)
39
39
  end
40
40
 
41
+ def supports_scrubbing?
42
+ true
43
+ end
44
+
45
+ def scrub(transcript)
46
+ transcript.
47
+ gsub(%r((<n1:Password>).+(</n1:Password>)), '\1[FILTERED]\2').
48
+ gsub(%r((<n1:Username>).+(</n1:Username>)), '\1[FILTERED]\2').
49
+ gsub(%r((<n2:CreditCardNumber>).+(</n2:CreditCardNumber)), '\1[FILTERED]\2').
50
+ gsub(%r((<n2:CVV2>).+(</n2:CVV2)), '\1[FILTERED]\2')
51
+ end
52
+
41
53
  private
42
54
 
43
55
  def define_transaction_type(transaction_arg)
@@ -2,6 +2,8 @@ module ActiveMerchant #:nodoc:
2
2
  module Billing #:nodoc:
3
3
  # This module is included in both PaypalGateway and PaypalExpressGateway
4
4
  module PaypalCommonAPI
5
+ include Empty
6
+
5
7
  API_VERSION = '72'
6
8
 
7
9
  URLS = {
@@ -573,7 +575,7 @@ module ActiveMerchant #:nodoc:
573
575
  xml.tag! 'n2:Custom', options[:custom] unless options[:custom].blank?
574
576
 
575
577
  xml.tag! 'n2:InvoiceID', (options[:order_id] || options[:invoice_id]) unless (options[:order_id] || options[:invoice_id]).blank?
576
- xml.tag! 'n2:ButtonSource', application_id.to_s.slice(0,32) unless application_id.blank?
578
+ add_button_source(xml)
577
579
 
578
580
  # The notify URL applies only to DoExpressCheckoutPayment.
579
581
  # This value is ignored when set in SetExpressCheckout or GetExpressCheckoutDetails
@@ -593,6 +595,13 @@ module ActiveMerchant #:nodoc:
593
595
  end
594
596
  end
595
597
 
598
+ def add_button_source(xml)
599
+ button_source = (@options[:button_source] || application_id)
600
+ if !empty?(button_source)
601
+ xml.tag! 'n2:ButtonSource', button_source.to_s.slice(0, 32)
602
+ end
603
+ end
604
+
596
605
  def add_express_only_payment_details(xml, options = {})
597
606
  add_optional_fields(xml,
598
607
  %w{n2:NoteText n2:SoftDescriptor
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/paypal_common_api'
1
+ require 'active_merchant/billing/gateways/paypal/paypal_common_api'
2
2
 
3
3
  module ActiveMerchant #:nodoc:
4
4
  module Billing #:nodoc:
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/paypal'
1
+ require 'active_merchant/billing/gateways/paypal'
2
2
 
3
3
  module ActiveMerchant #:nodoc:
4
4
  module Billing #:nodoc:
@@ -1,6 +1,6 @@
1
- require File.dirname(__FILE__) + '/paypal/paypal_common_api'
2
- require File.dirname(__FILE__) + '/paypal/paypal_express_response'
3
- require File.dirname(__FILE__) + '/paypal_express_common'
1
+ require 'active_merchant/billing/gateways/paypal/paypal_common_api'
2
+ require 'active_merchant/billing/gateways/paypal/paypal_express_response'
3
+ require 'active_merchant/billing/gateways/paypal_express_common'
4
4
 
5
5
  module ActiveMerchant #:nodoc:
6
6
  module Billing #:nodoc:
@@ -1,7 +1,7 @@
1
- require File.dirname(__FILE__) + '/paypal/paypal_common_api'
2
- require File.dirname(__FILE__) + '/paypal/paypal_express_response'
3
- require File.dirname(__FILE__) + '/paypal/paypal_recurring_api'
4
- require File.dirname(__FILE__) + '/paypal_express_common'
1
+ require 'active_merchant/billing/gateways/paypal/paypal_common_api'
2
+ require 'active_merchant/billing/gateways/paypal/paypal_express_response'
3
+ require 'active_merchant/billing/gateways/paypal/paypal_recurring_api'
4
+ require 'active_merchant/billing/gateways/paypal_express_common'
5
5
 
6
6
  module ActiveMerchant #:nodoc:
7
7
  module Billing #:nodoc:
@@ -148,9 +148,12 @@ module ActiveMerchant #:nodoc:
148
148
  url = "#{test? ? test_url : live_url}/#{action}"
149
149
 
150
150
  begin
151
- body = parse(ssl_request(method, url, post_data(params), headers(options)))
151
+ raw_response = ssl_request(method, url, post_data(params), headers(options))
152
+ body = parse(raw_response)
152
153
  rescue ResponseError => e
153
154
  body = parse(e.response.body)
155
+ rescue JSON::ParserError
156
+ return unparsable_response(raw_response)
154
157
  end
155
158
 
156
159
  if body["response"]
@@ -181,6 +184,12 @@ module ActiveMerchant #:nodoc:
181
184
  )
182
185
  end
183
186
 
187
+ def unparsable_response(raw_response)
188
+ message = "Invalid JSON response received from Pin Payments. Please contact support@pin.net.au if you continue to receive this message."
189
+ message += " (The raw response returned by the API was #{raw_response.inspect})"
190
+ return Response.new(false, message)
191
+ end
192
+
184
193
  def token(response)
185
194
  response['token']
186
195
  end
@@ -0,0 +1,278 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ class QuickbooksGateway < Gateway
4
+ self.test_url = 'https://sandbox.api.intuit.com'
5
+ self.live_url = 'https://api.intuit.com'
6
+
7
+ self.supported_countries = ['US']
8
+ self.default_currency = 'USD'
9
+ self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners]
10
+
11
+ self.homepage_url = 'http://payments.intuit.com'
12
+ self.display_name = 'QuickBooks Payments'
13
+ ENDPOINT = "/quickbooks/v4/payments/charges"
14
+ OAUTH_ENDPOINTS = {
15
+ site: 'https://oauth.intuit.com',
16
+ request_token_path: '/oauth/v1/get_request_token',
17
+ authorize_url: 'https://appcenter.intuit.com/Connect/Begin',
18
+ access_token_path: '/oauth/v1/get_access_token'
19
+ }
20
+
21
+ # https://developer.intuit.com/docs/0150_payments/0300_developer_guides/error_handling
22
+
23
+ STANDARD_ERROR_CODE_MAPPING = {
24
+ # Fraud Warnings
25
+ 'PMT-1000' => STANDARD_ERROR_CODE[:processing_error], # payment was accepted, but refund was unsuccessful
26
+ 'PMT-1001' => STANDARD_ERROR_CODE[:invalid_cvc], # payment processed, but cvc was invalid
27
+ 'PMT-1002' => STANDARD_ERROR_CODE[:incorrect_address], # payment processed, incorrect address info
28
+ 'PMT-1003' => STANDARD_ERROR_CODE[:processing_error], # payment processed, address info couldn't be validated
29
+
30
+ # Fraud Errors
31
+ 'PMT-2000' => STANDARD_ERROR_CODE[:incorrect_cvc], # Incorrect CVC
32
+ 'PMT-2001' => STANDARD_ERROR_CODE[:invalid_cvc], # CVC check unavaliable
33
+ 'PMT-2002' => STANDARD_ERROR_CODE[:incorrect_address], # Incorrect address
34
+ 'PMT-2003' => STANDARD_ERROR_CODE[:incorrect_address], # Address info unavailable
35
+
36
+ 'PMT-3000' => STANDARD_ERROR_CODE[:processing_error], # Merchant account could not be validated
37
+
38
+ # Invalid Request
39
+ 'PMT-4000' => STANDARD_ERROR_CODE[:processing_error], # Object is invalid
40
+ 'PMT-4001' => STANDARD_ERROR_CODE[:processing_error], # Object not found
41
+ 'PMT-4002' => STANDARD_ERROR_CODE[:processing_error], # Object is required
42
+
43
+ # Transaction Declined
44
+ 'PMT-5000' => STANDARD_ERROR_CODE[:card_declined], # Request was declined
45
+ 'PMT-5001' => STANDARD_ERROR_CODE[:card_declined], # Merchant does not support given payment method
46
+
47
+ # System Error
48
+ 'PMT-6000' => STANDARD_ERROR_CODE[:processing_error], # A temporary Issue prevented this request from being processed.
49
+ }
50
+
51
+ FRAUD_WARNING_CODES = ['PMT-1000','PMT-1001','PMT-1002','PMT-1003']
52
+
53
+ def initialize(options = {})
54
+ requires!(options, :consumer_key, :consumer_secret, :access_token, :token_secret, :realm)
55
+ @options = options
56
+ super
57
+ end
58
+
59
+ def purchase(money, payment, options = {})
60
+ post = {}
61
+ add_amount(post, money, options)
62
+ add_charge_data(post, payment, options)
63
+ post[:capture] = "true"
64
+
65
+ commit(ENDPOINT, post)
66
+ end
67
+
68
+ def authorize(money, payment, options = {})
69
+ post = {}
70
+ add_amount(post, money, options)
71
+ add_charge_data(post, payment, options)
72
+ post[:capture] = "false"
73
+
74
+ commit(ENDPOINT, post)
75
+ end
76
+
77
+ def capture(money, authorization, options = {})
78
+ post = {}
79
+ capture_uri = "#{ENDPOINT}/#{CGI.escape(authorization)}/capture"
80
+ post[:amount] = localized_amount(money, currency(money))
81
+
82
+ commit(capture_uri, post)
83
+ end
84
+
85
+ def refund(money, authorization, options = {})
86
+ post = {}
87
+ post[:amount] = localized_amount(money, currency(money))
88
+
89
+ commit(refund_uri(authorization), post)
90
+ end
91
+
92
+ def verify(credit_card, options = {})
93
+ authorize(1.00, credit_card, options)
94
+ end
95
+
96
+ def supports_scrubbing?
97
+ true
98
+ end
99
+
100
+ def scrub(transcript)
101
+ transcript.
102
+ gsub(%r((realm=\")\w+), '\1[FILTERED]').
103
+ gsub(%r((oauth_consumer_key=\")\w+), '\1[FILTERED]').
104
+ gsub(%r((oauth_nonce=\")\w+), '\1[FILTERED]').
105
+ gsub(%r((oauth_signature=\")[a-zA-Z%0-9]+), '\1[FILTERED]').
106
+ gsub(%r((oauth_token=\")\w+), '\1[FILTERED]').
107
+ gsub(%r((\"card\":{\"number\":\")\d+), '\1[FILTERED]').
108
+ gsub(%r((\"cvc\":\")\d+), '\1[FILTERED]')
109
+ end
110
+
111
+ private
112
+
113
+ def add_charge_data(post, payment, options = {})
114
+ add_payment(post, payment, options)
115
+ add_address(post, options)
116
+ end
117
+
118
+ def add_address(post, options)
119
+ return unless post[:card] && post[:card].kind_of?(Hash)
120
+
121
+ card_address = {}
122
+ if address = options[:billing_address] || options[:address]
123
+ card_address[:streetAddress] = address[:address1]
124
+ card_address[:city] = address[:city]
125
+ card_address[:region] = address[:state] || address[:region]
126
+ card_address[:country] = address[:country]
127
+ card_address[:postalCode] = address[:zip] if address[:zip]
128
+ end
129
+ post[:card][:address] = card_address
130
+ end
131
+
132
+ def add_amount(post, money, options = {})
133
+ currency = options[:currency] || currency(money)
134
+ post[:amount] = localized_amount(money, currency)
135
+ post[:currency] = currency.upcase
136
+ end
137
+
138
+ def add_payment(post, payment, options = {})
139
+ add_creditcard(post, payment, options)
140
+ end
141
+
142
+ def add_creditcard(post, creditcard, options = {})
143
+ card = {}
144
+ card[:number] = creditcard.number
145
+ card[:expMonth] = "%02d" % creditcard.month
146
+ card[:expYear] = creditcard.year
147
+ card[:cvc] = creditcard.verification_value if creditcard.verification_value?
148
+ card[:name] = creditcard.name if creditcard.name
149
+ card[:commercialCardCode] = options[:card_code] if options[:card_code]
150
+
151
+ post[:card] = card
152
+ end
153
+
154
+ def parse(body)
155
+ JSON.parse(body)
156
+ end
157
+
158
+ def commit(uri, body = {}, method = :post)
159
+ endpoint = gateway_url + uri
160
+ # The QuickBooks API returns HTTP 4xx on failed transactions, which causes a
161
+ # ResponseError raise, so we have to inspect the response and discern between
162
+ # a legitimate HTTP error and an actual gateway transactional error.
163
+ response = begin
164
+ case method
165
+ when :post
166
+ ssl_post(endpoint, post_data(body), headers(:post, endpoint))
167
+ when :get
168
+ ssl_request(:get, endpoint, nil, headers(:get, endpoint))
169
+ else
170
+ raise ArgumentError, "Invalid HTTP method: #{method}. Valid methods are :post and :get"
171
+ end
172
+ rescue ResponseError => e
173
+ extract_response_body_or_raise(e)
174
+ end
175
+
176
+ response_object(response)
177
+ end
178
+
179
+ def response_object(raw_response)
180
+ parsed_response = parse(raw_response)
181
+
182
+ Response.new(
183
+ success?(parsed_response),
184
+ message_from(parsed_response),
185
+ parsed_response,
186
+ authorization: authorization_from(parsed_response),
187
+ test: test?,
188
+ cvv_result: cvv_code_from(parsed_response),
189
+ error_code: errors_from(parsed_response),
190
+ fraud_review: fraud_review_status_from(parsed_response)
191
+ )
192
+ end
193
+
194
+ def gateway_url
195
+ test? ? test_url : live_url
196
+ end
197
+
198
+ def post_data(data = {})
199
+ data.to_json
200
+ end
201
+
202
+ def headers(method, uri)
203
+ raise ArgumentError, "Invalid HTTP method: #{method}. Valid methods are :post and :get" unless [:post, :get].include?(method)
204
+ request_uri = URI.parse(uri)
205
+
206
+ # Following the guidelines from http://nouncer.com/oauth/authentication.html
207
+ oauth_parameters = {
208
+ oauth_nonce: generate_unique_id,
209
+ oauth_timestamp: Time.now.to_i.to_s,
210
+ oauth_signature_method: 'HMAC-SHA1',
211
+ oauth_version: "1.0",
212
+ oauth_consumer_key: @options[:consumer_key],
213
+ oauth_token: @options[:access_token]
214
+ }
215
+
216
+ # prepare components for signature
217
+ oauth_signature_base_string = [method.to_s.upcase, request_uri.to_s, oauth_parameters.to_param].map{|v| CGI.escape(v) }.join('&')
218
+ oauth_signing_key = [@options[:consumer_secret], @options[:token_secret]].map{|v| CGI.escape(v)}.join('&')
219
+ hmac_signature = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'), oauth_signing_key, oauth_signature_base_string)
220
+
221
+ # append signature to required OAuth parameters
222
+ oauth_parameters[:oauth_signature] = CGI.escape(Base64.encode64(hmac_signature).chomp.gsub(/\n/, ''))
223
+
224
+ # prepare Authorization header string
225
+ oauth_parameters = Hash[oauth_parameters.sort_by {|k, _| k}]
226
+ oauth_headers = ["OAuth realm=\"#{@options[:realm]}\""]
227
+ oauth_headers += oauth_parameters.map {|k, v| "#{k}=\"#{v}\""}
228
+
229
+ {
230
+ "Content-type" => "application/json",
231
+ "Request-Id" => generate_unique_id,
232
+ "Authorization" => oauth_headers.join(', ')
233
+ }
234
+ end
235
+
236
+ def cvv_code_from(response)
237
+ if response['errors'].present?
238
+ FRAUD_WARNING_CODES.include?(response['errors'].first['code']) ? 'I' : ''
239
+ else
240
+ success?(response) ? 'M' : ''
241
+ end
242
+ end
243
+
244
+ def success?(response)
245
+ response['errors'].present? ? FRAUD_WARNING_CODES.concat(['0']).include?(response['errors'].first['code']) : true
246
+ end
247
+
248
+ def message_from(response)
249
+ response['errors'].present? ? response["errors"].map {|error_hash| error_hash["message"] }.join(" ") : "Transaction Approved"
250
+ end
251
+
252
+ def errors_from(response)
253
+ response['errors'].present? ? STANDARD_ERROR_CODE_MAPPING[response["errors"].first["code"]] : ""
254
+ end
255
+
256
+ def authorization_from(response)
257
+ response['id']
258
+ end
259
+
260
+ def fraud_review_status_from(response)
261
+ response['errors'] && FRAUD_WARNING_CODES.include?(response['errors'].first['code'])
262
+ end
263
+
264
+ def extract_response_body_or_raise(response_error)
265
+ begin
266
+ parse(response_error.response.body)
267
+ rescue JSON::ParserError
268
+ raise response_error
269
+ end
270
+ response_error.response.body
271
+ end
272
+
273
+ def refund_uri(authorization)
274
+ "#{ENDPOINT}/#{CGI.escape(authorization)}/refunds"
275
+ end
276
+ end
277
+ end
278
+ end