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 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: []