ideal-payment 1.0.2

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.
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'ideal'
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ require 'nokogiri'
4
+ require 'rest'
5
+
6
+ require 'ideal/acquirers'
7
+ require 'ideal/gateway'
8
+ require 'ideal/response'
9
+ require 'ideal/version'
10
+
11
+ require "nokogiri"
12
+ require "openssl"
13
+ require "base64"
14
+ require 'xmldsig'
15
+
16
+ module Ideal
17
+ NAMESPACES = {
18
+ "ds" => "http://www.w3.org/2000/09/xmldsig#",
19
+ "ec" => "http://www.w3.org/2001/10/xml-exc-c14n#"
20
+ }
21
+ end
@@ -0,0 +1,18 @@
1
+ # encoding: utf-8
2
+
3
+ module Ideal
4
+ ACQUIRERS = {
5
+ ing: {
6
+ live_url: 'https://ideal.secure-ing.com/ideal/iDEALv3',
7
+ test_url: 'https://idealtest.secure-ing.com/ideal/iDEALv3'
8
+ },
9
+ rabobank: {
10
+ live_url: 'https://ideal.rabobank.nl/ideal/iDEALv3',
11
+ test_url: 'https://idealtest.rabobank.nl/ideal/iDEALv3'
12
+ },
13
+ abnamro: {
14
+ live_url: 'https://abnamro.ideal-payment.de/ideal/iDeal',
15
+ test_url: 'https://abnamro-test.ideal-payment.de/ideal/iDeal'
16
+ }
17
+ }
18
+ end
@@ -0,0 +1,377 @@
1
+ # encoding: utf-8
2
+
3
+ require 'openssl'
4
+ require 'net/https'
5
+ require 'base64'
6
+
7
+ module Ideal
8
+ # The base class for all iDEAL response classes.
9
+ #
10
+ # Note that if the iDEAL system is under load it will _not_ allow more
11
+ # then two retries per request.
12
+ class Gateway
13
+ LANGUAGE = 'nl'
14
+ CURRENCY = 'EUR'
15
+ API_VERSION = '3.3.1'
16
+ XML_NAMESPACE = 'http://www.idealdesk.com/ideal/messages/mer-acq/3.3.1'
17
+
18
+ def self.acquirers
19
+ Ideal::ACQUIRERS
20
+ end
21
+
22
+ class << self
23
+ # Returns the current acquirer used
24
+ attr_reader :acquirer
25
+
26
+ # Holds the environment in which the run (default is test)
27
+ attr_accessor :environment
28
+
29
+ # Holds the global iDEAL merchant id. Make sure to use a string with
30
+ # leading zeroes if needed.
31
+ attr_accessor :merchant_id
32
+
33
+ # Holds the passphrase that should be used for the merchant private_key.
34
+ attr_accessor :passphrase
35
+
36
+ # Holds the test and production urls for your iDeal acquirer.
37
+ attr_accessor :live_url, :test_url
38
+ end
39
+
40
+ # Environment defaults to test
41
+ self.environment = :test
42
+
43
+ # Loads the global merchant private_key from disk.
44
+ def self.private_key_file=(pkey_file)
45
+ self.private_key = File.read(pkey_file)
46
+ end
47
+
48
+ # Instantiates and assings a OpenSSL::PKey::RSA instance with the
49
+ # provided private key data.
50
+ def self.private_key=(pkey_data)
51
+ @private_key = OpenSSL::PKey::RSA.new(pkey_data, passphrase)
52
+ end
53
+
54
+ # Returns the global merchant private_certificate.
55
+ def self.private_key
56
+ @private_key
57
+ end
58
+
59
+ # Loads the global merchant private_certificate from disk.
60
+ def self.private_certificate_file=(certificate_file)
61
+ self.private_certificate = File.read(certificate_file)
62
+ end
63
+
64
+ # Instantiates and assings a OpenSSL::X509::Certificate instance with the
65
+ # provided private certificate data.
66
+ def self.private_certificate=(certificate_data)
67
+ @private_certificate = OpenSSL::X509::Certificate.new(certificate_data)
68
+ end
69
+
70
+ # Returns the global merchant private_certificate.
71
+ def self.private_certificate
72
+ @private_certificate
73
+ end
74
+
75
+ # Loads the global merchant ideal_certificate from disk.
76
+ def self.ideal_certificate_file=(certificate_file)
77
+ self.ideal_certificate = File.read(certificate_file)
78
+ end
79
+
80
+ # Instantiates and assings a OpenSSL::X509::Certificate instance with the
81
+ # provided iDEAL certificate data.
82
+ def self.ideal_certificate=(certificate_data)
83
+ @ideal_certificate = OpenSSL::X509::Certificate.new(certificate_data)
84
+ end
85
+
86
+ # Returns the global merchant ideal_certificate.
87
+ def self.ideal_certificate
88
+ @ideal_certificate
89
+ end
90
+
91
+ # Returns whether we're in test mode or not.
92
+ def self.test?
93
+ environment.to_sym == :test
94
+ end
95
+
96
+ # Set the correct acquirer url based on the specific Bank
97
+ # Currently supported arguments: :ing, :rabobank, :abnamro
98
+ #
99
+ # Ideal::Gateway.acquirer = :ing
100
+ def self.acquirer=(acquirer)
101
+ @acquirer = acquirer
102
+ if self.acquirers.include?(@acquirer)
103
+ acquirers[@acquirer].each do |attr, value|
104
+ send("#{attr}=", value)
105
+ end
106
+ else
107
+ raise ArgumentError, "Unknown acquirer `#{acquirer}', please choose one of: #{self.acquirers.keys.join(', ')}"
108
+ end
109
+ end
110
+
111
+ # Returns the merchant `subID' being used for this Gateway instance.
112
+ # Defaults to 0.
113
+ attr_reader :sub_id
114
+
115
+ # Initializes a new Gateway instance.
116
+ #
117
+ # You can optionally specify <tt>:sub_id</tt>. Defaults to 0.
118
+ def initialize(options = {})
119
+ @sub_id = options[:sub_id] || 0
120
+ end
121
+
122
+ # Returns the endpoint for the request.
123
+ #
124
+ # Automatically uses test or live URLs based on the configuration.
125
+ def request_url
126
+ self.class.send("#{self.class.environment}_url")
127
+ end
128
+
129
+ # Sends a directory request to the acquirer and returns an
130
+ # DirectoryResponse. Use DirectoryResponse#list to receive the
131
+ # actuall array of available issuers.
132
+ #
133
+ # gateway.issuers.list # => [{ :id => '1006', :name => 'ABN AMRO Bank' }, …]
134
+ def issuers
135
+ x = build_directory_request
136
+
137
+ a= post_data request_url, x, DirectoryResponse
138
+
139
+ log('REQ',x)
140
+ log('RES',a.response)
141
+ a
142
+ end
143
+
144
+ # Starts a purchase by sending an acquirer transaction request for the
145
+ # specified +money+ amount in EURO cents.
146
+ #
147
+ # On success returns an TransactionResponse with the #transaction_id
148
+ # which is needed for the capture step. (See capture for an example.)
149
+ #
150
+ # The iDEAL specification states that it is _not_ allowed to use another
151
+ # window or frame when redirecting the consumer to the issuer. So the
152
+ # entire merchant’s page has to be replaced by the selected issuer’s page.
153
+ #
154
+ # === Options
155
+ #
156
+ # Note that all options that have a character limit are _also_ checked
157
+ # for diacritical characters. If it does contain diacritical characters,
158
+ # or exceeds the character limit, an ArgumentError is raised.
159
+ #
160
+ # ==== Required
161
+ #
162
+ # * <tt>:issuer_id</tt> - The <tt>:id</tt> of an issuer available at the acquirer to which the transaction should be made.
163
+ # * <tt>:order_id</tt> - The order number. Limited to 12 characters.
164
+ # * <tt>:description</tt> - A description of the transaction. Limited to 32 characters.
165
+ # * <tt>:return_url</tt> - A URL on the merchant’s system to which the consumer is redirected _after_ payment. The acquirer will add the following GET variables:
166
+ # * <tt>trxid</tt> - The <tt>:order_id</tt>.
167
+ # * <tt>ec</tt> - The <tt>:entrance_code</tt> _if_ it was specified.
168
+ #
169
+ # ==== Optional
170
+ #
171
+ # * <tt>:entrance_code</tt> - This code is an abitrary token which can be used to identify the transaction besides the <tt>:order_id</tt>. Limited to 40 characters.
172
+ # * <tt>:expiration_period</tt> - The period of validity of the payment request measured from the receipt by the issuer. The consumer must approve the payment within this period, otherwise the StatusResponse#status will be set to `Expired'. E.g., consider an <tt>:expiration_period</tt> of `P3DT6H10M':
173
+ # * P: relative time designation.
174
+ # * 3 days.
175
+ # * T: separator.
176
+ # * 6 hours.
177
+ # * 10 minutes.
178
+ #
179
+ # === Example
180
+ #
181
+ # transaction_response = gateway.setup_purchase(4321, valid_options)
182
+ # if transaction_response.success?
183
+ # @purchase.update_attributes!(:transaction_id => transaction_response.transaction_id)
184
+ # redirect_to transaction_response.service_url
185
+ # end
186
+ #
187
+ # See the Gateway class description for a more elaborate example.
188
+ def setup_purchase(money, options)
189
+ req = build_transaction_request(money, options)
190
+ log('purchase', req)
191
+ resp = post_data request_url, req, TransactionResponse
192
+ #raise SecurityError, "The message could not be verified" if !resp.verified?
193
+ resp
194
+ end
195
+
196
+ # Sends a acquirer status request for the specified +transaction_id+ and
197
+ # returns an StatusResponse.
198
+ #
199
+ # It is _your_ responsibility as the merchant to check if the payment has
200
+ # been made until you receive a response with a finished status like:
201
+ # `Success', `Cancelled', `Expired', everything else equals `Open'.
202
+ #
203
+ # === Example
204
+ #
205
+ # capture_response = gateway.capture(@purchase.transaction_id)
206
+ # if capture_response.success?
207
+ # @purchase.update_attributes!(:paid => true)
208
+ # flash[:notice] = "Congratulations, you are now the proud owner of a Dutch windmill!"
209
+ # end
210
+ #
211
+ # See the Gateway class description for a more elaborate example.
212
+ def capture(transaction_id)
213
+ a = build_status_request(:transaction_id => transaction_id)
214
+ log('REQ', a)
215
+ b = post_data request_url, a, StatusResponse
216
+ log('RES', b)
217
+ b
218
+ end
219
+
220
+ private
221
+
222
+ def ssl_post(url, body)
223
+ log('URL', url)
224
+ log('Request', body)
225
+ response = REST.post(url, body, {
226
+ 'Content-Type' => 'application/xml; charset=utf-8'
227
+ }, {
228
+ :tls_verify => true,
229
+ :tls_key => self.class.private_key,
230
+ :tls_certificate => self.class.private_certificate
231
+ })
232
+ log('Response', response.body)
233
+ response.body
234
+ end
235
+
236
+ def post_data(gateway_url, data, response_klass)
237
+ response_klass.new(ssl_post(gateway_url, data), :test => self.class.test?)
238
+ end
239
+
240
+ # This is the list of charaters that are not supported by iDEAL according
241
+ # to the PHP source provided by ING plus the same in capitals.
242
+ DIACRITICAL_CHARACTERS = /[ÀÁÂÃÄÅÇŒÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝàáâãäåçæèéêëìíîïñòóôõöøùúûüý]/ #:nodoc:
243
+
244
+ # Raises an ArgumentError if the +string+ exceeds the +max_length+ amount
245
+ # of characters or contains any diacritical characters.
246
+ def enforce_maximum_length(key, string, max_length)
247
+ raise ArgumentError, "The value for `#{key}' exceeds the limit of #{max_length} characters." if string.length > max_length
248
+ raise ArgumentError, "The value for `#{key}' contains diacritical characters `#{string}'." if string =~ DIACRITICAL_CHARACTERS
249
+ end
250
+
251
+ #signs the xml
252
+ def sign!(xml)
253
+ digest_val = digest_value(xml.doc.children[0])
254
+ xml.Signature(xmlns: 'http://www.w3.org/2000/09/xmldsig#') do |xml|
255
+ xml.SignedInfo do |xml|
256
+ xml.CanonicalizationMethod(Algorithm: 'http://www.w3.org/2001/10/xml-exc-c14n#')
257
+ xml.SignatureMethod(Algorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256')
258
+ xml.Reference(URI: '') do |xml|
259
+ xml.Transforms do |xml|
260
+ xml.Transform(Algorithm: 'http://www.w3.org/2000/09/xmldsig#enveloped-signature')
261
+ end
262
+ xml.DigestMethod(Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256')
263
+ xml.DigestValue digest_val
264
+ end
265
+ end
266
+ xml.SignatureValue signature_value(xml.doc.xpath("//Signature:SignedInfo", 'Signature' => 'http://www.w3.org/2000/09/xmldsig#')[0])
267
+ xml.KeyInfo do |xml|
268
+ xml.KeyName fingerprint
269
+ end
270
+ end
271
+ end
272
+
273
+ # Creates a +signatureValue+ from the xml+.
274
+ def signature_value(sig_val)
275
+ canonical = sig_val.canonicalize(Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0)
276
+ signature = Ideal::Gateway.private_key.sign(OpenSSL::Digest::SHA256.new, canonical)
277
+ Base64.encode64(signature)
278
+ end
279
+
280
+ # Creates a +digestValue+ from the xml+.
281
+ def digest_value(xml)
282
+ canonical = xml.canonicalize(Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0)
283
+ digest = OpenSSL::Digest::SHA256.new.digest canonical
284
+ Base64.encode64(digest)
285
+ end
286
+
287
+ # Creates a keyName value for the XML signature
288
+ def fingerprint
289
+ Digest::SHA1.hexdigest(Ideal::Gateway.private_certificate.to_der)
290
+ end
291
+
292
+ # Returns a string containing the current UTC time, formatted as per the
293
+ # iDeal specifications, except we don't use miliseconds.
294
+ def created_at_timestamp
295
+ Time.now.gmtime.strftime("%Y-%m-%dT%H:%M:%S.000Z")
296
+ end
297
+
298
+ def requires!(options, *keys)
299
+ missing = keys - options.keys
300
+ unless missing.empty?
301
+ raise ArgumentError, "Missing required options: #{missing.map { |m| m.to_s }.join(', ')}"
302
+ end
303
+ end
304
+
305
+ def build_status_request(options)
306
+ requires!(options, :transaction_id)
307
+
308
+ timestamp = created_at_timestamp
309
+ Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
310
+ xml.AcquirerStatusReq(xmlns: XML_NAMESPACE, version: API_VERSION) do |xml|
311
+ xml.createDateTimestamp created_at_timestamp
312
+ xml.Merchant do |xml|
313
+ xml.merchantID self.class.merchant_id
314
+ xml.subID @sub_id
315
+ end
316
+ xml.Transaction do |xml|
317
+ xml.transactionID options[:transaction_id]
318
+ end
319
+ sign!(xml)
320
+ end
321
+ end.to_xml(:save_with => Nokogiri::XML::Node::SaveOptions::AS_XML)
322
+ end
323
+
324
+ def build_directory_request
325
+ timestamp = created_at_timestamp
326
+ xml = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
327
+ xml.DirectoryReq(xmlns: XML_NAMESPACE, version: API_VERSION) do |xml|
328
+ xml.createDateTimestamp created_at_timestamp
329
+ xml.Merchant do |xml|
330
+ xml.merchantID self.class.merchant_id
331
+ xml.subID @sub_id
332
+ end
333
+ sign!(xml)
334
+ end
335
+ end.to_xml(:save_with => Nokogiri::XML::Node::SaveOptions::AS_XML)
336
+ end
337
+
338
+ def build_transaction_request(money, options)
339
+ requires!(options, :issuer_id, :expiration_period, :return_url, :order_id, :description, :entrance_code)
340
+
341
+ enforce_maximum_length(:money, money.to_s, 12)
342
+ enforce_maximum_length(:order_id, options[:order_id], 12)
343
+ enforce_maximum_length(:description, options[:description], 32)
344
+ enforce_maximum_length(:entrance_code, options[:entrance_code], 40)
345
+
346
+ timestamp = created_at_timestamp
347
+
348
+ Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
349
+ xml.AcquirerTrxReq(xmlns: XML_NAMESPACE, version: API_VERSION) do |xml|
350
+ xml.createDateTimestamp created_at_timestamp
351
+ xml.Issuer do |xml|
352
+ xml.issuerID options[:issuer_id]
353
+ end
354
+ xml.Merchant do |xml|
355
+ xml.merchantID self.class.merchant_id
356
+ xml.subID 0
357
+ xml.merchantReturnURL options[:return_url]
358
+ end
359
+ xml.Transaction do |xml|
360
+ xml.purchaseID options[:order_id]
361
+ xml.amount money
362
+ xml.currency CURRENCY
363
+ xml.expirationPeriod options[:expiration_period]
364
+ xml.language LANGUAGE
365
+ xml.description options[:description]
366
+ xml.entranceCode options[:entrance_code]
367
+ end
368
+ sign!(xml)
369
+ end
370
+ end.to_xml(:save_with => Nokogiri::XML::Node::SaveOptions::AS_XML)
371
+ end
372
+
373
+ def log(thing, contents)
374
+ $stderr.write("\n#{thing}:\n\n#{contents}\n") if $DEBUG
375
+ end
376
+ end
377
+ end
@@ -0,0 +1,274 @@
1
+ # encoding: utf-8
2
+
3
+ require 'cgi'
4
+ require 'openssl'
5
+ require 'base64'
6
+ #require 'rexml/document'
7
+
8
+ module Ideal
9
+ # === Response classes
10
+ #
11
+ # * Response
12
+ # * TransactionResponse
13
+ # * StatusResponse
14
+ # * DirectoryResponse
15
+ #
16
+ # See the Response class for more information on errors.
17
+ class Response
18
+ attr_accessor :response
19
+
20
+ def initialize(response_body, options = {})
21
+ #@response = REXML::Document.new(response_body).root
22
+ @body = response_body
23
+ doc = Nokogiri::XML(response_body)
24
+ doc.remove_namespaces!
25
+ @response = doc.root
26
+ @success = !error_occured?
27
+ @test = options[:test] ? options[:test] : false
28
+ end
29
+
30
+ # Returns whether we're running in test mode
31
+ def test?
32
+ @test
33
+ end
34
+
35
+ # Returns whether the request was a success
36
+ def success?
37
+ @success
38
+ end
39
+
40
+ # Returns a technical error message.
41
+ def error_message
42
+ text('//Error/errorMessage') unless success?
43
+ end
44
+
45
+ # Returns a consumer friendly error message.
46
+ def consumer_error_message
47
+ text('//Error/consumerMessage') unless success?
48
+ end
49
+
50
+ # Returns details on the error if available.
51
+ def error_details
52
+ text('//Error/errorDetail') unless success?
53
+ end
54
+
55
+ # Returns an error type inflected from the first two characters of the
56
+ # error code. See error_code for a full list of errors.
57
+ #
58
+ # Error code to type mappings:
59
+ #
60
+ # * +IX+ - <tt>:xml</tt>
61
+ # * +SO+ - <tt>:system</tt>
62
+ # * +SE+ - <tt>:security</tt>
63
+ # * +BR+ - <tt>:value</tt>
64
+ # * +AP+ - <tt>:application</tt>
65
+ def error_type
66
+ unless success?
67
+ case error_code[0, 2]
68
+ when 'IX' then
69
+ :xml
70
+ when 'SO' then
71
+ :system
72
+ when 'SE' then
73
+ :security
74
+ when 'BR' then
75
+ :value
76
+ when 'AP' then
77
+ :application
78
+ end
79
+ end
80
+ end
81
+
82
+ # Returns the code of the error that occured.
83
+ #
84
+ # === Codes
85
+ #
86
+ # ==== IX: Invalid XML and all related problems
87
+ #
88
+ # Such as incorrect encoding, invalid version, or otherwise unreadable:
89
+ #
90
+ # * <tt>IX1000</tt> - Received XML not well-formed.
91
+ # * <tt>IX1100</tt> - Received XML not valid.
92
+ # * <tt>IX1200</tt> - Encoding type not UTF-8.
93
+ # * <tt>IX1300</tt> - XML version number invalid.
94
+ # * <tt>IX1400</tt> - Unknown message.
95
+ # * <tt>IX1500</tt> - Mandatory main value missing. (Merchant ID ?)
96
+ # * <tt>IX1600</tt> - Mandatory value missing.
97
+ #
98
+ # ==== SO: System maintenance or failure
99
+ #
100
+ # The errors that are communicated in the event of system maintenance or
101
+ # system failure. Also covers the situation where new requests are no
102
+ # longer being accepted but requests already submitted will be dealt with
103
+ # (until a certain time):
104
+ #
105
+ # * <tt>SO1000</tt> - Failure in system.
106
+ # * <tt>SO1200</tt> - System busy. Try again later.
107
+ # * <tt>SO1400</tt> - Unavailable due to maintenance.
108
+ #
109
+ # ==== SE: Security and authentication errors
110
+ #
111
+ # Incorrect authentication methods and expired certificates:
112
+ #
113
+ # * <tt>SE2000</tt> - Authentication error.
114
+ # * <tt>SE2100</tt> - Authentication method not supported.
115
+ # * <tt>SE2700</tt> - Invalid electronic signature.
116
+ #
117
+ # ==== BR: Field errors
118
+ #
119
+ # Extra information on incorrect fields:
120
+ #
121
+ # * <tt>BR1200</tt> - iDEAL version number invalid.
122
+ # * <tt>BR1210</tt> - Value contains non-permitted character.
123
+ # * <tt>BR1220</tt> - Value too long.
124
+ # * <tt>BR1230</tt> - Value too short.
125
+ # * <tt>BR1240</tt> - Value too high.
126
+ # * <tt>BR1250</tt> - Value too low.
127
+ # * <tt>BR1250</tt> - Unknown entry in list.
128
+ # * <tt>BR1270</tt> - Invalid date/time.
129
+ # * <tt>BR1280</tt> - Invalid URL.
130
+ #
131
+ # ==== AP: Application errors
132
+ #
133
+ # Errors relating to IDs, account numbers, time zones, transactions:
134
+ #
135
+ # * <tt>AP1000</tt> - Acquirer ID unknown.
136
+ # * <tt>AP1100</tt> - Merchant ID unknown.
137
+ # * <tt>AP1200</tt> - Issuer ID unknown.
138
+ # * <tt>AP1300</tt> - Sub ID unknown.
139
+ # * <tt>AP1500</tt> - Merchant ID not active.
140
+ # * <tt>AP2600</tt> - Transaction does not exist.
141
+ # * <tt>AP2620</tt> - Transaction already submitted.
142
+ # * <tt>AP2700</tt> - Bank account number not 11-proof.
143
+ # * <tt>AP2900</tt> - Selected currency not supported.
144
+ # * <tt>AP2910</tt> - Maximum amount exceeded. (Detailed record states the maximum amount).
145
+ # * <tt>AP2915</tt> - Amount too low. (Detailed record states the minimum amount).
146
+ # * <tt>AP2920</tt> - Please adjust expiration period. See suggested expiration period.
147
+ def error_code
148
+ text('//errorCode') unless success?
149
+ end
150
+
151
+ private
152
+
153
+ def error_occured?
154
+ @response.name == 'AcquirerErrorRes'
155
+ end
156
+
157
+ def text(path)
158
+ @response.xpath(path)[0].text() unless @response.xpath(path)[0].nil?
159
+ end
160
+ end
161
+
162
+ # An instance of TransactionResponse is returned from
163
+ # Gateway#setup_purchase which returns the service_url to where the
164
+ # user should be redirected to perform the transaction _and_ the
165
+ # transaction ID.
166
+ class TransactionResponse < Response
167
+ # Returns the URL to the issuer’s page where the consumer should be
168
+ # redirected to in order to perform the payment.
169
+ def service_url
170
+ CGI::unescapeHTML(text('//issuerAuthenticationURL')).strip
171
+ end
172
+
173
+ def verified?
174
+ signed_document = Xmldsig::SignedDocument.new(@body)
175
+ @verified ||= signed_document.validate(Ideal::Gateway.ideal_certificate)
176
+ end
177
+
178
+ # Returns the transaction ID which is needed for requesting the status
179
+ # of a transaction. See Gateway#capture.
180
+ def transaction_id
181
+ text('//transactionID')
182
+ end
183
+
184
+ # Returns the <tt>:order_id</tt> for this transaction.
185
+ def order_id
186
+ text('//purchaseID')
187
+ end
188
+
189
+ def signature
190
+ Base64.decode64(text('//SignatureValue'))
191
+ end
192
+ end
193
+
194
+ # An instance of StatusResponse is returned from Gateway#capture
195
+ # which returns whether or not the transaction that was started with
196
+ # Gateway#setup_purchase was successful.
197
+ #
198
+ # It takes care of checking if the message was authentic by verifying the
199
+ # the message and its signature against the iDEAL certificate.
200
+ #
201
+ # If success? returns +false+ because the authenticity wasn't verified
202
+ # there will be no error_code, error_message, and error_type. Use verified?
203
+ # to check if the authenticity has been verified.
204
+ class StatusResponse < Response
205
+ def initialize(response_body, options = {})
206
+ super
207
+ @success = transaction_successful?
208
+ end
209
+
210
+ # Returns the status message, which is one of: <tt>:success</tt>,
211
+ # <tt>:cancelled</tt>, <tt>:expired</tt>, <tt>:open</tt>, or
212
+ # <tt>:failure</tt>.
213
+ def status
214
+ status = text('//status')
215
+ status.downcase.to_sym unless (status.nil? || status.strip == '')
216
+ end
217
+
218
+ # Returns whether or not the authenticity of the message could be
219
+ # verified.
220
+ def verified?
221
+ signed_document = Xmldsig::SignedDocument.new(@body)
222
+ @verified ||= signed_document.validate(Ideal::Gateway.ideal_certificate)
223
+ end
224
+
225
+ # Returns the bankaccount number when the transaction was successful.
226
+ def consumer_iban
227
+ text('//consumerIBAN')
228
+ end
229
+
230
+ # Returns the name on the bankaccount of the customer when the
231
+ # transaction was successful.
232
+ def consumer_name
233
+ text('//consumerName')
234
+ end
235
+
236
+ # Returns the BIC of the bankaccount of the customer when the
237
+ # transaction was succesfull
238
+ def consumer_bic
239
+ text('//consumerBIC')
240
+ end
241
+
242
+ def signature
243
+ Base64.decode64(text('//SignatureValue'))
244
+ end
245
+
246
+ private
247
+
248
+ # Checks if no errors occured _and_ if the message was authentic.
249
+ def transaction_successful?
250
+ !error_occured? && status == :success && verified?
251
+ end
252
+
253
+ end
254
+
255
+
256
+ # An instance of DirectoryResponse is returned from
257
+ # Gateway#issuers which returns the list of issuers available at the
258
+ # acquirer.
259
+ class DirectoryResponse < Response
260
+ # Returns a list of issuers available at the acquirer.
261
+ #
262
+ # gateway.issuers.list # => [{ :id => '1006', :name => 'ABN AMRO Bank' }]
263
+ def list
264
+ list = Array.new
265
+ @response.xpath(".//Country").each { |country|
266
+ country_name = country.xpath(".//countryNames").first.text()
267
+ country.xpath(".//Issuer").each { |issuer|
268
+ list << {:id => issuer.xpath(".//issuerID").first.text(), :country => country_name, :name => issuer.xpath(".//issuerName").first.text()}
269
+ }
270
+ }
271
+ list
272
+ end
273
+ end
274
+ end