activemerchant 1.11.0 → 1.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (27) hide show
  1. data.tar.gz.sig +3 -1
  2. data/CHANGELOG +17 -0
  3. data/CONTRIBUTORS +16 -0
  4. data/README.rdoc +5 -0
  5. data/lib/active_merchant.rb +0 -1
  6. data/lib/active_merchant/billing/gateway.rb +1 -1
  7. data/lib/active_merchant/billing/gateways/authorize_net.rb +5 -2
  8. data/lib/active_merchant/billing/gateways/federated_canada.rb +169 -0
  9. data/lib/active_merchant/billing/gateways/garanti.rb +13 -1
  10. data/lib/active_merchant/billing/gateways/merchant_e_solutions.rb +4 -4
  11. data/lib/active_merchant/billing/gateways/nmi.rb +13 -0
  12. data/lib/active_merchant/billing/gateways/pay_junction.rb +8 -8
  13. data/lib/active_merchant/billing/gateways/paybox_direct.rb +3 -4
  14. data/lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb +2 -2
  15. data/lib/active_merchant/billing/gateways/paypal_express.rb +37 -5
  16. data/lib/active_merchant/billing/gateways/qbms.rb +290 -0
  17. data/lib/active_merchant/billing/integrations/direc_pay/helper.rb +25 -21
  18. data/lib/active_merchant/billing/integrations/moneybookers.rb +23 -0
  19. data/lib/active_merchant/billing/integrations/moneybookers/helper.rb +33 -0
  20. data/lib/active_merchant/billing/integrations/moneybookers/notification.rb +101 -0
  21. data/lib/active_merchant/billing/integrations/valitor/helper.rb +5 -1
  22. data/lib/active_merchant/billing/integrations/world_pay.rb +34 -0
  23. data/lib/active_merchant/billing/integrations/world_pay/helper.rb +100 -0
  24. data/lib/active_merchant/billing/integrations/world_pay/notification.rb +160 -0
  25. data/lib/active_merchant/version.rb +1 -1
  26. metadata +13 -4
  27. metadata.gz.sig +0 -0
@@ -78,6 +78,8 @@ module ActiveMerchant #:nodoc:
78
78
 
79
79
  xml.tag! 'n2:NotifyURL', options[:notify_url]
80
80
  xml.tag! 'n2:ButtonSource', application_id.to_s.slice(0,32) unless application_id.blank?
81
+ xml.tag! 'n2:InvoiceID', options[:order_id]
82
+ xml.tag! 'n2:OrderDescription', options[:description]
81
83
  end
82
84
  end
83
85
  end
@@ -94,20 +96,50 @@ module ActiveMerchant #:nodoc:
94
96
  xml.tag! 'SetExpressCheckoutRequest', 'xmlns:n2' => EBAY_NAMESPACE do
95
97
  xml.tag! 'n2:Version', API_VERSION
96
98
  xml.tag! 'n2:SetExpressCheckoutRequestDetails' do
97
- xml.tag! 'n2:PaymentAction', action
98
- xml.tag! 'n2:OrderTotal', amount(money).to_f.zero? ? localized_amount(100, currency_code) : localized_amount(money, currency_code), 'currencyID' => currency_code
99
99
  if options[:max_amount]
100
100
  xml.tag! 'n2:MaxAmount', localized_amount(options[:max_amount], currency_code), 'currencyID' => currency_code
101
101
  end
