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