require 'nokogiri' module ActiveMerchant #:nodoc: module Billing #:nodoc: class WorldpayGateway < Gateway self.test_url = 'https://secure-test.worldpay.com/jsp/merchant/xml/paymentService.jsp' self.live_url = 'https://secure.worldpay.com/jsp/merchant/xml/paymentService.jsp' self.default_currency = 'GBP' self.money_format = :cents self.supported_countries = %w(AD AE AG AI AL AM AO AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BM BN BO BR BS BT BW BY BZ CA CC CF CH CK CL CM CN CO CR CV CX CY CZ DE DJ DK DO DZ EC EE EG EH ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IS IT JE JM JO JP KE KG KH KI KM KN KR KW KY KZ LA LC LI LK LS LT LU LV MA MC MD ME MG MH MK ML MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PH PK PL PN PR PT PW PY QA RE RO RS RU RW SA SB SC SE SG SI SK SL SM SN ST SV SZ TC TD TF TG TH TJ TK TM TO TR TT TV TW TZ UA UG US UY UZ VA VC VE VI VN VU WF WS YE YT ZA ZM) self.supported_cardtypes = %i[visa master american_express discover jcb maestro elo naranja cabal unionpay] self.currencies_without_fractions = %w(HUF IDR JPY KRW BEF XOF XAF XPF GRD GNF ITL LUF MGA MGF PYG PTE RWF ESP TRL VND KMF) self.currencies_with_three_decimal_places = %w(BHD KWD OMR TND LYD JOD IQD) self.homepage_url = 'http://www.worldpay.com/' self.display_name = 'Worldpay Global' NETWORK_TOKEN_TYPE = { apple_pay: 'APPLEPAY', google_pay: 'GOOGLEPAY', network_token: 'NETWORKTOKEN' } CARD_CODES = { 'visa' => 'VISA-SSL', 'master' => 'ECMC-SSL', 'discover' => 'DISCOVER-SSL', 'american_express' => 'AMEX-SSL', 'jcb' => 'JCB-SSL', 'maestro' => 'MAESTRO-SSL', 'diners_club' => 'DINERS-SSL', 'elo' => 'ELO-SSL', 'naranja' => 'NARANJA-SSL', 'cabal' => 'CABAL-SSL', 'unionpay' => 'CHINAUNIONPAY-SSL', 'unknown' => 'CARD-SSL' } AVS_CODE_MAP = { 'A' => 'M', # Match 'B' => 'P', # Postcode matches, address not verified 'C' => 'Z', # Postcode matches, address does not match 'D' => 'B', # Address matched; postcode not checked 'E' => 'I', # Address and postal code not checked 'F' => 'A', # Address matches, postcode does not match 'G' => 'C', # Address does not match, postcode not checked 'H' => 'I', # Address and postcode not provided 'I' => 'C', # Address not checked postcode does not match 'J' => 'C', # Address and postcode does not match } CVC_CODE_MAP = { 'A' => 'M', # CVV matches 'B' => 'P', # Not provided 'C' => 'P', # Not checked 'D' => 'N', # Does not match } def initialize(options = {}) requires!(options, :login, :password) super end def purchase(money, payment_method, options = {}) MultiResponse.run do |r| r.process { authorize(money, payment_method, options) } r.process { capture(money, r.authorization, options.merge(authorization_validated: true)) } unless options[:skip_capture] end end def authorize(money, payment_method, options = {}) requires!(options, :order_id) payment_details = payment_details(payment_method) authorize_request(money, payment_method, payment_details.merge(options)) end def capture(money, authorization, options = {}) authorization = order_id_from_authorization(authorization.to_s) MultiResponse.run do |r| r.process { inquire_request(authorization, options, 'AUTHORISED', 'CAPTURED') } unless options[:authorization_validated] if r.params authorization_currency = r.params['amount_currency_code'] options = options.merge(currency: authorization_currency) if authorization_currency.present? end r.process { capture_request(money, authorization, options) } end end def void(authorization, options = {}) authorization = order_id_from_authorization(authorization.to_s) MultiResponse.run do |r| r.process { inquire_request(authorization, options, 'AUTHORISED') } unless options[:authorization_validated] r.process { cancel_request(authorization, options) } end end def refund(money, authorization, options = {}) authorization = order_id_from_authorization(authorization.to_s) success_criteria = %w(CAPTURED SETTLED SETTLED_BY_MERCHANT SENT_FOR_REFUND) success_criteria.push('AUTHORIZED') if options[:cancel_or_refund] response = MultiResponse.run do |r| r.process { inquire_request(authorization, options, *success_criteria) } unless options[:authorization_validated] r.process { refund_request(money, authorization, options) } end if !response.success? && options[:force_full_refund_if_unsettled] && response.params['last_event'] == 'AUTHORISED' void(authorization, options) else response end end # Credits only function on a Merchant ID/login/profile flagged for Payouts # aka Credit Fund Transfers (CFT), whereas normal purchases, refunds, # and other transactions should be performed on a normal eCom-flagged # merchant ID. def credit(money, payment_method, options = {}) payment_details = payment_details(payment_method) if options[:fast_fund_credit] fast_fund_credit_request(money, payment_method, payment_details.merge(credit: true, **options)) else credit_request(money, payment_method, payment_details.merge(credit: true, **options)) end end def verify(payment_method, options = {}) amount = (eligible_for_0_auth?(payment_method, options) ? 0 : 100) MultiResponse.run(:use_first_response) do |r| r.process { authorize(amount, payment_method, options) } r.process(:ignore_result) { void(r.authorization, options.merge(authorization_validated: true)) } end end def store(credit_card, options = {}) requires!(options, :customer) store_request(credit_card, options) end def inquire(authorization, options = {}) order_id = order_id_from_authorization(authorization.to_s) || options[:order_id] commit('direct_inquiry', build_order_inquiry_request(order_id, options), :ok, options) end def supports_scrubbing true end def supports_network_tokenization? true end def scrub(transcript) transcript. gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). gsub(%r(()\d+()), '\1[FILTERED]\2'). gsub(%r(()[^<]+()), '\1[FILTERED]\2'). gsub(%r(()\d+()), '\1[FILTERED]\2'). gsub(%r(()[^<]+()), '\1[FILTERED]\2') end private def authorize_request(money, payment_method, options) commit('authorize', build_authorization_request(money, payment_method, options), 'AUTHORISED', 'CAPTURED', options) end def capture_request(money, authorization, options) commit('capture', build_capture_request(money, authorization, options), 'CAPTURED', :ok, options) end def cancel_request(authorization, options) commit('cancel', build_void_request(authorization, options), :ok, options) end def inquire_request(authorization, options, *success_criteria) commit('inquiry', build_order_inquiry_request(authorization, options), *success_criteria, options) end def refund_request(money, authorization, options) commit('refund', build_refund_request(money, authorization, options), :ok, 'SENT_FOR_REFUND', options) end def credit_request(money, payment_method, options) commit('credit', build_authorization_request(money, payment_method, options), :ok, 'SENT_FOR_REFUND', options) end def fast_fund_credit_request(money, payment_method, options) commit('fast_credit', build_fast_fund_credit_request(money, payment_method, options), :ok, 'PUSH_APPROVED', options) end def store_request(credit_card, options) commit('store', build_store_request(credit_card, options), options) end def build_request xml = Builder::XmlMarkup.new indent: 2 xml.instruct! :xml, encoding: 'UTF-8' xml.declare! :DOCTYPE, :paymentService, :PUBLIC, '-//WorldPay//DTD WorldPay PaymentService v1//EN', 'http://dtd.worldpay.com/paymentService_v1.dtd' xml.paymentService 'version' => '1.4', 'merchantCode' => @options[:login] do yield xml end xml.target! end def build_order_modify_request(authorization) build_request do |xml| xml.modify do xml.orderModification 'orderCode' => authorization do yield xml end end end end def build_order_inquiry_request(authorization, options) build_request do |xml| xml.inquiry do xml.orderInquiry 'orderCode' => authorization end end end def build_authorization_request(money, payment_method, options) build_request do |xml| xml.submit do xml.order order_tag_attributes(options) do xml.description(options[:description].blank? ? 'Purchase' : options[:description]) add_amount(xml, money, options) add_order_content(xml, options) add_payment_method(xml, money, payment_method, options) add_shopper(xml, options) add_statement_narrative(xml, options) add_risk_data(xml, options[:risk_data]) if options[:risk_data] add_sub_merchant_data(xml, options[:sub_merchant_data]) if options[:sub_merchant_data] add_hcg_additional_data(xml, options) if options[:hcg_additional_data] add_instalments_data(xml, options) if options[:instalments] add_additional_data(xml, money, options) if options[:level_2_data] || options[:level_3_data] add_moto_flag(xml, options) if options.dig(:metadata, :manual_entry) add_additional_3ds_data(xml, options) if options[:execute_threed] && options[:three_ds_version] && options[:three_ds_version] =~ /^2/ add_3ds_exemption(xml, options) if options[:exemption_type] end end end end def add_additional_data(xml, amount, options) level_two_data = options[:level_2_data] || {} level_three_data = options[:level_3_data] || {} level_two_and_three_data = level_two_data.merge(level_three_data).symbolize_keys xml.branchSpecificExtension do xml.purchase do add_level_two_and_three_data(xml, amount, level_two_and_three_data) end end end def add_level_two_and_three_data(xml, amount, data) xml.invoiceReferenceNumber data[:invoice_reference_number] if data.include?(:invoice_reference_number) xml.customerReference data[:customer_reference] if data.include?(:customer_reference) xml.cardAcceptorTaxId data[:card_acceptor_tax_id] if data.include?(:card_acceptor_tax_id) { sales_tax: 'salesTax', discount_amount: 'discountAmount', shipping_amount: 'shippingAmount', duty_amount: 'dutyAmount' }.each do |key, tag| next unless data.include?(key) xml.tag! tag do data_amount = data[key].symbolize_keys add_amount(xml, data_amount[:amount].to_i, data_amount) end end xml.discountName data[:discount_name] if data.include?(:discount_name) xml.discountCode data[:discount_code] if data.include?(:discount_code) add_date_element(xml, 'shippingDate', data[:shipping_date]) if data.include?(:shipping_date) if data.include?(:shipping_courier) xml.shippingCourier( data[:shipping_courier][:priority], data[:shipping_courier][:tracking_number], data[:shipping_courier][:name] ) end add_optional_data_level_two_and_three(xml, data) if data.include?(:item) && data[:item].kind_of?(Array) data[:item].each { |item| add_items_into_level_three_data(xml, item.symbolize_keys) } elsif data.include?(:item) add_items_into_level_three_data(xml, data[:item].symbolize_keys) end end def add_items_into_level_three_data(xml, item) xml.item do xml.description item[:description] if item[:description] xml.productCode item[:product_code] if item[:product_code] xml.commodityCode item[:commodity_code] if item[:commodity_code] xml.quantity item[:quantity] if item[:quantity] { unit_cost: 'unitCost', item_total: 'itemTotal', item_total_with_tax: 'itemTotalWithTax', item_discount_amount: 'itemDiscountAmount', tax_amount: 'taxAmount' }.each do |key, tag| next unless item.include?(key) xml.tag! tag do data_amount = item[key].symbolize_keys add_amount(xml, data_amount[:amount].to_i, data_amount) end end end end def add_optional_data_level_two_and_three(xml, data) xml.shipFromPostalCode data[:ship_from_postal_code] if data.include?(:ship_from_postal_code) xml.destinationPostalCode data[:destination_postal_code] if data.include?(:destination_postal_code) xml.destinationCountryCode data[:destination_country_code] if data.include?(:destination_country_code) add_date_element(xml, 'orderDate', data[:order_date].symbolize_keys) if data.include?(:order_date) xml.taxExempt data[:tax_exempt] if data.include?(:tax_exempt) end def order_tag_attributes(options) { 'orderCode' => clean_order_id(options[:order_id]), 'installationId' => options[:inst_id] || @options[:inst_id] }.reject { |_, v| !v.present? } end def clean_order_id(order_id) order_id.to_s.gsub(/(\s|\||<|>|'|")/, '')[0..64] end def add_order_content(xml, options) return unless options[:order_content] xml.orderContent do xml.cdata! options[:order_content] end end def build_capture_request(money, authorization, options) build_order_modify_request(authorization) do |xml| xml.capture do time = Time.now xml.date 'dayOfMonth' => time.day, 'month' => time.month, 'year' => time.year add_amount(xml, money, options) end end end def build_void_request(authorization, options) if options[:cancel_or_refund] build_order_modify_request(authorization, &:cancelOrRefund) else build_order_modify_request(authorization, &:cancel) end end def build_refund_request(money, authorization, options) build_order_modify_request(authorization) do |xml| if options[:cancel_or_refund] # Worldpay docs claim amount must be passed. This causes an error. xml.cancelOrRefund # { add_amount(xml, money, options.merge(debit_credit_indicator: 'credit')) } else xml.refund do add_amount(xml, money, options.merge(debit_credit_indicator: 'credit')) end end end end def build_store_request(credit_card, options) build_request do |xml| xml.submit do xml.paymentTokenCreate do add_authenticated_shopper_id(xml, options) xml.createToken xml.paymentInstrument do xml.cardDetails do add_card(xml, credit_card, options) end end add_transaction_identifier(xml, options) if network_transaction_id(options) end end end end def network_transaction_id(options) options[:stored_credential_transaction_id] || options.dig(:stored_credential, :network_transaction_id) end def add_transaction_identifier(xml, options) xml.storedCredentials 'usage' => 'FIRST' do xml.schemeTransactionIdentifier network_transaction_id(options) end end def build_fast_fund_credit_request(money, payment_method, options) build_request do |xml| xml.submit do xml.order order_tag_attributes(options) do xml.description(options[:description].blank? ? 'Fast Fund Credit' : options[:description]) add_amount(xml, money, options) add_order_content(xml, options) add_payment_details_for_ff_credit(xml, payment_method, options) add_shopper_id(xml, options) end end end end def add_payment_details_for_ff_credit(xml, payment_method, options) xml.paymentDetails do xml.tag! 'FF_DISBURSE-SSL' do if payment_method.is_a?(CreditCard) add_card_for_ff_credit(xml, payment_method, options) else add_token_for_ff_credit(xml, payment_method, options) end end end end def add_card_for_ff_credit(xml, payment_method, options) xml.recipient do xml.paymentInstrument do xml.cardDetails do add_card(xml, payment_method, options) end end end end def add_token_for_ff_credit(xml, payment_method, options) return unless payment_method.is_a?(String) token_details = token_details_from_authorization(payment_method) xml.tag! 'recipient', 'tokenScope' => token_details[:token_scope] do xml.paymentTokenID token_details[:token_id] add_authenticated_shopper_id(xml, token_details) end end def add_additional_3ds_data(xml, options) additional_data = { 'dfReferenceId' => options[:session_id] } additional_data['challengeWindowSize'] = options[:browser_size] if options[:browser_size] xml.additional3DSData additional_data end def add_3ds_exemption(xml, options) xml.exemption 'type' => options[:exemption_type], 'placement' => options[:exemption_placement] || 'AUTHORISATION' end def add_risk_data(xml, risk_data) xml.riskData do add_authentication_risk_data(xml, risk_data[:authentication_risk_data]) add_shopper_account_risk_data(xml, risk_data[:shopper_account_risk_data]) add_transaction_risk_data(xml, risk_data[:transaction_risk_data]) end end def add_authentication_risk_data(xml, authentication_risk_data) return unless authentication_risk_data timestamp = authentication_risk_data.fetch(:authentication_date, {}) xml.authenticationRiskData('authenticationMethod' => authentication_risk_data[:authentication_method]) do xml.authenticationTimestamp do xml.date( 'dayOfMonth' => timestamp[:day_of_month], 'month' => timestamp[:month], 'year' => timestamp[:year], 'hour' => timestamp[:hour], 'minute' => timestamp[:minute], 'second' => timestamp[:second] ) end end end def add_sub_merchant_data(xml, options) xml.subMerchantData do xml.pfId options[:pf_id] if options[:pf_id] xml.subName options[:sub_name] if options[:sub_name] xml.subId options[:sub_id] if options[:sub_id] xml.subStreet options[:sub_street] if options[:sub_street] xml.subCity options[:sub_city] if options[:sub_city] xml.subState options[:sub_state] if options[:sub_state] xml.subCountryCode options[:sub_country_code] if options[:sub_country_code] xml.subPostalCode options[:sub_postal_code] if options[:sub_postal_code] xml.subTaxId options[:sub_tax_id] if options[:sub_tax_id] end end def add_shopper_account_risk_data(xml, shopper_account_risk_data) return unless shopper_account_risk_data data = { 'transactionsAttemptedLastDay' => shopper_account_risk_data[:transactions_attempted_last_day], 'transactionsAttemptedLastYear' => shopper_account_risk_data[:transactions_attempted_last_year], 'purchasesCompletedLastSixMonths' => shopper_account_risk_data[:purchases_completed_last_six_months], 'addCardAttemptsLastDay' => shopper_account_risk_data[:add_card_attempts_last_day], 'previousSuspiciousActivity' => shopper_account_risk_data[:previous_suspicious_activity], 'shippingNameMatchesAccountName' => shopper_account_risk_data[:shipping_name_matches_account_name], 'shopperAccountAgeIndicator' => shopper_account_risk_data[:shopper_account_age_indicator], 'shopperAccountChangeIndicator' => shopper_account_risk_data[:shopper_account_change_indicator], 'shopperAccountPasswordChangeIndicator' => shopper_account_risk_data[:shopper_account_password_change_indicator], 'shopperAccountShippingAddressUsageIndicator' => shopper_account_risk_data[:shopper_account_shipping_address_usage_indicator], 'shopperAccountPaymentAccountIndicator' => shopper_account_risk_data[:shopper_account_payment_account_indicator] }.reject { |_k, v| v.nil? } xml.shopperAccountRiskData(data) do add_date_element(xml, 'shopperAccountCreationDate', shopper_account_risk_data[:shopper_account_creation_date]) add_date_element(xml, 'shopperAccountModificationDate', shopper_account_risk_data[:shopper_account_modification_date]) add_date_element(xml, 'shopperAccountPasswordChangeDate', shopper_account_risk_data[:shopper_account_password_change_date]) add_date_element(xml, 'shopperAccountShippingAddressFirstUseDate', shopper_account_risk_data[:shopper_account_shipping_address_first_use_date]) add_date_element(xml, 'shopperAccountPaymentAccountFirstUseDate', shopper_account_risk_data[:shopper_account_payment_account_first_use_date]) end end def add_transaction_risk_data(xml, transaction_risk_data) return unless transaction_risk_data data = { 'shippingMethod' => transaction_risk_data[:shipping_method], 'deliveryTimeframe' => transaction_risk_data[:delivery_timeframe], 'deliveryEmailAddress' => transaction_risk_data[:delivery_email_address], 'reorderingPreviousPurchases' => transaction_risk_data[:reordering_previous_purchases], 'preOrderPurchase' => transaction_risk_data[:pre_order_purchase], 'giftCardCount' => transaction_risk_data[:gift_card_count] }.reject { |_k, v| v.nil? } xml.transactionRiskData(data) do xml.transactionRiskDataGiftCardAmount do amount_hash = { 'value' => transaction_risk_data.dig(:transaction_risk_data_gift_card_amount, :value), 'currencyCode' => transaction_risk_data.dig(:transaction_risk_data_gift_card_amount, :currency), 'exponent' => transaction_risk_data.dig(:transaction_risk_data_gift_card_amount, :exponent) } debit_credit_indicator = transaction_risk_data.dig(:transaction_risk_data_gift_card_amount, :debit_credit_indicator) amount_hash['debitCreditIndicator'] = debit_credit_indicator if debit_credit_indicator xml.amount(amount_hash) end add_date_element(xml, 'transactionRiskDataPreOrderDate', transaction_risk_data[:transaction_risk_data_pre_order_date]) end end def add_date_element(xml, name, date) xml.tag! name do xml.date('dayOfMonth' => date[:day_of_month], 'month' => date[:month], 'year' => date[:year]) end end def add_amount(xml, money, options) currency = options[:currency] || currency(money) amount_hash = { :value => localized_amount(money, currency), 'currencyCode' => currency, 'exponent' => currency_exponent(currency) } amount_hash['debitCreditIndicator'] = options[:debit_credit_indicator] if options[:debit_credit_indicator] xml.amount amount_hash end def add_payment_method(xml, amount, payment_method, options) case options[:payment_type] when :pay_as_order add_amount_for_pay_as_order(xml, amount, payment_method, options) when :network_token add_network_tokenization_card(xml, payment_method) else add_card_or_token(xml, payment_method, options) end end def add_amount_for_pay_as_order(xml, amount, payment_method, options) if options[:merchant_code] xml.payAsOrder 'orderCode' => payment_method, 'merchantCode' => options[:merchant_code] do add_amount(xml, amount, options) end else xml.payAsOrder 'orderCode' => payment_method do add_amount(xml, amount, options) end end end def add_network_tokenization_card(xml, payment_method) token_type = NETWORK_TOKEN_TYPE.fetch(payment_method.source, 'NETWORKTOKEN') xml.paymentDetails do xml.tag! 'EMVCO_TOKEN-SSL', 'type' => token_type do xml.tokenNumber payment_method.number xml.expiryDate do xml.date( 'month' => format(payment_method.month, :two_digits), 'year' => format(payment_method.year, :four_digits_year) ) end name = card_holder_name(payment_method, options) eci = format(payment_method.eci, :two_digits) xml.cardHolderName name if name.present? xml.cryptogram payment_method.payment_cryptogram xml.eciIndicator eci.empty? ? '07' : eci end end end def add_card_or_token(xml, payment_method, options) xml.paymentDetails credit_fund_transfer_attribute(options) do if options[:payment_type] == :token add_token_details(xml, options) else add_card_details(xml, payment_method, options) end add_stored_credential_options(xml, options) add_shopper_id(xml, options) add_three_d_secure(xml, options) end end def add_token_details(xml, options) xml.tag! 'TOKEN-SSL', 'tokenScope' => options[:token_scope] do xml.paymentTokenID options[:token_id] end end def add_card_details(xml, payment_method, options) xml.tag! card_code_for(payment_method) do add_card(xml, payment_method, options) end end def add_shopper_id(xml, options) if options[:ip] && options[:session_id] xml.session 'shopperIPAddress' => options[:ip], 'id' => options[:session_id] else xml.session 'shopperIPAddress' => options[:ip] if options[:ip] xml.session 'id' => options[:session_id] if options[:session_id] end end def add_three_d_secure(xml, options) return unless three_d_secure = options[:three_d_secure] xml.info3DSecure do xml.threeDSVersion three_d_secure[:version] if three_d_secure[:version] && three_d_secure[:ds_transaction_id] xml.dsTransactionId three_d_secure[:ds_transaction_id] else xml.xid three_d_secure[:xid] end xml.cavv three_d_secure[:cavv] xml.eci three_d_secure[:eci] end end def add_card(xml, payment_method, options) xml.cardNumber payment_method.number xml.expiryDate do xml.date( 'month' => format(payment_method.month, :two_digits), 'year' => format(payment_method.year, :four_digits_year) ) end name = card_holder_name(payment_method, options) xml.cardHolderName name if name.present? xml.cvc payment_method.verification_value add_address(xml, (options[:billing_address] || options[:address]), options) end def add_stored_credential_options(xml, options = {}) if options[:stored_credential] add_stored_credential_using_normalized_fields(xml, options) else add_stored_credential_using_gateway_specific_fields(xml, options) end end def add_stored_credential_using_normalized_fields(xml, options) if options[:stored_credential][:initial_transaction] xml.storedCredentials 'usage' => 'FIRST' else reason = case options[:stored_credential][:reason_type] when 'installment' then 'INSTALMENT' when 'recurring' then 'RECURRING' when 'unscheduled' then 'UNSCHEDULED' end xml.storedCredentials 'usage' => 'USED', 'merchantInitiatedReason' => reason do xml.schemeTransactionIdentifier options[:stored_credential][:network_transaction_id] if options[:stored_credential][:network_transaction_id] end end end def add_stored_credential_using_gateway_specific_fields(xml, options) return unless options[:stored_credential_usage] if options[:stored_credential_initiated_reason] xml.storedCredentials 'usage' => options[:stored_credential_usage], 'merchantInitiatedReason' => options[:stored_credential_initiated_reason] do xml.schemeTransactionIdentifier options[:stored_credential_transaction_id] if options[:stored_credential_transaction_id] end else xml.storedCredentials 'usage' => options[:stored_credential_usage] end end def add_shopper(xml, options) return unless options[:execute_threed] || options[:email] || options[:customer] xml.shopper do xml.shopperEmailAddress options[:email] if options[:email] add_authenticated_shopper_id(xml, options) xml.browser do xml.acceptHeader options[:accept_header] xml.userAgentHeader options[:user_agent] end end end def add_statement_narrative(xml, options) xml.statementNarrative truncate(options[:statement_narrative], 50) if options[:statement_narrative] end def add_authenticated_shopper_id(xml, options) xml.authenticatedShopperID options[:customer] if options[:customer] end def add_address(xml, address, options) return unless address address = address_with_defaults(address) xml.cardAddress do xml.address do if m = /^\s*([^\s]+)\s+(.+)$/.match(address[:name]) xml.firstName m[1] xml.lastName m[2] end xml.address1 address[:address1] xml.address2 address[:address2] if address[:address2] xml.postalCode address[:zip] xml.city address[:city] xml.state address[:state] unless address[:country] != 'US' && options[:execute_threed] xml.countryCode address[:country] xml.telephoneNumber address[:phone] if address[:phone] end end end def add_hcg_additional_data(xml, options) xml.hcgAdditionalData do options[:hcg_additional_data].each do |k, v| xml.param({ name: k.to_s }, v) end end end def add_instalments_data(xml, options) xml.thirdPartyData do xml.instalments options[:instalments] xml.cpf options[:cpf] if options[:cpf] end end def add_moto_flag(xml, options) xml.dynamicInteractionType 'type' => 'MOTO' end def address_with_defaults(address) address ||= {} address.delete_if { |_, v| v.blank? } address.reverse_merge!(default_address) end def default_address { zip: '0000', country: 'US', city: 'N/A', address1: 'N/A' } end def parse(action, xml) xml = xml.strip.gsub(/\&/, '&') doc = Nokogiri::XML(xml, &:strict) doc.remove_namespaces! resp_params = { action: action } parse_elements(doc.root, resp_params) extract_issuer_response(doc.root, resp_params) resp_params end def extract_issuer_response(doc, response) return unless issuer_response = doc.at_xpath('//paymentService//reply//orderStatus//payment//IssuerResponseCode') response[:issuer_response_code] = issuer_response['code'] response[:issuer_response_description] = issuer_response['description'] end def parse_elements(node, response) node_name = node.name.underscore node.attributes.each do |k, v| response["#{node_name}_#{k.underscore}".to_sym] = v.value end if node.elements.empty? response[node_name.to_sym] = node.text unless node.text.blank? else response[node_name.to_sym] = true unless node.name.blank? node.elements.each do |childnode| parse_elements(childnode, response) end end end def headers(options) idempotency_key = options[:idempotency_key] headers = { 'Content-Type' => 'text/xml', 'Authorization' => encoded_credentials } # ensure cookie included on follow-up '3ds' and 'capture_request' calls, using the cookie saved from the preceding response # cookie should be present in options on the 3ds and capture calls, but also still saved in the instance var in case cookie = options[:cookie] || @cookie || nil headers['Cookie'] = cookie if cookie headers['Idempotency-Key'] = idempotency_key if idempotency_key headers end def commit(action, request, *success_criteria, options) xml = ssl_post(url, request, headers(options)) raw = parse(action, xml) if options[:execute_threed] raw[:cookie] = @cookie if defined?(@cookie) raw[:session_id] = options[:session_id] raw[:is3DSOrder] = true end success = success_from(action, raw, success_criteria) message = message_from(success, raw, success_criteria, action) Response.new( success, message, raw, authorization: authorization_from(action, raw, options), error_code: error_code_from(success, raw), test: test?, avs_result: AVSResult.new(code: AVS_CODE_MAP[raw[:avs_result_code_description]]), cvv_result: CVVResult.new(CVC_CODE_MAP[raw[:cvc_result_code_description]]) ) rescue Nokogiri::SyntaxError unparsable_response(xml) rescue ActiveMerchant::ResponseError => e if e.response.code.to_s == '401' return Response.new(false, 'Invalid credentials', {}, test: test?) else raise e end end def url test? ? self.test_url : self.live_url end def unparsable_response(raw_response) message = 'Unparsable response received from Worldpay. Please contact Worldpay if you continue to receive this message.' message += " (The raw response returned by the API was: #{raw_response.inspect})" return Response.new(false, message) end # Override the regular handle response so we can access the headers # Set-Cookie value is needed for 3DS transactions def handle_response(response) case response.code.to_i when 200...300 cookie = response.header['Set-Cookie']&.match('^[^;]*') @cookie = cookie[0] if cookie response.body else raise ResponseError.new(response) end end def success_from(action, raw, success_criteria) success_criteria_success?(raw, success_criteria) || action_success?(action, raw) end def message_from(success, raw, success_criteria, action) return 'SUCCESS' if success raw[:iso8583_return_code_description] || raw[:error] || required_status_message(raw, success_criteria, action) || raw[:issuer_response_description] end # success_criteria can be: # - a string or an array of strings (if one of many responses) # - An array of strings if one of many responses could be considered a # success. def success_criteria_success?(raw, success_criteria) return if raw[:error] raw[:ok].present? || (success_criteria.include?(raw[:last_event]) if raw[:last_event]) end def action_success?(action, raw) case action when 'store' raw[:token].present? when 'direct_inquiry' raw[:last_event].present? else false end end def error_code_from(success, raw) raw[:iso8583_return_code_code] || raw[:error_code] || nil unless success == 'SUCCESS' end def required_status_message(raw, success_criteria, action) return if success_criteria.include?(raw[:last_event]) return unless %w[cancel refund inquiry credit fast_credit].include?(action) "A transaction status of #{success_criteria.collect { |c| "'#{c}'" }.join(' or ')} is required." end def authorization_from(action, raw, options) order_id = order_id_from(raw) case action when 'store' authorization_from_token_details( order_id: order_id, token_id: raw[:payment_token_id], token_scope: 'shopper', customer: options[:customer] ) else order_id end end def order_id_from(raw) pair = raw.detect { |k, _v| k.to_s =~ /_order_code$/ } (pair ? pair.last : nil) end def authorization_from_token_details(options = {}) [options[:order_id], options[:token_id], options[:token_scope], options[:customer]].join('|') end def order_id_from_authorization(authorization) token_details_from_authorization(authorization)[:order_id] end def token_details_from_authorization(authorization) order_id, token_id, token_scope, customer = authorization.split('|') token_details = {} token_details[:order_id] = order_id if order_id.present? token_details[:token_id] = token_id if token_id.present? token_details[:token_scope] = token_scope if token_scope.present? token_details[:customer] = customer if customer.present? token_details end def payment_details(payment_method) case payment_method when String token_type_and_details(payment_method) else type = network_token?(payment_method) ? :network_token : :credit { payment_type: type } end end def network_token?(payment_method) payment_method.respond_to?(:source) && payment_method.respond_to?(:payment_cryptogram) && payment_method.respond_to?(:eci) end def token_type_and_details(token) token_details = token_details_from_authorization(token) token_details[:payment_type] = token_details.has_key?(:token_id) ? :token : :pay_as_order token_details end def credit_fund_transfer_attribute(options) return unless options[:credit] { 'action' => 'REFUND' } end def encoded_credentials credentials = "#{@options[:login]}:#{@options[:password]}" "Basic #{[credentials].pack('m').strip}" end def currency_exponent(currency) return 0 if non_fractional_currency?(currency) return 3 if three_decimal_currency?(currency) return 2 end def card_code_for(payment_method) CARD_CODES[card_brand(payment_method)] || CARD_CODES['unknown'] end def eligible_for_0_auth?(payment_method, options = {}) payment_method.is_a?(CreditCard) && %w(visa master).include?(payment_method.brand) && options[:zero_dollar_auth] end def card_holder_name(payment_method, options) test? && options[:execute_threed] && !options[:three_ds_version]&.start_with?('2') ? '3D' : payment_method.name end end end end