activemerchant 1.47.0 → 1.48.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/CHANGELOG +34 -0
  5. data/CONTRIBUTORS +16 -0
  6. data/README.md +8 -2
  7. data/lib/active_merchant/billing/credit_card.rb +16 -4
  8. data/lib/active_merchant/billing/gateway.rb +0 -1
  9. data/lib/active_merchant/billing/gateways/authorize_net.rb +48 -1
  10. data/lib/active_merchant/billing/gateways/axcessms.rb +181 -0
  11. data/lib/active_merchant/billing/gateways/braintree_blue.rb +16 -6
  12. data/lib/active_merchant/billing/gateways/cenpos.rb +272 -0
  13. data/lib/active_merchant/billing/gateways/epay.rb +8 -9
  14. data/lib/active_merchant/billing/gateways/exact.rb +9 -0
  15. data/lib/active_merchant/billing/gateways/fat_zebra.rb +33 -0
  16. data/lib/active_merchant/billing/gateways/firstdata_e4.rb +49 -3
  17. data/lib/active_merchant/billing/gateways/inspire.rb +7 -1
  18. data/lib/active_merchant/billing/gateways/iridium.rb +1 -1
  19. data/lib/active_merchant/billing/gateways/merchant_warrior.rb +2 -2
  20. data/lib/active_merchant/billing/gateways/monei.rb +307 -0
  21. data/lib/active_merchant/billing/gateways/nab_transact.rb +47 -37
  22. data/lib/active_merchant/billing/gateways/netbilling.rb +40 -7
  23. data/lib/active_merchant/billing/gateways/orbital.rb +45 -21
  24. data/lib/active_merchant/billing/gateways/pay_conex.rb +246 -0
  25. data/lib/active_merchant/billing/gateways/pay_hub.rb +213 -0
  26. data/lib/active_merchant/billing/gateways/paymill.rb +3 -2
  27. data/lib/active_merchant/billing/gateways/pin.rb +2 -2
  28. data/lib/active_merchant/billing/gateways/quickbooks.rb +6 -4
  29. data/lib/active_merchant/billing/gateways/qvalent.rb +179 -0
  30. data/lib/active_merchant/billing/gateways/redsys.rb +29 -15
  31. data/lib/active_merchant/billing/gateways/sage_pay.rb +1 -1
  32. data/lib/active_merchant/billing/gateways/stripe.rb +12 -3
  33. data/lib/active_merchant/billing/gateways/usa_epay_transaction.rb +8 -3
  34. data/lib/active_merchant/billing/gateways/worldpay.rb +2 -2
  35. data/lib/active_merchant/billing/gateways/worldpay_online_payments.rb +205 -0
  36. data/lib/active_merchant/billing/network_tokenization_credit_card.rb +12 -4
  37. data/lib/active_merchant/billing/response.rb +1 -1
  38. data/lib/active_merchant/posts_data.rb +6 -0
  39. data/lib/active_merchant/version.rb +1 -1
  40. data/lib/support/outbound_hosts.rb +13 -10
  41. metadata +9 -2
  42. metadata.gz.sig +0 -0
@@ -1,5 +1,15 @@
1
1
  module ActiveMerchant #:nodoc:
2
2
  module Billing #:nodoc:
3
+ # To perform PCI Compliant Repeat Billing
4
+ #
5
+ # Ensure that PCI Compliant Repeat Billing is enabled on your merchant account:
6
+ # "Enable PCI Compliant Repeat Billing, Up-selling and Cross-selling" in Step 6 of the Credit Cards setup page
7
+ #
8
+ # Instead of passing a credit_card to authorize or purchase, pass the transaction id (res.authorization)
9
+ # of a past Netbilling transaction
10
+ #
11
+ # To store billing information without performing an operation, use the 'store' method
12
+ # which invokes the tran_type 'Q' (Quasi) operation and returns a transaction id to use in future Repeat Billing operations
3
13
  class NetbillingGateway < Gateway
4
14
  self.live_url = self.test_url = 'https://secure.netbilling.com:1402/gw/sas/direct3.1'
