activemerchant 1.8.0 → 1.9.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 (30) hide show
  1. data.tar.gz.sig +0 -0
  2. data/CHANGELOG +9 -0
  3. data/CONTRIBUTORS +21 -1
  4. data/README.rdoc +16 -4
  5. data/lib/active_merchant.rb +2 -0
  6. data/lib/active_merchant/billing/gateways/inspire.rb +221 -0
  7. data/lib/active_merchant/billing/gateways/paybox_direct.rb +205 -0
  8. data/lib/active_merchant/billing/gateways/secure_net.rb +330 -0
  9. data/lib/active_merchant/billing/integrations/bogus.rb +1 -1
  10. data/lib/active_merchant/billing/integrations/chronopay.rb +1 -1
  11. data/lib/active_merchant/billing/integrations/direc_pay.rb +37 -0
  12. data/lib/active_merchant/billing/integrations/direc_pay/helper.rb +188 -0
  13. data/lib/active_merchant/billing/integrations/direc_pay/notification.rb +76 -0
  14. data/lib/active_merchant/billing/integrations/direc_pay/return.rb +32 -0
  15. data/lib/active_merchant/billing/integrations/direc_pay/status.rb +37 -0
  16. data/lib/active_merchant/billing/integrations/gestpay.rb +1 -1
  17. data/lib/active_merchant/billing/integrations/helper.rb +8 -5
  18. data/lib/active_merchant/billing/integrations/hi_trust.rb +1 -1
  19. data/lib/active_merchant/billing/integrations/nochex.rb +1 -1
  20. data/lib/active_merchant/billing/integrations/paypal.rb +1 -1
  21. data/lib/active_merchant/billing/integrations/return.rb +4 -2
  22. data/lib/active_merchant/billing/integrations/sage_pay_form.rb +37 -0
  23. data/lib/active_merchant/billing/integrations/sage_pay_form/encryption.rb +33 -0
  24. data/lib/active_merchant/billing/integrations/sage_pay_form/helper.rb +109 -0
  25. data/lib/active_merchant/billing/integrations/sage_pay_form/notification.rb +204 -0
  26. data/lib/active_merchant/billing/integrations/sage_pay_form/return.rb +27 -0
  27. data/lib/active_merchant/billing/integrations/two_checkout.rb +1 -1
  28. data/lib/active_merchant/version.rb +1 -1
  29. metadata +17 -4
  30. metadata.gz.sig +0 -0
@@ -0,0 +1,76 @@
1
+ require 'net/http'
2
+
3
+ module ActiveMerchant #:nodoc:
4
+ module Billing #:nodoc:
5
+ module Integrations #:nodoc:
6
+ module DirecPay
7
+ class Notification < ActiveMerchant::Billing::Integrations::Notification
8
+ RESPONSE_PARAMS = ['DirecPay Reference ID', 'Flag', 'Country', 'Currency', 'Other Details', 'Merchant Order No', 'Amount']
9
+
10
+ def acknowledge
11
+ true
12
+ end
13
+
14
+ def complete?
15
+ status == 'Completed' || status == 'Pending'
16
+ end
17
+
18
+ def status
19
+ case params['Flag']
20
+ when 'SUCCESS'
21
+ 'Completed'
22
+ when 'PENDING'
23
+ 'Pending'
24
+ when 'FAIL'
25
+ 'Failed'
26
+ else
27
+ 'Error'
28
+ end
29
+ end
30
+
31
+ def item_id
32
+ params['Merchant Order No']
33
+ end
34
+
35
+ def transaction_id
36
+ params['DirecPay Reference ID']
37
+ end
38
+
39
+ # the money amount we received in X.2 decimal
40
+ def gross
41
+ params['Amount']
42
+ end
43
+
44
+ def currency
45
+ params['Currency']
46
+ end
47
+
48
+ def country
49
+ params['Country']
50
+ end
51
+
52
+ def other_details
53
+ params['Other Details']
54
+ end
55
+
56
+ def test?
57
+ false
58
+ end
59
+
60
+ # Take the posted data and move the relevant data into a hash
61
+ def parse(post)
62
+ super
63
+
64
+ values = params['responseparams'].to_s.split('|')
65
+ response_params = values.size == 3 ? ['DirecPay Reference ID', 'Flag', 'Error message'] : RESPONSE_PARAMS
66
+ response_params.each_with_index do |name, index|
67
+ params[name] = values[index]
68
+ end
69
+ params
70
+ end
71
+
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,32 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ module Integrations #:nodoc:
4
+
5
+ module DirecPay
6
+ class Return < ActiveMerchant::Billing::Integrations::Return
7
+
8
+ def initialize(post_data, options = {})
9
+ @notification = Notification.new(treat_failure_as_pending(post_data), options)
10
+ end
11
+
12
+ def success?
13
+ notification.complete?
14
+ end
15
+
16
+ def message
17
+ notification.status
18
+ end
19
+
20
+
21
+ private
22
+
23
+ # Work around the issue that the initial return from DirecPay is always either SUCCESS or FAIL, there is no PENDING
24
+ def treat_failure_as_pending(post_data)
25
+ post_data.sub(/FAIL/, 'PENDING')
26
+ end
27
+ end
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,37 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ module Integrations #:nodoc:
4
+ module DirecPay
5
+
6
+ class Status
7
+ include PostsData
8
+
9
+ STATUS_TEST_URL = 'https://test.timesofmoney.com/direcpay/secure/dpPullMerchAtrnDtls.jsp'
10
+ STATUS_LIVE_URL = 'https://www.timesofmoney.com/direcpay/secure/dpPullMerchAtrnDtls.jsp'
11
+
12
+ attr_reader :account, :options
13
+
14
+ def initialize(account, options = {})
15
+ @account, @options = account, options
16
+ end
17
+
18
+
19
+ # Use this method to manually request a status update to the provided notification_url
20
+ def update(authorization, notification_url)
21
+ url = test? ? STATUS_TEST_URL : STATUS_LIVE_URL
22
+ parameters = [ authorization, account, notification_url ]
23
+ data = PostData.new
24
+ data[:requestparams] = parameters.join('|')
25
+
26
+ response = ssl_get("#{url}?#{data.to_post_data}")
27
+ end
28
+
29
+ def test?
30
+ ActiveMerchant::Billing::Base.integration_mode == :test || options[:test]
31
+ end
32
+
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -16,7 +16,7 @@ module ActiveMerchant #:nodoc:
16
16
  Notification.new(post)