102
+ if !options[:allow_note].nil?
103
+ xml.tag! 'n2:AllowNote', options[:allow_note] ? '1' : '0'
104
+ end
105
+ xml.tag! 'n2:PaymentDetails' do
106
+ xml.tag! 'n2:OrderTotal', amount(money).to_f.zero? ? localized_amount(100, currency_code) : localized_amount(money, currency_code), 'currencyID' => currency_code
107
+ # All of the values must be included together and add up to the order total
108
+ if [:subtotal, :shipping, :handling, :tax].all? { |o| options.has_key?(o) }
109
+ xml.tag! 'n2:ItemTotal', localized_amount(options[:subtotal], currency_code), 'currencyID' => currency_code
110
+ xml.tag! 'n2:ShippingTotal', localized_amount(options[:shipping], currency_code), 'currencyID' => currency_code
111
+ xml.tag! 'n2:HandlingTotal', localized_amount(options[:handling], currency_code), 'currencyID' => currency_code
112
+ xml.tag! 'n2:TaxTotal', localized_amount(options[:tax], currency_code), 'currencyID' => currency_code
113
+ end
114
+
115
+ xml.tag! 'n2:OrderDescription', options[:description]
116
+ xml.tag! 'n2:InvoiceID', options[:order_id]
117
+
118
+ if options[:items]
119
+ options[:items].each do |item|
120
+ xml.tag! 'n2:PaymentDetailsItem' do
121
+ xml.tag! 'n2:Name', item[:name]
122
+ xml.tag! 'n2:Number', item[:number]
123
+ xml.tag! 'n2:Quantity', item[:quantity]
124
+ if item[:amount]
125
+ xml.tag! 'n2:Amount', localized_amount(item[:amount], currency_code), 'currencyID' => currency_code
126
+ end
127
+ xml.tag! 'n2:Description', item[:description]
128
+ xml.tag! 'n2:ItemURL', item[:url]
129
+ end
130
+ end
131
+ end
132
+
133
+ xml.tag! 'n2:PaymentAction', action
134
+ end
135
+
102
136
  add_address(xml, 'n2:Address', options[:shipping_address] || options[:address])
103
137
  xml.tag! 'n2:AddressOverride', options[:address_override] ? '1' : '0'
104
138
  xml.tag! 'n2:NoShipping', options[:no_shipping] ? '1' : '0'
105
139
  xml.tag! 'n2:ReturnURL', options[:return_url]
106
140
  xml.tag! 'n2:CancelURL', options[:cancel_return_url]
107
141
  xml.tag! 'n2:IPAddress', options[:ip] unless options[:ip].blank?
108
- xml.tag! 'n2:OrderDescription', options[:description]
109
142
  xml.tag! 'n2:BuyerEmail', options[:email] unless options[:email].blank?
110
- xml.tag! 'n2:InvoiceID', options[:order_id]
111
143
 
112
144
  if options[:billing_agreement]
113
145
  xml.tag! 'n2:BillingAgreementDetails' do
@@ -119,7 +151,7 @@ module ActiveMerchant #:nodoc:
119
151
 
120
152
  # Customization of the payment page
121
153
  xml.tag! 'n2:PageStyle', options[:page_style] unless options[:page_style].blank?
122
- xml.tag! 'n2:cpp-image-header', options[:header_image] unless options[:header_image].blank?
154
+ xml.tag! 'n2:cpp-header-image', options[:header_image] unless options[:header_image].blank?
123
155
  xml.tag! 'n2:cpp-header-back-color', options[:header_background_color] unless options[:header_background_color].blank?
124
156
  xml.tag! 'n2:cpp-header-border-color', options[:header_border_color] unless options[:header_border_color].blank?
125
157
  xml.tag! 'n2:cpp-payflow-color', options[:background_color] unless options[:background_color].blank?
