ideal-payment 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
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