active_merchant_ideal 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,219 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+ require 'rexml/document'
4
+
5
+ module ActiveMerchant #:nodoc:
6
+ module Billing #:nodoc:
7
+ # The base class for all iDEAL response classes.
8
+ #
9
+ # Note that if the iDEAL system is under load it will _not_ allow more
10
+ # then two retries per request.
11
+ class IdealResponse < Response
12
+ def initialize(response_body, options = {})
13
+ @response = REXML::Document.new(response_body).root
14
+ @success = !error_occured?
15
+ @test = options[:test]
16
+ end
17
+
18
+ # Returns a technical error message.
19
+ def error_message
20
+ text('//Error/errorMessage') unless success?
21
+ end
22
+
23
+ # Returns a consumer friendly error message.
24
+ def consumer_error_message
25
+ text('//Error/consumerMessage') unless success?
26
+ end
27
+
28
+ # Returns details on the error if available.
29
+ def error_details
30
+ text('//Error/errorDetail') unless success?
31
+ end
32
+
33
+ # Returns an error type inflected from the first two characters of the
34
+ # error code. See error_code for a full list of errors.
35
+ #
36
+ # Error code to type mappings:
37
+ #
38
+ # * +IX+ - <tt>:xml</tt>
39
+ # * +SO+ - <tt>:system</tt>
40
+ # * +SE+ - <tt>:security</tt>
41
+ # * +BR+ - <tt>:value</tt>
42
+ # * +AP+ - <tt>:application</tt>
43
+ def error_type
44
+ unless success?
45
+ case error_code[0,2]
46
+ when 'IX' then :xml
47
+ when 'SO' then :system
48
+ when 'SE' then :security
49
+ when 'BR' then :value
50
+ when 'AP' then :application
51
+ end
52
+ end
53
+ end
54
+
55
+ # Returns the code of the error that occured.
56
+ #
57
+ # === Codes
58
+ #
59
+ # ==== IX: Invalid XML and all related problems
60
+ #
61
+ # Such as incorrect encoding, invalid version, or otherwise unreadable:
62
+ #
63
+ # * <tt>IX1000</tt> - Received XML not well-formed.
64
+ # * <tt>IX1100</tt> - Received XML not valid.
65
+ # * <tt>IX1200</tt> - Encoding type not UTF-8.
66
+ # * <tt>IX1300</tt> - XML version number invalid.
67
+ # * <tt>IX1400</tt> - Unknown message.
68
+ # * <tt>IX1500</tt> - Mandatory main value missing. (Merchant ID ?)
69
+ # * <tt>IX1600</tt> - Mandatory value missing.
70
+ #
71
+ # ==== SO: System maintenance or failure
72
+ #
73
+ # The errors that are communicated in the event of system maintenance or
74
+ # system failure. Also covers the situation where new requests are no
75
+ # longer being accepted but requests already submitted will be dealt with
76
+ # (until a certain time):
77
+ #
78
+ # * <tt>SO1000</tt> - Failure in system.
79
+ # * <tt>SO1200</tt> - System busy. Try again later.
80
+ # * <tt>SO1400</tt> - Unavailable due to maintenance.
81
+ #
82
+ # ==== SE: Security and authentication errors
83
+ #
84
+ # Incorrect authentication methods and expired certificates:
85
+ #
86
+ # * <tt>SE2000</tt> - Authentication error.
87
+ # * <tt>SE2100</tt> - Authentication method not supported.
88
+ # * <tt>SE2700</tt> - Invalid electronic signature.
89
+ #
90
+ # ==== BR: Field errors
91
+ #
92
+ # Extra information on incorrect fields:
93
+ #
94
+ # * <tt>BR1200</tt> - iDEAL version number invalid.
95
+ # * <tt>BR1210</tt> - Value contains non-permitted character.
96
+ # * <tt>BR1220</tt> - Value too long.
97
+ # * <tt>BR1230</tt> - Value too short.
98
+ # * <tt>BR1240</tt> - Value too high.
99
+ # * <tt>BR1250</tt> - Value too low.
100
+ # * <tt>BR1250</tt> - Unknown entry in list.
101
+ # * <tt>BR1270</tt> - Invalid date/time.
102
+ # * <tt>BR1280</tt> - Invalid URL.
103
+ #
104
+ # ==== AP: Application errors
105
+ #
106
+ # Errors relating to IDs, account numbers, time zones, transactions:
107
+ #
108
+ # * <tt>AP1000</tt> - Acquirer ID unknown.
109
+ # * <tt>AP1100</tt> - Merchant ID unknown.
110
+ # * <tt>AP1200</tt> - Issuer ID unknown.
111
+ # * <tt>AP1300</tt> - Sub ID unknown.
112
+ # * <tt>AP1500</tt> - Merchant ID not active.
113
+ # * <tt>AP2600</tt> - Transaction does not exist.
114
+ # * <tt>AP2620</tt> - Transaction already submitted.
115
+ # * <tt>AP2700</tt> - Bank account number not 11-proof.
116
+ # * <tt>AP2900</tt> - Selected currency not supported.
117
+ # * <tt>AP2910</tt> - Maximum amount exceeded. (Detailed record states the maximum amount).
118
+ # * <tt>AP2915</tt> - Amount too low. (Detailed record states the minimum amount).
119
+ # * <tt>AP2920</tt> - Please adjust expiration period. See suggested expiration period.
120
+ def error_code
121
+ text('//errorCode') unless success?
122
+ end
123
+
124
+ private
125
+
126
+ def error_occured?
127
+ @response.name == 'ErrorRes'
128
+ end
129
+
130
+ def text(path)
131
+ @response.get_text(path).to_s
132
+ end
133
+ end
134
+
135
+ # An instance of IdealTransactionResponse is returned from
136
+ # IdealGateway#setup_purchase which returns the service_url to where the
137
+ # user should be redirected to perform the transaction _and_ the
138
+ # transaction ID.
139
+ class IdealTransactionResponse < IdealResponse
140
+ # Returns the URL to the issuer’s page where the consumer should be
141
+ # redirected to in order to perform the payment.
142
+ def service_url
143
+ text('//issuerAuthenticationURL')
144
+ end
145
+
146
+ # Returns the transaction ID which is needed for requesting the status
147
+ # of a transaction. See IdealGateway#capture.
148
+ def transaction_id
149
+ text('//transactionID')
150
+ end
151
+
152
+ # Returns the <tt>:order_id</tt> for this transaction.
153
+ def order_id
154
+ text('//purchaseID')
155
+ end
156
+ end
157
+
158
+ # An instance of IdealStatusResponse is returned from IdealGateway#capture
159
+ # which returns whether or not the transaction that was started with
160
+ # IdealGateway#setup_purchase was successful.
161
+ #
162
+ # It takes care of checking if the message was authentic by verifying the
163
+ # the message and its signature against the iDEAL certificate.
164
+ #
165
+ # If success? returns +false+ because the authenticity wasn't verified
166
+ # there will be no error_code, error_message, and error_type. Use verified?
167
+ # to check if the authenticity has been verified.
168
+ class IdealStatusResponse < IdealResponse
169
+ def initialize(response_body, options = {})
170
+ super
171
+ @success = transaction_successful?
172
+ end
173
+
174
+ # Returns the status message, which is one of: <tt>:success</tt>,
175
+ # <tt>:cancelled</tt>, <tt>:expired</tt>, <tt>:open</tt>, or
176
+ # <tt>:failure</tt>.
177
+ def status
178
+ text('//status').downcase.to_sym
179
+ end
180
+
181
+ # Returns whether or not the authenticity of the message could be
182
+ # verified.
183
+ def verified?
184
+ @verified ||= IdealGateway.ideal_certificate.public_key.
185
+ verify(OpenSSL::Digest::SHA1.new, signature, message)
186
+ end
187
+
188
+ private
189
+
190
+ # Checks if no errors occured _and_ if the message was authentic.
191
+ def transaction_successful?
192
+ !error_occured? && status == :success && verified?
193
+ end
194
+
195
+ # The message that we need to verify the authenticity.
196
+ def message
197
+ text('//createDateTimeStamp') + text('//transactionID') + text('//status') + text('//consumerAccountNumber')
198
+ end
199
+
200
+ def signature
201
+ Base64.decode64(text('//signatureValue'))
202
+ end
203
+ end
204
+
205
+ # An instance of IdealDirectoryResponse is returned from
206
+ # IdealGateway#issuers which returns the list of issuers available at the
207
+ # acquirer.
208
+ class IdealDirectoryResponse < IdealResponse
209
+ # Returns a list of issuers available at the acquirer.
210
+ #
211
+ # gateway.issuers.list # => [{ :id => '1006', :name => 'ABN AMRO Bank' }]
212
+ def list
213
+ @response.get_elements('//Issuer').map do |issuer|
214
+ { :id => issuer.get_text('issuerID').to_s, :name => issuer.get_text('issuerName').to_s }
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,3 @@
1
+ require 'active_merchant'
2
+ require 'active_merchant_ideal/ideal.rb'
3
+ require 'active_merchant_ideal/ideal_response.rb'
data/test/fixtures.yml ADDED
@@ -0,0 +1,12 @@
1
+ # You can also paste the contents of the key and certificates here,
2
+ # if you want to do so remove the “_file” part of the keys.
3
+ ideal_ing_postbank:
4
+ test_url: https://idealtest.secure-ing.com:443/ideal/iDeal
5
+ merchant_id: ID
6
+ passphrase: PRIVATE KEY PASSPHRASE
7
+ private_key_file: |--
8
+ PASTE THE PATH TO YOUR PEM FILE HERE
9
+ private_certificate_file: |--
10
+ PASTE THE PATH TO YOUR CERTIFICATE FILE HERE
11
+ ideal_certificate_file: |--
12
+ PASTE THE PATH TO THE iDEAL CERTIFICATE FILE HERE
data/test/helper.rb ADDED
@@ -0,0 +1,143 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'mocha'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'active_merchant_ideal'
8
+
9
+ ActiveMerchant::Billing::Base.mode = :test
10
+
11
+ # Test gateways
12
+ class SimpleTestGateway < ActiveMerchant::Billing::Gateway
13
+ end
14
+
15
+ class SubclassGateway < SimpleTestGateway
16
+ end
17
+
18
+ class Hash #:nodoc:
19
+ # Return a new hash with all keys converted to symbols.
20
+ def symbolize_keys
21
+ inject({}) do |options, (key, value)|
22
+ options[(key.to_sym rescue key) || key] = value
23
+ options
24
+ end
25
+ end
26
+ # Destructively convert all keys to symbols.
27
+ def symbolize_keys!
28
+ self.replace(self.symbolize_keys)
29
+ end
30
+ end
31
+
32
+ module ActiveMerchant
33
+ module Assertions
34
+ def assert_field(field, value)
35
+ clean_backtrace do
36
+ assert_equal value, @helper.fields[field]
37
+ end
38
+ end
39
+
40
+ # Allows the testing of you to check for negative assertions:
41
+ #
42
+ # # Instead of
43
+ # assert !something_that_is_false
44
+ #
45
+ # # Do this
46
+ # assert_false something_that_should_be_false
47
+ #
48
+ # An optional +msg+ parameter is available to help you debug.
49
+ def assert_false(boolean, message = nil)
50
+ message = build_message message, '<?> is not false or nil.', boolean
51
+
52
+ clean_backtrace do
53
+ assert_block message do
54
+ not boolean
55
+ end
56
+ end
57
+ end
58
+
59
+ # A handy little assertion to check for a successful response:
60
+ #
61
+ # # Instead of
62
+ # assert response.success?
63
+ #
64
+ # # DRY that up with
65
+ # assert_success response
66
+ #
67
+ # A message will automatically show the inspection of the response
68
+ # object if things go afoul.
69
+ def assert_success(response)
70
+ clean_backtrace do
71
+ assert response.success?, "Response failed: #{response.inspect}"
72
+ end
73
+ end
74
+
75
+ # The negative of +assert_success+
76
+ def assert_failure(response)
77
+ clean_backtrace do
78
+ assert_false response.success?, "Response expected to fail: #{response.inspect}"
79
+ end
80
+ end
81
+
82
+ def assert_valid(validateable)
83
+ clean_backtrace do
84
+ assert validateable.valid?, "Expected to be valid"
85
+ end
86
+ end
87
+
88
+ def assert_not_valid(validateable)
89
+ clean_backtrace do
90
+ assert_false validateable.valid?, "Expected to not be valid"
91
+ end
92
+ end
93
+
94
+ private
95
+ def clean_backtrace(&block)
96
+ yield
97
+ rescue Test::Unit::AssertionFailedError => e
98
+ path = File.expand_path(__FILE__)
99
+ raise Test::Unit::AssertionFailedError, e.message, e.backtrace.reject { |line| File.expand_path(line) =~ /#{path}/ }
100
+ end
101
+ end
102
+ end
103
+
104
+
105
+ module Test
106
+ module Unit
107
+ class TestCase
108
+ HOME_DIR = RUBY_PLATFORM =~ /mswin32/ ? ENV['HOMEPATH'] : ENV['HOME'] unless defined?(HOME_DIR)
109
+ LOCAL_CREDENTIALS = File.join(HOME_DIR.to_s, '.active_merchant/fixtures.yml') unless defined?(LOCAL_CREDENTIALS)
110
+ DEFAULT_CREDENTIALS = File.join(File.dirname(__FILE__), 'fixtures.yml') unless defined?(DEFAULT_CREDENTIALS)
111
+
112
+ include ActiveMerchant::Billing
113
+ include ActiveMerchant::Assertions
114
+
115
+ private
116
+
117
+ def all_fixtures
118
+ @@fixtures ||= load_fixtures
119
+ end
120
+
121
+ def fixtures(key)
122
+ data = all_fixtures[key] || raise(StandardError, "No fixture data was found for '#{key}'")
123
+
124
+ data.dup
125
+ end
126
+
127
+ def load_fixtures
128
+ file = File.exists?(LOCAL_CREDENTIALS) ? LOCAL_CREDENTIALS : DEFAULT_CREDENTIALS
129
+ yaml_data = YAML.load(File.read(file))
130
+ symbolize_keys(yaml_data)
131
+
132
+ yaml_data
133
+ end
134
+
135
+ def symbolize_keys(hash)
136
+ return unless hash.is_a?(Hash)
137
+
138
+ hash.symbolize_keys!
139
+ hash.each{|k,v| symbolize_keys(v)}
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,138 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class IdealTest < Test::Unit::TestCase
4
+ def setup
5
+ Base.mode = :test
6
+ setup_ideal_gateway(fixtures(:ideal_ing_postbank))
7
+
8
+ @gateway = IdealGateway.new
9
+
10
+ @valid_options = {
11
+ :issuer_id => '0151',
12
+ :expiration_period => 'PT10M',
13
+ :return_url => 'http://return_to.example.com',
14
+ :order_id => '123456789012',
15
+ :currency => 'EUR',
16
+ :description => 'A classic Dutch windmill',
17
+ :entrance_code => '1234'
18
+ }
19
+ end
20
+
21
+ def test_making_test_requests
22
+ assert @gateway.issuers.test?
23
+ end
24
+
25
+ def test_setup_purchase_with_valid_options
26
+ response = @gateway.setup_purchase(550, @valid_options)
27
+
28
+ assert_success response
29
+ assert_not_nil response.service_url
30
+ assert_not_nil response.transaction_id
31
+ assert_equal @valid_options[:order_id], response.order_id
32
+ end
33
+
34
+ def test_setup_purchase_with_invalid_amount
35
+ response = @gateway.setup_purchase(0.5, @valid_options)
36
+
37
+ assert_failure response
38
+ assert_equal "BR1210", response.error_code
39
+ assert_not_nil response.error_message
40
+ assert_not_nil response.consumer_error_message
41
+ end
42
+
43
+ # TODO: Should we raise a SecurityError instead of setting success to false?
44
+ def test_status_response_with_invalid_signature
45
+ IdealStatusResponse.any_instance.stubs(:signature).returns('db82/jpJRvKQKoiDvu33X0yoDAQpayJOaW2Y8zbR1qk1i3epvTXi+6g+QVBY93YzGv4w+Va+vL3uNmzyRjYsm2309d1CWFVsn5Mk24NLSvhYfwVHEpznyMqizALEVUNSoiSHRkZUDfXowBAyLT/tQVGbuUuBj+TKblY826nRa7U=')
46
+ response = capture_transaction(:success)
47
+
48
+ assert_failure response
49
+ assert !response.verified?
50
+ end
51
+
52
+ ###
53
+ #
54
+ # These are the 7 integration tests of ING which need to be ran sucessfuly
55
+ # _before_ you'll get access to the live environment.
56
+ #
57
+ # See test_transaction_id for info on how the remote tests are ran.
58
+ #
59
+
60
+ def test_retrieval_of_issuers
61
+ assert_equal [{ :id => '0151', :name => 'Issuer Simulator' }], @gateway.issuers.list
62
+ end
63
+
64
+ def test_successful_transaction
65
+ assert_success capture_transaction(:success)
66
+ end
67
+
68
+ def test_cancelled_transaction
69
+ captured_response = capture_transaction(:cancelled)
70
+
71
+ assert_failure captured_response
72
+ assert_equal :cancelled, captured_response.status
73
+ end
74
+
75
+ def test_expired_transaction
76
+ captured_response = capture_transaction(:expired)
77
+
78
+ assert_failure captured_response
79
+ assert_equal :expired, captured_response.status
80
+ end
81
+
82
+ def test_still_open_transaction
83
+ captured_response = capture_transaction(:open)
84
+
85
+ assert_failure captured_response
86
+ assert_equal :open, captured_response.status
87
+ end
88
+
89
+ def test_failed_transaction
90
+ captured_response = capture_transaction(:failure)
91
+
92
+ assert_failure captured_response
93
+ assert_equal :failure, captured_response.status
94
+ end
95
+
96
+ def test_internal_server_error
97
+ captured_response = capture_transaction(:server_error)
98
+
99
+ assert_failure captured_response
100
+ assert_equal 'SO1000', captured_response.error_code
101
+ end
102
+
103
+ private
104
+
105
+ # Shortcut method which does a #setup_purchase through #test_transaction and
106
+ # captures the resulting transaction and returns the capture response.
107
+ def capture_transaction(type)
108
+ @gateway.capture test_transaction(type).transaction_id
109
+ end
110
+
111
+ # Calls #setup_purchase with the amount corresponding to the named test and
112
+ # returns the response. Before returning an assertion will be ran to test
113
+ # whether or not the transaction was successful.
114
+ def test_transaction(type)
115
+ amount = case type
116
+ when :success then 100
117
+ when :cancelled then 200
118
+ when :expired then 300
119
+ when :open then 400
120
+ when :failure then 500
121
+ when :server_error then 700
122
+ end
123
+
124
+ response = @gateway.setup_purchase(amount, @valid_options)
125
+ assert response.success?
126
+ response
127
+ end
128
+
129
+ # Setup the gateway by providing a hash of aatributes and values.
130
+ def setup_ideal_gateway(fixture)
131
+ fixture = fixture.dup
132
+ if passphrase = fixture.delete(:passphrase)
133
+ IdealGateway.passphrase = passphrase
134
+ end
135
+ fixture.each { |key, value| IdealGateway.send("#{key}=", value) }
136
+ IdealGateway.live_url = nil
137
+ end
138
+ end