@@ -0,0 +1,290 @@
1
+ require 'securerandom'
2
+
3
+ module ActiveMerchant #:nodoc:
4
+ module Billing #:nodoc:
5
+ class QbmsGateway < Gateway
6
+ API_VERSION = '4.0'
7
+
8
+ class_inheritable_accessor :test_url, :live_url
9
+
10
+ self.test_url = "https://webmerchantaccount.ptc.quickbooks.com/j/AppGateway"
11
+ self.live_url = "https://webmerchantaccount.quickbooks.com/j/AppGateway"
12
+
13
+ self.homepage_url = 'http://payments.intuit.com/'
14
+ self.display_name = 'QuickBooks Merchant Services'
15
+ self.default_currency = 'USD'
16
+ self.supported_cardtypes = [ :visa, :master, :discover, :american_express, :diners_club, :jcb ]
17
+ self.supported_countries = [ 'US' ]
18
+
19
+ TYPES = {
20
+ :authorize => 'CustomerCreditCardAuth',
21
+ :capture => 'CustomerCreditCardCapture',
22
+ :purchase => 'CustomerCreditCardCharge',
23
+ :refund => 'CustomerCreditCardTxnVoidOrRefund',
24
+ :void => 'CustomerCreditCardTxnVoid',
25
+ :query => 'MerchantAccountQuery',
26
+ }
27
+
28
+ # Creates a new QbmsGateway
29
+ #
30
+ # The gateway requires that a valid app id, app login, and ticket be passed
31
+ # in the +options+ hash.
32
+ #
33
+ # ==== Options
34
+ #
35
+ # * <tt>:login</tt> -- The App Login (REQUIRED)
36
+ # * <tt>:ticket</tt> -- The Connection Ticket. (REQUIRED)
37
+ # * <tt>:pem</tt> -- The PEM-encoded SSL client key and certificate. (REQUIRED)
38
+ # * <tt>:test</tt> -- +true+ or +false+. If true, perform transactions against the test server.
39
+ # Otherwise, perform transactions against the production server.
40
+ #
41
+ def initialize(options = {})
42
+ requires!(options, :login, :ticket)
43
+ test_mode = options[:test] || false
44
+ @options = options
45
+ super
46
+ end
47
+
48
+ # Performs an authorization, which reserves the funds on the customer's credit card, but does not
49
+ # charge the card.
50
+ #
51
+ # ==== Parameters
52
+ #
53
+ # * <tt>money</tt> -- The amount to be authorized as an Integer value in cents.
54
+ # * <tt>creditcard</tt> -- The CreditCard details for the transaction.
55
+ # * <tt>options</tt> -- A hash of optional parameters.
56
+ #
57
+ def authorize(money, creditcard, options = {})
58
+ commit(:authorize, money, options.merge(:credit_card => creditcard))
59
+ end
60
+
61
+ # Perform a purchase, which is essentially an authorization and capture in a single operation.
62
+ #
63
+ # ==== Parameters
64
+ #
65
+ # * <tt>money</tt> -- The amount to be purchased as an Integer value in cents.
66
+ # * <tt>creditcard</tt> -- The CreditCard details for the transaction.
67
+ # * <tt>options</tt> -- A hash of optional parameters.
68
+ #
69
+ def purchase(money, creditcard, options = {})
70
+ commit(:purchase, money, options.merge(:credit_card => creditcard))
71
+ end
72
+
73
+ # Captures the funds from an authorized transaction.
74
+ #
75
+ # ==== Parameters
76
+ #
77
+ # * <tt>money</tt> -- The amount to be captured as an Integer value in cents.
78
+ # * <tt>authorization</tt> -- The authorization returned from the previous authorize request.
79
+ #
80
+ def capture(money, authorization, options = {})
81
+ commit(:capture, money, options.merge(:transaction_id => authorization))
82
+ end
83
+
84
+ # Void a previous transaction
85
+ #
86
+ # ==== Parameters
87
+ #
88
+ # * <tt>authorization</tt> - The authorization returned from the previous authorize request.
89
+ #
90
+ def void(authorization, options = {})
91
+ commit(:void, nil, options.merge(:transaction_id => authorization))
92
+ end
93
+
94
+ # Credit an account.
95
+ #
96
+ # This transaction is also referred to as a Refund and indicates to the gateway that
97
+ # money should flow from the merchant to the customer.
98
+ #
99
+ # ==== Parameters
100
+ #
101
+ # * <tt>money</tt> -- The amount to be credited to the customer as an Integer value in cents.
102
+ # * <tt>identification</tt> -- The ID of the original transaction against which the credit is being issued.
103
+ # * <tt>options</tt> -- A hash of parameters.
104
+ #
105
+ #
106
+ def credit(money, identification, options = {})
107
+ commit(:refund, money, options.merge(:transaction_id => identification))
108
+ end
109
+
110
+ # Query the merchant account status
111
+ def query
112
+ commit(:query, nil, {})
113
+ end
114
+
115
+ private
116
+
117
+ def hosted?
118
+ @options[:pem]
119
+ end
120
+
121
+ def commit(action, money, parameters)
122
+ url = test? ? self.test_url : self.live_url
123
+
124
+ type = TYPES[action]
125
+ parameters[:trans_request_id] ||= SecureRandom.hex(10)
126
+
127
+ req = build_request(type, money, parameters)
128
+ data = ssl_post(url, req, "Content-Type" => "application/x-qbmsxml")
129
+ response = parse(type, data)
130
+ message = (response[:status_message] || '').strip
131
+
132
+ Response.new(success?(response), message, response,
133
+ :test => test?,
134
+ :authorization => response[:credit_card_trans_id],
135
+ :fraud_review => fraud_review?(response),
136
+ :avs_result => { :code => avs_result(response) },
137
+ :cvv_result => cvv_result(response)
138
+ )
139
+ end
140
+
141
+ def success?(response)
142
+ response[:status_code] == 0
143
+ end
144
+
145
+ def fraud_review?(response)
146
+ [10100, 10101].member? response[:status_code]
147
+ end
148
+
149
+ def parse(type, body)
150
+ xml = REXML::Document.new(body)
151
+
152
+ signon = REXML::XPath.first(xml, "//SignonMsgsRs/#{hosted? ? 'SignonAppCertRs' : 'SignonDesktopRs'}")
153
+ status_code = signon.attributes["statusCode"].to_i
154
+
155
+ if status_code != 0
156
+ return {
157
+ :status_code => status_code,
158
+ :status_message => signon.attributes["statusMessage"],
159
+ }
160
+ end
161
+
162
+ response = REXML::XPath.first(xml, "//QBMSXMLMsgsRs/#{type}Rs")
163
+
164
+ results = {
165
+ :status_code => response.attributes["statusCode"].to_i,
166
+ :status_message => response.attributes["statusMessage"],
167
+ }
168
+
169
+ response.elements.each do |e|
170
+ name = e.name.underscore.to_sym
171
+ value = e.text()
172
+
173
+ if old_value = results[name]
174
+ results[name] = [old_value] if !old_value.kind_of?(Array)
175
+ results[name] << value
176
+ else
177
+ results[name] = value
178
+ end
179
+ end
180
+
181
+ results
182
+ end
183
+
184
+ def build_request(type, money, parameters = {})
185
+ xml = Builder::XmlMarkup.new(:indent => 0)
186
+
187
+ xml.instruct!(:xml, :version => '1.0', :encoding => 'utf-8')
188
+ xml.instruct!(:qbmsxml, :version => API_VERSION)
189
+
190
+ xml.tag!("QBMSXML") do
191
+ xml.tag!("SignonMsgsRq") do
192
+ xml.tag!(hosted? ? "SignonAppCertRq" : "SignonDesktopRq") do
193
+ xml.tag!("ClientDateTime", Time.now.xmlschema)
194
+ xml.tag!("ApplicationLogin", @options[:login])
195
+ xml.tag!("ConnectionTicket", @options[:ticket])
196
+ end
197
+ end
198
+
199
+ xml.tag!("QBMSXMLMsgsRq") do
200
+ xml.tag!("#{type}Rq") do
201
+ method("build_#{type}").call(xml, money, parameters)
202
+ end
203
+ end
204
+ end
205
+
206
+ xml.target!
207
+ end
208
+
209
+ def build_CustomerCreditCardAuth(xml, money, parameters)
210
+ cc = parameters[:credit_card]
211
+ name = "#{cc.first_name} #{cc.last_name}"[0...30]
212
+
213
+ xml.tag!("TransRequestID", parameters[:trans_request_id])
214
+ xml.tag!("CreditCardNumber", cc.number)
215
+ xml.tag!("ExpirationMonth", cc.month)
216
+ xml.tag!("ExpirationYear", cc.year)
217
+ xml.tag!("IsECommerce", "true")
218
+ xml.tag!("Amount", amount(money))
219
+ xml.tag!("NameOnCard", name)
220
+ add_address(xml, parameters)
221
+ xml.tag!("CardSecurityCode", cc.verification_value) if cc.verification_value?
222
+ end
223
+
224
+ def build_CustomerCreditCardCapture(xml, money, parameters)
225
+ xml.tag!("TransRequestID", parameters[:trans_request_id])
226
+ xml.tag!("CreditCardTransID", parameters[:transaction_id])
227
+ xml.tag!("Amount", amount(money))
228
+ end
229
+
230
+ def build_CustomerCreditCardCharge(xml, money, parameters)
231
+ cc = parameters[:credit_card]
232
+ name = "#{cc.first_name} #{cc.last_name}"[0...30]
233
+
234
+ xml.tag!("TransRequestID", parameters[:trans_request_id])
235
+ xml.tag!("CreditCardNumber", cc.number)
236
+ xml.tag!("ExpirationMonth", cc.month)
237
+ xml.tag!("ExpirationYear", cc.year)
238
+ xml.tag!("IsECommerce", "true")
239
+ xml.tag!("Amount", amount(money))
240
+ xml.tag!("NameOnCard", name)
241
+ add_address(xml, parameters)
242
+ xml.tag!("CardSecurityCode", cc.verification_value) if cc.verification_value?
243
+ end
244
+
245
+ def build_CustomerCreditCardTxnVoidOrRefund(xml, money, parameters)
246
+ xml.tag!("TransRequestID", parameters[:trans_request_id])
247
+ xml.tag!("CreditCardTransID", parameters[:transaction_id])
248
+ xml.tag!("Amount", amount(money))
249
+ end
250
+
251
+ def build_CustomerCreditCardTxnVoid(xml, money, parameters)
252
+ xml.tag!("TransRequestID", parameters[:trans_request_id])
253
+ xml.tag!("CreditCardTransID", parameters[:transaction_id])
254
+ end
255
+
256
+ # Called reflectively by build_request
257
+ def build_MerchantAccountQuery(xml, money, parameters)
258
+ end
259
+
260
+ def add_address(xml, parameters)
261
+ if address = parameters[:billing_address] || parameters[:address]
262
+ xml.tag!("CreditCardAddress", address[:address1][0...30])
263
+ xml.tag!("CreditCardPostalCode", address[:zip][0...9])
264
+ end
265
+ end
266
+
267
+ def cvv_result(response)
268
+ case response[:card_security_code_match]
269
+ when "Pass" then 'M'
270
+ when "Fail" then 'N'
271
+ when "NotAvailable" then 'P'
272
+ end
273
+ end
274
+
275
+ def avs_result(response)
276
+ case "#{response[:avs_street]}|#{response[:avs_zip]}"
277
+ when "Pass|Pass" then "D"
278
+ when "Pass|Fail" then "A"
279
+ when "Pass|NotAvailable" then "B"
280
+ when "Fail|Pass" then "Z"
281
+ when "Fail|Fail" then "C"
282
+ when "Fail|NotAvailable" then "N"
283
+ when "NotAvailable|Pass" then "P"
284
+ when "NotAvailable|Fail" then "N"
285
+ when "NotAvailable|NotAvailable" then "U"
286
+ end
287
+ end
288
+ end
289
+ end
290
+ end
@@ -14,7 +14,7 @@ module ActiveMerchant #:nodoc:
14
14
  :state => 'custState',