5
15
 
@@ -9,7 +19,8 @@ module ActiveMerchant #:nodoc:
9
19
  :refund => 'R',
10
20
  :credit => 'C',
11
21
  :capture => 'D',
12
- :void => 'U'
22
+ :void => 'U',
23
+ :quasi => 'Q'
13
24
  }
14
25
 
15
26
  SUCCESS_CODES = [ '1', 'T' ]
@@ -27,23 +38,23 @@ module ActiveMerchant #:nodoc:
27
38
  super
28
39
  end
29
40
 
30
- def authorize(money, credit_card, options = {})
41
+ def authorize(money, payment_source, options = {})
31
42
  post = {}
32
43
  add_amount(post, money)
33
44
  add_invoice(post, options)
34
- add_credit_card(post, credit_card)
35
- add_address(post, credit_card, options)
45
+ add_payment_source(post, payment_source)
46
+ add_address(post, payment_source, options)
36
47
  add_customer_data(post, options)
37
48
 
38
49
  commit(:authorization, post)
39
50
  end
40
51
 
41
- def purchase(money, credit_card, options = {})
52
+ def purchase(money, payment_source, options = {})
42
53
  post = {}
43
54
  add_amount(post, money)
44
55
  add_invoice(post, options)
45
- add_credit_card(post, credit_card)
46
- add_address(post, credit_card, options)
56
+ add_payment_source(post, payment_source)
57
+ add_address(post, payment_source, options)
47
58
  add_customer_data(post, options)
48
59
 
49
60
  commit(:purchase, post)
@@ -79,6 +90,16 @@ module ActiveMerchant #:nodoc:
79
90
  commit(:void, post)
80
91
  end
81
92
 
93
+ def store(credit_card, options = {})
94
+ post = {}
95
+ add_amount(post, 0)
96
+ add_payment_source(post, credit_card)
97
+ add_address(post, credit_card, options)
98
+ add_customer_data(post, options)
99
+
100
+ commit(:quasi, post)
101
+ end
102
+
82
103
  def test?
83
104
  (@options[:login] == TEST_LOGIN || super)
84
105
  end
@@ -125,6 +146,18 @@ module ActiveMerchant #:nodoc:
125
146
  post[:description] = options[:description]
126
147
  end
127
148
 
149
+ def add_payment_source(params, source)
150
+ if source.is_a?(String)
151
+ add_transaction_id(params, source)
152
+ else
153
+ add_credit_card(params, source)
154
+ end
155
+ end
156
+
157
+ def add_transaction_id(post, transaction_id)
158
+ post[:card_number] = 'CS:' + transaction_id
159
+ end
160
+
128
161
  def add_credit_card(post, credit_card)
129
162
  post[:bill_name1] = credit_card.first_name
130
163
  post[:bill_name2] = credit_card.last_name
@@ -324,12 +324,12 @@ module ActiveMerchant #:nodoc:
324
324
  avs_supported = AVS_SUPPORTED_COUNTRIES.include?(address[:country].to_s)
325
325
 
326
326
  if avs_supported
327
- xml.tag! :AVSzip, (address[:zip] ? address[:zip].to_s[0..9] : nil)
328
- xml.tag! :AVSaddress1, (address[:address1] ? address[:address1][0..29] : nil)
329
- xml.tag! :AVSaddress2, (address[:address2] ? address[:address2][0..29] : nil)
330
- xml.tag! :AVScity, (address[:city] ? address[:city][0..19] : nil)
331
- xml.tag! :AVSstate, address[:state]
332
- xml.tag! :AVSphoneNum, (address[:phone] ? address[:phone].scan(/\d/).join.to_s[0..13] : nil)
327
+ xml.tag! :AVSzip, byte_limit(format_address_field(address[:zip]), 10)
328
+ xml.tag! :AVSaddress1, byte_limit(format_address_field(address[:address1]), 30)
329
+ xml.tag! :AVSaddress2, byte_limit(format_address_field(address[:address2]), 30)
330
+ xml.tag! :AVScity, byte_limit(format_address_field(address[:city]), 20)
331
+ xml.tag! :AVSstate, byte_limit(format_address_field(address[:state]), 2)
332
+ xml.tag! :AVSphoneNum, (address[:phone] ? address[:phone].scan(/\d/).join.to_s[0..13] : nil)
333
333
  end
