activemerchant-redsys_rest 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in active_merchant-redsys_rest.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "minitest", "~> 5.0"
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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ task default: :test
@@ -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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveMerchant
4
+ module RedsysRest
5
+ VERSION = "0.9.0"
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_merchant"
4
+ require_relative "redsys_rest/version"
5
+
6
+ require "active_merchant/billing/gateways/redsys_rest"
@@ -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: []