15
15
  :zip => 'custPinCode',
16
16
  :country => 'custCountry',
17
- :phone2 => 'custMobileNo'
17
+ :phone => 'custMobileNo'
18
18
 
19
19
  mapping :shipping_address, :name => 'deliveryName',
20
20
  :city => 'deliveryCity',
@@ -22,7 +22,7 @@ module ActiveMerchant #:nodoc:
22
22
  :state => 'deliveryState',
23
23
  :zip => 'deliveryPinCode',
24
24
  :country => 'deliveryCountry',
25
- :phone2 => 'deliveryMobileNo'
25
+ :phone => 'deliveryMobileNo'
26
26
 
27
27
  mapping :customer, :name => 'custName',
28
28
  :email => 'custEmailId'
@@ -79,16 +79,13 @@ module ActiveMerchant #:nodoc:
79
79
  end
80
80
 
81
81
  def shipping_address(params = {})
82
- add_street_address!(params)
82
+ update_address(:shipping_address, params)
83
83
  super(params.dup)
84
- add_field(mappings[:shipping_address][:name], fields[mappings[:customer][:name]]) if fields[mappings[:shipping_address][:name]].blank?
85
- add_phone_for!(:shipping_address, params)
86
84
  end
87
85
 
88
86
  def billing_address(params = {})