334
334
  # can't look in billing address?
335
335
  xml.tag! :AVSname, ((creditcard && creditcard.name) ? creditcard.name[0..29] : nil)
@@ -342,29 +342,33 @@ module ActiveMerchant #:nodoc:
342
342
 
343
343
  def add_destination_address(xml, address)
344
344
  if address[:dest_zip]
345
- xml.tag! :AVSDestzip, (address[:dest_zip] ? address[:dest_zip].to_s[0..9] : nil)
346
- xml.tag! :AVSDestaddress1, (address[:dest_address1] ? address[:dest_address1][0..29] : nil)
347
- xml.tag! :AVSDestaddress2, (address[:dest_address2] ? address[:dest_address2][0..29] : nil)
348
- xml.tag! :AVSDestcity, (address[:dest_city] ? address[:dest_city][0..19] : nil)
349
- xml.tag! :AVSDeststate, address[:dest_state]
345
+ avs_supported = AVS_SUPPORTED_COUNTRIES.include?(address[:dest_country].to_s)
346
+
347
+ xml.tag! :AVSDestzip, byte_limit(format_address_field(address[:dest_zip]), 10)
348
+ xml.tag! :AVSDestaddress1, byte_limit(format_address_field(address[:dest_address1]), 30)
349
+ xml.tag! :AVSDestaddress2, byte_limit(format_address_field(address[:dest_address2]), 30)
350
+ xml.tag! :AVSDestcity, byte_limit(format_address_field(address[:dest_city]), 20)
351
+ xml.tag! :AVSDeststate, byte_limit(format_address_field(address[:dest_state]), 2)
350
352
  xml.tag! :AVSDestphoneNum, (address[:dest_phone] ? address[:dest_phone].scan(/\d/).join.to_s[0..13] : nil)
351
353
 
352
- xml.tag! :AVSDestname, (address[:dest_name] ? address[:dest_name][0..29] : nil)
353
- xml.tag! :AVSDestcountryCode, address[:dest_country]
354
+ xml.tag! :AVSDestname, byte_limit(address[:dest_name], 30)
355
+ xml.tag! :AVSDestcountryCode, (avs_supported ? address[:dest_country] : '')
354
356
  end
355
357
  end
356
358
 
357
359
  # For Profile requests
358
360
  def add_customer_address(xml, options)
359
361
  if(address = (options[:billing_address] || options[:address]))
360
- xml.tag! :CustomerAddress1, (address[:address1] ? address[:address1][0..29] : nil)
361
- xml.tag! :CustomerAddress2, (address[:address2] ? address[:address2][0..29] : nil)
362
- xml.tag! :CustomerCity, (address[:city] ? address[:city][0..19] : nil)
363
- xml.tag! :CustomerState, address[:state]
364
- xml.tag! :CustomerZIP, (address[:zip] ? address[:zip].to_s[0..9] : nil)
365
- xml.tag! :CustomerEmail, address[:email].to_s[0..49] if address[:email]
362
+ avs_supported = AVS_SUPPORTED_COUNTRIES.include?(address[:country].to_s)
363
+
364
+ xml.tag! :CustomerAddress1, byte_limit(format_address_field(address[:address1]), 30)
365
+ xml.tag! :CustomerAddress2, byte_limit(format_address_field(address[:address2]), 30)
366
+ xml.tag! :CustomerCity, byte_limit(format_address_field(address[:city]), 20)
367
+ xml.tag! :CustomerState, byte_limit(format_address_field(address[:state]), 2)
368
+ xml.tag! :CustomerZIP, byte_limit(format_address_field(address[:zip]), 10)
369
+ xml.tag! :CustomerEmail, byte_limit(address[:email], 50) if address[:email]
366
370
  xml.tag! :CustomerPhone, (address[:phone] ? address[:phone].scan(/\d/).join.to_s : nil)
