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.
- 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
|