17
17
  end
18
18
 
19
- def self.return(query_string)
19
+ def self.return(query_string, options = {})
20
20
  Return.new(query_string)
21
21
  end
22
22
  end
@@ -14,12 +14,15 @@ module ActiveMerchant #:nodoc:
14
14
  self.application_id = 'ActiveMerchant'
15
15
 
16
16
  def initialize(order, account, options = {})
17
- options.assert_valid_keys([:amount, :currency, :test])
17
+ options.assert_valid_keys([:amount, :currency, :test, :credential2, :credential3, :credential4])
18
18
  @fields = {}
19
- self.order = order
20
- self.account = account
21
- self.amount = options[:amount]
22
- self.currency = options[:currency]
19
+ self.order = order
20
+ self.account = account
21
+ self.amount = options[:amount]
22
+ self.currency = options[:currency]
23
+ self.credential2 = options[:credential2]
24
+ self.credential3 = options[:credential3]
25
+ self.credential4 = options[:credential4]
23
26
  end
24
27
 
25
28
  def self.mapping(attribute, options = {})
@@ -18,7 +18,7 @@ module ActiveMerchant #:nodoc:
18
18
  Notification.new(post)
19
19
  end
20
20
 
21
- def self.return(query_string)
21
+ def self.return(query_string, options = {})
22
22
  Return.new(query_string)
23
23
  end
24
24
  end
@@ -79,7 +79,7 @@ module ActiveMerchant #:nodoc:
79
79
  Notification.new(post)
80
80
  end
81
81
 
82
- def self.return(query_string)
82
+ def self.return(query_string, options = {})
83
83
  Return.new(query_string)
84
84
  end
85
85
  end
@@ -30,7 +30,7 @@ module ActiveMerchant #:nodoc:
30
30
  Notification.new(post)
31
31
  end
32
32
 
33
- def self.return(query_string)
33
+ def self.return(query_string, options = {})
34
34
  Return.new(query_string)
35
35
  end
36
36
  end
@@ -3,9 +3,11 @@ module ActiveMerchant #:nodoc:
3
3
  module Integrations #:nodoc:
4
4
  class Return
5
5
  attr_accessor :params
6
+ attr_reader :notification
6
7
 
7
- def initialize(query_string)
8
- @params = parse(query_string)
8
+ def initialize(query_string, options = {})
9
+ @params = parse(query_string)
10
+ @options = options
9
11
  end
10
12
 
11
13
  # Successful by default. Overridden in the child class