367
- xml.tag! :CustomerCountryCode, (address[:country] ? address[:country][0..1] : nil)
371
+ xml.tag! :CustomerCountryCode, (avs_supported ? address[:country] : '')
368
372
  end
369
373
  end
370
374
 
@@ -630,12 +634,32 @@ module ActiveMerchant #:nodoc:
630
634
  # 2. - , $ @ & and a space character, though the space character cannot be the leading character
631
635
  # 3. PINless Debit transactions can only use uppercase and lowercase alpha (A-Z, a-z) and numeric (0-9)
632
636
  def format_order_id(order_id)
633
- illegal_characters = /[^,$@\- \w]/
637
+ illegal_characters = /[^,$@&\- \w]/
634
638
  order_id = order_id.to_s.gsub(/\./, '-')
635
639
  order_id.gsub!(illegal_characters, '')
640
+ order_id.lstrip!
636
641
  order_id[0...22]
637
642
  end
638
643
 
644
+ # Address-related fields cannot contain % | ^ \ /
645
+ # Returns the value with these characters removed, or nil
646
+ def format_address_field(value)
647
+ value.gsub(/[%\|\^\\\/]/, '') if value.respond_to?(:gsub)
648
+ end
649
+
650
+ # Field lengths should be limited by byte count instead of character count
651
+ # Returns the truncated value or nil
652
+ def byte_limit(value, byte_length)
653
+ limited_value = ""
654
+
655
+ value.to_s.each_char do |c|
656
+ break if((limited_value.bytesize + c.bytesize) > byte_length)
657
+ limited_value << c
658
+ end
659
+
660
+ limited_value
661
+ end
662
+
639
663
  def build_customer_request_xml(creditcard, options = {})
640
664
  xml = xml_envelope
641
665
  xml.tag! :Request do
