activemerchant-redsys_rest 0.9.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.
- checksums.yaml +7 -0
- data/Gemfile +10 -0
- data/README.md +29 -0
- data/Rakefile +12 -0
- data/active_merchant-redsys_rest.gemspec +36 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/active_merchant/billing/gateways/redsys_rest.rb +573 -0
- data/lib/active_merchant/redsys_rest/version.rb +7 -0
- data/lib/active_merchant/redsys_rest.rb +6 -0
- data/lib/activemerchant/redsys_rest.rb +1 -0
- metadata +111 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 110fd25da7062da36667197af8f5de733279bd0892b39f08e4a260e39bc3ef3d
|
|
4
|
+
data.tar.gz: ed30518eb6e8b4f8741467d595f8a3943f3733341dc5891d0216e844a9ad3f9c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e18d1823112b3483a204619875de399681c7467b9b83dc335d26f18fbf41e1d82019bd39847d344a94f5abac2f9faea98e535908f8e82fd0c6ce8c8eed06ecd5
|
|
7
|
+
data.tar.gz: 72512daa1fcc48638284b2df09e7ad0fb1ac399b77339e23e9b4638ea2e6fcf77782b8173357123bea14788ade486335c1b61e92a9abb864429445d03f596e7d
|
data/Gemfile
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# ActiveMerchant::RedsysRest
|
|
2
|
+
|
|
3
|
+
Active Merchant extension to support Redsys payment gateway.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'activemerchant-redsys_rest'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
$ bundle install
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
TODO: Write usage instructions here
|
|
20
|
+
|
|
21
|
+
## Development
|
|
22
|
+
|
|
23
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
24
|
+
|
|
25
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
26
|
+
|
|
27
|
+
## Contributing
|
|
28
|
+
|
|
29
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/varyonic/activemerchant-redsys_rest.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/active_merchant/redsys_rest/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "activemerchant-redsys_rest"
|
|
7
|
+
spec.version = ActiveMerchant::RedsysRest::VERSION
|
|
8
|
+
spec.authors = ["Piers Chambers"]
|
|
9
|
+
spec.email = ["piers@varyonic.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = %q{Active Merchant extension to support Redsys payment gateway}
|
|
12
|
+
spec.homepage = "https://github.com/varyonic/activemerchant-redsys_rest"
|
|
13
|
+
spec.required_ruby_version = ">= 2.6.0"
|
|
14
|
+
|
|
15
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
16
|
+
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
19
|
+
|
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
22
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
23
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
24
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
spec.bindir = "exe"
|
|
28
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
29
|
+
spec.require_paths = ["lib"]
|
|
30
|
+
|
|
31
|
+
spec.add_dependency 'activemerchant'
|
|
32
|
+
spec.add_dependency 'rexml' # required for ActiveMerchant for Ruby 3
|
|
33
|
+
|
|
34
|
+
spec.add_development_dependency('test-unit', '~> 3')
|
|
35
|
+
spec.add_development_dependency('mocha', '~> 1')
|
|
36
|
+
end
|
data/bin/console
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
require "active_merchant/redsys_rest"
|
|
6
|
+
|
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
9
|
+
|
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
11
|
+
# require "pry"
|
|
12
|
+
# Pry.start
|
|
13
|
+
|
|
14
|
+
require "irb"
|
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
module ActiveMerchant #:nodoc:
|
|
4
|
+
module Billing #:nodoc:
|
|
5
|
+
# = Redsys Merchant Gateway
|
|
6
|
+
#
|
|
7
|
+
# Gateway support for the Spanish "Redsys" payment gateway system. This is
|
|
8
|
+
# used by many banks in Spain and is particularly well supported by
|
|
9
|
+
# Catalunya Caixa's ecommerce department.
|
|
10
|
+
#
|
|
11
|
+
# Redsys requires an order_id be provided with each transaction and it must
|
|
12
|
+
# follow a specific format. The rules are as follows:
|
|
13
|
+
#
|
|
14
|
+
# * First 4 digits must be numerical
|
|
15
|
+
# * Remaining 8 digits may be alphanumeric
|
|
16
|
+
# * Max length: 12
|
|
17
|
+
#
|
|
18
|
+
# If an invalid order_id is provided, we do our best to clean it up.
|
|
19
|
+
#
|
|
20
|
+
# Written by Piers Chambers (Varyonic.com)
|
|
21
|
+
#
|
|
22
|
+
# *** SHA256 Authentication Update ***
|
|
23
|
+
#
|
|
24
|
+
# Redsys has dropped support for the SHA1 authentication method.
|
|
25
|
+
class RedsysRestGateway < Gateway
|
|
26
|
+
self.test_url = 'https://sis-t.redsys.es:25443/sis/rest/%sREST'
|
|
27
|
+
self.live_url = 'https://sis.redsys.es/sis/rest/%sREST'
|
|
28
|
+
|
|
29
|
+
self.supported_countries = ['ES']
|
|
30
|
+
self.default_currency = 'EUR'
|
|
31
|
+
self.money_format = :cents
|
|
32
|
+
# Not all card types may be activated by the bank!
|
|
33
|
+
self.supported_cardtypes = %i[visa master american_express jcb diners_club unionpay]
|
|
34
|
+
self.homepage_url = 'http://www.redsys.es/'
|
|
35
|
+
self.display_name = 'Redsys (REST)'
|
|
36
|
+
|
|
37
|
+
CURRENCY_CODES = {
|
|
38
|
+
'AED' => '784',
|
|
39
|
+
'ARS' => '32',
|
|
40
|
+
'AUD' => '36',
|
|
41
|
+
'BRL' => '986',
|
|
42
|
+
'BOB' => '68',
|
|
43
|
+
'CAD' => '124',
|
|
44
|
+
'CHF' => '756',
|
|
45
|
+
'CLP' => '152',
|
|
46
|
+
'CNY' => '156',
|
|
47
|
+
'COP' => '170',
|
|
48
|
+
'CRC' => '188',
|
|
49
|
+
'CZK' => '203',
|
|
50
|
+
'DKK' => '208',
|
|
51
|
+
'DOP' => '214',
|
|
52
|
+
'EUR' => '978',
|
|
53
|
+
'GBP' => '826',
|
|
54
|
+
'GTQ' => '320',
|
|
55
|
+
'HUF' => '348',
|
|
56
|
+
'IDR' => '360',
|
|
57
|
+
'INR' => '356',
|
|
58
|
+
'JPY' => '392',
|
|
59
|
+
'KRW' => '410',
|
|
60
|
+
'MYR' => '458',
|
|
61
|
+
'MXN' => '484',
|
|
62
|
+
'NOK' => '578',
|
|
63
|
+
'NZD' => '554',
|
|
64
|
+
'PEN' => '604',
|
|
65
|
+
'PLN' => '985',
|
|
66
|
+
'RUB' => '643',
|
|
67
|
+
'SAR' => '682',
|
|
68
|
+
'SEK' => '752',
|
|
69
|
+
'SGD' => '702',
|
|
70
|
+
'THB' => '764',
|
|
71
|
+
'TWD' => '901',
|
|
72
|
+
'USD' => '840',
|
|
73
|
+
'UYU' => '858'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# The set of supported transactions for this gateway.
|
|
77
|
+
# More operations are supported by the gateway itself, but
|
|
78
|
+
# are not supported in this library.
|
|
79
|
+
SUPPORTED_TRANSACTIONS = {
|
|
80
|
+
purchase: '0',
|
|
81
|
+
authorize: '1',
|
|
82
|
+
capture: '2',
|
|
83
|
+
refund: '3',
|
|
84
|
+
cancel: '9'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# These are the text meanings sent back by the acquirer when
|
|
88
|
+
# a card has been rejected. Syntax or general request errors
|
|
89
|
+
# are not covered here.
|
|
90
|
+
RESPONSE_TEXTS = {
|
|
91
|
+
0 => 'Transaction Approved',
|
|
92
|
+
400 => 'Cancellation Accepted',
|
|
93
|
+
481 => 'Cancellation Accepted',
|
|
94
|
+
500 => 'Reconciliation Accepted',
|
|
95
|
+
900 => 'Refund / Confirmation approved',
|
|
96
|
+
|
|
97
|
+
101 => 'Card expired',
|
|
98
|
+
102 => 'Card blocked temporarily or under susciption of fraud',
|
|
99
|
+
104 => 'Transaction not permitted',
|
|
100
|
+
107 => 'Contact the card issuer',
|
|
101
|
+
109 => 'Invalid identification by merchant or POS terminal',
|
|
102
|
+
110 => 'Invalid amount',
|
|
103
|
+
114 => 'Card cannot be used to the requested transaction',
|
|
104
|
+
116 => 'Insufficient credit',
|
|
105
|
+
118 => 'Non-registered card',
|
|
106
|
+
125 => 'Card not effective',
|
|
107
|
+
129 => 'CVV2/CVC2 Error',
|
|
108
|
+
167 => 'Contact the card issuer: suspected fraud',
|
|
109
|
+
180 => 'Card out of service',
|
|
110
|
+
181 => 'Card with credit or debit restrictions',
|
|
111
|
+
182 => 'Card with credit or debit restrictions',
|
|
112
|
+
184 => 'Authentication error',
|
|
113
|
+
190 => 'Refusal with no specific reason',
|
|
114
|
+
191 => 'Expiry date incorrect',
|
|
115
|
+
195 => 'Requires SCA authentication',
|
|
116
|
+
|
|
117
|
+
201 => 'Card expired',
|
|
118
|
+
202 => 'Card blocked temporarily or under suspicion of fraud',
|
|
119
|
+
204 => 'Transaction not permitted',
|
|
120
|
+
207 => 'Contact the card issuer',
|
|
121
|
+
208 => 'Lost or stolen card',
|
|
122
|
+
209 => 'Lost or stolen card',
|
|
123
|
+
280 => 'CVV2/CVC2 Error',
|
|
124
|
+
290 => 'Declined with no specific reason',
|
|
125
|
+
|
|
126
|
+
480 => 'Original transaction not located, or time-out exceeded',
|
|
127
|
+
501 => 'Original transaction not located, or time-out exceeded',
|
|
128
|
+
502 => 'Original transaction not located, or time-out exceeded',
|
|
129
|
+
503 => 'Original transaction not located, or time-out exceeded',
|
|
130
|
+
|
|
131
|
+
904 => 'Merchant not registered at FUC',
|
|
132
|
+
909 => 'System error',
|
|
133
|
+
912 => 'Issuer not available',
|
|
134
|
+
913 => 'Duplicate transmission',
|
|
135
|
+
916 => 'Amount too low',
|
|
136
|
+
928 => 'Time-out exceeded',
|
|
137
|
+
940 => 'Transaction cancelled previously',
|
|
138
|
+
941 => 'Authorization operation already cancelled',
|
|
139
|
+
942 => 'Original authorization declined',
|
|
140
|
+
943 => 'Different details from origin transaction',
|
|
141
|
+
944 => 'Session error',
|
|
142
|
+
945 => 'Duplicate transmission',
|
|
143
|
+
946 => 'Cancellation of transaction while in progress',
|
|
144
|
+
947 => 'Duplicate tranmission while in progress',
|
|
145
|
+
949 => 'POS Inoperative',
|
|
146
|
+
950 => 'Refund not possible',
|
|
147
|
+
9064 => 'Card number incorrect',
|
|
148
|
+
9078 => 'No payment method available',
|
|
149
|
+
9093 => 'Non-existent card',
|
|
150
|
+
9218 => 'Recursive transaction in bad gateway',
|
|
151
|
+
9253 => 'Check-digit incorrect',
|
|
152
|
+
9256 => 'Preauth not allowed for merchant',
|
|
153
|
+
9257 => 'Preauth not allowed for card',
|
|
154
|
+
9261 => 'Operating limit exceeded',
|
|
155
|
+
9912 => 'Issuer not available',
|
|
156
|
+
9913 => 'Confirmation error',
|
|
157
|
+
9914 => 'KO Confirmation'
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# Expected values as per documentation
|
|
161
|
+
THREE_DS_V1 = '1.0.2'
|
|
162
|
+
THREE_DS_V2 = '2.1.0'
|
|
163
|
+
|
|
164
|
+
# Creates a new instance
|
|
165
|
+
#
|
|
166
|
+
# Redsys requires a login and secret_key, and optionally also accepts a
|
|
167
|
+
# non-default terminal.
|
|
168
|
+
#
|
|
169
|
+
# ==== Options
|
|
170
|
+
#
|
|
171
|
+
# * <tt>:login</tt> -- The Redsys Merchant ID (REQUIRED)
|
|
172
|
+
# * <tt>:secret_key</tt> -- The Redsys Secret Key. (REQUIRED)
|
|
173
|
+
# * <tt>:terminal</tt> -- The Redsys Terminal. Defaults to 1. (OPTIONAL)
|
|
174
|
+
# * <tt>:test</tt> -- +true+ or +false+. Defaults to +false+. (OPTIONAL)
|
|
175
|
+
def initialize(options = {})
|
|
176
|
+
requires!(options, :login, :secret_key)
|
|
177
|
+
options[:terminal] ||= 1
|
|
178
|
+
options[:signature_algorithm] = 'sha256'
|
|
179
|
+
super
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def purchase(money, payment, options = {})
|
|
183
|
+
requires!(options, :order_id)
|
|
184
|
+
|
|
185
|
+
data = {}
|
|
186
|
+
add_action(data, :purchase, options)
|
|
187
|
+
add_amount(data, money, options)
|
|
188
|
+
add_order(data, options[:order_id])
|
|
189
|
+
add_payment(data, payment)
|
|
190
|
+
add_external_mpi_fields(data, options)
|
|
191
|
+
add_threeds(data, options)
|
|
192
|
+
add_stored_credential_options(data, options)
|
|
193
|
+
data[:description] = options[:description]
|
|
194
|
+
data[:store_in_vault] = options[:store]
|
|
195
|
+
data[:sca_exemption] = options[:sca_exemption]
|
|
196
|
+
data[:sca_exemption_direct_payment_enabled] = options[:sca_exemption_direct_payment_enabled]
|
|
197
|
+
|
|
198
|
+
commit data, options
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def authorize(money, payment, options = {})
|
|
202
|
+
requires!(options, :order_id)
|
|
203
|
+
|
|
204
|
+
data = {}
|
|
205
|
+
add_action(data, :authorize, options)
|
|
206
|
+
add_amount(data, money, options)
|
|
207
|
+
add_order(data, options[:order_id])
|
|
208
|
+
add_payment(data, payment)
|
|
209
|
+
add_external_mpi_fields(data, options)
|
|
210
|
+
add_threeds(data, options)
|
|
211
|
+
add_stored_credential_options(data, options)
|
|
212
|
+
data[:description] = options[:description]
|
|
213
|
+
data[:store_in_vault] = options[:store]
|
|
214
|
+
data[:sca_exemption] = options[:sca_exemption]
|
|
215
|
+
data[:sca_exemption_direct_payment_enabled] = options[:sca_exemption_direct_payment_enabled]
|
|
216
|
+
|
|
217
|
+
commit data, options
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def capture(money, authorization, options = {})
|
|
221
|
+
requires!(options, :order_id)
|
|
222
|
+
|
|
223
|
+
data = {}
|
|
224
|
+
add_action(data, :capture)
|
|
225
|
+
add_amount(data, money, options)
|
|
226
|
+
order_id, = split_authorization(authorization)
|
|
227
|
+
add_order(data, order_id)
|
|
228
|
+
data[:description] = options[:description]
|
|
229
|
+
|
|
230
|
+
commit data, options
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def void(authorization, options = {})
|
|
234
|
+
requires!(options, :order_id)
|
|
235
|
+
|
|
236
|
+
data = {}
|
|
237
|
+
add_action(data, :cancel)
|
|
238
|
+
order_id, amount, currency = split_authorization(authorization)
|
|
239
|
+
add_amount(data, amount, currency: currency)
|
|
240
|
+
add_order(data, order_id)
|
|
241
|
+
data[:description] = options[:description]
|
|
242
|
+
|
|
243
|
+
commit data, options
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def refund(money, authorization, options = {})
|
|
247
|
+
requires!(options, :order_id)
|
|
248
|
+
|
|
249
|
+
data = {}
|
|
250
|
+
add_action(data, :refund)
|
|
251
|
+
add_amount(data, money, options)
|
|
252
|
+
order_id, = split_authorization(authorization)
|
|
253
|
+
add_order(data, order_id)
|
|
254
|
+
data[:description] = options[:description]
|
|
255
|
+
|
|
256
|
+
commit data, options
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def verify(creditcard, options = {})
|
|
260
|
+
requires!(options, :order_id)
|
|
261
|
+
|
|
262
|
+
MultiResponse.run(:use_first_response) do |r|
|
|
263
|
+
r.process { authorize(100, creditcard, options) }
|
|
264
|
+
r.process(:ignore_result) { void(r.authorization, options) }
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def supports_scrubbing?
|
|
269
|
+
true
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def scrub(transcript)
|
|
273
|
+
transcript.
|
|
274
|
+
gsub(%r((PAN\"=>\")(\d+)), '\1[FILTERED]').
|
|
275
|
+
gsub(%r((CVV2\"=>\")(\d+)), '\1[FILTERED]')
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
private
|
|
279
|
+
|
|
280
|
+
def add_action(data, action, options = {})
|
|
281
|
+
data[:action] = transaction_code(action)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def add_amount(data, money, options)
|
|
285
|
+
data[:amount] = amount(money).to_s
|
|
286
|
+
data[:currency] = currency_code(options[:currency] || currency(money))
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def add_order(data, order_id)
|
|
290
|
+
data[:order_id] = clean_order_id(order_id)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def add_payment(data, card)
|
|
294
|
+
if card.is_a?(String)
|
|
295
|
+
data[:credit_card_token] = card
|
|
296
|
+
else
|
|
297
|
+
name = [card.first_name, card.last_name].join(' ').slice(0, 60)
|
|
298
|
+
year = sprintf('%.4i', card.year)
|
|
299
|
+
month = sprintf('%.2i', card.month)
|
|
300
|
+
data[:card] = {
|
|
301
|
+
name: name,
|
|
302
|
+
pan: card.number,
|
|
303
|
+
date: "#{year[2..3]}#{month}",
|
|
304
|
+
cvv: card.verification_value
|
|
305
|
+
}
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def add_external_mpi_fields(data, options)
|
|
310
|
+
return unless options[:three_d_secure]
|
|
311
|
+
|
|
312
|
+
if options[:three_d_secure][:version] == THREE_DS_V2
|
|
313
|
+
data[:threeDSServerTransID] = options[:three_d_secure][:three_ds_server_trans_id] if options[:three_d_secure][:three_ds_server_trans_id]
|
|
314
|
+
data[:dsTransID] = options[:three_d_secure][:ds_transaction_id] if options[:three_d_secure][:ds_transaction_id]
|
|
315
|
+
data[:authenticacionValue] = options[:three_d_secure][:cavv] if options[:three_d_secure][:cavv]
|
|
316
|
+
data[:protocolVersion] = options[:three_d_secure][:version] if options[:three_d_secure][:version]
|
|
317
|
+
data[:authenticacionMethod] = options[:authentication_method] if options[:authentication_method]
|
|
318
|
+
data[:authenticacionType] = options[:authentication_type] if options[:authentication_type]
|
|
319
|
+
data[:authenticacionFlow] = options[:authentication_flow] if options[:authentication_flow]
|
|
320
|
+
data[:eci_v2] = options[:three_d_secure][:eci] if options[:three_d_secure][:eci]
|
|
321
|
+
elsif options[:three_d_secure][:version] == THREE_DS_V1
|
|
322
|
+
data[:txid] = options[:three_d_secure][:xid] if options[:three_d_secure][:xid]
|
|
323
|
+
data[:cavv] = options[:three_d_secure][:cavv] if options[:three_d_secure][:cavv]
|
|
324
|
+
data[:eci_v1] = options[:three_d_secure][:eci] if options[:three_d_secure][:eci]
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def add_stored_credential_options(data, options)
|
|
329
|
+
return unless options[:stored_credential]
|
|
330
|
+
|
|
331
|
+
case options[:stored_credential][:initial_transaction]
|
|
332
|
+
when true
|
|
333
|
+
data[:DS_MERCHANT_COF_INI] = 'S'
|
|
334
|
+
when false
|
|
335
|
+
data[:DS_MERCHANT_COF_INI] = 'N'
|
|
336
|
+
data[:DS_MERCHANT_COF_TXNID] = options[:stored_credential][:network_transaction_id] if options[:stored_credential][:network_transaction_id]
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
case options[:stored_credential][:reason_type]
|
|
340
|
+
when 'recurring'
|
|
341
|
+
data[:DS_MERCHANT_COF_TYPE] = 'R'
|
|
342
|
+
when 'installment'
|
|
343
|
+
data[:DS_MERCHANT_COF_TYPE] = 'I'
|
|
344
|
+
when 'unscheduled'
|
|
345
|
+
return
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def add_threeds(data, options)
|
|
350
|
+
options[:threeds] = { threeDSInfo: 'CardData' } if options[:execute_threed]
|
|
351
|
+
data[:threeds] = options[:threeds] if options[:threeds]
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def determine_3ds_action(threeds_hash)
|
|
355
|
+
return 'trataPeticion' if threeds_hash.nil?
|
|
356
|
+
return 'iniciaPeticion' if threeds_hash[:threeDSInfo] == 'CardData'
|
|
357
|
+
return 'trataPeticion' if threeds_hash[:threeDSInfo] == 'AuthenticationData' ||
|
|
358
|
+
threeds_hash[:threeDSInfo] == 'ChallengeResponse'
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def commit(data, options)
|
|
362
|
+
url = (test? ? test_url : live_url)
|
|
363
|
+
action = determine_3ds_action(data[:threeds])
|
|
364
|
+
parse(ssl_post(url % action, post_data(data, options)))
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def post_data(data, options)
|
|
368
|
+
merchant_parameters = build_merchant_data(Hash.new, data, options)
|
|
369
|
+
merchant_parameters.transform_values! { |v| v.to_s }
|
|
370
|
+
logger.info "merchant_parameters: #{merchant_parameters}" if ENV['DEBUG_ACTIVE_MERCHANT']
|
|
371
|
+
encoded_parameters = Base64.strict_encode64(merchant_parameters.to_json)
|
|
372
|
+
|
|
373
|
+
post_data = PostData.new
|
|
374
|
+
post_data['Ds_SignatureVersion'] = 'HMAC_SHA256_V1'
|
|
375
|
+
post_data['Ds_MerchantParameters'] = encoded_parameters
|
|
376
|
+
post_data['Ds_Signature'] = sign_request(encoded_parameters, data[:order_id])
|
|
377
|
+
post_data.to_post_data
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Template Method to allow AM API clients to override decision to escape, based on their own criteria.
|
|
381
|
+
def escape_special_chars?(data, options = {})
|
|
382
|
+
data[:threeds]
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def build_merchant_data(merchant_data, data, options = {})
|
|
386
|
+
merchant_data.tap do |post|
|
|
387
|
+
# Basic elements
|
|
388
|
+
post['DS_MERCHANT_CURRENCY'] = data[:currency]
|
|
389
|
+
post['DS_MERCHANT_AMOUNT'] = data[:amount]
|
|
390
|
+
post['DS_MERCHANT_ORDER'] = data[:order_id]
|
|
391
|
+
post['DS_MERCHANT_TRANSACTIONTYPE'] = data[:action]
|
|
392
|
+
if data[:description] && escape_special_chars?(data, options)
|
|
393
|
+
post['DS_MERCHANT_PRODUCTDESCRIPTION'] = CGI.escape(data[:description])
|
|
394
|
+
else
|
|
395
|
+
post['DS_MERCHANT_PRODUCTDESCRIPTION'] = data[:description]
|
|
396
|
+
end
|
|
397
|
+
post['DS_MERCHANT_TERMINAL'] = options[:terminal] || @options[:terminal]
|
|
398
|
+
post['DS_MERCHANT_MERCHANTCODE'] = @options[:login]
|
|
399
|
+
|
|
400
|
+
action = determine_3ds_action(data[:threeds]) if data[:threeds]
|
|
401
|
+
if action == 'iniciaPeticion' && data[:sca_exemption]
|
|
402
|
+
post['DS_MERCHANT_EXCEP_SCA'] = 'Y'
|
|
403
|
+
else
|
|
404
|
+
post['DS_MERCHANT_EXCEP_SCA'] = data[:sca_exemption] if data[:sca_exemption]
|
|
405
|
+
post['DS_MERCHANT_DIRECTPAYMENT'] = data[:sca_exemption_direct_payment_enabled] if data[:sca_exemption_direct_payment_enabled]
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Only when card is present
|
|
409
|
+
if data[:card]
|
|
410
|
+
if data[:card][:name] && escape_special_chars?(data, options)
|
|
411
|
+
post['DS_MERCHANT_TITULAR'] = CGI.escape(data[:card][:name])
|
|
412
|
+
else
|
|
413
|
+
post['DS_MERCHANT_TITULAR'] = data[:card][:name]
|
|
414
|
+
end
|
|
415
|
+
post['DS_MERCHANT_PAN'] = data[:card][:pan]
|
|
416
|
+
post['DS_MERCHANT_EXPIRYDATE'] = data[:card][:date]
|
|
417
|
+
post['DS_MERCHANT_CVV2'] = data[:card][:cvv]
|
|
418
|
+
post['DS_MERCHANT_IDENTIFIER'] = 'REQUIRED' if data[:store_in_vault]
|
|
419
|
+
|
|
420
|
+
build_merchant_mpi_external(post, data)
|
|
421
|
+
|
|
422
|
+
elsif data[:credit_card_token]
|
|
423
|
+
post['DS_MERCHANT_IDENTIFIER'] = data[:credit_card_token]
|
|
424
|
+
post['DS_MERCHANT_DIRECTPAYMENT'] = 'true'
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Set moto flag only if explicitly requested via moto field
|
|
428
|
+
# Requires account configuration to be able to use
|
|
429
|
+
post['DS_MERCHANT_DIRECTPAYMENT'] = 'moto' if options.dig(:moto) && options.dig(:metadata, :manual_entry)
|
|
430
|
+
|
|
431
|
+
post['DS_MERCHANT_EMV3DS'] = data[:threeds].to_json if data[:threeds]
|
|
432
|
+
|
|
433
|
+
if options[:stored_credential]
|
|
434
|
+
post['DS_MERCHANT_COF_INI'] = data[:DS_MERCHANT_COF_INI]
|
|
435
|
+
post['DS_MERCHANT_COF_TYPE'] = data[:DS_MERCHANT_COF_TYPE]
|
|
436
|
+
post['DS_MERCHANT_COF_TXNID'] = data[:DS_MERCHANT_COF_TXNID] if data[:DS_MERCHANT_COF_TXNID]
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def build_merchant_mpi_external(post, data)
|
|
442
|
+
return unless data[:txid] || data[:threeDSServerTransID]
|
|
443
|
+
|
|
444
|
+
ds_merchant_mpi_external = {}
|
|
445
|
+
ds_merchant_mpi_external[:TXID] = data[:txid] if data[:txid]
|
|
446
|
+
ds_merchant_mpi_external[:CAVV] = data[:cavv] if data[:cavv]
|
|
447
|
+
ds_merchant_mpi_external[:ECI] = data[:eci_v1] if data[:eci_v1]
|
|
448
|
+
|
|
449
|
+
ds_merchant_mpi_external[:threeDSServerTransID] = data[:threeDSServerTransID] if data[:threeDSServerTransID]
|
|
450
|
+
ds_merchant_mpi_external[:dsTransID] = data[:dsTransID] if data[:dsTransID]
|
|
451
|
+
ds_merchant_mpi_external[:authenticacionValue] = data[:authenticacionValue] if data[:authenticacionValue]
|
|
452
|
+
ds_merchant_mpi_external[:protocolVersion] = data[:protocolVersion] if data[:protocolVersion]
|
|
453
|
+
ds_merchant_mpi_external[:Eci] = data[:eci_v2] if data[:eci_v2]
|
|
454
|
+
ds_merchant_mpi_external[:authenticacionMethod] = data[:authenticacionMethod] if data[:authenticacionMethod]
|
|
455
|
+
ds_merchant_mpi_external[:authenticacionType] = data[:authenticacionType] if data[:authenticacionType]
|
|
456
|
+
ds_merchant_mpi_external[:authenticacionFlow] = data[:authenticacionFlow] if data[:authenticacionFlow]
|
|
457
|
+
|
|
458
|
+
post['DS_MERCHANT_MPIEXTERNAL'] = ds_merchant_mpi_external.to_json unless ds_merchant_mpi_external.empty?
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def parse(body)
|
|
462
|
+
params = {}
|
|
463
|
+
success = false
|
|
464
|
+
message = ''
|
|
465
|
+
options = @options.merge(test: test?)
|
|
466
|
+
|
|
467
|
+
json = JSON.parse(body)
|
|
468
|
+
base64_payload = json['Ds_MerchantParameters']
|
|
469
|
+
signature = json['Ds_Signature']
|
|
470
|
+
|
|
471
|
+
if base64_payload
|
|
472
|
+
payload = Base64.decode64(base64_payload)
|
|
473
|
+
params = JSON.parse(payload).transform_keys!(&:downcase).with_indifferent_access
|
|
474
|
+
logger.info "response params: #{params}" if ENV['DEBUG_ACTIVE_MERCHANT']
|
|
475
|
+
|
|
476
|
+
if validate_signature(base64_payload, signature, params[:ds_order])
|
|
477
|
+
if params[:ds_response]
|
|
478
|
+
message = response_text(params[:ds_response])
|
|
479
|
+
options[:authorization] = build_authorization(params)
|
|
480
|
+
success = success_response?(params[:ds_response])
|
|
481
|
+
elsif params[:ds_emv3ds]
|
|
482
|
+
message = response_text_3ds(params)
|
|
483
|
+
params[:ds_emv3ds] = params[:ds_emv3ds].to_json
|
|
484
|
+
options[:authorization] = build_authorization(params)
|
|
485
|
+
success = params.size > 0 && success_response?(params[:ds_response])
|
|
486
|
+
else
|
|
487
|
+
message = 'Unexpected response'
|
|
488
|
+
end
|
|
489
|
+
else
|
|
490
|
+
message = 'Response failed validation check'
|
|
491
|
+
end
|
|
492
|
+
else
|
|
493
|
+
message = "#{json['errorCode']} ERROR"
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
Response.new(success, message, params, options)
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def validate_signature(data, signature, order_number)
|
|
500
|
+
key = encrypt(@options[:secret_key], order_number)
|
|
501
|
+
Base64.urlsafe_encode64(mac256(key, data)) == signature
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def build_authorization(params)
|
|
505
|
+
[params[:ds_order], params[:ds_amount], params[:ds_currency]].join('|')
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def split_authorization(authorization)
|
|
509
|
+
order_id, amount, currency = authorization.split('|')
|
|
510
|
+
[order_id, amount.to_i, currency]
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def currency_code(currency)
|
|
514
|
+
return currency if currency =~ /^\d+$/
|
|
515
|
+
raise ArgumentError, "Unknown currency #{currency}" unless CURRENCY_CODES[currency]
|
|
516
|
+
|
|
517
|
+
CURRENCY_CODES[currency]
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def transaction_code(type)
|
|
521
|
+
SUPPORTED_TRANSACTIONS[type]
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def response_text(code)
|
|
525
|
+
code = code.to_i
|
|
526
|
+
code = 0 if code < 100
|
|
527
|
+
RESPONSE_TEXTS[code] || 'Unknown code, please check in manual'
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def response_text_3ds(params)
|
|
531
|
+
params[:ds_emv3ds]['threeDSInfo']
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def success_response?(code)
|
|
535
|
+
(code.to_i < 100) || [400, 481, 500, 900].include?(code.to_i)
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def clean_order_id(order_id)
|
|
539
|
+
cleansed = order_id.gsub(/[^\da-zA-Z]/, '')
|
|
540
|
+
if /^\d{4}/.match?(cleansed)
|
|
541
|
+
cleansed[0..11]
|
|
542
|
+
else
|
|
543
|
+
'%04d%s' % [rand(0..9999), cleansed[0...8]]
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def sign_request(encoded_parameters, order_id)
|
|
548
|
+
raise(ArgumentError, 'missing order_id') unless order_id
|
|
549
|
+
key = encrypt(@options[:secret_key], order_id)
|
|
550
|
+
Base64.strict_encode64(mac256(key, encoded_parameters))
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def encrypt(key, order_id)
|
|
554
|
+
block_length = 8
|
|
555
|
+
cipher = OpenSSL::Cipher.new('DES3')
|
|
556
|
+
cipher.encrypt
|
|
557
|
+
|
|
558
|
+
cipher.key = Base64.strict_decode64(key)
|
|
559
|
+
# The OpenSSL default of an all-zeroes ("\\0") IV is used.
|
|
560
|
+
cipher.padding = 0
|
|
561
|
+
|
|
562
|
+
order_id += "\0" until order_id.bytesize % block_length == 0 # Pad with zeros
|
|
563
|
+
|
|
564
|
+
output = cipher.update(order_id) + cipher.final
|
|
565
|
+
output
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def mac256(key, data)
|
|
569
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, data)
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require 'active_merchant/redsys_rest'
|
metadata
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: activemerchant-redsys_rest
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.9.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Piers Chambers
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2022-05-03 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activemerchant
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rexml
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: test-unit
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: mocha
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '1'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1'
|
|
69
|
+
description:
|
|
70
|
+
email:
|
|
71
|
+
- piers@varyonic.com
|
|
72
|
+
executables: []
|
|
73
|
+
extensions: []
|
|
74
|
+
extra_rdoc_files: []
|
|
75
|
+
files:
|
|
76
|
+
- Gemfile
|
|
77
|
+
- README.md
|
|
78
|
+
- Rakefile
|
|
79
|
+
- active_merchant-redsys_rest.gemspec
|
|
80
|
+
- bin/console
|
|
81
|
+
- bin/setup
|
|
82
|
+
- lib/active_merchant/billing/gateways/redsys_rest.rb
|
|
83
|
+
- lib/active_merchant/redsys_rest.rb
|
|
84
|
+
- lib/active_merchant/redsys_rest/version.rb
|
|
85
|
+
- lib/activemerchant/redsys_rest.rb
|
|
86
|
+
homepage: https://github.com/varyonic/activemerchant-redsys_rest
|
|
87
|
+
licenses: []
|
|
88
|
+
metadata:
|
|
89
|
+
allowed_push_host: https://rubygems.org
|
|
90
|
+
homepage_uri: https://github.com/varyonic/activemerchant-redsys_rest
|
|
91
|
+
source_code_uri: https://github.com/varyonic/activemerchant-redsys_rest
|
|
92
|
+
post_install_message:
|
|
93
|
+
rdoc_options: []
|
|
94
|
+
require_paths:
|
|
95
|
+
- lib
|
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - ">="
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: 2.6.0
|
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
102
|
+
requirements:
|
|
103
|
+
- - ">="
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: '0'
|
|
106
|
+
requirements: []
|
|
107
|
+
rubygems_version: 3.2.15
|
|
108
|
+
signing_key:
|
|
109
|
+
specification_version: 4
|
|
110
|
+
summary: Active Merchant extension to support Redsys payment gateway
|
|
111
|
+
test_files: []
|