89
- add_street_address!(params)
87
+ update_address(:billing_address, params)
90
88
  super(params.dup)
91
- add_phone_for!(:billing_address, params)
92
89
  end
93
90
 
94
91
  def form_fields
@@ -123,24 +120,27 @@ module ActiveMerchant #:nodoc:
123
120
  end
124
121
  end
125
122
 
126
- def add_street_address!(params)
123
+ def update_address(address_type, params)
127
124
  address = params[:address1]
128
125
  address << " #{params[:address2]}" if params[:address2]
129
- params.merge!(:address1 => address)
126
+ params[:address1] = address
127
+ params[:phone] = normalize_phone_number(params[:phone])
128
+ add_land_line_phone_for(address_type, params)
129
+
130
+ if address_type == :shipping_address && params[:name].blank?
131
+ add_field(mappings[:shipping_address][:name], fields[mappings[:customer][:name]])
132
+ end
130
133
  end
131
-
132
- def add_phone_for!(address_type, params)
134
+
135
+ # Split a single phone number into the country code, area code and local number as best as possible
136
+ def add_land_line_phone_for(address_type, params)
133
137
  address_field = address_type == :billing_address ? 'custPhoneNo' : 'deliveryPhNo'
134
138
 
135
- if params.has_key?(:phone)
136
- country = fields[mappings[address_type][:country]]
137
- phone = params[:phone].to_s
138
- # Remove all non digits
139
- phone.gsub!(/[^\d ]+/, '')
140
-
139
+ if params.has_key?(:phone2)
140
+ phone = normalize_phone_number(params[:phone2])
141
141
  phone_country_code, phone_area_code, phone_number = nil
142
-
143
- if country == 'IN' && phone =~ /(91)? *(\d{3}) *(\d{4,})$/
142
+
143
+ if params[:country] == 'IN' && phone =~ /(91)? *(\d{3}) *(\d{4,})$/
144
144
  phone_country_code, phone_area_code, phone_number = $1, $2, $3
145
145
  else
146
146
  numbers = phone.split(' ')
@@ -154,13 +154,17 @@ module ActiveMerchant #:nodoc:
154
154
  phone_area_code, phone_number = $1, $2
155
155
  end
156
156
  end
157
-
158
- add_field("#{address_field}1", phone_country_code || phone_code_for_country(country) || '91')
157
+
158
+ add_field("#{address_field}1", phone_country_code || phone_code_for_country(params[:country]) || '91')
159
159
  add_field("#{address_field}2", phone_area_code)
160
160
  add_field("#{address_field}3", phone_number)
161
161
  end
162
162
  end
163
163
 
164
+ def normalize_phone_number(phone)
165
+ phone.gsub(/[^\d ]+/, '') if phone
166
+ end
167
+
164
168
  # Special characters are NOT allowed while posting transaction parameters on DirecPay system
165
169
  def remove_special_characters(string)
166
170
  string.gsub(/[~"'&#%]/, '-')