lib/active_merchant/billing/gateways/redsys.rb in activemerchant-1.98.0 vs lib/active_merchant/billing/gateways/redsys.rb in activemerchant-1.99.0
- old
+ new
@@ -191,65 +191,69 @@
def purchase(money, payment, options = {})
requires!(options, :order_id)
data = {}
- add_action(data, :purchase)
+ add_action(data, :purchase, options)
add_amount(data, money, options)
add_order(data, options[:order_id])
add_payment(data, payment)
+ add_threeds(data, options) if options[:execute_threed]
data[:description] = options[:description]
data[:store_in_vault] = options[:store]
+ data[:sca_exemption] = options[:sca_exemption]
- commit data
+ commit data, options
end
def authorize(money, payment, options = {})
requires!(options, :order_id)
data = {}
- add_action(data, :authorize)
+ add_action(data, :authorize, options)
add_amount(data, money, options)
add_order(data, options[:order_id])
add_payment(data, payment)
+ add_threeds(data, options) if options[:execute_threed]
data[:description] = options[:description]
data[:store_in_vault] = options[:store]
+ data[:sca_exemption] = options[:sca_exemption]
- commit data
+ commit data, options
end
def capture(money, authorization, options = {})
data = {}
add_action(data, :capture)
add_amount(data, money, options)
order_id, _, _ = split_authorization(authorization)
add_order(data, order_id)
data[:description] = options[:description]
- commit data
+ commit data, options
end
def void(authorization, options = {})
data = {}
add_action(data, :cancel)
order_id, amount, currency = split_authorization(authorization)
add_amount(data, amount, :currency => currency)
add_order(data, order_id)
data[:description] = options[:description]
- commit data
+ commit data, options
end
def refund(money, authorization, options = {})
data = {}
add_action(data, :refund)
add_amount(data, money, options)
order_id, _, _ = split_authorization(authorization)
add_order(data, order_id)
data[:description] = options[:description]
- commit data
+ commit data, options
end
def verify(creditcard, options = {})
MultiResponse.run(:use_first_response) do |r|
r.process { authorize(100, creditcard, options) }
@@ -276,12 +280,12 @@
gsub(%r((<DS_MERCHANT_CVV2>)\s+(</DS_MERCHANT_CVV2>))i, '\1[BLANK]\2')
end
private
- def add_action(data, action)
- data[:action] = transaction_code(action)
+ def add_action(data, action, options = {})
+ data[:action] = options[:execute_threed].present? ? '0' : transaction_code(action)
end
def add_amount(data, money, options)
data[:amount] = amount(money).to_s
data[:currency] = currency_code(options[:currency] || currency(money))
@@ -293,10 +297,14 @@
def url
test? ? test_url : live_url
end
+ def threeds_url
+ test? ? 'https://sis-t.redsys.es:25443/sis/services/SerClsWSEntradaV2': 'https://sis.redsys.es/sis/services/SerClsWSEntradaV2'
+ end
+
def add_payment(data, card)
if card.is_a?(String)
data[:credit_card_token] = card
else
name = [card.first_name, card.last_name].join(' ').slice(0, 60)
@@ -309,25 +317,61 @@
:cvv => card.verification_value
}
end
end
- def commit(data)
- parse(ssl_post(url, "entrada=#{CGI.escape(xml_request_from(data))}", headers))
+ def add_threeds(data, options)
+ if options[:execute_threed] == true
+ data[:threeds] = {threeDSInfo: 'CardData'}
+ end
end
- def headers
- {
- 'Content-Type' => 'application/x-www-form-urlencoded'
- }
+ def determine_3ds_action(threeds_hash)
+ return 'iniciaPeticion' if threeds_hash[:threeDSInfo] == 'CardData'
+ return 'trataPeticion' if threeds_hash[:threeDSInfo] == 'AuthenticationData' ||
+ threeds_hash[:threeDSInfo] == 'ChallengeResponse'
end
- def xml_request_from(data)
+ def commit(data, options = {})
+ if data[:threeds]
+ action = determine_3ds_action(data[:threeds])
+ request = <<-EOS
+ <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:apachesoap="http://xml.apache.org/xml-soap" xmlns:impl="http://webservice.sis.sermepa.es" xmlns:intf="http://webservice.sis.sermepa.es" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" >
+ <soapenv:Header/>
+ <soapenv:Body>
+ <intf:#{action} xmlns:intf="http://webservice.sis.sermepa.es">
+ <intf:datoEntrada>
+ <![CDATA[#{xml_request_from(data, options)}]]>
+ </intf:datoEntrada>
+ </intf:#{action}>
+ </soapenv:Body>
+ </soapenv:Envelope>
+ EOS
+ parse(ssl_post(threeds_url, request, headers(action)), action)
+ else
+ parse(ssl_post(url, "entrada=#{CGI.escape(xml_request_from(data, options))}", headers), action)
+ end
+ end
+
+ def headers(action=nil)
+ if action
+ {
+ 'Content-Type' => 'text/xml',
+ 'SOAPAction' => action
+ }
+ else
+ {
+ 'Content-Type' => 'application/x-www-form-urlencoded'
+ }
+ end
+ end
+
+ def xml_request_from(data, options = {})
if sha256_authentication?
- build_sha256_xml_request(data)
+ build_sha256_xml_request(data, options)
else
- build_sha1_xml_request(data)
+ build_sha1_xml_request(data, options)
end
end
def build_signature(data)
str = data[:amount] +
@@ -349,45 +393,46 @@
str << @options[:secret_key]
Digest::SHA1.hexdigest(str)
end
- def build_sha256_xml_request(data)
+ def build_sha256_xml_request(data, options = {})
xml = Builder::XmlMarkup.new
xml.instruct!
xml.REQUEST do
- build_merchant_data(xml, data)
+ build_merchant_data(xml, data, options)
xml.DS_SIGNATUREVERSION 'HMAC_SHA256_V1'
- xml.DS_SIGNATURE sign_request(merchant_data_xml(data), data[:order_id])
+ xml.DS_SIGNATURE sign_request(merchant_data_xml(data, options), data[:order_id])
end
xml.target!
end
- def build_sha1_xml_request(data)
+ def build_sha1_xml_request(data, options = {})
xml = Builder::XmlMarkup.new :indent => 2
- build_merchant_data(xml, data)
+ build_merchant_data(xml, data, options)
xml.target!
end
- def merchant_data_xml(data)
+ def merchant_data_xml(data, options = {})
xml = Builder::XmlMarkup.new
- build_merchant_data(xml, data)
+ build_merchant_data(xml, data, options)
xml.target!
end
- def build_merchant_data(xml, data)
+ def build_merchant_data(xml, data, options = {})
xml.DATOSENTRADA do
# Basic elements
xml.DS_Version 0.1
xml.DS_MERCHANT_CURRENCY data[:currency]
xml.DS_MERCHANT_AMOUNT data[:amount]
xml.DS_MERCHANT_ORDER data[:order_id]
xml.DS_MERCHANT_TRANSACTIONTYPE data[:action]
xml.DS_MERCHANT_PRODUCTDESCRIPTION data[:description]
- xml.DS_MERCHANT_TERMINAL @options[:terminal]
+ xml.DS_MERCHANT_TERMINAL options[:terminal] || @options[:terminal]
xml.DS_MERCHANT_MERCHANTCODE @options[:login]
xml.DS_MERCHANT_MERCHANTSIGNATURE build_signature(data) unless sha256_authentication?
+ xml.DS_MERCHANT_EXCEP_SCA data[:sca_exemption] if data[:sca_exemption]
# Only when card is present
if data[:card]
xml.DS_MERCHANT_TITULAR data[:card][:name]
xml.DS_MERCHANT_PAN data[:card][:pan]
@@ -396,26 +441,46 @@
xml.DS_MERCHANT_IDENTIFIER 'REQUIRED' if data[:store_in_vault]
elsif data[:credit_card_token]
xml.DS_MERCHANT_IDENTIFIER data[:credit_card_token]
xml.DS_MERCHANT_DIRECTPAYMENT 'true'
end
+
+ # Set moto flag only if explicitly requested via moto field
+ # Requires account configuration to be able to use
+ if options.dig(:moto) && options.dig(:metadata, :manual_entry)
+ xml.DS_MERCHANT_DIRECTPAYMENT 'moto'
+ end
+
+ if data[:threeds]
+ xml.DS_MERCHANT_EMV3DS data[:threeds].to_json
+ end
end
end
- def parse(data)
+ def parse(data, action)
params = {}
success = false
message = ''
options = @options.merge(:test => test?)
xml = Nokogiri::XML(data)
code = xml.xpath('//RETORNOXML/CODIGO').text
- if code == '0'
+
+ if ['iniciaPeticion', 'trataPeticion'].include?(action)
+ vxml = Nokogiri::XML(data).remove_namespaces!.xpath("//Envelope/Body/#{action}Response/#{action}Return").inner_text
+ xml = Nokogiri::XML(vxml)
+ node = (action == 'iniciaPeticion' ? 'INFOTARJETA' : 'OPERACION')
+ op = xml.xpath("//RETORNOXML/#{node}")
+ op.children.each do |element|
+ params[element.name.downcase.to_sym] = element.text
+ end
+ message = response_text_3ds(xml, params)
+ success = params.size > 0 && is_success_response?(params[:ds_response])
+ elsif code == '0'
op = xml.xpath('//RETORNOXML/OPERACION')
op.children.each do |element|
params[element.name.downcase.to_sym] = element.text
end
-
if validate_signature(params)
message = response_text(params[:ds_response])
options[:authorization] = build_authorization(params)
success = is_success_response?(params[:ds_response])
else
@@ -472,10 +537,24 @@
code = code.to_i
code = 0 if code < 100
RESPONSE_TEXTS[code] || 'Unkown code, please check in manual'
end
+ def response_text_3ds(xml, params)
+ code = xml.xpath('//RETORNOXML/CODIGO').text
+ message = ''
+ if code != '0'
+ message = "#{code} ERROR"
+ elsif params[:ds_emv3ds]
+ three_ds_data = JSON.parse(params[:ds_emv3ds])
+ message = three_ds_data['threeDSInfo']
+ elsif params[:ds_response]
+ message = response_text(params[:ds_response])
+ end
+ message
+ end
+
def is_success_response?(code)
(code.to_i < 100) || [400, 481, 500, 900].include?(code.to_i)
end
def clean_order_id(order_id)
@@ -519,9 +598,13 @@
xml_signed_fields = data[:ds_amount] + data[:ds_order] + data[:ds_merchantcode] +
data[:ds_currency] + data[:ds_response]
if data[:ds_cardnumber]
xml_signed_fields += data[:ds_cardnumber]
+ end
+
+ if data[:ds_emv3ds]
+ xml_signed_fields += data[:ds_emv3ds]
end
xml_signed_fields + data[:ds_transactiontype] + data[:ds_securepayment]
end