ideal-payment 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +36 -0
- data/.travis.yml +14 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +67 -0
- data/LICENSE +21 -0
- data/README.md +148 -0
- data/Vagrantfile +35 -0
- data/certs/bestandsnaam.cer +25 -0
- data/certs/bestandsnaam.key +30 -0
- data/certs/ideal.cer +18 -0
- data/ideal.gemspec +31 -0
- data/init.rb +1 -0
- data/lib/ideal.rb +21 -0
- data/lib/ideal/acquirers.rb +18 -0
- data/lib/ideal/gateway.rb +377 -0
- data/lib/ideal/response.rb +274 -0
- data/lib/ideal/version.rb +5 -0
- data/provision.sh +24 -0
- data/spec/expectation_xml/directory_request.xml +25 -0
- data/spec/expectation_xml/status_request.xml +28 -0
- data/spec/expectation_xml/transaction_request.xml +38 -0
- data/spec/fixtures.yml +7 -0
- data/spec/remote_spec.rb +205 -0
- data/spec/spec.rb +500 -0
- data/spec/test_xml/error_response.xml +12 -0
- data/spec/test_xml/large_directory_response.xml +36 -0
- data/spec/test_xml/small_directory_response.xml +17 -0
- data/spec/test_xml/status_response_succeeded.xml +20 -0
- data/spec/test_xml/status_response_succeeded_incorrect.xml +18 -0
- data/spec/test_xml/transaction_response.xml +15 -0
- metadata +157 -0
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'ideal'
|
data/lib/ideal.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'rest'
|
5
|
+
|
6
|
+
require 'ideal/acquirers'
|
7
|
+
require 'ideal/gateway'
|
8
|
+
require 'ideal/response'
|
9
|
+
require 'ideal/version'
|
10
|
+
|
11
|
+
require "nokogiri"
|
12
|
+
require "openssl"
|
13
|
+
require "base64"
|
14
|
+
require 'xmldsig'
|
15
|
+
|
16
|
+
module Ideal
|
17
|
+
NAMESPACES = {
|
18
|
+
"ds" => "http://www.w3.org/2000/09/xmldsig#",
|
19
|
+
"ec" => "http://www.w3.org/2001/10/xml-exc-c14n#"
|
20
|
+
}
|
21
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Ideal
|
4
|
+
ACQUIRERS = {
|
5
|
+
ing: {
|
6
|
+
live_url: 'https://ideal.secure-ing.com/ideal/iDEALv3',
|
7
|
+
test_url: 'https://idealtest.secure-ing.com/ideal/iDEALv3'
|
8
|
+
},
|
9
|
+
rabobank: {
|
10
|
+
live_url: 'https://ideal.rabobank.nl/ideal/iDEALv3',
|
11
|
+
test_url: 'https://idealtest.rabobank.nl/ideal/iDEALv3'
|
12
|
+
},
|
13
|
+
abnamro: {
|
14
|
+
live_url: 'https://abnamro.ideal-payment.de/ideal/iDeal',
|
15
|
+
test_url: 'https://abnamro-test.ideal-payment.de/ideal/iDeal'
|
16
|
+
}
|
17
|
+
}
|
18
|
+
end
|
@@ -0,0 +1,377 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'net/https'
|
5
|
+
require 'base64'
|
6
|
+
|
7
|
+
module Ideal
|
8
|
+
# The base class for all iDEAL response classes.
|
9
|
+
#
|
10
|
+
# Note that if the iDEAL system is under load it will _not_ allow more
|
11
|
+
# then two retries per request.
|
12
|
+
class Gateway
|
13
|
+
LANGUAGE = 'nl'
|
14
|
+
CURRENCY = 'EUR'
|
15
|
+
API_VERSION = '3.3.1'
|
16
|
+
XML_NAMESPACE = 'http://www.idealdesk.com/ideal/messages/mer-acq/3.3.1'
|
17
|
+
|
18
|
+
def self.acquirers
|
19
|
+
Ideal::ACQUIRERS
|
20
|
+
end
|
21
|
+
|
22
|
+
class << self
|
23
|
+
# Returns the current acquirer used
|
24
|
+
attr_reader :acquirer
|
25
|
+
|
26
|
+
# Holds the environment in which the run (default is test)
|
27
|
+
attr_accessor :environment
|
28
|
+
|
29
|
+
# Holds the global iDEAL merchant id. Make sure to use a string with
|
30
|
+
# leading zeroes if needed.
|
31
|
+
attr_accessor :merchant_id
|
32
|
+
|
33
|
+
# Holds the passphrase that should be used for the merchant private_key.
|
34
|
+
attr_accessor :passphrase
|
35
|
+
|
36
|
+
# Holds the test and production urls for your iDeal acquirer.
|
37
|
+
attr_accessor :live_url, :test_url
|
38
|
+
end
|
39
|
+
|
40
|
+
# Environment defaults to test
|
41
|
+
self.environment = :test
|
42
|
+
|
43
|
+
# Loads the global merchant private_key from disk.
|
44
|
+
def self.private_key_file=(pkey_file)
|
45
|
+
self.private_key = File.read(pkey_file)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Instantiates and assings a OpenSSL::PKey::RSA instance with the
|
49
|
+
# provided private key data.
|
50
|
+
def self.private_key=(pkey_data)
|
51
|
+
@private_key = OpenSSL::PKey::RSA.new(pkey_data, passphrase)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns the global merchant private_certificate.
|
55
|
+
def self.private_key
|
56
|
+
@private_key
|
57
|
+
end
|
58
|
+
|
59
|
+
# Loads the global merchant private_certificate from disk.
|
60
|
+
def self.private_certificate_file=(certificate_file)
|
61
|
+
self.private_certificate = File.read(certificate_file)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Instantiates and assings a OpenSSL::X509::Certificate instance with the
|
65
|
+
# provided private certificate data.
|
66
|
+
def self.private_certificate=(certificate_data)
|
67
|
+
@private_certificate = OpenSSL::X509::Certificate.new(certificate_data)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns the global merchant private_certificate.
|
71
|
+
def self.private_certificate
|
72
|
+
@private_certificate
|
73
|
+
end
|
74
|
+
|
75
|
+
# Loads the global merchant ideal_certificate from disk.
|
76
|
+
def self.ideal_certificate_file=(certificate_file)
|
77
|
+
self.ideal_certificate = File.read(certificate_file)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Instantiates and assings a OpenSSL::X509::Certificate instance with the
|
81
|
+
# provided iDEAL certificate data.
|
82
|
+
def self.ideal_certificate=(certificate_data)
|
83
|
+
@ideal_certificate = OpenSSL::X509::Certificate.new(certificate_data)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns the global merchant ideal_certificate.
|
87
|
+
def self.ideal_certificate
|
88
|
+
@ideal_certificate
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns whether we're in test mode or not.
|
92
|
+
def self.test?
|
93
|
+
environment.to_sym == :test
|
94
|
+
end
|
95
|
+
|
96
|
+
# Set the correct acquirer url based on the specific Bank
|
97
|
+
# Currently supported arguments: :ing, :rabobank, :abnamro
|
98
|
+
#
|
99
|
+
# Ideal::Gateway.acquirer = :ing
|
100
|
+
def self.acquirer=(acquirer)
|
101
|
+
@acquirer = acquirer
|
102
|
+
if self.acquirers.include?(@acquirer)
|
103
|
+
acquirers[@acquirer].each do |attr, value|
|
104
|
+
send("#{attr}=", value)
|
105
|
+
end
|
106
|
+
else
|
107
|
+
raise ArgumentError, "Unknown acquirer `#{acquirer}', please choose one of: #{self.acquirers.keys.join(', ')}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns the merchant `subID' being used for this Gateway instance.
|
112
|
+
# Defaults to 0.
|
113
|
+
attr_reader :sub_id
|
114
|
+
|
115
|
+
# Initializes a new Gateway instance.
|
116
|
+
#
|
117
|
+
# You can optionally specify <tt>:sub_id</tt>. Defaults to 0.
|
118
|
+
def initialize(options = {})
|
119
|
+
@sub_id = options[:sub_id] || 0
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns the endpoint for the request.
|
123
|
+
#
|
124
|
+
# Automatically uses test or live URLs based on the configuration.
|
125
|
+
def request_url
|
126
|
+
self.class.send("#{self.class.environment}_url")
|
127
|
+
end
|
128
|
+
|
129
|
+
# Sends a directory request to the acquirer and returns an
|
130
|
+
# DirectoryResponse. Use DirectoryResponse#list to receive the
|
131
|
+
# actuall array of available issuers.
|
132
|
+
#
|
133
|
+
# gateway.issuers.list # => [{ :id => '1006', :name => 'ABN AMRO Bank' }, …]
|
134
|
+
def issuers
|
135
|
+
x = build_directory_request
|
136
|
+
|
137
|
+
a= post_data request_url, x, DirectoryResponse
|
138
|
+
|
139
|
+
log('REQ',x)
|
140
|
+
log('RES',a.response)
|
141
|
+
a
|
142
|
+
end
|
143
|
+
|
144
|
+
# Starts a purchase by sending an acquirer transaction request for the
|
145
|
+
# specified +money+ amount in EURO cents.
|
146
|
+
#
|
147
|
+
# On success returns an TransactionResponse with the #transaction_id
|
148
|
+
# which is needed for the capture step. (See capture for an example.)
|
149
|
+
#
|
150
|
+
# The iDEAL specification states that it is _not_ allowed to use another
|
151
|
+
# window or frame when redirecting the consumer to the issuer. So the
|
152
|
+
# entire merchant’s page has to be replaced by the selected issuer’s page.
|
153
|
+
#
|
154
|
+
# === Options
|
155
|
+
#
|
156
|
+
# Note that all options that have a character limit are _also_ checked
|
157
|
+
# for diacritical characters. If it does contain diacritical characters,
|
158
|
+
# or exceeds the character limit, an ArgumentError is raised.
|
159
|
+
#
|
160
|
+
# ==== Required
|
161
|
+
#
|
162
|
+
# * <tt>:issuer_id</tt> - The <tt>:id</tt> of an issuer available at the acquirer to which the transaction should be made.
|
163
|
+
# * <tt>:order_id</tt> - The order number. Limited to 12 characters.
|
164
|
+
# * <tt>:description</tt> - A description of the transaction. Limited to 32 characters.
|
165
|
+
# * <tt>:return_url</tt> - A URL on the merchant’s system to which the consumer is redirected _after_ payment. The acquirer will add the following GET variables:
|
166
|
+
# * <tt>trxid</tt> - The <tt>:order_id</tt>.
|
167
|
+
# * <tt>ec</tt> - The <tt>:entrance_code</tt> _if_ it was specified.
|
168
|
+
#
|
169
|
+
# ==== Optional
|
170
|
+
#
|
171
|
+
# * <tt>:entrance_code</tt> - This code is an abitrary token which can be used to identify the transaction besides the <tt>:order_id</tt>. Limited to 40 characters.
|
172
|
+
# * <tt>:expiration_period</tt> - The period of validity of the payment request measured from the receipt by the issuer. The consumer must approve the payment within this period, otherwise the StatusResponse#status will be set to `Expired'. E.g., consider an <tt>:expiration_period</tt> of `P3DT6H10M':
|
173
|
+
# * P: relative time designation.
|
174
|
+
# * 3 days.
|
175
|
+
# * T: separator.
|
176
|
+
# * 6 hours.
|
177
|
+
# * 10 minutes.
|
178
|
+
#
|
179
|
+
# === Example
|
180
|
+
#
|
181
|
+
# transaction_response = gateway.setup_purchase(4321, valid_options)
|
182
|
+
# if transaction_response.success?
|
183
|
+
# @purchase.update_attributes!(:transaction_id => transaction_response.transaction_id)
|
184
|
+
# redirect_to transaction_response.service_url
|
185
|
+
# end
|
186
|
+
#
|
187
|
+
# See the Gateway class description for a more elaborate example.
|
188
|
+
def setup_purchase(money, options)
|
189
|
+
req = build_transaction_request(money, options)
|
190
|
+
log('purchase', req)
|
191
|
+
resp = post_data request_url, req, TransactionResponse
|
192
|
+
#raise SecurityError, "The message could not be verified" if !resp.verified?
|
193
|
+
resp
|
194
|
+
end
|
195
|
+
|
196
|
+
# Sends a acquirer status request for the specified +transaction_id+ and
|
197
|
+
# returns an StatusResponse.
|
198
|
+
#
|
199
|
+
# It is _your_ responsibility as the merchant to check if the payment has
|
200
|
+
# been made until you receive a response with a finished status like:
|
201
|
+
# `Success', `Cancelled', `Expired', everything else equals `Open'.
|
202
|
+
#
|
203
|
+
# === Example
|
204
|
+
#
|
205
|
+
# capture_response = gateway.capture(@purchase.transaction_id)
|
206
|
+
# if capture_response.success?
|
207
|
+
# @purchase.update_attributes!(:paid => true)
|
208
|
+
# flash[:notice] = "Congratulations, you are now the proud owner of a Dutch windmill!"
|
209
|
+
# end
|
210
|
+
#
|
211
|
+
# See the Gateway class description for a more elaborate example.
|
212
|
+
def capture(transaction_id)
|
213
|
+
a = build_status_request(:transaction_id => transaction_id)
|
214
|
+
log('REQ', a)
|
215
|
+
b = post_data request_url, a, StatusResponse
|
216
|
+
log('RES', b)
|
217
|
+
b
|
218
|
+
end
|
219
|
+
|
220
|
+
private
|
221
|
+
|
222
|
+
def ssl_post(url, body)
|
223
|
+
log('URL', url)
|
224
|
+
log('Request', body)
|
225
|
+
response = REST.post(url, body, {
|
226
|
+
'Content-Type' => 'application/xml; charset=utf-8'
|
227
|
+
}, {
|
228
|
+
:tls_verify => true,
|
229
|
+
:tls_key => self.class.private_key,
|
230
|
+
:tls_certificate => self.class.private_certificate
|
231
|
+
})
|
232
|
+
log('Response', response.body)
|
233
|
+
response.body
|
234
|
+
end
|
235
|
+
|
236
|
+
def post_data(gateway_url, data, response_klass)
|
237
|
+
response_klass.new(ssl_post(gateway_url, data), :test => self.class.test?)
|
238
|
+
end
|
239
|
+
|
240
|
+
# This is the list of charaters that are not supported by iDEAL according
|
241
|
+
# to the PHP source provided by ING plus the same in capitals.
|
242
|
+
DIACRITICAL_CHARACTERS = /[ÀÁÂÃÄÅÇŒÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝàáâãäåçæèéêëìíîïñòóôõöøùúûüý]/ #:nodoc:
|
243
|
+
|
244
|
+
# Raises an ArgumentError if the +string+ exceeds the +max_length+ amount
|
245
|
+
# of characters or contains any diacritical characters.
|
246
|
+
def enforce_maximum_length(key, string, max_length)
|
247
|
+
raise ArgumentError, "The value for `#{key}' exceeds the limit of #{max_length} characters." if string.length > max_length
|
248
|
+
raise ArgumentError, "The value for `#{key}' contains diacritical characters `#{string}'." if string =~ DIACRITICAL_CHARACTERS
|
249
|
+
end
|
250
|
+
|
251
|
+
#signs the xml
|
252
|
+
def sign!(xml)
|
253
|
+
digest_val = digest_value(xml.doc.children[0])
|
254
|
+
xml.Signature(xmlns: 'http://www.w3.org/2000/09/xmldsig#') do |xml|
|
255
|
+
xml.SignedInfo do |xml|
|
256
|
+
xml.CanonicalizationMethod(Algorithm: 'http://www.w3.org/2001/10/xml-exc-c14n#')
|
257
|
+
xml.SignatureMethod(Algorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256')
|
258
|
+
xml.Reference(URI: '') do |xml|
|
259
|
+
xml.Transforms do |xml|
|
260
|
+
xml.Transform(Algorithm: 'http://www.w3.org/2000/09/xmldsig#enveloped-signature')
|
261
|
+
end
|
262
|
+
xml.DigestMethod(Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256')
|
263
|
+
xml.DigestValue digest_val
|
264
|
+
end
|
265
|
+
end
|
266
|
+
xml.SignatureValue signature_value(xml.doc.xpath("//Signature:SignedInfo", 'Signature' => 'http://www.w3.org/2000/09/xmldsig#')[0])
|
267
|
+
xml.KeyInfo do |xml|
|
268
|
+
xml.KeyName fingerprint
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
# Creates a +signatureValue+ from the xml+.
|
274
|
+
def signature_value(sig_val)
|
275
|
+
canonical = sig_val.canonicalize(Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0)
|
276
|
+
signature = Ideal::Gateway.private_key.sign(OpenSSL::Digest::SHA256.new, canonical)
|
277
|
+
Base64.encode64(signature)
|
278
|
+
end
|
279
|
+
|
280
|
+
# Creates a +digestValue+ from the xml+.
|
281
|
+
def digest_value(xml)
|
282
|
+
canonical = xml.canonicalize(Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0)
|
283
|
+
digest = OpenSSL::Digest::SHA256.new.digest canonical
|
284
|
+
Base64.encode64(digest)
|
285
|
+
end
|
286
|
+
|
287
|
+
# Creates a keyName value for the XML signature
|
288
|
+
def fingerprint
|
289
|
+
Digest::SHA1.hexdigest(Ideal::Gateway.private_certificate.to_der)
|
290
|
+
end
|
291
|
+
|
292
|
+
# Returns a string containing the current UTC time, formatted as per the
|
293
|
+
# iDeal specifications, except we don't use miliseconds.
|
294
|
+
def created_at_timestamp
|
295
|
+
Time.now.gmtime.strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
296
|
+
end
|
297
|
+
|
298
|
+
def requires!(options, *keys)
|
299
|
+
missing = keys - options.keys
|
300
|
+
unless missing.empty?
|
301
|
+
raise ArgumentError, "Missing required options: #{missing.map { |m| m.to_s }.join(', ')}"
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def build_status_request(options)
|
306
|
+
requires!(options, :transaction_id)
|
307
|
+
|
308
|
+
timestamp = created_at_timestamp
|
309
|
+
Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
310
|
+
xml.AcquirerStatusReq(xmlns: XML_NAMESPACE, version: API_VERSION) do |xml|
|
311
|
+
xml.createDateTimestamp created_at_timestamp
|
312
|
+
xml.Merchant do |xml|
|
313
|
+
xml.merchantID self.class.merchant_id
|
314
|
+
xml.subID @sub_id
|
315
|
+
end
|
316
|
+
xml.Transaction do |xml|
|
317
|
+
xml.transactionID options[:transaction_id]
|
318
|
+
end
|
319
|
+
sign!(xml)
|
320
|
+
end
|
321
|
+
end.to_xml(:save_with => Nokogiri::XML::Node::SaveOptions::AS_XML)
|
322
|
+
end
|
323
|
+
|
324
|
+
def build_directory_request
|
325
|
+
timestamp = created_at_timestamp
|
326
|
+
xml = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
327
|
+
xml.DirectoryReq(xmlns: XML_NAMESPACE, version: API_VERSION) do |xml|
|
328
|
+
xml.createDateTimestamp created_at_timestamp
|
329
|
+
xml.Merchant do |xml|
|
330
|
+
xml.merchantID self.class.merchant_id
|
331
|
+
xml.subID @sub_id
|
332
|
+
end
|
333
|
+
sign!(xml)
|
334
|
+
end
|
335
|
+
end.to_xml(:save_with => Nokogiri::XML::Node::SaveOptions::AS_XML)
|
336
|
+
end
|
337
|
+
|
338
|
+
def build_transaction_request(money, options)
|
339
|
+
requires!(options, :issuer_id, :expiration_period, :return_url, :order_id, :description, :entrance_code)
|
340
|
+
|
341
|
+
enforce_maximum_length(:money, money.to_s, 12)
|
342
|
+
enforce_maximum_length(:order_id, options[:order_id], 12)
|
343
|
+
enforce_maximum_length(:description, options[:description], 32)
|
344
|
+
enforce_maximum_length(:entrance_code, options[:entrance_code], 40)
|
345
|
+
|
346
|
+
timestamp = created_at_timestamp
|
347
|
+
|
348
|
+
Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
349
|
+
xml.AcquirerTrxReq(xmlns: XML_NAMESPACE, version: API_VERSION) do |xml|
|
350
|
+
xml.createDateTimestamp created_at_timestamp
|
351
|
+
xml.Issuer do |xml|
|
352
|
+
xml.issuerID options[:issuer_id]
|
353
|
+
end
|
354
|
+
xml.Merchant do |xml|
|
355
|
+
xml.merchantID self.class.merchant_id
|
356
|
+
xml.subID 0
|
357
|
+
xml.merchantReturnURL options[:return_url]
|
358
|
+
end
|
359
|
+
xml.Transaction do |xml|
|
360
|
+
xml.purchaseID options[:order_id]
|
361
|
+
xml.amount money
|
362
|
+
xml.currency CURRENCY
|
363
|
+
xml.expirationPeriod options[:expiration_period]
|
364
|
+
xml.language LANGUAGE
|
365
|
+
xml.description options[:description]
|
366
|
+
xml.entranceCode options[:entrance_code]
|
367
|
+
end
|
368
|
+
sign!(xml)
|
369
|
+
end
|
370
|
+
end.to_xml(:save_with => Nokogiri::XML::Node::SaveOptions::AS_XML)
|
371
|
+
end
|
372
|
+
|
373
|
+
def log(thing, contents)
|
374
|
+
$stderr.write("\n#{thing}:\n\n#{contents}\n") if $DEBUG
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
@@ -0,0 +1,274 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'cgi'
|
4
|
+
require 'openssl'
|
5
|
+
require 'base64'
|
6
|
+
#require 'rexml/document'
|
7
|
+
|
8
|
+
module Ideal
|
9
|
+
# === Response classes
|
10
|
+
#
|
11
|
+
# * Response
|
12
|
+
# * TransactionResponse
|
13
|
+
# * StatusResponse
|
14
|
+
# * DirectoryResponse
|
15
|
+
#
|
16
|
+
# See the Response class for more information on errors.
|
17
|
+
class Response
|
18
|
+
attr_accessor :response
|
19
|
+
|
20
|
+
def initialize(response_body, options = {})
|
21
|
+
#@response = REXML::Document.new(response_body).root
|
22
|
+
@body = response_body
|
23
|
+
doc = Nokogiri::XML(response_body)
|
24
|
+
doc.remove_namespaces!
|
25
|
+
@response = doc.root
|
26
|
+
@success = !error_occured?
|
27
|
+
@test = options[:test] ? options[:test] : false
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns whether we're running in test mode
|
31
|
+
def test?
|
32
|
+
@test
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns whether the request was a success
|
36
|
+
def success?
|
37
|
+
@success
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns a technical error message.
|
41
|
+
def error_message
|
42
|
+
text('//Error/errorMessage') unless success?
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns a consumer friendly error message.
|
46
|
+
def consumer_error_message
|
47
|
+
text('//Error/consumerMessage') unless success?
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns details on the error if available.
|
51
|
+
def error_details
|
52
|
+
text('//Error/errorDetail') unless success?
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns an error type inflected from the first two characters of the
|
56
|
+
# error code. See error_code for a full list of errors.
|
57
|
+
#
|
58
|
+
# Error code to type mappings:
|
59
|
+
#
|
60
|
+
# * +IX+ - <tt>:xml</tt>
|
61
|
+
# * +SO+ - <tt>:system</tt>
|
62
|
+
# * +SE+ - <tt>:security</tt>
|
63
|
+
# * +BR+ - <tt>:value</tt>
|
64
|
+
# * +AP+ - <tt>:application</tt>
|
65
|
+
def error_type
|
66
|
+
unless success?
|
67
|
+
case error_code[0, 2]
|
68
|
+
when 'IX' then
|
69
|
+
:xml
|
70
|
+
when 'SO' then
|
71
|
+
:system
|
72
|
+
when 'SE' then
|
73
|
+
:security
|
74
|
+
when 'BR' then
|
75
|
+
:value
|
76
|
+
when 'AP' then
|
77
|
+
:application
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns the code of the error that occured.
|
83
|
+
#
|
84
|
+
# === Codes
|
85
|
+
#
|
86
|
+
# ==== IX: Invalid XML and all related problems
|
87
|
+
#
|
88
|
+
# Such as incorrect encoding, invalid version, or otherwise unreadable:
|
89
|
+
#
|
90
|
+
# * <tt>IX1000</tt> - Received XML not well-formed.
|
91
|
+
# * <tt>IX1100</tt> - Received XML not valid.
|
92
|
+
# * <tt>IX1200</tt> - Encoding type not UTF-8.
|
93
|
+
# * <tt>IX1300</tt> - XML version number invalid.
|
94
|
+
# * <tt>IX1400</tt> - Unknown message.
|
95
|
+
# * <tt>IX1500</tt> - Mandatory main value missing. (Merchant ID ?)
|
96
|
+
# * <tt>IX1600</tt> - Mandatory value missing.
|
97
|
+
#
|
98
|
+
# ==== SO: System maintenance or failure
|
99
|
+
#
|
100
|
+
# The errors that are communicated in the event of system maintenance or
|
101
|
+
# system failure. Also covers the situation where new requests are no
|
102
|
+
# longer being accepted but requests already submitted will be dealt with
|
103
|
+
# (until a certain time):
|
104
|
+
#
|
105
|
+
# * <tt>SO1000</tt> - Failure in system.
|
106
|
+
# * <tt>SO1200</tt> - System busy. Try again later.
|
107
|
+
# * <tt>SO1400</tt> - Unavailable due to maintenance.
|
108
|
+
#
|
109
|
+
# ==== SE: Security and authentication errors
|
110
|
+
#
|
111
|
+
# Incorrect authentication methods and expired certificates:
|
112
|
+
#
|
113
|
+
# * <tt>SE2000</tt> - Authentication error.
|
114
|
+
# * <tt>SE2100</tt> - Authentication method not supported.
|
115
|
+
# * <tt>SE2700</tt> - Invalid electronic signature.
|
116
|
+
#
|
117
|
+
# ==== BR: Field errors
|
118
|
+
#
|
119
|
+
# Extra information on incorrect fields:
|
120
|
+
#
|
121
|
+
# * <tt>BR1200</tt> - iDEAL version number invalid.
|
122
|
+
# * <tt>BR1210</tt> - Value contains non-permitted character.
|
123
|
+
# * <tt>BR1220</tt> - Value too long.
|
124
|
+
# * <tt>BR1230</tt> - Value too short.
|
125
|
+
# * <tt>BR1240</tt> - Value too high.
|
126
|
+
# * <tt>BR1250</tt> - Value too low.
|
127
|
+
# * <tt>BR1250</tt> - Unknown entry in list.
|
128
|
+
# * <tt>BR1270</tt> - Invalid date/time.
|
129
|
+
# * <tt>BR1280</tt> - Invalid URL.
|
130
|
+
#
|
131
|
+
# ==== AP: Application errors
|
132
|
+
#
|
133
|
+
# Errors relating to IDs, account numbers, time zones, transactions:
|
134
|
+
#
|
135
|
+
# * <tt>AP1000</tt> - Acquirer ID unknown.
|
136
|
+
# * <tt>AP1100</tt> - Merchant ID unknown.
|
137
|
+
# * <tt>AP1200</tt> - Issuer ID unknown.
|
138
|
+
# * <tt>AP1300</tt> - Sub ID unknown.
|
139
|
+
# * <tt>AP1500</tt> - Merchant ID not active.
|
140
|
+
# * <tt>AP2600</tt> - Transaction does not exist.
|
141
|
+
# * <tt>AP2620</tt> - Transaction already submitted.
|
142
|
+
# * <tt>AP2700</tt> - Bank account number not 11-proof.
|
143
|
+
# * <tt>AP2900</tt> - Selected currency not supported.
|
144
|
+
# * <tt>AP2910</tt> - Maximum amount exceeded. (Detailed record states the maximum amount).
|
145
|
+
# * <tt>AP2915</tt> - Amount too low. (Detailed record states the minimum amount).
|
146
|
+
# * <tt>AP2920</tt> - Please adjust expiration period. See suggested expiration period.
|
147
|
+
def error_code
|
148
|
+
text('//errorCode') unless success?
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def error_occured?
|
154
|
+
@response.name == 'AcquirerErrorRes'
|
155
|
+
end
|
156
|
+
|
157
|
+
def text(path)
|
158
|
+
@response.xpath(path)[0].text() unless @response.xpath(path)[0].nil?
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# An instance of TransactionResponse is returned from
|
163
|
+
# Gateway#setup_purchase which returns the service_url to where the
|
164
|
+
# user should be redirected to perform the transaction _and_ the
|
165
|
+
# transaction ID.
|
166
|
+
class TransactionResponse < Response
|
167
|
+
# Returns the URL to the issuer’s page where the consumer should be
|
168
|
+
# redirected to in order to perform the payment.
|
169
|
+
def service_url
|
170
|
+
CGI::unescapeHTML(text('//issuerAuthenticationURL')).strip
|
171
|
+
end
|
172
|
+
|
173
|
+
def verified?
|
174
|
+
signed_document = Xmldsig::SignedDocument.new(@body)
|
175
|
+
@verified ||= signed_document.validate(Ideal::Gateway.ideal_certificate)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Returns the transaction ID which is needed for requesting the status
|
179
|
+
# of a transaction. See Gateway#capture.
|
180
|
+
def transaction_id
|
181
|
+
text('//transactionID')
|
182
|
+
end
|
183
|
+
|
184
|
+
# Returns the <tt>:order_id</tt> for this transaction.
|
185
|
+
def order_id
|
186
|
+
text('//purchaseID')
|
187
|
+
end
|
188
|
+
|
189
|
+
def signature
|
190
|
+
Base64.decode64(text('//SignatureValue'))
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# An instance of StatusResponse is returned from Gateway#capture
|
195
|
+
# which returns whether or not the transaction that was started with
|
196
|
+
# Gateway#setup_purchase was successful.
|
197
|
+
#
|
198
|
+
# It takes care of checking if the message was authentic by verifying the
|
199
|
+
# the message and its signature against the iDEAL certificate.
|
200
|
+
#
|
201
|
+
# If success? returns +false+ because the authenticity wasn't verified
|
202
|
+
# there will be no error_code, error_message, and error_type. Use verified?
|
203
|
+
# to check if the authenticity has been verified.
|
204
|
+
class StatusResponse < Response
|
205
|
+
def initialize(response_body, options = {})
|
206
|
+
super
|
207
|
+
@success = transaction_successful?
|
208
|
+
end
|
209
|
+
|
210
|
+
# Returns the status message, which is one of: <tt>:success</tt>,
|
211
|
+
# <tt>:cancelled</tt>, <tt>:expired</tt>, <tt>:open</tt>, or
|
212
|
+
# <tt>:failure</tt>.
|
213
|
+
def status
|
214
|
+
status = text('//status')
|
215
|
+
status.downcase.to_sym unless (status.nil? || status.strip == '')
|
216
|
+
end
|
217
|
+
|
218
|
+
# Returns whether or not the authenticity of the message could be
|
219
|
+
# verified.
|
220
|
+
def verified?
|
221
|
+
signed_document = Xmldsig::SignedDocument.new(@body)
|
222
|
+
@verified ||= signed_document.validate(Ideal::Gateway.ideal_certificate)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Returns the bankaccount number when the transaction was successful.
|
226
|
+
def consumer_iban
|
227
|
+
text('//consumerIBAN')
|
228
|
+
end
|
229
|
+
|
230
|
+
# Returns the name on the bankaccount of the customer when the
|
231
|
+
# transaction was successful.
|
232
|
+
def consumer_name
|
233
|
+
text('//consumerName')
|
234
|
+
end
|
235
|
+
|
236
|
+
# Returns the BIC of the bankaccount of the customer when the
|
237
|
+
# transaction was succesfull
|
238
|
+
def consumer_bic
|
239
|
+
text('//consumerBIC')
|
240
|
+
end
|
241
|
+
|
242
|
+
def signature
|
243
|
+
Base64.decode64(text('//SignatureValue'))
|
244
|
+
end
|
245
|
+
|
246
|
+
private
|
247
|
+
|
248
|
+
# Checks if no errors occured _and_ if the message was authentic.
|
249
|
+
def transaction_successful?
|
250
|
+
!error_occured? && status == :success && verified?
|
251
|
+
end
|
252
|
+
|
253
|
+
end
|
254
|
+
|
255
|
+
|
256
|
+
# An instance of DirectoryResponse is returned from
|
257
|
+
# Gateway#issuers which returns the list of issuers available at the
|
258
|
+
# acquirer.
|
259
|
+
class DirectoryResponse < Response
|
260
|
+
# Returns a list of issuers available at the acquirer.
|
261
|
+
#
|
262
|
+
# gateway.issuers.list # => [{ :id => '1006', :name => 'ABN AMRO Bank' }]
|
263
|
+
def list
|
264
|
+
list = Array.new
|
265
|
+
@response.xpath(".//Country").each { |country|
|
266
|
+
country_name = country.xpath(".//countryNames").first.text()
|
267
|
+
country.xpath(".//Issuer").each { |issuer|
|
268
|
+
list << {:id => issuer.xpath(".//issuerID").first.text(), :country => country_name, :name => issuer.xpath(".//issuerName").first.text()}
|
269
|
+
}
|
270
|
+
}
|
271
|
+
list
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|