ideal-payment 1.0.2
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.
- 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
|