active_merchant_ideal 0.1.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.
@@ -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