activemerchant-realex3ds 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -0
- data/Gemfile.lock +60 -0
- data/MIT-LICENSE +20 -0
- data/README.md +25 -0
- data/Rakefile +58 -0
- data/activemerchant-realex3ds.gemspec +28 -0
- data/lib/active_merchant/billing/gateways/realex3ds.rb +610 -0
- data/lib/active_merchant/billing/gateways/realex3ds_development.rb +111 -0
- data/test/assert_equal_xml.rb +5 -0
- data/test/comm_stub.rb +40 -0
- data/test/fixtures.yml +71 -0
- data/test/remote/gateways/remote_realex3ds_test.rb +476 -0
- data/test/test_helper.rb +265 -0
- data/test/unit/gateways/realex3ds_test.rb +975 -0
- metadata +149 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
activemerchant-realex3ds (1.0.0)
|
5
|
+
activemerchant (~> 1.34)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
actionmailer (2.3.18)
|
11
|
+
actionpack (= 2.3.18)
|
12
|
+
actionpack (2.3.18)
|
13
|
+
activesupport (= 2.3.18)
|
14
|
+
rack (~> 1.1.0)
|
15
|
+
active_utils (1.0.5)
|
16
|
+
activesupport (>= 2.3.11)
|
17
|
+
i18n
|
18
|
+
activemerchant (1.34.1)
|
19
|
+
active_utils (>= 1.0.2)
|
20
|
+
activesupport (>= 2.3.14)
|
21
|
+
builder (>= 2.0.0)
|
22
|
+
i18n
|
23
|
+
json (>= 1.5.1)
|
24
|
+
money
|
25
|
+
nokogiri (< 1.6.0)
|
26
|
+
activerecord (2.3.18)
|
27
|
+
activesupport (= 2.3.18)
|
28
|
+
activeresource (2.3.18)
|
29
|
+
activesupport (= 2.3.18)
|
30
|
+
activesupport (2.3.18)
|
31
|
+
builder (3.2.2)
|
32
|
+
equivalent-xml (0.3.0)
|
33
|
+
nokogiri (>= 1.4.3)
|
34
|
+
i18n (0.6.4)
|
35
|
+
json (1.8.0)
|
36
|
+
metaclass (0.0.1)
|
37
|
+
mocha (0.13.3)
|
38
|
+
metaclass (~> 0.0.1)
|
39
|
+
money (5.1.1)
|
40
|
+
i18n (~> 0.6.0)
|
41
|
+
nokogiri (1.5.10)
|
42
|
+
rack (1.1.6)
|
43
|
+
rails (2.3.18)
|
44
|
+
actionmailer (= 2.3.18)
|
45
|
+
actionpack (= 2.3.18)
|
46
|
+
activerecord (= 2.3.18)
|
47
|
+
activeresource (= 2.3.18)
|
48
|
+
activesupport (= 2.3.18)
|
49
|
+
rake (>= 0.8.3)
|
50
|
+
rake (10.1.0)
|
51
|
+
|
52
|
+
PLATFORMS
|
53
|
+
ruby
|
54
|
+
|
55
|
+
DEPENDENCIES
|
56
|
+
activemerchant-realex3ds!
|
57
|
+
equivalent-xml
|
58
|
+
mocha (~> 0.13.0)
|
59
|
+
rails (>= 2.3.14)
|
60
|
+
rake
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2005-2010 Tobias Luetke
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
Realex 3D Secure Gateway for ActiveMerchant
|
2
|
+
===========================================
|
3
|
+
|
4
|
+
The Gateway included in ActiveMerchant does not support 3D Secure. This implementation was done by David Rice and sponsored by [Ticketsolve](http://ticketsolve.com).
|
5
|
+
|
6
|
+
In your Gemfile
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
gem 'activemerchant-realex3ds'
|
10
|
+
```
|
11
|
+
|
12
|
+
In your code
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
require 'active_merchant'
|
16
|
+
require 'active_merchant/billing/gateways/realex3ds'
|
17
|
+
|
18
|
+
gateway = ActiveMerchant::Billing::Realex3dsGateway.new(
|
19
|
+
:login => 'TestMerchant',
|
20
|
+
:password => 'password')
|
21
|
+
```
|
22
|
+
|
23
|
+
See [ActiveMerchant](http://github.com/Shopify/active_merchant) for more details.
|
24
|
+
|
25
|
+
Ported to current version of ActiveMerchant and extracted into a Gem by Arne Brasseur.
|
data/Rakefile
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
$:.unshift File.expand_path('../lib', __FILE__)
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'bundler'
|
5
|
+
Bundler.setup
|
6
|
+
rescue LoadError => e
|
7
|
+
puts "Error loading bundler (#{e.message}): \"gem install bundler\" for bundler support."
|
8
|
+
require 'rubygems'
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'rake'
|
12
|
+
require 'rake/testtask'
|
13
|
+
require 'rubygems/package_task'
|
14
|
+
# require 'support/gateway_support'
|
15
|
+
# require 'support/ssl_verify'
|
16
|
+
# require 'support/outbound_hosts'
|
17
|
+
|
18
|
+
desc "Run the unit test suite"
|
19
|
+
task :default => 'test:units'
|
20
|
+
|
21
|
+
task :test => 'test:units'
|
22
|
+
|
23
|
+
namespace :test do
|
24
|
+
|
25
|
+
Rake::TestTask.new(:units) do |t|
|
26
|
+
t.pattern = 'test/unit/**/*_test.rb'
|
27
|
+
t.ruby_opts << '-rubygems'
|
28
|
+
t.libs << 'test'
|
29
|
+
t.verbose = true
|
30
|
+
end
|
31
|
+
|
32
|
+
Rake::TestTask.new(:remote) do |t|
|
33
|
+
t.pattern = 'test/remote/**/*_test.rb'
|
34
|
+
t.ruby_opts << '-rubygems'
|
35
|
+
t.libs << 'test'
|
36
|
+
t.verbose = true
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
desc "Delete tar.gz / zip"
|
42
|
+
task :cleanup => [ :clobber_package ]
|
43
|
+
|
44
|
+
spec = eval(File.read('activemerchant-realex3ds.gemspec'))
|
45
|
+
|
46
|
+
Gem::PackageTask.new(spec) do |p|
|
47
|
+
p.gem_spec = spec
|
48
|
+
end
|
49
|
+
|
50
|
+
desc "Release the gems and docs to RubyForge"
|
51
|
+
task :release => [ 'gemcutter:publish' ]
|
52
|
+
|
53
|
+
namespace :gemcutter do
|
54
|
+
desc "Publish to gemcutter"
|
55
|
+
task :publish => :package do
|
56
|
+
sh "gem push pkg/activemerchant-realex3ds-#{spec.version}.gem"
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.platform = Gem::Platform::RUBY
|
7
|
+
s.name = 'activemerchant-realex3ds'
|
8
|
+
s.version = '1.0.0'
|
9
|
+
s.summary = 'Realex gateway for ActiveMerchant with 3D Secure support'
|
10
|
+
s.description = 'Realex is the leading payment provider for Ireland. The default gateway included in ActiveMerchant does not support 3D Secure. This implementation does, it was sponsored by Ticketsolve, written by David Rice, and released as a gem for the current version of ActiveMerchant by Arne Brasseur.'
|
11
|
+
|
12
|
+
s.authors = ['David Rice', 'Arne Brasseur']
|
13
|
+
s.email = 'arne@arnebrasseur.net'
|
14
|
+
s.homepage = 'https://github.com/plexus/active_merchant-realex3ds'
|
15
|
+
s.rubyforge_project = 'activemerchant-realex3ds'
|
16
|
+
|
17
|
+
s.require_paths = %w[lib]
|
18
|
+
s.files = `git ls-files`.split($/)
|
19
|
+
s.test_files = `git ls-files -- spec`.split($/)
|
20
|
+
s.extra_rdoc_files = %w[README.md]
|
21
|
+
|
22
|
+
s.add_dependency('activemerchant', '~> 1.34')
|
23
|
+
|
24
|
+
s.add_development_dependency('rake')
|
25
|
+
s.add_development_dependency('mocha', '~> 0.13.0')
|
26
|
+
s.add_development_dependency('rails', '>= 2.3.14')
|
27
|
+
s.add_development_dependency('equivalent-xml')
|
28
|
+
end
|
@@ -0,0 +1,610 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
require 'digest/sha1'
|
3
|
+
|
4
|
+
module ActiveMerchant
|
5
|
+
module Billing
|
6
|
+
# For more information on the Realex Payment Gateway visit their site {realexpayments.com}[http://realexpayments.com].
|
7
|
+
# Realex is the leading gateway in Ireland
|
8
|
+
#
|
9
|
+
# === Merchant ID and Password
|
10
|
+
#
|
11
|
+
# To be able to use this library you will need to obtain an account from Realex, you can find contact them
|
12
|
+
# via their website.
|
13
|
+
#
|
14
|
+
# === Caveats
|
15
|
+
#
|
16
|
+
# Realex requires that you specify the account to which your transactions are made.
|
17
|
+
#
|
18
|
+
# gateway = ActiveMerchant::Billing::Realex3dsGateway.new(:login => 'xxx', :password => 'xxx', :acction => 'xxx')
|
19
|
+
#
|
20
|
+
# If you wish to accept multiple currencies, you need to create an account per currency.
|
21
|
+
# This you would need to handle within your application logic.
|
22
|
+
# Again, contact Realex for more information.
|
23
|
+
#
|
24
|
+
# They also require accepting payment from a Diners card (Mastercard) go through a different account.
|
25
|
+
#
|
26
|
+
# Realex also requires that you send several (extra) required identifiers with credit and void methods
|
27
|
+
#
|
28
|
+
# * order_id
|
29
|
+
# * pasref
|
30
|
+
# * authorization
|
31
|
+
#
|
32
|
+
# The pasref can be accessed from the response params. i.e.
|
33
|
+
# response.params['pasref']
|
34
|
+
#
|
35
|
+
# === Testing
|
36
|
+
#
|
37
|
+
# Realex provide test card numbers on a per-account basis, you will need to request these.
|
38
|
+
# Then if you copy the fixtures file that comes with this library to ~/.active_merchant/fixtures.yml
|
39
|
+
# you can add in the required card number (and account) fixtures.
|
40
|
+
#
|
41
|
+
class Realex3dsGateway < Gateway
|
42
|
+
URL = 'https://epage.payandshop.com/epage-remote.cgi'
|
43
|
+
THREE_D_SECURE_URL = 'https://epage.payandshop.com/epage-3dsecure.cgi'
|
44
|
+
RECURRING_PAYMENTS_URL = "https://epage.payandshop.com/epage-remote-plugins.cgi"
|
45
|
+
|
46
|
+
CARD_MAPPING = {
|
47
|
+
'master' => 'MC',
|
48
|
+
'visa' => 'VISA',
|
49
|
+
'visa_delta' => 'VISA',
|
50
|
+
'visa_electron' => 'VISA',
|
51
|
+
'american_express' => 'AMEX',
|
52
|
+
'diners_club' => 'DINERS',
|
53
|
+
'switch' => 'SWITCH',
|
54
|
+
'solo' => 'SWITCH',
|
55
|
+
'laser' => 'LASER'
|
56
|
+
}
|
57
|
+
|
58
|
+
self.money_format = :cents
|
59
|
+
self.default_currency = 'EUR'
|
60
|
+
self.supported_cardtypes = [ :visa, :master, :american_express, :diners_club, :switch, :solo, :laser ]
|
61
|
+
self.supported_countries = [ 'IE', 'GB' ]
|
62
|
+
self.homepage_url = 'http://www.realexpayments.com/'
|
63
|
+
self.display_name = 'Realex'
|
64
|
+
|
65
|
+
SUCCESS, DECLINED = "Successful", "Declined"
|
66
|
+
BANK_ERROR = REALEX_ERROR = "Gateway is in maintenance. Please try again later."
|
67
|
+
ERROR = CLIENT_DEACTIVATED = "Gateway Error"
|
68
|
+
|
69
|
+
def initialize(options = {})
|
70
|
+
requires!(options, :login, :password)
|
71
|
+
options[:refund_hash] = Digest::SHA1.hexdigest(options[:rebate_secret]) if options.has_key?(:rebate_secret)
|
72
|
+
@options = options
|
73
|
+
super
|
74
|
+
end
|
75
|
+
|
76
|
+
# Performs an authorization, which reserves the funds on the customer's credit card, but does not
|
77
|
+
# charge the card.
|
78
|
+
#
|
79
|
+
# ==== Parameters
|
80
|
+
#
|
81
|
+
# * <tt>money</tt> -- The amount to be authorized. Either an Integer value in cents or a Money object.
|
82
|
+
# * <tt>creditcard</tt> -- The CreditCard details for the transaction.
|
83
|
+
# * <tt>options</tt> -- A hash of optional parameters.
|
84
|
+
#
|
85
|
+
# ==== Options
|
86
|
+
#
|
87
|
+
# * <tt>:order_id</tt> -- The application generated order identifier. (REQUIRED)
|
88
|
+
#
|
89
|
+
def authorize(money, creditcard, options = {})
|
90
|
+
requires!(options, :order_id)
|
91
|
+
|
92
|
+
if options[:three_d_secure]
|
93
|
+
three_d_secure_request = build_3d_secure_verify_signature_or_enrolled_request("3ds-verifyenrolled", money, creditcard, options)
|
94
|
+
three_d_secure_response = commit(three_d_secure_request, :three_d_secure)
|
95
|
+
return three_d_secure_response if three_d_secure_response.enrolled?
|
96
|
+
end
|
97
|
+
|
98
|
+
request = build_purchase_or_authorization_request(:authorization, money, creditcard, options)
|
99
|
+
commit(request)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Perform a purchase, which is essentially an authorization and capture in a single operation.
|
103
|
+
#
|
104
|
+
# ==== Parameters
|
105
|
+
#
|
106
|
+
# * <tt>money</tt> -- The amount to be purchased. Either an Integer value in cents or a Money object.
|
107
|
+
# * <tt>creditcard</tt> -- The CreditCard details for the transaction.
|
108
|
+
# * <tt>options</tt> -- A hash of optional parameters.
|
109
|
+
#
|
110
|
+
# ==== Options
|
111
|
+
#
|
112
|
+
# * <tt>:order_id</tt> -- The application generated order identifier. (REQUIRED)
|
113
|
+
#
|
114
|
+
def purchase(money, creditcard, options = {})
|
115
|
+
requires!(options, :order_id)
|
116
|
+
|
117
|
+
if options[:three_d_secure_auth]
|
118
|
+
three_d_secure_request = build_3d_secure_verify_signature_or_enrolled_request("3ds-verifysig", money, creditcard, options)
|
119
|
+
three_d_secure_response = commit(three_d_secure_request, :three_d_secure)
|
120
|
+
result = three_d_secure_response.params['result']
|
121
|
+
if result == '00'
|
122
|
+
status = three_d_secure_response.params['threedsecure_status']
|
123
|
+
# success
|
124
|
+
if status == 'Y' || status == 'A'
|
125
|
+
# Y: 3d Secure complete.
|
126
|
+
# A: ACS service aknowledges.
|
127
|
+
# not-liable. continue
|
128
|
+
options[:three_d_secure_sig] = {}
|
129
|
+
options[:three_d_secure_sig][:eci] = three_d_secure_response.params['threedsecure_eci']
|
130
|
+
options[:three_d_secure_sig][:xid] = three_d_secure_response.params['threedsecure_xid']
|
131
|
+
options[:three_d_secure_sig][:cavv] = three_d_secure_response.params['threedsecure_cavv']
|
132
|
+
# TODO add option[:accept_liability_authentication_failed]
|
133
|
+
# TODO add option[:accept_liability_acs_failure]
|
134
|
+
|
135
|
+
elsif status == 'N'
|
136
|
+
# password entered incorrectly
|
137
|
+
# liable. abort?
|
138
|
+
return Response.new(false, "3DSecure password entered incorrectly. Aborting transaction.",{},{})
|
139
|
+
elsif status == 'U'
|
140
|
+
# Bank ACS service having dificulty.
|
141
|
+
# liable. abort?
|
142
|
+
return Response.new(false, "3DSecure Bank ACS service 500 errors. Aborting transaction.",{},{})
|
143
|
+
end
|
144
|
+
elsif result == "110"
|
145
|
+
# fail, message tampered with.
|
146
|
+
return Response.new(false, "3DSecure message tampered. Aborting transaction.",{},{})
|
147
|
+
else
|
148
|
+
return Response.new(false, "Unknown 3DSecure Error.",{},{})
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
if options[:three_d_secure] && !options[:three_d_secure_auth]
|
153
|
+
three_d_secure_request = build_3d_secure_verify_signature_or_enrolled_request("3ds-verifyenrolled", money, creditcard, options)
|
154
|
+
three_d_secure_response = commit(three_d_secure_request, :three_d_secure)
|
155
|
+
return three_d_secure_response if three_d_secure_response.enrolled?
|
156
|
+
end
|
157
|
+
|
158
|
+
request = build_purchase_or_authorization_request(:purchase, money, creditcard, options)
|
159
|
+
commit(request)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Captures the funds from an authorized transaction.
|
163
|
+
#
|
164
|
+
# ==== Parameters
|
165
|
+
#
|
166
|
+
# * <tt>money</tt> -- The amount to be captured. Either an Integer value in cents or a Money object.
|
167
|
+
# * <tt>authorization</tt> -- The authorization returned from the previous authorize request.
|
168
|
+
#
|
169
|
+
# ==== Options
|
170
|
+
#
|
171
|
+
# * <tt>:order_id</tt> -- The application generated order identifier. (REQUIRED)
|
172
|
+
# * <tt>:pasref</tt> -- The realex payments reference of the original transaction. (REQUIRED)
|
173
|
+
#
|
174
|
+
def capture(money, authorization, options = {})
|
175
|
+
requires!(options, :pasref)
|
176
|
+
requires!(options, :order_id)
|
177
|
+
|
178
|
+
request = build_capture_request(authorization, options)
|
179
|
+
commit(request)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Credit an account.
|
183
|
+
#
|
184
|
+
# This transaction is also referred to as a Refund (or Rebate) and indicates to the gateway that
|
185
|
+
# money should flow from the merchant to the customer.
|
186
|
+
#
|
187
|
+
# ==== Parameters
|
188
|
+
#
|
189
|
+
# * <tt>money</tt> -- The amount to be credited to the customer. Either an Integer value in cents or a Money object.
|
190
|
+
# * <tt>authorization</tt> - The authorization returned from the previous authorize request.
|
191
|
+
# * <tt>options</tt> -- A hash of parameters.
|
192
|
+
#
|
193
|
+
# ==== Options
|
194
|
+
#
|
195
|
+
# * <tt>:order_id</tt> -- The application generated order identifier. (REQUIRED)
|
196
|
+
# * <tt>:pasref</tt> -- The realex payments reference of the original transaction. (REQUIRED)
|
197
|
+
#
|
198
|
+
def credit(money, authorization, options = {})
|
199
|
+
requires!(options, :order_id)
|
200
|
+
requires!(options, :pasref)
|
201
|
+
|
202
|
+
request = build_credit_request(money, authorization, options)
|
203
|
+
commit(request)
|
204
|
+
end
|
205
|
+
|
206
|
+
# Void a previous transaction
|
207
|
+
#
|
208
|
+
# ==== Parameters
|
209
|
+
#
|
210
|
+
# * <tt>authorization</tt> - The authorization returned from the previous authorize request.
|
211
|
+
#
|
212
|
+
# ==== Options
|
213
|
+
#
|
214
|
+
# * <tt>:order_id</tt> -- The application generated order identifier. (REQUIRED)
|
215
|
+
# * <tt>:pasref</tt> -- The realex payments reference of the original transaction. (REQUIRED)
|
216
|
+
#
|
217
|
+
def void(authorization, options = {})
|
218
|
+
requires!(options, :order_id)
|
219
|
+
requires!(options, :pasref)
|
220
|
+
|
221
|
+
request = build_void_request(authorization, options)
|
222
|
+
commit(request)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Recurring Payments
|
226
|
+
|
227
|
+
def recurring(money, credit_card, options = {})
|
228
|
+
requires!(options, :order_id)
|
229
|
+
|
230
|
+
request = build_receipt_in_request(money, credit_card, options)
|
231
|
+
commit(request, :recurring)
|
232
|
+
end
|
233
|
+
|
234
|
+
def store(credit_card, options = {})
|
235
|
+
requires!(options, :order_id)
|
236
|
+
request = build_new_card_request(credit_card, options)
|
237
|
+
commit(request, :recurring)
|
238
|
+
end
|
239
|
+
|
240
|
+
def unstore(creditcard, options = {})
|
241
|
+
request = build_cancel_card_request(creditcard, options)
|
242
|
+
commit(request, :recurring)
|
243
|
+
end
|
244
|
+
|
245
|
+
def store_user(options = {})
|
246
|
+
requires!(options, :order_id)
|
247
|
+
request = build_new_payee_request(options)
|
248
|
+
commit(request, :recurring)
|
249
|
+
end
|
250
|
+
|
251
|
+
private
|
252
|
+
def commit(request, endpoint=:default)
|
253
|
+
url = URL
|
254
|
+
url = THREE_D_SECURE_URL if endpoint == :three_d_secure
|
255
|
+
url = RECURRING_PAYMENTS_URL if endpoint == :recurring
|
256
|
+
|
257
|
+
response = ssl_post(url, request)
|
258
|
+
parsed = parse(response)
|
259
|
+
|
260
|
+
options = {
|
261
|
+
:test => parsed[:message] =~ /\[ test system \]/,
|
262
|
+
:authorization => parsed[:authcode],
|
263
|
+
:cvv_result => parsed[:cvnresult],
|
264
|
+
:body => response,
|
265
|
+
:avs_result => {
|
266
|
+
:street_match => parsed[:avsaddressresponse],
|
267
|
+
:postal_match => parsed[:avspostcoderesponse]
|
268
|
+
}
|
269
|
+
}
|
270
|
+
|
271
|
+
if endpoint == :three_d_secure
|
272
|
+
options.merge!({
|
273
|
+
:pa_req => parsed[:pareq],
|
274
|
+
:acs_url => parsed[:url],
|
275
|
+
:three_d_secure => true,
|
276
|
+
:xid => parsed[:xid],
|
277
|
+
:three_d_secure_enrolled => parsed[:enrolled] == "Y" ? true : false
|
278
|
+
})
|
279
|
+
end
|
280
|
+
|
281
|
+
Response.new(parsed[:result] == "00", message_from(parsed), parsed, options)
|
282
|
+
end
|
283
|
+
|
284
|
+
def parse(xml)
|
285
|
+
response = {}
|
286
|
+
|
287
|
+
xml = REXML::Document.new(xml)
|
288
|
+
|
289
|
+
return response unless xml.root
|
290
|
+
|
291
|
+
xml.elements.each('//response/*') do |node|
|
292
|
+
|
293
|
+
if (node.elements.size == 0)
|
294
|
+
response[node.name.downcase.to_sym] = normalize(node.text)
|
295
|
+
else
|
296
|
+
node.elements.each do |childnode|
|
297
|
+
name = "#{node.name.downcase}_#{childnode.name.downcase}"
|
298
|
+
response[name.to_sym] = normalize(childnode.text)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
end
|
303
|
+
|
304
|
+
response
|
305
|
+
end
|
306
|
+
|
307
|
+
def build_purchase_or_authorization_request(action, money, credit_card, options)
|
308
|
+
timestamp = self.class.timestamp
|
309
|
+
xml = Builder::XmlMarkup.new :indent => 2
|
310
|
+
xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'auth' do
|
311
|
+
add_merchant_details(xml, options)
|
312
|
+
xml.tag! 'orderid', sanitize_order_id(options[:order_id])
|
313
|
+
add_ammount(xml, money, options)
|
314
|
+
add_card(xml, credit_card)
|
315
|
+
xml.tag! 'autosettle', 'flag' => auto_settle_flag(action)
|
316
|
+
add_three_d_secure(xml, options) if options[:three_d_secure_sig]
|
317
|
+
add_signed_digest(xml, timestamp, @options[:login], options[:order_id], amount(money), (options[:currency] || currency(money)), credit_card.number)
|
318
|
+
add_comments(xml, options)
|
319
|
+
add_address_and_customer_info(xml, options)
|
320
|
+
end
|
321
|
+
xml.target!
|
322
|
+
end
|
323
|
+
|
324
|
+
def build_capture_request(authorization, options)
|
325
|
+
timestamp = self.class.timestamp
|
326
|
+
xml = Builder::XmlMarkup.new :indent => 2
|
327
|
+
xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'settle' do
|
328
|
+
add_merchant_details(xml, options)
|
329
|
+
add_transaction_identifiers(xml, authorization, options)
|
330
|
+
add_comments(xml, options)
|
331
|
+
add_signed_digest(xml, timestamp, @options[:login], options[:order_id], '', '', '')
|
332
|
+
end
|
333
|
+
xml.target!
|
334
|
+
end
|
335
|
+
|
336
|
+
def build_credit_request(money, authorization, options)
|
337
|
+
timestamp = self.class.timestamp
|
338
|
+
xml = Builder::XmlMarkup.new :indent => 2
|
339
|
+
xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'rebate' do
|
340
|
+
add_merchant_details(xml, options)
|
341
|
+
add_transaction_identifiers(xml, authorization, options)
|
342
|
+
xml.tag! 'amount', amount(money), 'currency' => options[:currency] || currency(money)
|
343
|
+
xml.tag! 'refundhash', @options[:refund_hash] if @options[:refund_hash]
|
344
|
+
xml.tag! 'autosettle', 'flag' => 1
|
345
|
+
add_comments(xml, options)
|
346
|
+
add_signed_digest(xml, timestamp, @options[:login], options[:order_id], amount(money), (options[:currency] || currency(money)), '')
|
347
|
+
end
|
348
|
+
xml.target!
|
349
|
+
end
|
350
|
+
|
351
|
+
def build_void_request(authorization, options)
|
352
|
+
timestamp = self.class.timestamp
|
353
|
+
xml = Builder::XmlMarkup.new :indent => 2
|
354
|
+
xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'void' do
|
355
|
+
add_merchant_details(xml, options)
|
356
|
+
add_transaction_identifiers(xml, authorization, options)
|
357
|
+
add_comments(xml, options)
|
358
|
+
add_signed_digest(xml, timestamp, @options[:login], options[:order_id], '', '', '')
|
359
|
+
end
|
360
|
+
xml.target!
|
361
|
+
end
|
362
|
+
|
363
|
+
def build_cancel_card_request(creditcard, options = {})
|
364
|
+
timestamp = self.class.timestamp
|
365
|
+
xml = Builder::XmlMarkup.new :indent => 2
|
366
|
+
xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'card-cancel-card' do
|
367
|
+
add_merchant_details(xml, options)
|
368
|
+
xml.tag! 'card' do
|
369
|
+
xml.tag! 'ref', options[:payment_method]
|
370
|
+
xml.tag! 'payerref', options[:user][:id]
|
371
|
+
xml.tag! 'expdate', expiry_date(creditcard)
|
372
|
+
end
|
373
|
+
# TODO userid . card ref . expiry date
|
374
|
+
add_signed_digest(xml, timestamp, @options[:login], options[:user][:id], options[:payment_method])
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
def build_new_card_request(credit_card, options = {})
|
379
|
+
timestamp = self.class.timestamp
|
380
|
+
xml = Builder::XmlMarkup.new :indent => 2
|
381
|
+
xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'card-new' do
|
382
|
+
add_merchant_details(xml, options)
|
383
|
+
xml.tag! 'orderid', sanitize_order_id(options[:order_id])
|
384
|
+
xml.tag! 'card' do
|
385
|
+
xml.tag! 'ref', options[:payment_method]
|
386
|
+
xml.tag! 'payerref', options[:user][:id]
|
387
|
+
xml.tag! 'number', credit_card.number
|
388
|
+
xml.tag! 'expdate', expiry_date(credit_card)
|
389
|
+
xml.tag! 'chname', credit_card.name
|
390
|
+
xml.tag! 'type', CARD_MAPPING[card_brand(credit_card).to_s]
|
391
|
+
xml.tag! 'issueno', credit_card.issue_number
|
392
|
+
xml.tag! 'cvn' do
|
393
|
+
xml.tag! 'number', credit_card.verification_value
|
394
|
+
xml.tag! 'presind', (options['presind'] || (credit_card.verification_value? ? 1 : nil))
|
395
|
+
end
|
396
|
+
end
|
397
|
+
# timestamp.merchantid.orderid.amount.currency.payerref.chname.(card)number
|
398
|
+
add_signed_digest(xml, timestamp, @options[:login], options[:order_id], '', '', options[:user][:id], credit_card.name, credit_card.number)
|
399
|
+
end
|
400
|
+
xml.target!
|
401
|
+
end
|
402
|
+
|
403
|
+
def build_new_payee_request(options)
|
404
|
+
timestamp = self.class.timestamp
|
405
|
+
xml = Builder::XmlMarkup.new :indent => 2
|
406
|
+
xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'payer-new' do
|
407
|
+
add_merchant_details(xml, options)
|
408
|
+
xml.tag! 'orderid', sanitize_order_id(options[:order_id])
|
409
|
+
xml.tag! 'payer', 'type' => 'Business', 'ref' => options[:user][:id] do
|
410
|
+
xml.tag! 'firstname', options[:user][:first_name]
|
411
|
+
xml.tag! 'surname', options[:user][:last_name]
|
412
|
+
end
|
413
|
+
add_signed_digest(xml, timestamp, @options[:login], options[:order_id], '', '', options[:user][:id])
|
414
|
+
end
|
415
|
+
xml.target!
|
416
|
+
end
|
417
|
+
|
418
|
+
def build_3d_secure_verify_signature_or_enrolled_request(action, money, credit_card, options)
|
419
|
+
timestamp = self.class.timestamp
|
420
|
+
xml = Builder::XmlMarkup.new :indent => 2
|
421
|
+
xml.tag! 'request', 'timestamp' => timestamp, 'type' => action do
|
422
|
+
add_merchant_details(xml, options)
|
423
|
+
xml.tag! 'orderid', sanitize_order_id(options[:order_id])
|
424
|
+
add_ammount(xml, money, options)
|
425
|
+
add_card(xml, credit_card)
|
426
|
+
xml.tag!('pares', options[:three_d_secure_auth][:pa_res]) if(action == '3ds-verifysig' && options[:three_d_secure_auth] )
|
427
|
+
add_signed_digest(xml, timestamp, @options[:login], options[:order_id], amount(money), (options[:currency] || currency(money)), credit_card.number)
|
428
|
+
add_comments(xml, options)
|
429
|
+
end
|
430
|
+
xml.target!
|
431
|
+
end
|
432
|
+
|
433
|
+
def add_three_d_secure(xml, options)
|
434
|
+
if options[:three_d_secure_sig]
|
435
|
+
xml.tag! 'mpi' do
|
436
|
+
xml.tag! 'cavv', options[:three_d_secure_sig][:cavv]
|
437
|
+
xml.tag! 'xid', options[:three_d_secure_sig][:xid]
|
438
|
+
xml.tag! 'eci', options[:three_d_secure_sig][:eci]
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
def build_receipt_in_request(money, credit_card, options)
|
444
|
+
timestamp = self.class.timestamp
|
445
|
+
xml = Builder::XmlMarkup.new :indent => 2
|
446
|
+
xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'receipt-in' do
|
447
|
+
add_merchant_details(xml, options)
|
448
|
+
xml.tag! 'orderid', sanitize_order_id(options[:order_id])
|
449
|
+
add_ammount(xml, money, options)
|
450
|
+
xml.tag! 'payerref', options[:user][:id]
|
451
|
+
xml.tag! 'paymentmethod', options[:payment_method]
|
452
|
+
xml.tag! 'autosettle', 'flag' => '1'
|
453
|
+
add_signed_digest(xml, timestamp, @options[:login], options[:order_id], amount(money), (options[:currency] || currency(money)), options[:user][:id])
|
454
|
+
add_comments(xml, options)
|
455
|
+
add_address_and_customer_info(xml, options)
|
456
|
+
end
|
457
|
+
xml.target!
|
458
|
+
end
|
459
|
+
|
460
|
+
def add_address_and_customer_info(xml, options)
|
461
|
+
billing_address = options[:billing_address] || options[:address]
|
462
|
+
shipping_address = options[:shipping_address]
|
463
|
+
|
464
|
+
return unless billing_address || shipping_address || options[:customer] || options[:invoice] || options[:ip]
|
465
|
+
|
466
|
+
xml.tag! 'tssinfo' do
|
467
|
+
|
468
|
+
xml.tag! 'custnum', options[:customer] if options[:customer]
|
469
|
+
xml.tag! 'prodid', options[:invoice] if options[:invoice]
|
470
|
+
xml.tag! 'custipaddress', options[:ip] if options[:ip]
|
471
|
+
# xml.tag! 'varref'
|
472
|
+
|
473
|
+
if billing_address
|
474
|
+
xml.tag! 'address', 'type' => 'billing' do
|
475
|
+
xml.tag! 'code', avs_input_code_or_zip( billing_address, options )
|
476
|
+
xml.tag! 'country', billing_address[:country]
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
if shipping_address
|
481
|
+
xml.tag! 'address', 'type' => 'shipping' do
|
482
|
+
xml.tag! 'code', shipping_address[:zip]
|
483
|
+
xml.tag! 'country', shipping_address[:country]
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
def avs_input_code_or_zip(address, options)
|
491
|
+
options[ :skip_avs_check ] ? address[ :zip ] : avs_input_code( address )
|
492
|
+
end
|
493
|
+
|
494
|
+
def add_merchant_details(xml, options)
|
495
|
+
xml.tag! 'merchantid', @options[:login]
|
496
|
+
if options[:account] || @options[:account]
|
497
|
+
xml.tag! 'account', options[:account] || @options[:account]
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
def add_transaction_identifiers(xml, authorization, options)
|
502
|
+
xml.tag! 'orderid', sanitize_order_id(options[:order_id])
|
503
|
+
xml.tag! 'pasref', options[:pasref]
|
504
|
+
xml.tag! 'authcode', authorization
|
505
|
+
end
|
506
|
+
|
507
|
+
def add_comments(xml, options)
|
508
|
+
return unless options[:description]
|
509
|
+
xml.tag! 'comments' do
|
510
|
+
xml.tag! 'comment', options[:description], 'id' => 1
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
def add_ammount(xml, money, options)
|
515
|
+
xml.tag! 'amount', amount(money), 'currency' => options[:currency] || currency(money)
|
516
|
+
end
|
517
|
+
|
518
|
+
def add_card(xml, credit_card)
|
519
|
+
xml.tag! 'card' do
|
520
|
+
xml.tag! 'number', credit_card.number
|
521
|
+
xml.tag! 'expdate', expiry_date(credit_card)
|
522
|
+
xml.tag! 'chname', credit_card.name
|
523
|
+
xml.tag! 'type', CARD_MAPPING[card_brand(credit_card).to_s]
|
524
|
+
xml.tag! 'issueno', credit_card.issue_number
|
525
|
+
xml.tag! 'cvn' do
|
526
|
+
xml.tag! 'number', credit_card.verification_value
|
527
|
+
xml.tag! 'presind', (options['presind'] || (credit_card.verification_value? ? 1 : nil))
|
528
|
+
end
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
def avs_input_code(address)
|
533
|
+
address.values_at(:zip, :address1).map{ |v| extract_digits(v) }.join('|')
|
534
|
+
end
|
535
|
+
|
536
|
+
def extract_digits(string)
|
537
|
+
return "" if string.nil?
|
538
|
+
string.gsub(/[\D]/,'')
|
539
|
+
end
|
540
|
+
|
541
|
+
def stringify_values(values)
|
542
|
+
string = ""
|
543
|
+
values.each do |val|
|
544
|
+
string << "#{val}"
|
545
|
+
string << "." unless val.equal?(values.last)
|
546
|
+
end
|
547
|
+
string
|
548
|
+
end
|
549
|
+
|
550
|
+
def add_signed_digest(xml, *values)
|
551
|
+
string = stringify_values(values)
|
552
|
+
xml.tag! 'sha1hash', sha1from(string)
|
553
|
+
end
|
554
|
+
|
555
|
+
def auto_settle_flag(action)
|
556
|
+
action == :authorization ? '0' : '1'
|
557
|
+
end
|
558
|
+
|
559
|
+
def expiry_date(credit_card)
|
560
|
+
"#{format(credit_card.month, :two_digits)}#{format(credit_card.year, :two_digits)}"
|
561
|
+
end
|
562
|
+
|
563
|
+
def sha1from(string)
|
564
|
+
Digest::SHA1.hexdigest("#{Digest::SHA1.hexdigest(string)}.#{@options[:password]}")
|
565
|
+
end
|
566
|
+
|
567
|
+
def normalize(field)
|
568
|
+
case field
|
569
|
+
when "true" then true
|
570
|
+
when "false" then false
|
571
|
+
when "" then nil
|
572
|
+
when "null" then nil
|
573
|
+
else field
|
574
|
+
end
|
575
|
+
end
|
576
|
+
|
577
|
+
def message_from(response)
|
578
|
+
message = nil
|
579
|
+
case response[:result]
|
580
|
+
when "00"
|
581
|
+
message = SUCCESS
|
582
|
+
when "101"
|
583
|
+
message = response[:message]
|
584
|
+
when "102", "103"
|
585
|
+
message = DECLINED
|
586
|
+
when /^2[0-9][0-9]/
|
587
|
+
message = BANK_ERROR
|
588
|
+
when /^3[0-9][0-9]/
|
589
|
+
message = REALEX_ERROR
|
590
|
+
when /^5[0-9][0-9]/
|
591
|
+
message = response[:message]
|
592
|
+
when "600", "601", "603"
|
593
|
+
message = ERROR
|
594
|
+
when "666"
|
595
|
+
message = CLIENT_DEACTIVATED
|
596
|
+
else
|
597
|
+
message = DECLINED
|
598
|
+
end
|
599
|
+
end
|
600
|
+
|
601
|
+
def sanitize_order_id(order_id)
|
602
|
+
order_id.to_s.gsub(/[^a-zA-Z0-9\-_]/, '')
|
603
|
+
end
|
604
|
+
|
605
|
+
def self.timestamp
|
606
|
+
Time.now.strftime('%Y%m%d%H%M%S')
|
607
|
+
end
|
608
|
+
end
|
609
|
+
end
|
610
|
+
end
|