@@ -0,0 +1,37 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ module Integrations #:nodoc:
4
+ module SagePayForm
5
+ autoload :Helper, File.dirname(__FILE__) + '/sage_pay_form/helper.rb'
6
+ autoload :Return, File.dirname(__FILE__) + '/sage_pay_form/return.rb'
7
+ autoload :Notification, File.dirname(__FILE__) + '/sage_pay_form/notification.rb'
8
+ autoload :Encryption, File.dirname(__FILE__) + '/sage_pay_form/encryption.rb'
9
+
10
+ mattr_accessor :production_url
11
+ mattr_accessor :test_url
12
+ mattr_accessor :simulate_url
13
+ self.production_url = 'https://live.sagepay.com/gateway/service/vspform-register.vsp'
14
+ self.test_url = 'https://test.sagepay.com/gateway/service/vspform-register.vsp'
15
+ self.simulate_url = 'https://test.sagepay.com/Simulator/VSPFormGateway.asp'
16
+
17
+ def self.return(query_string, options = {})
18
+ Return.new(query_string, options)
19
+ end
20
+
21
+ def self.service_url
22
+ mode = ActiveMerchant::Billing::Base.integration_mode
23
+ case mode
24
+ when :production
25
+ self.production_url
26
+ when :test
27
+ self.test_url
28
+ when :simulate
29
+ self.simulate_url
30
+ else
31
+ raise StandardError, "Integration mode set to an invalid value: #{mode}"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,33 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ module Integrations #:nodoc:
4
+ module SagePayForm
5
+ module Encryption
6
+ def sage_encrypt(plaintext, key)
7
+ ActiveSupport::Base64.encode64s(sage_encrypt_xor(plaintext, key))
8
+ end
9
+
10
+ def sage_decrypt(ciphertext, key)
11
+ sage_encrypt_xor(ActiveSupport::Base64.decode64(ciphertext), key)
12
+ end
13
+
14
+ def sage_encrypt_salt(min, max)
15
+ length = rand(max - min + 1) + min
16
+ SecureRandom.base64(length + 4)[0, length]
17
+ end
18
+
19
+ private
20
+
21
+ def sage_encrypt_xor(data, key)
22
+ raise 'No key provided' if key.blank?
23
+
24
+ key *= (data.length.to_f / key.length.to_f).ceil
25
+ key = key[0, data.length]
26
+
27
+ data.bytes.zip(key.bytes).map { |b1, b2| (b1 ^ b2).chr }.join
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,109 @@
1
+ require 'uri'
2
+
3
+ module ActiveMerchant #:nodoc:
4
+ module Billing #:nodoc:
5
+ module Integrations #:nodoc:
6
+ module SagePayForm
7
+ class Helper < ActiveMerchant::Billing::Integrations::Helper
8
+ include Encryption
9
+
10
+ mapping :credential2, 'EncryptKey'
11
+
12
+ mapping :account, 'Vendor'
13
+ mapping :amount, 'Amount'
14
+ mapping :currency, 'Currency'
15
+
16
+ mapping :order, 'VendorTxCode'
17
+
18
+ mapping :customer,
19
+ :first_name => 'BillingFirstnames',
20
+ :last_name => 'BillingSurname',
21
+ :email => 'CustomerEMail',
22
+ :phone => 'BillingPhone'
23
+
24
+ mapping :billing_address,
25
+ :city => 'BillingCity',
26
+ :address1 => 'BillingAddress1',
27
+ :address2 => 'BillingAddress2',
28
+ :state => 'BillingState',
29
+ :zip => 'BillingPostCode',
30
+ :country => 'BillingCountry'
31
+
32
+ mapping :shipping_address,
33
+ :city => 'DeliveryCity',
34
+ :address1 => 'DeliveryAddress1',
35
+ :address2 => 'DeliveryAddress2',
36
+ :state => 'DeliveryState',
37
+ :zip => 'DeliveryPostCode',
38
+ :country => 'DeliveryCountry'
39
+
40
+ mapping :return_url, 'SuccessURL'
41
+ mapping :description, 'Description'
42
+
43
+ def form_fields
44
+ fields['DeliveryFirstnames'] ||= fields['BillingFirstnames']
45
+ fields['DeliverySurname'] ||= fields['BillingSurname']
46
+
47
+ fields['FailureURL'] ||= fields['SuccessURL']
48
+
49
+ crypt_skip = ['Vendor', 'EncryptKey']
50
+ crypt_skip << 'BillingState' unless fields['BillingCountry'] == 'US'
51
+ crypt_skip << 'DeliveryState' unless fields['DeliveryCountry'] == 'US'
52
+
53
+ key = fields['EncryptKey']
54
+ @crypt ||= create_crypt_field(fields.except(*crypt_skip), key)
55
+
56
+ {
57
+ 'VPSProtocol' => '2.23',
58
+ 'TxType' => 'PAYMENT',
59
+ 'Vendor' => @fields['Vendor'],
60
+ 'Crypt' => @crypt
61
+ }
62
+ end
63
+
64
+ private
65
+
66
+ def create_crypt_field(fields, key)
67
+ parts = fields.map { |k, v| "#{k}=#{sanitize(k, v)}" unless v.nil? }.compact.shuffle
68
+ parts.unshift(sage_encrypt_salt(key.length, key.length * 2))
69
+ sage_encrypt(parts.join('&'), key)
70
+ end
71
+
72
+ def sanitize(key, value)
73
+ reject = exact = nil
74
+
75
+ case key
76
+ when /URL$/
77
+ # allow all
78
+ when 'VendorTxCode'
79
+ reject = /[^A-Za-z0-9{}._-]+/
80
+ when /[Nn]ames?$/
81
+ reject = %r{[^[:alpha:] /\\.'-]+}
82
+ when /(?:Address[12]|City)$/
83
+ reject = %r{[^[:alnum:] +'/\\:,.\n()-]+}
84
+ when /PostCode$/
85
+ reject = /[^A-Za-z0-9 -]+/
86
+ when /Phone$/
87
+ reject = /[^0-9A-Za-z+ ()-]+/
88
+ when 'Currency'
89
+ exact = /^[A-Z]{3}$/
90
+ when /State$/
91
+ exact = /^[A-Z]{2}$/
92
+ else
93
+ reject = /&+/
94
+ end
95
+
96
+ if exact
97
+ raise ArgumentError, "Invalid value for #{key}: #{value.inspect}" unless value =~ exact
98
+ value
99
+ elsif reject
100
+ value.gsub(reject, ' ')
101
+ else
102
+ value
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,204 @@
1
+ require 'net/http'
2
+
3
+ module ActiveMerchant #:nodoc:
4
+ module Billing #:nodoc:
5
+ module Integrations #:nodoc:
6
+ module SagePayForm
7
+ class Notification < ActiveMerchant::Billing::Integrations::Notification
8
+ class CryptError < StandardError; end
9
+
10
+ include Encryption
11
+
12
+ def initialize(post_data, options)
13
+ super
14
+ load_crypt_params(params['crypt'], options[:credential2])
15
+ end
16
+
17
+ # Was the transaction complete?
18
+ def complete?
19
+ status_code == 'OK'
20
+ end
21
+
22
+ # Text version of #complete?, since we don't support Pending.
23
+ def status
24
+ complete? ? 'Completed' : 'Failed'
25
+ end
26
+
27
+ # Status of transaction. List of possible values:
28
+ # <tt>OK</tt>:: Transaction completed successfully.
29
+ # <tt>NOTAUTHED</tt>:: Incorrect card details / insufficient funds.
30
+ # <tt>MALFORMED</tt>:: Invalid input data.
31
+ # <tt>INVALID</tt>:: Valid input data, but some fields are incorrect.
32
+ # <tt>ABORT</tt>:: User hit cancel button or went idle for 15+ minutes.
33
+ # <tt>REJECTED</tt>:: Rejected by account fraud screening rules.
34
+ # <tt>AUTHENTICATED</tt>:: Authenticated card details secured at SagePay.
35
+ # <tt>REGISTERED</tt>:: Non-authenticated card details secured at SagePay.
36
+ # <tt>ERROR</tt>:: Problem internal to SagePay.
37
+ def status_code
38
+ params['Status']
39
+ end
40
+
41
+ # Check this if #completed? is false.
42
+ def message
43
+ params['StatusDetail']
44
+ end
45
+
46
+ # Vendor-supplied code (:order mapping).
47
+ def item_id
48
+ params['VendorTxCode']
49
+ end
50
+
51
+ # Internal SagePay code, typically "{LONG-UUID}".
52
+ def transaction_id
53
+ params['VPSTxId']
54
+ end
55
+
56
+ # Authorization number (only if #completed?).
57
+ def auth_id
58
+ params['TxAuthNo']
59
+ end
60
+
61
+ # Total amount (no fees).
62
+ def gross
63
+ params['Amount']
64
+ end
65
+
66
+ # AVS and CV2 check results. Possible values:
67
+ # <tt>ALL MATCH</tt>::
68
+ # <tt>SECURITY CODE MATCH ONLY</tt>::
69
+ # <tt>ADDRESS MATCH ONLY</tt>::
70
+ # <tt>NO DATA MATCHES</tt>::
71
+ # <tt>DATA NOT CHECKED</tt>::
72
+ def avs_cv2_result
73
+ params['AVSCV2']
74
+ end
75
+
76
+ # Numeric address check. Possible values:
77
+ # <tt>NOTPROVIDED</tt>::
78
+ # <tt>NOTCHECKED</tt>::
79
+ # <tt>MATCHED</tt>::
80
+ # <tt>NOTMATCHED</tt>::
81
+ def address_result
82
+ params['AddressResult']
83
+ end
84
+
85
+ # Post code check. Possible values:
86
+ # <tt>NOTPROVIDED</tt>::
87
+ # <tt>NOTCHECKED</tt>::
88
+ # <tt>MATCHED</tt>::
89
+ # <tt>NOTMATCHED</tt>::
90
+ def post_code_result
91
+ params['PostCodeResult']
92
+ end
93
+
94
+ # CV2 code check. Possible values:
95
+ # <tt>NOTPROVIDED</tt>::
96
+ # <tt>NOTCHECKED</tt>::
97
+ # <tt>MATCHED</tt>::
98
+ # <tt>NOTMATCHED</tt>::
99
+ def cv2_result
100
+ params['CV2Result']
101
+ end
102
+
103
+ # Was the Gift Aid box checked?
104
+ def gift_aid?
105
+ params['GiftAid'] == '1'
106
+ end
107
+
108
+ # Result of 3D Secure checks. Possible values:
109
+ # <tt>OK</tt>:: Authenticated correctly.
110
+ # <tt>NOTCHECKED</tt>:: Authentication not performed.
111
+ # <tt>NOTAVAILABLE</tt>:: Card not auth-capable, or auth is otherwise impossible.
112
+ # <tt>NOTAUTHED</tt>:: User failed authentication.
113
+ # <tt>INCOMPLETE</tt>:: Authentication unable to complete.
114
+ # <tt>ERROR</tt>:: Unable to attempt authentication due to data / service errors.
115
+ def buyer_auth_result
116
+ params['3DSecureStatus']
117
+ end
118
+
119
+ # Encoded 3D Secure result code.
120
+ def buyer_auth_result_code
121
+ params['CAVV']
122
+ end
123
+
124
+ # Address confirmation status. PayPal only. Possible values:
125
+ # <tt>NONE</tt>::
126
+ # <tt>CONFIRMED</tt>::
127
+ # <tt>UNCONFIRMED</tt>::
128
+ def address_status
129
+ params['AddressStatus']
130
+ end
131
+
132
+ # Payer verification. Undocumented.
133
+ def payer_verified?
134
+ params['PayerStatus'] == 'VERIFIED'
135
+ end
136
+
137
+ # Credit card type. Possible values:
138
+ # <tt>VISA</tt>:: Visa
139
+ # <tt>MC</tt>:: MasterCard
140
+ # <tt>DELTA</tt>:: Delta
141
+ # <tt>SOLO</tt>:: Solo
142
+ # <tt>MAESTRO</tt>:: Maestro (UK and International)
143
+ # <tt>UKE</tt>:: Visa Electron
144
+ # <tt>AMEX</tt>:: American Express
145
+ # <tt>DC</tt>:: Diners Club
146
+ # <tt>JCB</tt>:: JCB
147
+ # <tt>LASER</tt>:: Laser
148
+ # <tt>PAYPAL</tt>:: PayPal
149
+ def credit_card_type
150
+ params['CardType']
151
+ end
152
+
153
+ # Last four digits of credit card.
154
+ def credit_card_last_4_digits
155
+ params['Last4Digits']
156
+ end
157
+
158
+ # Used by composition methods, but not supplied by SagePay.
159
+ def currency
160
+ nil
161
+ end
162
+
163
+ def test?
164
+ false
165
+ end
166
+
167
+ def acknowledge
168
+ true
169
+ end
170
+
171
+ private
172
+
173
+ def load_crypt_params(crypt, key)
174
+ raise MissingCryptData if crypt.blank?
175
+ raise MissingCryptKey if key.blank?
176
+
177
+ crypt_data = sage_decrypt(crypt.gsub(' ', '+'), key)
178
+ raise InvalidCryptData unless crypt_data =~ /(^|&)Status=/
179
+
180
+ params.clear
181
+ parse(crypt_data)
182
+ end
183
+
184
+ class MissingCryptKey < CryptError
185
+ def message
186
+ 'No merchant decryption key supplied'
187
+ end
188
+ end
189
+ class MissingCryptData < CryptError
190
+ def message
191
+ 'No data received from SagePay'
192
+ end
193
+ end
194
+ class InvalidCryptData < CryptError
195
+ def message
196
+ 'Invalid data received from SagePay'
197
+ end
198
+ end
199
+
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end