@@ -0,0 +1,246 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ class PayConexGateway < Gateway
4
+ include Empty
5
+
6
+ self.test_url = "https://cert.payconex.net/api/qsapi/3.8/"
7
+ self.live_url = "https://secure.payconex.net/api/qsapi/3.8/"
8
+
9
+ self.supported_countries = %w(US CA)
10
+ self.default_currency = "USD"
11
+ self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :diners_club]
12
+
13
+ self.homepage_url = "http://www.bluefincommerce.com/"
14
+ self.display_name = "PayConex"
15
+
16
+ def initialize(options={})
17
+ requires!(options, :account_id, :api_accesskey)
18
+ super
19
+ end
20
+
21
+ def purchase(money, payment_method, options={})
22
+ post = {}
23
+ add_auth_purchase_params(post, money, payment_method, options)
24
+ commit("SALE", post)
25
+ end
26
+
27
+ def authorize(money, payment_method, options={})
28
+ post = {}
29
+ add_auth_purchase_params(post, money, payment_method, options)
30
+ commit("AUTHORIZATION", post)
31
+ end
32
+
33
+ def capture(money, authorization, options={})
34
+ post = {}
35
+ add_reference_params(post, authorization, options)
36
+ add_amount(post, money, options)
37
+ commit("CAPTURE", post)
38
+ end
39
+
40
+ def refund(money, authorization, options={})
41
+ post = {}
42
+ add_reference_params(post, authorization, options)
43
+ add_amount(post, money, options)
44
+ commit("REFUND", post)
45
+ end
46
+
47
+ def void(authorization, options = {})
48
+ post = {}
49
+ add_reference_params(post, authorization, options)
50
+ commit("REVERSAL", post)
51
+ end
52
+
53
+ def credit(money, payment_method, options={})
54
+ if payment_method.is_a?(String)
55
+ raise ArgumentError, "Reference credits are not supported. Please supply the original credit card or use the #refund method."
56
+ end
57
+
58
+ post = {}
59
+ add_auth_purchase_params(post, money, payment_method, options)
60
+ commit("CREDIT", post)
61
+ end
62
+
63
+ def verify(payment_method, options={})
64
+ authorize(0, payment_method, options)
65
+ end
66
+
67
+ def store(payment_method, options={})
68
+ post = {}
69
+ add_credentials(post)
70
+ add_payment_method(post, payment_method)
71
+ add_address(post, options)
72
+ add_common_options(post, options)
73
+ commit("STORE", post)
74
+ end
75
+
76
+ def supports_scrubbing?
77
+ true
78
+ end
79
+
80
+ def scrub(transcript)
81
+ force_utf8(transcript).
82
+ gsub(%r((api_accesskey=)\w+), '\1[FILTERED]').
83
+ gsub(%r((card_number=)\w+), '\1[FILTERED]').
84
+ gsub(%r((card_verification=)\w+), '\1[FILTERED]')
85
+ end
86
+
87
+ private
88
+
89
+ def force_utf8(string)
90
+ return nil unless string
91
+ binary = string.encode("BINARY", invalid: :replace, undef: :replace, replace: "?") # Needed for Ruby 2.0 since #encode is a no-op if the string is already UTF-8. It's not needed for Ruby 2.1 and up since it's not a no-op there.
92
+ binary.encode("UTF-8", invalid: :replace, undef: :replace, replace: "?")
93
+ end
94
+
95
+ def add_credentials(post)
96
+ post[:account_id] = @options[:account_id]
97
+ post[:api_accesskey] = @options[:api_accesskey]
98
+ end
99
+
100
+ def add_auth_purchase_params(post, money, payment_method, options)
101
+ add_credentials(post)
102
+ add_payment_method(post, payment_method)
103
+ add_address(post, options)
104
+ add_common_options(post, options)
105
+ add_amount(post, money, options)
106
+ add_if_present(post, :email, options[:email])
107
+ end
108
+
109
+ def add_reference_params(post, authorization, options)
110
+ add_credentials(post)
111
+ add_common_options(post, options)
112
+ add_token_id(post, authorization)
113
+ end
114
+
115
+ def add_amount(post, money, options)
116
+ post[:transaction_amount] = amount(money)
117
+ currency_code = (options[:currency] || currency(money))
118
+ add_if_present(post, :currency, currency_code)
119
+ end
120
+
121
+ def add_payment_method(post, payment_method)
122
+ case payment_method
123
+ when String
124
+ add_token_payment_method(post, payment_method)
125
+ when Check
126
+ add_check(post, payment_method)
127
+ else
128
+ if payment_method.respond_to?(:track_data) && payment_method.track_data.present?
129
+ add_card_present_payment_method(post, payment_method)
130
+ else
131
+ add_credit_card(post, payment_method)
132
+ end
133
+ end
134
+ end
135
+
136
+ def add_credit_card(post, payment_method)
137
+ post[:tender_type] = "CARD"
138
+ post[:card_number] = payment_method.number
139
+ post[:card_expiration] = expdate(payment_method)
140
+ post[:card_verification] = payment_method.verification_value
141
+ post[:first_name] = payment_method.first_name
142
+ post[:last_name] = payment_method.last_name
143
+ end
144
+
145
+ def add_token_payment_method(post, payment_method)
146
+ post[:tender_type] = "CARD"
147
+ post[:token_id] = payment_method
148
+ post[:reissue] = true
149
+ end
150
+
151
+ def add_card_present_payment_method(post, payment_method)
152
+ post[:tender_type] = "CARD"
153
+ post[:card_tracks] = payment_method.track_data
154
+ end
155
+
156
+ def add_check(post, payment_method)
157
+ post[:tender_type] = "ACH"
158
+ post[:first_name] = payment_method.first_name
159
+ post[:last_name] = payment_method.last_name
160
+ post[:bank_account_number] = payment_method.account_number
161
+ post[:bank_routing_number] = payment_method.routing_number
162
+ post[:check_number] = payment_method.number
163
+ add_if_present(post, :ach_account_type, payment_method.account_type)
164
+ end
165
+
166
+ def add_address(post, options)
167
+ address = options[:billing_address]
168
+ return unless address
169
+
170
+ add_if_present(post, :street_address1, address[:address1])
171
+ add_if_present(post, :street_address2, address[:address2])
172
+ add_if_present(post, :city, address[:city])
173
+ add_if_present(post, :state, address[:state])
174
+ add_if_present(post, :zip, address[:zip])
175
+ add_if_present(post, :country, address[:country])
176
+ add_if_present(post, :phone, address[:phone])
177
+ end
178
+
179
+ def add_common_options(post, options)
180
+ add_if_present(post, :transaction_description, options[:description])
181
+ add_if_present(post, :custom_id, options[:custom_id])
182
+ add_if_present(post, :custom_data, options[:custom_data])
183
+ add_if_present(post, :ip_address, options[:ip])
184
+ add_if_present(post, :payment_type, options[:payment_type])
185
+ add_if_present(post, :cashier, options[:cashier])
186
+
187
+ post[:disable_cvv] = options[:disable_cvv] unless options[:disable_cvv].nil?
188
+ post[:response_format] = 'JSON'
189
+ end
190
+
191
+ def add_if_present(post, key, value)
192
+ post[key] = value unless empty?(value)
193
+ end
194
+
195
+ def add_token_id(post, authorization)
196
+ post[:token_id] = authorization
197
+ end
198
+
199
+ def parse(body)
200
+ JSON.parse(body)
201
+ end
202
+
203
+ def commit(action, params)
204
+ raw_response = ssl_post(url, post_data(action, params))
205
+ response = parse(raw_response)
206
+
207
+ Response.new(
208
+ success_from(response),
209
+ message_from(response),
210
+ response,
211
+ authorization: response["transaction_id"],
212
+ :avs_result => AVSResult.new(code: response["avs_response"]),
213
+ :cvv_result => CVVResult.new(response["cvv2_response"]),
214
+ test: test?
215
+ )
216
+
217
+ rescue JSON::ParserError
218
+ unparsable_response(raw_response)
219
+ end
220
+
221
+ def url
222
+ test? ? test_url : live_url
223
+ end
224
+
225
+ def success_from(response)
226
+ response["transaction_approved"] || !response["error"]
227
+ end
228
+
229
+ def message_from(response)
230
+ success_from(response) ? response["authorization_message"] : response["error_message"]
231
+ end
232
+
233
+ def post_data(action, params)
234
+ params[:transaction_type] = action
235
+ params.map {|k, v| "#{k}=#{CGI.escape(v.to_s)}"}.join('&')
236
+ end
237
+
238
+ def unparsable_response(raw_response)
239
+ message = "Invalid JSON response received from PayConex. Please contact PayConex if you continue to receive this message."
240
+ message += " (The raw response returned by the API was #{raw_response.inspect})"
241
+ return Response.new(false, message)
242
+ end
243
+
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,213 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ class PayHubGateway < Gateway
4
+ self.live_url = 'https://checkout.payhub.com/transaction/api'
5
+
6
+ self.supported_countries = ['US']
7
+ self.default_currency = 'USD'
8
+ self.supported_cardtypes = [:visa, :master, :american_express, :discover]
9
+
10
+ self.homepage_url = 'http://www.payhub.com/'
11
+ self.display_name = 'PayHub'
12
+
13
+ CVV_CODE_TRANSLATOR = {
14
+ 'M' => 'CVV matches',
15
+ 'N' => 'CVV does not match',
16
+ 'P' => 'CVV not processed',
17
+ 'S' => 'CVV should have been present',
18
+ 'U' => 'CVV request unable to be processed by issuer'
19
+ }
20
+
21
+ AVS_CODE_TRANSLATOR = {
22
+ '0' => "Approved, Address verification was not requested.",
23
+ 'A' => "Approved, Address matches only.",
24
+ 'B' => "Address Match. Street Address math for international transaction Postal Code not verified because of incompatible formats (Acquirer sent both street address and Postal Code)",
25
+ 'C' => "Serv Unavailable. Street address and Postal Code not verified for international transaction because of incompatible formats (Acquirer sent both street and Postal Code).",
26
+ 'D' => "Exact Match, Street Address and Postal Code match for international transaction.",
27
+ 'F' => "Exact Match, Street Address and Postal Code match. Applies to UK only.",
28
+ 'G' => "Ver Unavailable, Non-U.S. Issuer does not participate.",
29
+ 'I' => "Ver Unavailable, Address information not verified for international transaction",
30
+ 'M' => "Exact Match, Street Address and Postal Code match for international transaction",
31
+ 'N' => "No - Address and ZIP Code does not match",
32
+ 'P' => "Zip Match, Postal Codes match for international transaction Street address not verified because of incompatible formats (Acquirer sent both street address and Postal Code).",
33
+ 'R' => "Retry - Issuer system unavailable",
34
+ 'S' => "Serv Unavailable, Service not supported",
35
+ 'U' => "Ver Unavailable, Address unavailable.",
36
+ 'W' => "ZIP match - Nine character numeric ZIP match only.",
37
+ 'X' => "Exact match, Address and nine-character ZIP match.",
38
+ 'Y' => "Exact Match, Address and five character ZIP match.",
39
+ 'Z' => "Zip Match, Five character numeric ZIP match only.",
40
+ '1' => "Cardholder name and ZIP match AMEX only.",
41
+ '2' => "Cardholder name, address, and ZIP match AMEX only.",
42
+ '3' => "Cardholder name and address match AMEX only.",
43
+ '4' => "Cardholder name match AMEX only.",
44
+ '5' => "Cardholder name incorrect, ZIP match AMEX only.",
45
+ '6' => "Cardholder name incorrect, address and ZIP match AMEX only.",
46
+ '7' => "Cardholder name incorrect, address match AMEX only.",
47
+ '8' => "Cardholder, all do not match AMEX only."
48
+ }
49
+
50
+ STANDARD_ERROR_CODE_MAPPING = {
51
+ '14' => STANDARD_ERROR_CODE[:invalid_number],
52
+ '80' => STANDARD_ERROR_CODE[:invalid_expiry_date],
53
+ '82' => STANDARD_ERROR_CODE[:invalid_cvc],
54
+ '54' => STANDARD_ERROR_CODE[:expired_card],
55
+ '51' => STANDARD_ERROR_CODE[:card_declined],
56
+ '05' => STANDARD_ERROR_CODE[:card_declined],
57
+ '61' => STANDARD_ERROR_CODE[:card_declined],
58
+ '62' => STANDARD_ERROR_CODE[:card_declined],
59
+ '65' => STANDARD_ERROR_CODE[:card_declined],
60
+ '93' => STANDARD_ERROR_CODE[:card_declined],
61
+ '01' => STANDARD_ERROR_CODE[:call_issuer],
62
+ '02' => STANDARD_ERROR_CODE[:call_issuer],
63
+ '04' => STANDARD_ERROR_CODE[:pickup_card],
64
+ '07' => STANDARD_ERROR_CODE[:pickup_card],
65
+ '41' => STANDARD_ERROR_CODE[:pickup_card],
66
+ '43' => STANDARD_ERROR_CODE[:pickup_card]
67
+ }
68
+
69
+ def initialize(options={})
70
+ requires!(options, :orgid, :username, :password, :tid)
71
+
72
+ super
73
+ end
74
+
75
+ def authorize(amount, creditcard, options = {})
76
+ post = setup_post('auth')
77
+ add_creditcard(post, creditcard)
78
+ add_amount(post, amount)
79
+ add_address(post, (options[:address] || options[:billing_address]))
80
+ add_customer_data(post, options)
81
+
82
+ commit(post)
83
+ end
84
+
85
+ def purchase(amount, creditcard, options={})
86
+ post = setup_post('sale')
87
+ add_creditcard(post, creditcard)
88
+ add_amount(post, amount)
89
+ add_address(post, (options[:address] || options[:billing_address]))
90
+ add_customer_data(post, options)
91
+
92
+ commit(post)
93
+ end
94
+
95
+ def refund(amount, trans_id, options={})
96
+ # Attempt a void in case the transaction is unsettled
97
+ post = setup_post('void')
98
+ add_reference(post, trans_id)
99
+ response = commit(post)
100
+ return response if response.success?
101
+
102
+ post = setup_post('refund')
103
+ add_reference(post, trans_id)
104
+ commit(post)
105
+ end
106
+
107
+ def capture(amount, trans_id, options = {})
108
+ post = setup_post('capture')
109
+
110
+ add_reference(post, trans_id)
111
+ add_amount(post, amount)
112
+
113
+ commit(post)
114
+ end
115
+
116
+ # No void, as PayHub's void does not work on authorizations
117
+
118
+ def verify(creditcard, options={})
119
+ authorize(100, creditcard, options)
120
+ end
121
+
122
+ private
123
+
124
+ def setup_post(action)
125
+ post = {}
126
+ post[:orgid] = @options[:orgid]
127
+ post[:tid] = @options[:tid]
128
+ post[:username] = @options[:username]
129
+ post[:password] = @options[:password]
130
+ post[:mode] = (test? ? 'demo' : 'live')
131
+ post[:trans_type] = action
132
+ post
133
+ end
134
+
135
+ def add_reference(post, trans_id)
136
+ post[:trans_id] = trans_id
137
+ end
138
+
139
+ def add_customer_data(post, options = {})
140
+ post[:first_name] = options[:first_name]
141
+ post[:last_name] = options[:last_name]
142
+ post[:phone] = options[:phone]
143
+ post[:email] = options[:email]
144
+ end
145
+
146
+ def add_address(post, address)
147
+ return unless address
148
+ post[:address1] = address[:address1]
149
+ post[:address2] = address[:address2]
150
+ post[:zip] = address[:zip]
151
+ post[:state] = address[:state]
152
+ post[:city] = address[:city]
153
+ end
154
+
155
+ def add_amount(post, amount)
156
+ post[:amount] = amount(amount)
157
+ end
158
+
159
+ def add_creditcard(post, creditcard)
160
+ post[:cc] = creditcard.number
161
+ post[:month] = creditcard.month.to_s
162
+ post[:year] = creditcard.year.to_s
163
+ post[:cvv] = creditcard.verification_value
164
+ end
165
+
166
+ def parse(body)
167
+ JSON.parse(body)
168
+ end
169
+
170
+ def commit(post)
171
+ success = false
172
+
173
+ begin
174
+ raw_response = ssl_post(live_url, post.to_json, {'Content-Type' => 'application/json'} )
175
+ response = parse(raw_response)
176
+ success = (response['RESPONSE_CODE'] == "00")
177
+ rescue ResponseError => e
178
+ raw_response = e.response.body
179
+ response = response_error(raw_response)
180
+ rescue JSON::ParserError
181
+ response = json_error(raw_response)
182
+ end
183
+
184
+ Response.new(success,
185
+ response_message(response),
186
+ response,
187
+ test: test?,
188
+ avs_result: {code: response['AVS_RESULT_CODE']},
189
+ cvv_result: response['VERIFICATION_RESULT_CODE'],
190
+ error_code: (success ? nil : STANDARD_ERROR_CODE_MAPPING[response['RESPONSE_CODE']]),
191
+ authorization: response['TRANSACTION_ID']
192
+ )
193
+ end
194
+
195
+ def response_error(raw_response)
196
+ parse(raw_response)
197
+ rescue JSON::ParserError
198
+ json_error(raw_response)
199
+ end
200
+
201
+ def json_error(raw_response)
202
+ {
203
+ error_message: 'Invalid response received from the Payhub API. Please contact wecare@payhub.com if you continue to receive this message.' +
204
+ ' (The raw response returned by the API was #{raw_response.inspect})'
205
+ }
206
+ end
207
+
208
+ def response_message(response)
209
+ (response['RESPONSE_TEXT'] || response["RESPONSE_CODE"] || response[:error_message])
210
+ end
211
+ end
212
+ end
213
+ end