module ActiveMerchant #:nodoc: module Billing #:nodoc: class AdyenGateway < Gateway # we recommend setting up merchant-specific endpoints. # https://docs.adyen.com/developers/api-manual#apiendpoints self.test_url = 'https://pal-test.adyen.com/pal/servlet/' self.live_url = 'https://pal-live.adyen.com/pal/servlet/' self.supported_countries = %w(AT AU BE BG BR CH CY CZ DE DK EE ES FI FR GB GI GR HK HU IE IS IT LI LT LU LV MC MT MX NL NO PL PT RO SE SG SK SI US) self.default_currency = 'USD' self.currencies_without_fractions = %w(CVE DJF GNF IDR JPY KMF KRW PYG RWF UGX VND VUV XAF XOF XPF) self.currencies_with_three_decimal_places = %w(BHD IQD JOD KWD LYD OMR TND) self.supported_cardtypes = %i[visa master american_express diners_club jcb dankort maestro discover elo naranja cabal unionpay] self.money_format = :cents self.homepage_url = 'https://www.adyen.com/' self.display_name = 'Adyen' PAYMENT_API_VERSION = 'v68' RECURRING_API_VERSION = 'v68' STANDARD_ERROR_CODE_MAPPING = { '0' => STANDARD_ERROR_CODE[:processing_error], '10' => STANDARD_ERROR_CODE[:config_error], '100' => STANDARD_ERROR_CODE[:invalid_amount], '101' => STANDARD_ERROR_CODE[:incorrect_number], '103' => STANDARD_ERROR_CODE[:invalid_cvc], '104' => STANDARD_ERROR_CODE[:incorrect_address], '131' => STANDARD_ERROR_CODE[:incorrect_address], '132' => STANDARD_ERROR_CODE[:incorrect_address], '133' => STANDARD_ERROR_CODE[:incorrect_address], '134' => STANDARD_ERROR_CODE[:incorrect_address], '135' => STANDARD_ERROR_CODE[:incorrect_address] } def initialize(options = {}) requires!(options, :username, :password, :merchant_account) @username, @password, @merchant_account = options.values_at(:username, :password, :merchant_account) super end def purchase(money, payment, options = {}) if options[:execute_threed] || options[:threed_dynamic] authorize(money, payment, options) else MultiResponse.run do |r| r.process { authorize(money, payment, options) } r.process { capture(money, r.authorization, capture_options(options)) } end end end def authorize(money, payment, options = {}) requires!(options, :order_id) post = init_post(options) add_invoice(post, money, options) add_payment(post, payment, options) add_extra_data(post, payment, options) add_stored_credentials(post, payment, options) add_address(post, options) add_installments(post, options) if options[:installments] add_3ds(post, options) add_3ds_authenticated_data(post, options) add_splits(post, options) add_recurring_contract(post, options) add_network_transaction_reference(post, options) add_application_info(post, options) add_level_2_data(post, options) add_level_3_data(post, options) add_data_airline(post, options) add_data_lodging(post, options) add_metadata(post, options) commit('authorise', post, options) end def capture(money, authorization, options = {}) post = init_post(options) add_invoice_for_modification(post, money, options) add_reference(post, authorization, options) add_splits(post, options) add_network_transaction_reference(post, options) add_shopper_statement(post, options) commit('capture', post, options) end def refund(money, authorization, options = {}) post = init_post(options) add_invoice_for_modification(post, money, options) add_reference(post, authorization, options) add_splits(post, options) add_network_transaction_reference(post, options) commit('refund', post, options) end def credit(money, payment, options = {}) action = options[:payout] ? 'payout' : 'refundWithData' post = init_post(options) add_invoice(post, money, options) add_payment(post, payment, options, action) add_shopper_reference(post, options) add_network_transaction_reference(post, options) if action == 'payout' add_shopper_interaction(post, payment, options) add_fraud_offset(post, options) add_fund_source(post, options) add_recurring_contract(post, options) add_shopper_data(post, payment, options) if (address = options[:billing_address] || options[:address]) && address[:country] add_billing_address(post, options, address) end post[:dateOfBirth] = options[:date_of_birth] if options[:date_of_birth] post[:nationality] = options[:nationality] if options[:nationality] end commit(action, post, options) end def void(authorization, options = {}) post = init_post(options) endpoint = options[:cancel_or_refund] ? 'cancelOrRefund' : 'cancel' add_reference(post, authorization, options) add_network_transaction_reference(post, options) commit(endpoint, post, options) end def adjust(money, authorization, options = {}) post = init_post(options) add_invoice_for_modification(post, money, options) add_reference(post, authorization, options) add_extra_data(post, nil, options) commit('adjustAuthorisation', post, options) end def store(credit_card, options = {}) requires!(options, :order_id) post = init_post(options) add_invoice(post, 0, options) add_payment(post, credit_card, options) add_extra_data(post, credit_card, options) add_stored_credentials(post, credit_card, options) add_address(post, options) add_network_transaction_reference(post, options) options[:recurring_contract_type] ||= 'RECURRING' add_recurring_contract(post, options) action = options[:tokenize_only] ? 'storeToken' : 'authorise' initial_response = commit(action, post, options) if initial_response.success? && card_not_stored?(initial_response) unsupported_failure_response(initial_response) else initial_response end end def unstore(options = {}) requires!(options, :shopper_reference, :recurring_detail_reference) post = {} add_shopper_reference(post, options) add_merchant_account(post, options) post[:recurringDetailReference] = options[:recurring_detail_reference] commit('disable', post, options) end def verify(credit_card, options = {}) amount = options[:verify_amount]&.to_i || 0 MultiResponse.run(:use_first_response) do |r| r.process { authorize(amount, credit_card, options) } options[:idempotency_key] = nil r.process(:ignore_result) { void(r.authorization, options) } end 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(("number\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). gsub(%r(("cvc\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). gsub(%r(("cavv\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). gsub(%r(("bankLocationId\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). gsub(%r(("iban\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). gsub(%r(("bankAccountNumber\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]') end private AVS_MAPPING = { '0' => 'R', # Unknown '1' => 'A', # Address matches, postal code doesn't '2' => 'N', # Neither postal code nor address match '3' => 'R', # AVS unavailable '4' => 'E', # AVS not supported for this card type '5' => 'U', # No AVS data provided '6' => 'Z', # Postal code matches, address doesn't match '7' => 'D', # Both postal code and address match '8' => 'U', # Address not checked, postal code unknown '9' => 'B', # Address matches, postal code unknown '10' => 'N', # Address doesn't match, postal code unknown '11' => 'U', # Postal code not checked, address unknown '12' => 'B', # Address matches, postal code not checked '13' => 'U', # Address doesn't match, postal code not checked '14' => 'P', # Postal code matches, address unknown '15' => 'P', # Postal code matches, address not checked '16' => 'N', # Postal code doesn't match, address unknown '17' => 'U', # Postal code doesn't match, address not checked '18' => 'I', # Neither postal code nor address were checked '19' => 'L', # Name and postal code matches. '20' => 'V', # Name, address and postal code matches. '21' => 'O', # Name and address matches. '22' => 'K', # Name matches. '23' => 'F', # Postal code matches, name doesn't match. '24' => 'H', # Both postal code and address matches, name doesn't match. '25' => 'T', # Address matches, name doesn't match. '26' => 'N' # Neither postal code, address nor name matches. } CVC_MAPPING = { '0' => 'P', # Unknown '1' => 'M', # Matches '2' => 'N', # Does not match '3' => 'P', # Not checked '4' => 'S', # No CVC/CVV provided, but was required '5' => 'U', # Issuer not certifed by CVC/CVV '6' => 'P' # No CVC/CVV provided } NETWORK_TOKENIZATION_CARD_SOURCE = { 'apple_pay' => 'applepay', 'android_pay' => 'androidpay', 'google_pay' => 'googlepay' } def add_extra_data(post, payment, options) post[:telephoneNumber] = (options[:billing_address][:phone_number] if options.dig(:billing_address, :phone_number)) || (options[:billing_address][:phone] if options.dig(:billing_address, :phone)) || '' post[:selectedBrand] = options[:selected_brand] if options[:selected_brand] post[:selectedBrand] ||= NETWORK_TOKENIZATION_CARD_SOURCE[payment.source.to_s] if payment.is_a?(NetworkTokenizationCreditCard) post[:deliveryDate] = options[:delivery_date] if options[:delivery_date] post[:merchantOrderReference] = options[:merchant_order_reference] if options[:merchant_order_reference] post[:captureDelayHours] = options[:capture_delay_hours] if options[:capture_delay_hours] post[:deviceFingerprint] = options[:device_fingerprint] if options[:device_fingerprint] post[:shopperIP] = options[:shopper_ip] || options[:ip] if options[:shopper_ip] || options[:ip] post[:shopperStatement] = options[:shopper_statement] if options[:shopper_statement] post[:store] = options[:store] if options[:store] post[:mcc] = options[:mcc] if options[:mcc] add_shopper_data(post, payment, options) add_additional_data(post, payment, options) add_risk_data(post, options) add_shopper_reference(post, options) add_merchant_data(post, options) add_fraud_offset(post, options) end def add_fraud_offset(post, options) post[:fraudOffset] = options[:fraud_offset] if options[:fraud_offset] end def add_additional_data(post, payment, options) post[:additionalData] ||= {} post[:additionalData][:overwriteBrand] = normalize(options[:overwrite_brand]) if options[:overwrite_brand] post[:additionalData][:customRoutingFlag] = options[:custom_routing_flag] if options[:custom_routing_flag] post[:additionalData]['paymentdatasource.type'] = NETWORK_TOKENIZATION_CARD_SOURCE[payment.source.to_s] if payment.is_a?(NetworkTokenizationCreditCard) post[:additionalData][:authorisationType] = options[:authorisation_type] if options[:authorisation_type] post[:additionalData][:adjustAuthorisationData] = options[:adjust_authorisation_data] if options[:adjust_authorisation_data] post[:additionalData][:industryUsage] = options[:industry_usage] if options[:industry_usage] post[:additionalData][:RequestedTestAcquirerResponseCode] = options[:requested_test_acquirer_response_code] if options[:requested_test_acquirer_response_code] && test? post[:additionalData][:updateShopperStatement] = options[:update_shopper_statement] if options[:update_shopper_statement] end def extract_and_transform(mapper, from) mapper.each_with_object({}) do |key_map, hsh| key, item_key = key_map[0], key_map[1] hsh[key] = from[item_key.to_sym] end end def add_level_2_data(post, options) return unless options[:level_2_data].present? mapper = { "enhancedSchemeData.totalTaxAmount": 'total_tax_amount', "enhancedSchemeData.customerReference": 'customer_reference' } post[:additionalData].merge!(extract_and_transform(mapper, options[:level_2_data])) end def add_level_3_data(post, options) return unless options[:level_3_data].present? mapper = { "enhancedSchemeData.freightAmount": 'freight_amount', "enhancedSchemeData.destinationStateProvinceCode": 'destination_state_province_code', "enhancedSchemeData.shipFromPostalCode": 'ship_from_postal_code', "enhancedSchemeData.orderDate": 'order_date', "enhancedSchemeData.destinationPostalCode": 'destination_postal_code', "enhancedSchemeData.destinationCountryCode": 'destination_country_code', "enhancedSchemeData.dutyAmount": 'duty_amount' } post[:additionalData].merge!(extract_and_transform(mapper, options[:level_3_data])) item_detail_keys = %w[description product_code quantity unit_of_measure unit_price discount_amount total_amount commodity_code] if options[:level_3_data][:items].present? options[:level_3_data][:items].last(9).each.with_index(1) do |item, index| mapper = item_detail_keys.each_with_object({}) do |key, hsh| hsh["enhancedSchemeData.itemDetailLine#{index}.#{key.camelize(:lower)}"] = key end post[:additionalData].merge!(extract_and_transform(mapper, item)) end end post[:additionalData].compact! end def add_data_airline(post, options) return unless options[:additional_data_airline] mapper = %w[ agency_invoice_number agency_plan_name airline_code airline_designator_code boarding_fee computerized_reservation_system customer_reference_number document_type flight_date ticket_issue_address ticket_number travel_agency_code travel_agency_name passenger_name ].each_with_object({}) { |value, hash| hash["airline.#{value}"] = value } post[:additionalData].merge!(extract_and_transform(mapper, options[:additional_data_airline])) if options[:additional_data_airline][:leg].present? leg_data = %w[ carrier_code class_of_travel date_of_travel depart_airport depart_tax destination_code fare_base_code flight_number stop_over_code ].each_with_object({}) { |value, hash| hash["airline.leg.#{value}"] = value } post[:additionalData].merge!(extract_and_transform(leg_data, options[:additional_data_airline][:leg])) end if options[:additional_data_airline][:passenger].present? passenger_data = %w[ date_of_birth first_name last_name telephone_number traveller_type ].each_with_object({}) { |value, hash| hash["airline.passenger.#{value}"] = value } post[:additionalData].merge!(extract_and_transform(passenger_data, options[:additional_data_airline][:passenger])) end post[:additionalData].compact! end def add_data_lodging(post, options) return unless options[:additional_data_lodging] mapper = { 'lodging.checkInDate': 'check_in_date', 'lodging.checkOutDate': 'check_out_date', 'lodging.customerServiceTollFreeNumber': 'customer_service_toll_free_number', 'lodging.fireSafetyActIndicator': 'fire_safety_act_indicator', 'lodging.folioCashAdvances': 'folio_cash_advances', 'lodging.folioNumber': 'folio_number', 'lodging.foodBeverageCharges': 'food_beverage_charges', 'lodging.noShowIndicator': 'no_show_indicator', 'lodging.prepaidExpenses': 'prepaid_expenses', 'lodging.propertyPhoneNumber': 'property_phone_number', 'lodging.room1.numberOfNights': 'number_of_nights', 'lodging.room1.rate': 'rate', 'lodging.totalRoomTax': 'total_room_tax', 'lodging.totalTax': 'totalTax', 'travelEntertainmentAuthData.duration': 'duration', 'travelEntertainmentAuthData.market': 'market' } post[:additionalData].merge!(extract_and_transform(mapper, options[:additional_data_lodging])) post[:additionalData].compact! end def add_shopper_statement(post, options) return unless options[:shopper_statement] post[:additionalData] = { shopperStatement: options[:shopper_statement] } end def add_merchant_data(post, options) post[:additionalData][:subMerchantID] = options[:sub_merchant_id] if options[:sub_merchant_id] post[:additionalData][:subMerchantName] = options[:sub_merchant_name] if options[:sub_merchant_name] post[:additionalData][:subMerchantStreet] = options[:sub_merchant_street] if options[:sub_merchant_street] post[:additionalData][:subMerchantCity] = options[:sub_merchant_city] if options[:sub_merchant_city] post[:additionalData][:subMerchantState] = options[:sub_merchant_state] if options[:sub_merchant_state] post[:additionalData][:subMerchantPostalCode] = options[:sub_merchant_postal_code] if options[:sub_merchant_postal_code] post[:additionalData][:subMerchantCountry] = options[:sub_merchant_country] if options[:sub_merchant_country] post[:additionalData][:subMerchantTaxId] = options[:sub_merchant_tax_id] if options[:sub_merchant_tax_id] post[:additionalData][:subMerchantMCC] = options[:sub_merchant_mcc] if options[:sub_merchant_mcc] post[:additionalData] = post[:additionalData].merge(options[:sub_merchant_data]) if options[:sub_merchant_data] end def add_risk_data(post, options) if (risk_data = options[:risk_data]) risk_data = risk_data.map { |k, v| ["riskdata.#{k}", v] }.to_h post[:additionalData].merge!(risk_data) end end def add_splits(post, options) return unless split_data = options[:splits] splits = [] split_data.each do |split| amount = { value: split['amount']['value'] } amount[:currency] = split['amount']['currency'] if split['amount']['currency'] split_hash = { amount: amount, type: split['type'], reference: split['reference'] } split_hash['account'] = split['account'] if split['account'] splits.push(split_hash) end post[:splits] = splits end def add_stored_credentials(post, payment, options) add_shopper_interaction(post, payment, options) add_recurring_processing_model(post, options) end def add_merchant_account(post, options) post[:merchantAccount] = options[:merchant_account] || @merchant_account end def add_shopper_reference(post, options) post[:shopperReference] = options[:shopper_reference] if options[:shopper_reference] end def add_shopper_interaction(post, payment, options = {}) if (options.dig(:stored_credential, :initial_transaction) && options.dig(:stored_credential, :initiator) == 'cardholder') || (payment.respond_to?(:verification_value) && payment.verification_value && options.dig(:stored_credential, :initial_transaction).nil?) || payment.is_a?(NetworkTokenizationCreditCard) shopper_interaction = 'Ecommerce' else shopper_interaction = 'ContAuth' end post[:shopperInteraction] = options[:shopper_interaction] || shopper_interaction end def add_recurring_processing_model(post, options) return unless options.dig(:stored_credential, :reason_type) || options[:recurring_processing_model] if options.dig(:stored_credential, :reason_type) == 'unscheduled' if options.dig(:stored_credential, :initiator) == 'merchant' recurring_processing_model = 'UnscheduledCardOnFile' else recurring_processing_model = 'CardOnFile' end else recurring_processing_model = 'Subscription' end post[:recurringProcessingModel] = options[:recurring_processing_model] || recurring_processing_model end def add_address(post, options) if address = options[:shipping_address] post[:deliveryAddress] = {} post[:deliveryAddress][:street] = options[:address_override] == true ? address[:address2] : address[:address1] || 'NA' post[:deliveryAddress][:houseNumberOrName] = options[:address_override] == true ? address[:address1] : address[:address2] || 'NA' post[:deliveryAddress][:postalCode] = address[:zip] if address[:zip] post[:deliveryAddress][:city] = address[:city] || 'NA' post[:deliveryAddress][:stateOrProvince] = get_state(address) post[:deliveryAddress][:country] = get_country(address) end return unless post[:bankAccount]&.kind_of?(Hash) || post[:card]&.kind_of?(Hash) if (address = options[:billing_address] || options[:address]) && address[:country] add_billing_address(post, options, address) end end def add_billing_address(post, options, address) post[:billingAddress] = {} post[:billingAddress][:street] = options[:address_override] == true ? address[:address2] : address[:address1] || 'NA' post[:billingAddress][:houseNumberOrName] = options[:address_override] == true ? address[:address1] : address[:address2] || 'NA' post[:billingAddress][:postalCode] = address[:zip] if address[:zip] post[:billingAddress][:city] = address[:city] || 'NA' post[:billingAddress][:stateOrProvince] = get_state(address) post[:billingAddress][:country] = get_country(address) post[:telephoneNumber] = address[:phone_number] || address[:phone] || '' end def get_state(address) address[:state] && !address[:state].blank? ? address[:state] : 'NA' end def get_country(address) address[:country].present? ? address[:country] : 'ZZ' end def add_invoice(post, money, options) currency = options[:currency] || currency(money) amount = { value: localized_amount(money, currency), currency: currency } post[:amount] = amount end def add_invoice_for_modification(post, money, options) currency = options[:currency] || currency(money) amount = { value: localized_amount(money, currency), currency: currency } post[:modificationAmount] = amount end def add_payment(post, payment, options, action = nil) if payment.is_a?(String) _, _, recurring_detail_reference = payment.split('#') post[:selectedRecurringDetailReference] = recurring_detail_reference options[:recurring_contract_type] ||= 'RECURRING' elsif payment.is_a?(Check) add_bank_account(post, payment, options, action) else add_mpi_data_for_network_tokenization_card(post, payment, options) if payment.is_a?(NetworkTokenizationCreditCard) add_card(post, payment) end end def add_bank_account(post, bank_account, options, action) bank = { bankAccountNumber: bank_account.account_number, ownerName: bank_account.name, countryCode: options[:billing_address].try(:[], :country) } action == 'refundWithData' ? bank[:iban] = bank_account.routing_number : bank[:bankLocationId] = bank_account.routing_number requires!(bank, :bankAccountNumber, :ownerName, :countryCode) post[:bankAccount] = bank end def add_card(post, credit_card) card = { expiryMonth: credit_card.month, expiryYear: credit_card.year, holderName: credit_card.name, number: credit_card.number, cvc: credit_card.verification_value } card.delete_if { |_k, v| v.blank? } card[:holderName] ||= 'Not Provided' requires!(card, :expiryMonth, :expiryYear, :holderName, :number) post[:card] = card end def add_shopper_data(post, payment, options) if payment && !payment.is_a?(String) post[:shopperName] = {} post[:shopperName][:firstName] = payment.first_name post[:shopperName][:lastName] = payment.last_name end post[:shopperEmail] = options[:email] if options[:email] post[:shopperEmail] = options[:shopper_email] if options[:shopper_email] end def capture_options(options) return options.merge(idempotency_key: "#{options[:idempotency_key]}-cap") if options[:idempotency_key] options end def add_network_transaction_reference(post, options) return unless ntid = options[:network_transaction_id] || options.dig(:stored_credential, :network_transaction_id) post[:additionalData] = {} unless post[:additionalData] post[:additionalData][:networkTxReference] = ntid end def add_reference(post, authorization, options = {}) original_reference = authorization.split('#').reject(&:empty?).first post[:originalReference] = original_reference end def add_mpi_data_for_network_tokenization_card(post, payment, options) return if options[:skip_mpi_data] == 'Y' post[:mpiData] = {} post[:mpiData][:authenticationResponse] = 'Y' post[:mpiData][:cavv] = payment.payment_cryptogram post[:mpiData][:directoryResponse] = 'Y' post[:mpiData][:eci] = payment.eci || '07' end def add_recurring_contract(post, options = {}) return unless options[:recurring_contract_type] post[:recurring] = {} post[:recurring][:contract] = options[:recurring_contract_type] post[:recurring][:recurringDetailName] = options[:recurring_detail_name] if options[:recurring_detail_name] post[:recurring][:recurringExpiry] = options[:recurring_expiry] if options[:recurring_expiry] post[:recurring][:recurringFrequency] = options[:recurring_frequency] if options[:recurring_frequency] post[:recurring][:tokenService] = options[:token_service] if options[:token_service] end def add_application_info(post, options) post[:applicationInfo] ||= {} add_external_platform(post, options) add_merchant_application(post, options) end def add_external_platform(post, options) options.update(externalPlatform: application_id) if application_id return unless options[:externalPlatform] post[:applicationInfo][:externalPlatform] = { name: options[:externalPlatform][:name], version: options[:externalPlatform][:version] } end def add_merchant_application(post, options) return unless options[:merchantApplication] post[:applicationInfo][:merchantApplication] = { name: options[:merchantApplication][:name], version: options[:merchantApplication][:version] } end def add_installments(post, options) post[:installments] = { value: options[:installments] } end def add_3ds(post, options) if three_ds_2_options = options[:three_ds_2] device_channel = three_ds_2_options[:channel] if device_channel == 'app' post[:threeDS2RequestData] = { deviceChannel: device_channel } else add_browser_info(three_ds_2_options[:browser_info], post) post[:threeDS2RequestData] = { deviceChannel: device_channel, notificationURL: three_ds_2_options[:notification_url] } end if options.has_key?(:execute_threed) post[:additionalData][:executeThreeD] = options[:execute_threed] post[:additionalData][:scaExemption] = options[:sca_exemption] if options[:sca_exemption] end else return unless !options[:execute_threed].nil? || !options[:threed_dynamic].nil? post[:browserInfo] = { userAgent: options[:user_agent], acceptHeader: options[:accept_header] } if options[:execute_threed] || options[:threed_dynamic] post[:additionalData] ||= {} post[:additionalData][:executeThreeD] = options[:execute_threed] if !options[:execute_threed].nil? end end def add_3ds_authenticated_data(post, options) if options[:three_d_secure] && options[:three_d_secure][:eci] && options[:three_d_secure][:xid] add_3ds1_authenticated_data(post, options) elsif options[:three_d_secure] add_3ds2_authenticated_data(post, options) end end def add_3ds1_authenticated_data(post, options) three_d_secure_options = options[:three_d_secure] post[:mpiData] = { cavv: three_d_secure_options[:cavv], cavvAlgorithm: three_d_secure_options[:cavv_algorithm], eci: three_d_secure_options[:eci], xid: three_d_secure_options[:xid], directoryResponse: three_d_secure_options[:enrolled], authenticationResponse: three_d_secure_options[:authentication_response_status] } end def add_3ds2_authenticated_data(post, options) three_d_secure_options = options[:three_d_secure] # If the transaction was authenticated in a frictionless flow, send the transStatus from the ARes. if three_d_secure_options[:authentication_response_status].nil? authentication_response = three_d_secure_options[:directory_response_status] else authentication_response = three_d_secure_options[:authentication_response_status] end post[:mpiData] = { threeDSVersion: three_d_secure_options[:version], eci: three_d_secure_options[:eci], cavv: three_d_secure_options[:cavv], dsTransID: three_d_secure_options[:ds_transaction_id], directoryResponse: three_d_secure_options[:directory_response_status], authenticationResponse: authentication_response } end def add_fund_source(post, options) return unless fund_source = options[:fund_source] post[:fundSource] = {} post[:fundSource][:additionalData] = fund_source[:additional_data] if fund_source[:additional_data] if fund_source[:first_name] && fund_source[:last_name] post[:fundSource][:shopperName] = {} post[:fundSource][:shopperName][:firstName] = fund_source[:first_name] post[:fundSource][:shopperName][:lastName] = fund_source[:last_name] end if (address = fund_source[:billing_address]) add_billing_address(post[:fundSource], options, address) end end def add_metadata(post, options = {}) return unless options[:metadata] post[:metadata] ||= {} post[:metadata].merge!(options[:metadata]) if options[:metadata] end def add_header_fields(response) return unless @response_headers.present? headers = {} headers['response_headers'] = {} headers['response_headers']['transient_error'] = @response_headers['transient-error'] if @response_headers['transient-error'] response.merge!(headers) end def parse(body) return {} if body.blank? response = JSON.parse(body) add_header_fields(response) response end # Override the regular handle response so we can access the headers # set header fields and values so we can add them to the response body def handle_response(response) @response_headers = response.each_header.to_h if response.respond_to?(:header) case response.code.to_i when 200...300 response.body else raise ResponseError.new(response) end end def commit(action, parameters, options) begin raw_response = ssl_post(url(action), post_data(action, parameters), request_headers(options)) response = parse(raw_response) rescue ResponseError => e raw_response = e.response.body response = parse(raw_response) end success = success_from(action, response, options) Response.new( success, message_from(action, response, options), response, authorization: authorization_from(action, parameters, response), test: test?, error_code: success ? nil : error_code_from(response), network_transaction_id: network_transaction_id_from(response), avs_result: AVSResult.new(code: avs_code_from(response)), cvv_result: CVVResult.new(cvv_result_from(response)) ) end def avs_code_from(response) AVS_MAPPING[response['additionalData']['avsResult'][0..1].strip] if response.dig('additionalData', 'avsResult') end def cvv_result_from(response) CVC_MAPPING[response['additionalData']['cvcResult'][0]] if response.dig('additionalData', 'cvcResult') end def endpoint(action) case action when 'disable', 'storeToken' "Recurring/#{RECURRING_API_VERSION}/#{action}" when 'payout' "Payout/#{PAYMENT_API_VERSION}/#{action}" else "Payment/#{PAYMENT_API_VERSION}/#{action}" end end def url(action) if test? "#{test_url}#{endpoint(action)}" elsif @options[:subdomain] "https://#{@options[:subdomain]}-pal-live.adyenpayments.com/pal/servlet/#{endpoint(action)}" else "#{live_url}#{endpoint(action)}" end end def basic_auth Base64.strict_encode64("#{@username}:#{@password}") end def request_headers(options) headers = { 'Content-Type' => 'application/json', 'Authorization' => "Basic #{basic_auth}" } headers['Idempotency-Key'] = options[:idempotency_key] if options[:idempotency_key] headers end def success_from(action, response, options) if %w[RedirectShopper ChallengeShopper].include?(response.dig('resultCode')) && !options[:execute_threed] && (!options[:threed_dynamic] || options[:ignore_threed_dynamic]) response['refusalReason'] = 'Received unexpected 3DS authentication response, but a 3DS initiation flag was not included in the request.' return false end case action.to_s when 'authorise', 'authorise3d' %w[Authorised Received RedirectShopper].include?(response['resultCode']) when 'capture', 'refund', 'cancel', 'cancelOrRefund' response['response'] == "[#{action}-received]" when 'adjustAuthorisation' response['response'] == 'Authorised' || response['response'] == '[adjustAuthorisation-received]' when 'storeToken' response['result'] == 'Success' when 'disable' response['response'] == '[detail-successfully-disabled]' when 'refundWithData' response['resultCode'] == 'Received' when 'payout' return false unless response['resultCode'] && response['authCode'] %[AuthenticationFinished Authorised Received].include?(response['resultCode']) else false end end def message_from(action, response, options = {}) case action.to_s when 'authorise', 'authorise3d', 'authorise3ds2' authorize_message_from(response, options) when 'payout' response['refusalReason'] || response['resultCode'] || response['message'] else response['response'] || response['message'] || response['result'] || response['resultCode'] end end def authorize_message_from(response, options = {}) return raw_authorize_error_message(response) if options[:raw_error_message] if response['refusalReason'] && response['additionalData'] && (response['additionalData']['merchantAdviceCode'] || response['additionalData']['refusalReasonRaw']) "#{response['refusalReason']} | #{response['additionalData']['merchantAdviceCode'] || response['additionalData']['refusalReasonRaw']}" else response['refusalReason'] || response['resultCode'] || response['message'] || response['result'] end end def raw_authorize_error_message(response) if response['refusalReason'] && response['additionalData'] && response['additionalData']['refusalReasonRaw'] "#{response['refusalReason']} | #{response['additionalData']['refusalReasonRaw']}" else response['refusalReason'] || response['resultCode'] || response['message'] || response['result'] end end def authorization_from(action, parameters, response) return nil if response['pspReference'].nil? recurring = response['additionalData']['recurring.recurringDetailReference'] if response['additionalData'] recurring = response['recurringDetailReference'] if action == 'storeToken' "#{parameters[:originalReference]}##{response['pspReference']}##{recurring}" end def init_post(options = {}) post = {} add_merchant_account(post, options) post[:reference] = options[:order_id][0..79] if options[:order_id] post end def post_data(action, parameters = {}) JSON.generate(parameters) end def error_code_from(response) response.dig('additionalData', 'refusalReasonRaw').try(:match, /^([a-zA-Z0-9 ]{1,5})(?=:)/).try(:[], 1).try(:strip) || STANDARD_ERROR_CODE_MAPPING[response['errorCode']] || response['errorCode'] || response['refusalReason'] end def network_transaction_id_from(response) response.dig('additionalData', 'networkTxReference') end def add_browser_info(browser_info, post) return unless browser_info post[:browserInfo] = { acceptHeader: browser_info[:accept_header], colorDepth: browser_info[:depth], javaEnabled: browser_info[:java], language: browser_info[:language], screenHeight: browser_info[:height], screenWidth: browser_info[:width], timeZoneOffset: browser_info[:timezone], userAgent: browser_info[:user_agent] } end def unsupported_failure_response(initial_response) Response.new( false, 'Recurring transactions are not supported for this card type.', initial_response.params, authorization: initial_response.authorization, test: initial_response.test, error_code: initial_response.error_code, avs_result: initial_response.avs_result, cvv_result: initial_response.cvv_result[:code] ) end def card_not_stored?(response) response.authorization ? response.authorization.split('#')[2].nil? : true end end end end