ideal 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,425 @@
1
+ # encoding: utf-8
2
+
3
+ require 'openssl'
4
+ require 'net/https'
5
+ require 'base64'
6
+ require 'digest/sha1'
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 Gateway
18
+ AUTHENTICATION_TYPE = 'SHA1_RSA'
19
+ LANGUAGE = 'nl'
20
+ CURRENCY = 'EUR'
21
+ API_VERSION = '1.1.0'
22
+ XML_NAMESPACE = 'http://www.idealdesk.com/Message'
23
+
24
+ def self.acquirers
25
+ Ideal::ACQUIRERS
26
+ end
27
+
28
+ class << self
29
+ # Returns the current acquirer used
30
+ attr_reader :acquirer
31
+
32
+ # Holds the environment in which the run (default is test)
33
+ attr_accessor :environment
34
+
35
+ # Holds the global iDEAL merchant id. Make sure to use a string with
36
+ # leading zeroes if needed.
37
+ attr_accessor :merchant_id
38
+
39
+ # Holds the passphrase that should be used for the merchant private_key.
40
+ attr_accessor :passphrase
41
+
42
+ # Holds the test and production urls for your iDeal acquirer.
43
+ attr_accessor :live_url, :test_url
44
+
45
+ # For ABN AMRO only assign the three separate directory, transaction and status urls.
46
+ attr_accessor :live_directory_url, :test_directory_url
47
+ attr_accessor :live_transaction_url, :test_transaction_url
48
+ attr_accessor :live_status_url, :test_status_url
49
+ end
50
+
51
+ # Environment defaults to test
52
+ self.environment = :test
53
+
54
+ # Loads the global merchant private_key from disk.
55
+ def self.private_key_file=(pkey_file)
56
+ self.private_key = File.read(pkey_file)
57
+ end
58
+
59
+ # Instantiates and assings a OpenSSL::PKey::RSA instance with the
60
+ # provided private key data.
61
+ def self.private_key=(pkey_data)
62
+ @private_key = OpenSSL::PKey::RSA.new(pkey_data, passphrase)
63
+ end
64
+
65
+ # Returns the global merchant private_certificate.
66
+ def self.private_key
67
+ @private_key
68
+ end
69
+
70
+ # Loads the global merchant private_certificate from disk.
71
+ def self.private_certificate_file=(certificate_file)
72
+ self.private_certificate = File.read(certificate_file)
73
+ end
74
+
75
+ # Instantiates and assings a OpenSSL::X509::Certificate instance with the
76
+ # provided private certificate data.
77
+ def self.private_certificate=(certificate_data)
78
+ @private_certificate = OpenSSL::X509::Certificate.new(certificate_data)
79
+ end
80
+
81
+ # Returns the global merchant private_certificate.
82
+ def self.private_certificate
83
+ @private_certificate
84
+ end
85
+
86
+ # Loads the global merchant ideal_certificate from disk.
87
+ def self.ideal_certificate_file=(certificate_file)
88
+ self.ideal_certificate = File.read(certificate_file)
89
+ end
90
+
91
+ # Instantiates and assings a OpenSSL::X509::Certificate instance with the
92
+ # provided iDEAL certificate data.
93
+ def self.ideal_certificate=(certificate_data)
94
+ @ideal_certificate = OpenSSL::X509::Certificate.new(certificate_data)
95
+ end
96
+
97
+ # Returns the global merchant ideal_certificate.
98
+ def self.ideal_certificate
99
+ @ideal_certificate
100
+ end
101
+
102
+ # Returns whether we're in test mode or not.
103
+ def self.test?
104
+ environment.to_sym == :test
105
+ end
106
+
107
+ # Set the correct acquirer url based on the specific Bank
108
+ # Currently supported arguments: :ing, :rabobank, :abnamro
109
+ #
110
+ # Ideal::Gateway.acquirer = :ing
111
+ def self.acquirer=(acquirer)
112
+ @acquirer = acquirer.to_s
113
+ if self.acquirers.include?(@acquirer)
114
+ acquirers[@acquirer].each do |attr, value|
115
+ send("#{attr}=", value)
116
+ end
117
+ else
118
+ raise ArgumentError, "Unknown acquirer `#{acquirer}', please choose one of: #{self.acquirers.keys.join(', ')}"
119
+ end
120
+ end
121
+
122
+ # Returns true when the acquirer was set to :abnamro
123
+ def self.abn_amro?
124
+ acquirer.to_sym == :abnamro
125
+ end
126
+
127
+ # Returns the merchant `subID' being used for this Gateway instance.
128
+ # Defaults to 0.
129
+ attr_reader :sub_id
130
+
131
+ # Initializes a new Gateway instance.
132
+ #
133
+ # You can optionally specify <tt>:sub_id</tt>. Defaults to 0.
134
+ def initialize(options = {})
135
+ @sub_id = options[:sub_id] || 0
136
+ end
137
+
138
+ # Returns the endpoint for a certain request.
139
+ #
140
+ # Automatically uses test or live URLs based on the configuration.
141
+ def request_url(request_type)
142
+ self.class.send("#{self.class.environment}#{request_url_prefix(request_type)}_url")
143
+ end
144
+
145
+ # Sends a directory request to the acquirer and returns an
146
+ # DirectoryResponse. Use DirectoryResponse#list to receive the
147
+ # actuall array of available issuers.
148
+ #
149
+ # gateway.issuers.list # => [{ :id => '1006', :name => 'ABN AMRO Bank' }, …]
150
+ def issuers
151
+ post_data request_url(:directory), build_directory_request_body, DirectoryResponse
152
+ end
153
+
154
+ # Starts a purchase by sending an acquirer transaction request for the
155
+ # specified +money+ amount in EURO cents.
156
+ #
157
+ # On success returns an TransactionResponse with the #transaction_id
158
+ # which is needed for the capture step. (See capture for an example.)
159
+ #
160
+ # The iDEAL specification states that it is _not_ allowed to use another
161
+ # window or frame when redirecting the consumer to the issuer. So the
162
+ # entire merchant’s page has to be replaced by the selected issuer’s page.
163
+ #
164
+ # === Options
165
+ #
166
+ # Note that all options that have a character limit are _also_ checked
167
+ # for diacritical characters. If it does contain diacritical characters,
168
+ # or exceeds the character limit, an ArgumentError is raised.
169
+ #
170
+ # ==== Required
171
+ #
172
+ # * <tt>:issuer_id</tt> - The <tt>:id</tt> of an issuer available at the acquirer to which the transaction should be made.
173
+ # * <tt>:order_id</tt> - The order number. Limited to 12 characters.
174
+ # * <tt>:description</tt> - A description of the transaction. Limited to 32 characters.
175
+ # * <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:
176
+ # * <tt>trxid</tt> - The <tt>:order_id</tt>.
177
+ # * <tt>ec</tt> - The <tt>:entrance_code</tt> _if_ it was specified.
178
+ #
179
+ # ==== Optional
180
+ #
181
+ # * <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.
182
+ # * <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':
183
+ # * P: relative time designation.
184
+ # * 3 days.
185
+ # * T: separator.
186
+ # * 6 hours.
187
+ # * 10 minutes.
188
+ #
189
+ # === Example
190
+ #
191
+ # transaction_response = gateway.setup_purchase(4321, valid_options)
192
+ # if transaction_response.success?
193
+ # @purchase.update_attributes!(:transaction_id => transaction_response.transaction_id)
194
+ # redirect_to transaction_response.service_url
195
+ # end
196
+ #
197
+ # See the Gateway class description for a more elaborate example.
198
+ def setup_purchase(money, options)
199
+ post_data request_url(:transaction), build_transaction_request_body(money, options), TransactionResponse
200
+ end
201
+
202
+ # Sends a acquirer status request for the specified +transaction_id+ and
203
+ # returns an StatusResponse.
204
+ #
205
+ # It is _your_ responsibility as the merchant to check if the payment has
206
+ # been made until you receive a response with a finished status like:
207
+ # `Success', `Cancelled', `Expired', everything else equals `Open'.
208
+ #
209
+ # === Example
210
+ #
211
+ # capture_response = gateway.capture(@purchase.transaction_id)
212
+ # if capture_response.success?
213
+ # @purchase.update_attributes!(:paid => true)
214
+ # flash[:notice] = "Congratulations, you are now the proud owner of a Dutch windmill!"
215
+ # end
216
+ #
217
+ # See the Gateway class description for a more elaborate example.
218
+ def capture(transaction_id)
219
+ post_data request_url(:status), build_status_request_body(:transaction_id => transaction_id), StatusResponse
220
+ end
221
+
222
+ private
223
+
224
+ # Returns the prefix to locate the setting for a certain request type. Only applies
225
+ # in the case of ABN AMRO because they use more than one endpoint.
226
+ def request_url_prefix(request_type)
227
+ (request_type && self.class.abn_amro?) ? "_#{request_type.to_s}" : nil
228
+ end
229
+
230
+ def ssl_post(url, body)
231
+ response = REST.post(url, body, {}, {
232
+ :tls_verify => true,
233
+ :tls_key => self.class.private_key,
234
+ :tls_certificate => self.class.private_certificate
235
+ })
236
+ response.body
237
+ end
238
+
239
+ def post_data(gateway_url, data, response_klass)
240
+ response_klass.new(ssl_post(gateway_url, data), :test => self.class.test?)
241
+ end
242
+
243
+ # This is the list of charaters that are not supported by iDEAL according
244
+ # to the PHP source provided by ING plus the same in capitals.
245
+ DIACRITICAL_CHARACTERS = /[ÀÁÂÃÄÅÇŒÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝàáâãäåçæèéêëìíîïñòóôõöøùúûüý]/ #:nodoc:
246
+
247
+ # Raises an ArgumentError if the +string+ exceeds the +max_length+ amount
248
+ # of characters or contains any diacritical characters.
249
+ def enforce_maximum_length(key, string, max_length)
250
+ raise ArgumentError, "The value for `#{key}' exceeds the limit of #{max_length} characters." if string.length > max_length
251
+ raise ArgumentError, "The value for `#{key}' contains diacritical characters `#{string}'." if string =~ DIACRITICAL_CHARACTERS
252
+ end
253
+
254
+ # Returns the +token+ as specified in section 2.8.4 of the iDeal specs.
255
+ #
256
+ # This is the params['AcquirerStatusRes']['Signature']['fingerprint'] in
257
+ # a StatusResponse instance.
258
+ def token
259
+ Digest::SHA1.hexdigest(self.class.private_certificate.to_der).upcase
260
+ end
261
+
262
+ def strip_whitespace(str)
263
+ self.class.abn_amro? ? str.gsub(/(\f|\n|\r|\t|\v)/m, '') : str.gsub(/\s/m,'')
264
+ end
265
+
266
+ # Creates a +tokenCode+ from the specified +message+.
267
+ def token_code(message)
268
+ signature = self.class.private_key.sign(OpenSSL::Digest::SHA1.new, strip_whitespace(message))
269
+ strip_whitespace(Base64.encode64(signature))
270
+ end
271
+
272
+ # Returns a string containing the current UTC time, formatted as per the
273
+ # iDeal specifications, except we don't use miliseconds.
274
+ def created_at_timestamp
275
+ Time.now.gmtime.strftime("%Y-%m-%dT%H:%M:%S.000Z")
276
+ end
277
+
278
+ def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
279
+ if first_letter_in_uppercase
280
+ lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
281
+ else
282
+ lower_case_and_underscored_word.to_s[0].chr.downcase + camelize(lower_case_and_underscored_word)[1..-1]
283
+ end
284
+ end
285
+
286
+ # iDeal doesn't really seem to care about nice looking keys in their XML.
287
+ # Probably some Java XML class, hence the method name.
288
+ def javaize_key(key)
289
+ key = key.to_s
290
+ case key
291
+ when 'acquirer_transaction_request'
292
+ 'AcquirerTrxReq'
293
+ when 'acquirer_status_request'
294
+ 'AcquirerStatusReq'
295
+ when 'directory_request'
296
+ 'DirectoryReq'
297
+ when 'issuer', 'merchant', 'transaction'
298
+ key.capitalize
299
+ when 'created_at'
300
+ 'createDateTimeStamp'
301
+ when 'merchant_return_url'
302
+ 'merchantReturnURL'
303
+ when 'token_code', 'expiration_period', 'entrance_code'
304
+ key[0,1] + camelize(key)[1..-1]
305
+ when /^(\w+)_id$/
306
+ "#{$1}ID"
307
+ else
308
+ key
309
+ end
310
+ end
311
+
312
+ # Creates xml with a given hash of tag-value pairs according to the iDeal
313
+ # requirements.
314
+ def xml_for(name, tags_and_values)
315
+ xml = Builder::XmlMarkup.new
316
+ xml.instruct!
317
+ xml.tag!(javaize_key(name), 'xmlns' => XML_NAMESPACE, 'version' => API_VERSION) { xml_from_array(xml, tags_and_values) }
318
+ xml.target!
319
+ end
320
+
321
+ # Recursively creates xml for a given hash of tag-value pair. Uses
322
+ # javaize_key on the tags to create the tags needed by iDeal.
323
+ def xml_from_array(builder, tags_and_values)
324
+ tags_and_values.each do |tag, value|
325
+ tag = javaize_key(tag)
326
+ if value.is_a?(Array)
327
+ builder.tag!(tag) { xml_from_array(builder, value) }
328
+ else
329
+ builder.tag!(tag, value)
330
+ end
331
+ end
332
+ end
333
+
334
+ def requires!(options, *keys)
335
+ missing = keys - options.keys
336
+ unless missing.empty?
337
+ raise ArgumentError, "Missing required options: #{missing.map { |m| m.to_s }.join(', ')}"
338
+ end
339
+ end
340
+
341
+ def build_status_request_body(options)
342
+ requires!(options, :transaction_id)
343
+
344
+ timestamp = created_at_timestamp
345
+ message = "#{timestamp}#{self.class.merchant_id}#{@sub_id}#{options[:transaction_id]}"
346
+
347
+ xml_for(:acquirer_status_request, [
348
+ [:created_at, timestamp],
349
+ [:merchant, [
350
+ [:merchant_id, self.class.merchant_id],
351
+ [:sub_id, @sub_id],
352
+ [:authentication, AUTHENTICATION_TYPE],
353
+ [:token, token],
354
+ [:token_code, token_code(message)]
355
+ ]],
356
+
357
+ [:transaction, [
358
+ [:transaction_id, options[:transaction_id]]
359
+ ]]
360
+ ])
361
+ end
362
+
363
+ def build_directory_request_body
364
+ timestamp = created_at_timestamp
365
+ message = "#{timestamp}#{self.class.merchant_id}#{@sub_id}"
366
+
367
+ xml_for(:directory_request, [
368
+ [:created_at, timestamp],
369
+ [:merchant, [
370
+ [:merchant_id, self.class.merchant_id],
371
+ [:sub_id, @sub_id],
372
+ [:authentication, AUTHENTICATION_TYPE],
373
+ [:token, token],
374
+ [:token_code, token_code(message)]
375
+ ]]
376
+ ])
377
+ end
378
+
379
+ def build_transaction_request_body(money, options)
380
+ requires!(options, :issuer_id, :expiration_period, :return_url, :order_id, :description, :entrance_code)
381
+
382
+ enforce_maximum_length(:money, money.to_s, 12)
383
+ enforce_maximum_length(:order_id, options[:order_id], 12)
384
+ enforce_maximum_length(:description, options[:description], 32)
385
+ enforce_maximum_length(:entrance_code, options[:entrance_code], 40)
386
+
387
+ timestamp = created_at_timestamp
388
+ message = timestamp +
389
+ options[:issuer_id] +
390
+ self.class.merchant_id +
391
+ @sub_id.to_s +
392
+ options[:return_url] +
393
+ options[:order_id] +
394
+ money.to_s +
395
+ CURRENCY +
396
+ LANGUAGE +
397
+ options[:description] +
398
+ options[:entrance_code]
399
+
400
+ xml_for(:acquirer_transaction_request, [
401
+ [:created_at, timestamp],
402
+ [:issuer, [[:issuer_id, options[:issuer_id]]]],
403
+
404
+ [:merchant, [
405
+ [:merchant_id, self.class.merchant_id],
406
+ [:sub_id, @sub_id],
407
+ [:authentication, AUTHENTICATION_TYPE],
408
+ [:token, token],
409
+ [:token_code, token_code(message)],
410
+ [:merchant_return_url, options[:return_url]]
411
+ ]],
412
+
413
+ [:transaction, [
414
+ [:purchase_id, options[:order_id]],
415
+ [:amount, money],
416
+ [:currency, CURRENCY],
417
+ [:expiration_period, options[:expiration_period]],
418
+ [:language, LANGUAGE],
419
+ [:description, options[:description]],
420
+ [:entrance_code, options[:entrance_code]]
421
+ ]]
422
+ ])
423
+ end
424
+ end
425
+ end
@@ -0,0 +1,250 @@
1
+ # encoding: utf-8
2
+
3
+ require 'cgi'
4
+ require 'openssl'
5
+ require 'base64'
6
+ require 'rexml/document'
7
+
8
+ module Ideal
9
+ # The base class for all iDEAL response classes.
10
+ #
11
+ # Note that if the iDEAL system is under load it will _not_ allow more
12
+ # then two retries per request.
13
+ class Response
14
+ attr_accessor :response
15
+
16
+ def initialize(response_body, options = {})
17
+ @response = REXML::Document.new(response_body).root
18
+ @success = !error_occured?
19
+ @test = options[:test]
20
+ end
21
+
22
+ # Returns whether we're running in test mode
23
+ def test?
24
+ @test
25
+ end
26
+
27
+ # Returns whether the request was a success
28
+ def success?
29
+ @success
30
+ end
31
+
32
+ # Returns a technical error message.
33
+ def error_message
34
+ text('//Error/errorMessage') unless success?
35
+ end
36
+
37
+ # Returns a consumer friendly error message.
38
+ def consumer_error_message
39
+ text('//Error/consumerMessage') unless success?
40
+ end
41
+
42
+ # Returns details on the error if available.
43
+ def error_details
44
+ text('//Error/errorDetail') unless success?
45
+ end
46
+
47
+ # Returns an error type inflected from the first two characters of the
48
+ # error code. See error_code for a full list of errors.
49
+ #
50
+ # Error code to type mappings:
51
+ #
52
+ # * +IX+ - <tt>:xml</tt>
53
+ # * +SO+ - <tt>:system</tt>
54
+ # * +SE+ - <tt>:security</tt>
55
+ # * +BR+ - <tt>:value</tt>
56
+ # * +AP+ - <tt>:application</tt>
57
+ def error_type
58
+ unless success?
59
+ case error_code[0,2]
60
+ when 'IX' then :xml
61
+ when 'SO' then :system
62
+ when 'SE' then :security
63
+ when 'BR' then :value
64
+ when 'AP' then :application
65
+ end
66
+ end
67
+ end
68
+
69
+ # Returns the code of the error that occured.
70
+ #
71
+ # === Codes
72
+ #
73
+ # ==== IX: Invalid XML and all related problems
74
+ #
75
+ # Such as incorrect encoding, invalid version, or otherwise unreadable:
76
+ #
77
+ # * <tt>IX1000</tt> - Received XML not well-formed.
78
+ # * <tt>IX1100</tt> - Received XML not valid.
79
+ # * <tt>IX1200</tt> - Encoding type not UTF-8.
80
+ # * <tt>IX1300</tt> - XML version number invalid.
81
+ # * <tt>IX1400</tt> - Unknown message.
82
+ # * <tt>IX1500</tt> - Mandatory main value missing. (Merchant ID ?)
83
+ # * <tt>IX1600</tt> - Mandatory value missing.
84
+ #
85
+ # ==== SO: System maintenance or failure
86
+ #
87
+ # The errors that are communicated in the event of system maintenance or
88
+ # system failure. Also covers the situation where new requests are no
89
+ # longer being accepted but requests already submitted will be dealt with
90
+ # (until a certain time):
91
+ #
92
+ # * <tt>SO1000</tt> - Failure in system.
93
+ # * <tt>SO1200</tt> - System busy. Try again later.
94
+ # * <tt>SO1400</tt> - Unavailable due to maintenance.
95
+ #
96
+ # ==== SE: Security and authentication errors
97
+ #
98
+ # Incorrect authentication methods and expired certificates:
99
+ #
100
+ # * <tt>SE2000</tt> - Authentication error.
101
+ # * <tt>SE2100</tt> - Authentication method not supported.
102
+ # * <tt>SE2700</tt> - Invalid electronic signature.
103
+ #
104
+ # ==== BR: Field errors
105
+ #
106
+ # Extra information on incorrect fields:
107
+ #
108
+ # * <tt>BR1200</tt> - iDEAL version number invalid.
109
+ # * <tt>BR1210</tt> - Value contains non-permitted character.
110
+ # * <tt>BR1220</tt> - Value too long.
111
+ # * <tt>BR1230</tt> - Value too short.
112
+ # * <tt>BR1240</tt> - Value too high.
113
+ # * <tt>BR1250</tt> - Value too low.
114
+ # * <tt>BR1250</tt> - Unknown entry in list.
115
+ # * <tt>BR1270</tt> - Invalid date/time.
116
+ # * <tt>BR1280</tt> - Invalid URL.
117
+ #
118
+ # ==== AP: Application errors
119
+ #
120
+ # Errors relating to IDs, account numbers, time zones, transactions:
121
+ #
122
+ # * <tt>AP1000</tt> - Acquirer ID unknown.
123
+ # * <tt>AP1100</tt> - Merchant ID unknown.
124
+ # * <tt>AP1200</tt> - Issuer ID unknown.
125
+ # * <tt>AP1300</tt> - Sub ID unknown.
126
+ # * <tt>AP1500</tt> - Merchant ID not active.
127
+ # * <tt>AP2600</tt> - Transaction does not exist.
128
+ # * <tt>AP2620</tt> - Transaction already submitted.
129
+ # * <tt>AP2700</tt> - Bank account number not 11-proof.
130
+ # * <tt>AP2900</tt> - Selected currency not supported.
131
+ # * <tt>AP2910</tt> - Maximum amount exceeded. (Detailed record states the maximum amount).
132
+ # * <tt>AP2915</tt> - Amount too low. (Detailed record states the minimum amount).
133
+ # * <tt>AP2920</tt> - Please adjust expiration period. See suggested expiration period.
134
+ def error_code
135
+ text('//errorCode') unless success?
136
+ end
137
+
138
+ private
139
+
140
+ def error_occured?
141
+ @response.name == 'ErrorRes'
142
+ end
143
+
144
+ def text(path)
145
+ @response.get_text(path).to_s
146
+ end
147
+ end
148
+
149
+ # An instance of TransactionResponse is returned from
150
+ # Gateway#setup_purchase which returns the service_url to where the
151
+ # user should be redirected to perform the transaction _and_ the
152
+ # transaction ID.
153
+ class TransactionResponse < Response
154
+ # Returns the URL to the issuer’s page where the consumer should be
155
+ # redirected to in order to perform the payment.
156
+ def service_url
157
+ CGI::unescapeHTML(text('//issuerAuthenticationURL'))
158
+ end
159
+
160
+ # Returns the transaction ID which is needed for requesting the status
161
+ # of a transaction. See Gateway#capture.
162
+ def transaction_id
163
+ text('//transactionID')
164
+ end
165
+
166
+ # Returns the <tt>:order_id</tt> for this transaction.
167
+ def order_id
168
+ text('//purchaseID')
169
+ end
170
+ end
171
+
172
+ # An instance of StatusResponse is returned from Gateway#capture
173
+ # which returns whether or not the transaction that was started with
174
+ # Gateway#setup_purchase was successful.
175
+ #
176
+ # It takes care of checking if the message was authentic by verifying the
177
+ # the message and its signature against the iDEAL certificate.
178
+ #
179
+ # If success? returns +false+ because the authenticity wasn't verified
180
+ # there will be no error_code, error_message, and error_type. Use verified?
181
+ # to check if the authenticity has been verified.
182
+ class StatusResponse < Response
183
+ def initialize(response_body, options = {})
184
+ super
185
+ @success = transaction_successful?
186
+ end
187
+
188
+ # Returns the status message, which is one of: <tt>:success</tt>,
189
+ # <tt>:cancelled</tt>, <tt>:expired</tt>, <tt>:open</tt>, or
190
+ # <tt>:failure</tt>.
191
+ def status
192
+ status = text('//status')
193
+ status.downcase.to_sym unless (status.strip == '')
194
+ end
195
+
196
+ # Returns whether or not the authenticity of the message could be
197
+ # verified.
198
+ def verified?
199
+ @verified ||= Gateway.ideal_certificate.public_key.
200
+ verify(OpenSSL::Digest::SHA1.new, signature, message)
201
+ end
202
+
203
+ # Returns the bankaccount number when the transaction was successful.
204
+ def consumer_account_number
205
+ text('//consumerAccountNumber')
206
+ end
207
+
208
+ # Returns the name on the bankaccount of the customer when the
209
+ # transaction was successful.
210
+ def consumer_name
211
+ text('//consumerName')
212
+ end
213
+
214
+ # Returns the city on the bankaccount of the customer when the
215
+ # transaction was successful.
216
+ def consumer_city
217
+ text('//consumerCity')
218
+ end
219
+
220
+ private
221
+
222
+ # Checks if no errors occured _and_ if the message was authentic.
223
+ def transaction_successful?
224
+ !error_occured? && status == :success && verified?
225
+ end
226
+
227
+ # The message that we need to verify the authenticity.
228
+ def message
229
+ text('//createDateTimeStamp') + text('//transactionID') + text('//status') + text('//consumerAccountNumber')
230
+ end
231
+
232
+ def signature
233
+ Base64.decode64(text('//signatureValue'))
234
+ end
235
+ end
236
+
237
+ # An instance of DirectoryResponse is returned from
238
+ # Gateway#issuers which returns the list of issuers available at the
239
+ # acquirer.
240
+ class DirectoryResponse < Response
241
+ # Returns a list of issuers available at the acquirer.
242
+ #
243
+ # gateway.issuers.list # => [{ :id => '1006', :name => 'ABN AMRO Bank' }]
244
+ def list
245
+ @response.get_elements('//Issuer').map do |issuer|
246
+ { :id => issuer.get_text('issuerID').to_s, :name => issuer.get_text('issuerName').to_s }
247
+ end
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+
3
+ module Ideal
4
+ VERSION = '0.2.0'
5
+ end
data/lib/ideal.rb ADDED
@@ -0,0 +1,12 @@
1
+ # encoding: utf-8
2
+
3
+ require 'builder'
4
+ require 'rest'
5
+
6
+ require 'ideal/acquirers'
7
+ require 'ideal/gateway'
8
+ require 'ideal/response'
9
+ require 'ideal/version'
10
+
11
+ module Ideal
12
+ end