ideal 0.2.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,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