ideal 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/.travis.yml +10 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +24 -0
- data/LICENSE +22 -0
- data/README.textile +204 -0
- data/Rakefile +19 -0
- data/ideal.gemspec +28 -0
- data/init.rb +1 -0
- data/lib/ideal/acquirers.rb +23 -0
- data/lib/ideal/gateway.rb +425 -0
- data/lib/ideal/response.rb +250 -0
- data/lib/ideal/version.rb +5 -0
- data/lib/ideal.rb +12 -0
- data/test/fixtures.yml +12 -0
- data/test/gateway_test.rb +766 -0
- data/test/helper.rb +36 -0
- data/test/remote_test.rb +203 -0
- metadata +132 -0
@@ -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
|