lib/active_merchant/billing/gateways/cyber_source.rb in activemerchant-1.133.0 vs lib/active_merchant/billing/gateways/cyber_source.rb in activemerchant-1.137.0

- old
+ new

@@ -31,10 +31,19 @@ american_express: 'aesk', jcb: 'js', discover: 'pb', diners_club: 'pb' }.freeze + THREEDS_EXEMPTIONS = { + authentication_outage: 'authenticationOutageExemptionIndicator', + corporate_card: 'secureCorporatePaymentIndicator', + delegated_authentication: 'delegatedAuthenticationExemptionIndicator', + low_risk: 'riskAnalysisExemptionIndicator', + low_value: 'lowValueExemptionIndicator', + stored_credential: 'stored_credential', + trusted_merchant: 'trustedMerchantExemptionIndicator' + } DEFAULT_COLLECTION_INDICATOR = 2 self.supported_cardtypes = %i[visa master american_express discover diners_club jcb dankort maestro elo] self.supported_countries = %w(US AE BR CA CN DK FI FR DE IN JP MX NO SE GB SG LB PK) @@ -51,11 +60,12 @@ discover: '004', diners_club: '005', jcb: '007', dankort: '034', maestro: '042', - elo: '054' + elo: '054', + carnet: '058' } @@decision_codes = { accept: 'ACCEPT', review: 'REVIEW' @@ -130,15 +140,20 @@ r701: 'Export bill_country/ship_country match', r702: 'Export email_country match', r703: 'Export hostname_country/ip_country match' } - @@payment_solution = { + @@wallet_payment_solution = { apple_pay: '001', google_pay: '012' } + NT_PAYMENT_SOLUTION = { + 'master' => '014', + 'visa' => '015' + } + # These are the options that can be used when creating a new CyberSource # Gateway object. # # :login => your username # @@ -159,23 +174,35 @@ def initialize(options = {}) requires!(options, :login, :password) super end - def authorize(money, creditcard_or_reference, options = {}) - setup_address_hash(options) - commit(build_auth_request(money, creditcard_or_reference, options), :authorize, money, options) + def authorize(money, payment_method, options = {}) + if valid_payment_method?(payment_method) + setup_address_hash(options) + commit(build_auth_request(money, payment_method, options), :authorize, money, options) + else + # this is for NetworkToken, ApplePay or GooglePay brands that aren't supported at CyberSource + payment_type = payment_method.source.to_s.gsub('_', ' ').titleize.gsub(' ', '') + Response.new(false, "#{card_brand(payment_method).capitalize} is not supported by #{payment_type} at CyberSource, check https://developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/CreatingOnlineAuth/CreatingAuthReqPNT.html") + end end def capture(money, authorization, options = {}) setup_address_hash(options) commit(build_capture_request(money, authorization, options), :capture, money, options) end - def purchase(money, payment_method_or_reference, options = {}) - setup_address_hash(options) - commit(build_purchase_request(money, payment_method_or_reference, options), :purchase, money, options) + def purchase(money, payment_method, options = {}) + if valid_payment_method?(payment_method) + setup_address_hash(options) + commit(build_purchase_request(money, payment_method, options), :purchase, money, options) + else + # this is for NetworkToken, ApplePay or GooglePay brands that aren't supported at CyberSource + payment_type = payment_method.source.to_s.gsub('_', ' ').titleize.gsub(' ', '') + Response.new(false, "#{card_brand(payment_method).capitalize} is not supported by #{payment_type} at CyberSource, check https://developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/CreatingOnlineAuth/CreatingAuthReqPNT.html") + end end def void(identification, options = {}) commit(build_void_request(identification, options), :void, nil, options) end @@ -204,12 +231,18 @@ # Stores a customer subscription/profile with type "on-demand". # To charge the card while creating a profile, pass # options[:setup_fee] => money def store(payment_method, options = {}) - setup_address_hash(options) - commit(build_create_subscription_request(payment_method, options), :store, nil, options) + if valid_payment_method?(payment_method) + setup_address_hash(options) + commit(build_create_subscription_request(payment_method, options), :store, nil, options) + else + # this is for NetworkToken, ApplePay or GooglePay brands that aren't supported at CyberSource + payment_type = payment_method.source.to_s.gsub('_', ' ').titleize.gsub(' ', '') + Response.new(false, "#{card_brand(payment_method).capitalize} is not supported by #{payment_type} at CyberSource, check https://developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/CreatingOnlineAuth/CreatingAuthReqPNT.html") + end end # Updates a customer subscription/profile def update(reference, creditcard, options = {}) requires!(options, :order_id) @@ -270,10 +303,12 @@ gsub(%r((<wsse:Password [^>]*>)[^<]*(</wsse:Password>))i, '\1[FILTERED]\2'). gsub(%r((<accountNumber>)[^<]*(</accountNumber>))i, '\1[FILTERED]\2'). gsub(%r((<cvNumber>)[^<]*(</cvNumber>))i, '\1[FILTERED]\2'). gsub(%r((<cavv>)[^<]*(</cavv>))i, '\1[FILTERED]\2'). gsub(%r((<xid>)[^<]*(</xid>))i, '\1[FILTERED]\2'). + gsub(%r((<networkTokenCryptogram>)[^<]*(</networkTokenCryptogram>))i, '\1[FILTERED]\2'). + gsub(%r((<requestorID>)[^<]*(</requestorID>))i, '\1[FILTERED]\2'). gsub(%r((<authenticationData>)[^<]*(</authenticationData>))i, '\1[FILTERED]\2') end def supports_network_tokenization? true @@ -284,10 +319,16 @@ response.params['reasonCode'] == '102' end private + def valid_payment_method?(payment_method) + return true unless payment_method.is_a?(NetworkTokenizationCreditCard) + + %w(visa master american_express).include?(card_brand(payment_method)) + end + # Create all required address hash key value pairs # If a value of nil is received, that value will be passed on to the gateway and will not be replaced with a default value # Billing address fields received without an override value or with an empty string value will be replaced with the default_address values def setup_address_hash(options) default_address = { @@ -315,21 +356,23 @@ def build_auth_request(money, creditcard_or_reference, options) xml = Builder::XmlMarkup.new indent: 2 add_customer_id(xml, options) add_payment_method_or_subscription(xml, money, creditcard_or_reference, options) - add_other_tax(xml, options) add_threeds_2_ucaf_data(xml, creditcard_or_reference, options) + add_mastercard_network_tokenization_ucaf_data(xml, creditcard_or_reference, options) add_decision_manager_fields(xml, options) + add_other_tax(xml, options) add_mdd_fields(xml, options) add_auth_service(xml, creditcard_or_reference, options) + add_capture_service_fields_with_run_false(xml, options) add_threeds_services(xml, options) add_business_rules_data(xml, creditcard_or_reference, options) add_airline_data(xml, options) add_sales_slip_number(xml, options) - add_payment_network_token(xml) if network_tokenization?(creditcard_or_reference) - add_payment_solution(xml, creditcard_or_reference.source) if network_tokenization?(creditcard_or_reference) + add_payment_network_token(xml, creditcard_or_reference, options) + add_payment_solution(xml, creditcard_or_reference) add_tax_management_indicator(xml, options) add_stored_credential_subsequent_auth(xml, options) add_issuer_additional_data(xml, options) add_partner_solution_id(xml) add_stored_credential_options(xml, options) @@ -378,13 +421,14 @@ def build_purchase_request(money, payment_method_or_reference, options) xml = Builder::XmlMarkup.new indent: 2 add_customer_id(xml, options) add_payment_method_or_subscription(xml, money, payment_method_or_reference, options) - add_other_tax(xml, options) add_threeds_2_ucaf_data(xml, payment_method_or_reference, options) + add_mastercard_network_tokenization_ucaf_data(xml, payment_method_or_reference, options) add_decision_manager_fields(xml, options) + add_other_tax(xml, options) add_mdd_fields(xml, options) if (!payment_method_or_reference.is_a?(String) && card_brand(payment_method_or_reference) == 'check') || reference_is_a_check?(payment_method_or_reference) add_check_service(xml) add_airline_data(xml, options) add_sales_slip_number(xml, options) @@ -396,12 +440,12 @@ add_purchase_service(xml, payment_method_or_reference, options) add_threeds_services(xml, options) add_business_rules_data(xml, payment_method_or_reference, options) add_airline_data(xml, options) add_sales_slip_number(xml, options) - add_payment_network_token(xml) if network_tokenization?(payment_method_or_reference) - add_payment_solution(xml, payment_method_or_reference.source) if network_tokenization?(payment_method_or_reference) + add_payment_network_token(xml, payment_method_or_reference, options) + add_payment_solution(xml, payment_method_or_reference) add_tax_management_indicator(xml, options) add_stored_credential_subsequent_auth(xml, options) add_issuer_additional_data(xml, options) add_partner_solution_id(xml) add_stored_credential_options(xml, options) @@ -485,11 +529,11 @@ if options[:setup_fee] if card_brand(payment_method) == 'check' add_check_service(xml) else add_purchase_service(xml, payment_method, options) - add_payment_network_token(xml) if network_tokenization?(payment_method) + add_payment_network_token(xml, payment_method, options) end end add_subscription_create_service(xml, options) add_business_rules_data(xml, payment_method, options) add_tax_management_indicator(xml, options) @@ -554,26 +598,38 @@ end end end def add_merchant_data(xml, options) - xml.tag! 'merchantID', @options[:login] + xml.tag! 'merchantID', options[:merchant_id] || @options[:login] xml.tag! 'merchantReferenceCode', options[:order_id] || generate_unique_id xml.tag! 'clientLibrary', 'Ruby Active Merchant' xml.tag! 'clientLibraryVersion', VERSION xml.tag! 'clientEnvironment', RUBY_PLATFORM add_merchant_descriptor(xml, options) end def add_merchant_descriptor(xml, options) - return unless options[:merchant_descriptor] || options[:user_po] || options[:taxable] || options[:reference_data_code] || options[:invoice_number] + return unless options[:merchant_descriptor] || + options[:user_po] || + options[:taxable] || + options[:reference_data_code] || + options[:invoice_number] || + options[:merchant_descriptor_city] || + options[:submerchant_id] || + options[:merchant_descriptor_state] || + options[:merchant_descriptor_country] xml.tag! 'invoiceHeader' do xml.tag! 'merchantDescriptor', options[:merchant_descriptor] if options[:merchant_descriptor] + xml.tag! 'merchantDescriptorCity', options[:merchant_descriptor_city] if options[:merchant_descriptor_city] + xml.tag! 'merchantDescriptorState', options[:merchant_descriptor_state] if options[:merchant_descriptor_state] + xml.tag! 'merchantDescriptorCountry', options[:merchant_descriptor_country] if options[:merchant_descriptor_country] xml.tag! 'userPO', options[:user_po] if options[:user_po] xml.tag! 'taxable', options[:taxable] if options[:taxable] + xml.tag! 'submerchantID', options[:submerchant_id] if options[:submerchant_id] xml.tag! 'referenceDataCode', options[:reference_data_code] if options[:reference_data_code] xml.tag! 'invoiceNumber', options[:invoice_number] if options[:invoice_number] end end @@ -675,14 +731,20 @@ xml.tag! 'enabled', options[:decision_manager_enabled] if options[:decision_manager_enabled] xml.tag! 'profile', options[:decision_manager_profile] if options[:decision_manager_profile] end end - def add_payment_solution(xml, source) - return unless (payment_solution = @@payment_solution[source]) + def add_payment_solution(xml, payment_method) + return unless network_tokenization?(payment_method) - xml.tag! 'paymentSolution', payment_solution + case payment_method.source + when :network_token + payment_solution = NT_PAYMENT_SOLUTION[payment_method.brand] + xml.tag! 'paymentSolution', payment_solution if payment_solution + when :apple_pay, :google_pay + xml.tag! 'paymentSolution', @@wallet_payment_solution[payment_method.source] + end end def add_issuer_additional_data(xml, options) return unless options[:issuer_additional_data] @@ -690,11 +752,11 @@ xml.tag! 'additionalData', options[:issuer_additional_data] end end def add_other_tax(xml, options) - return unless options[:local_tax_amount] || options[:national_tax_amount] || options[:national_tax_indicator] + return unless %i[vat_tax_rate local_tax_amount national_tax_amount national_tax_indicator].any? { |gsf| options.include?(gsf) } xml.tag! 'otherTax' do xml.tag! 'vatTaxRate', options[:vat_tax_rate] if options[:vat_tax_rate] xml.tag! 'localTaxAmount', options[:local_tax_amount] if options[:local_tax_amount] xml.tag! 'nationalTaxAmount', options[:national_tax_amount] if options[:national_tax_amount] @@ -729,25 +791,43 @@ end end def add_auth_service(xml, payment_method, options) if network_tokenization?(payment_method) - add_auth_network_tokenization(xml, payment_method, options) + if payment_method.source == :network_token + add_auth_network_tokenization(xml, payment_method, options) + else + add_auth_wallet(xml, payment_method, options) + end else xml.tag! 'ccAuthService', { 'run' => 'true' } do if options[:three_d_secure] add_normalized_threeds_2_data(xml, payment_method, options) + add_threeds_exemption_data(xml, options) if options[:three_ds_exemption_type] else indicator = options[:commerce_indicator] || stored_credential_commerce_indicator(options) xml.tag!('commerceIndicator', indicator) if indicator end + xml.tag!('aggregatorID', options[:aggregator_id]) if options[:aggregator_id] xml.tag!('reconciliationID', options[:reconciliation_id]) if options[:reconciliation_id] + xml.tag!('firstRecurringPayment', options[:first_recurring_payment]) if options[:first_recurring_payment] xml.tag!('mobileRemotePaymentType', options[:mobile_remote_payment_type]) if options[:mobile_remote_payment_type] end end end + def add_threeds_exemption_data(xml, options) + return unless options[:three_ds_exemption_type] + + exemption = options[:three_ds_exemption_type].to_sym + + case exemption + when :authentication_outage, :corporate_card, :delegated_authentication, :low_risk, :low_value, :trusted_merchant + xml.tag!(THREEDS_EXEMPTIONS[exemption], '1') + end + end + def add_incremental_auth_service(xml, authorization, options) xml.tag! 'ccIncrementalAuthService', { 'run' => 'true' } do xml.tag! 'authRequestID', authorization end xml.tag! 'subsequentAuthReason', options[:auth_reason] @@ -791,62 +871,90 @@ xml.tag!('collectionIndicator', options[:collection_indicator] || DEFAULT_COLLECTION_INDICATOR) end end def stored_credential_commerce_indicator(options) - return unless options[:stored_credential] + return unless (reason_type = options.dig(:stored_credential, :reason_type)) - return if options[:stored_credential][:initial_transaction] - - case options[:stored_credential][:reason_type] - when 'installment' then 'install' - when 'recurring' then 'recurring' + case reason_type + when 'installment' + 'install' + when 'recurring' + 'recurring' + else + 'internet' end end def network_tokenization?(payment_method) payment_method.is_a?(NetworkTokenizationCreditCard) end + def subsequent_nt_apple_pay_auth(source, options) + return unless options[:stored_credential] || options[:stored_credential_overrides] + return unless @@wallet_payment_solution[source] + + options.dig(:stored_credential_overrides, :subsequent_auth) || options.dig(:stored_credential, :initiator) == 'merchant' + end + def add_auth_network_tokenization(xml, payment_method, options) - return unless network_tokenization?(payment_method) + commerce_indicator = stored_credential_commerce_indicator(options) || 'internet' + xml.tag! 'ccAuthService', { 'run' => 'true' } do + xml.tag!('networkTokenCryptogram', payment_method.payment_cryptogram) + xml.tag!('commerceIndicator', commerce_indicator) + xml.tag!('reconciliationID', options[:reconciliation_id]) if options[:reconciliation_id] + end + end + def add_auth_wallet(xml, payment_method, options) + commerce_indicator = 'internet' if subsequent_nt_apple_pay_auth(payment_method.source, options) + brand = card_brand(payment_method).to_sym case brand when :visa xml.tag! 'ccAuthService', { 'run' => 'true' } do - xml.tag!('cavv', payment_method.payment_cryptogram) - xml.tag!('commerceIndicator', ECI_BRAND_MAPPING[brand]) - xml.tag!('xid', payment_method.payment_cryptogram) + xml.tag!('cavv', payment_method.payment_cryptogram) unless commerce_indicator + xml.commerceIndicator commerce_indicator.nil? ? ECI_BRAND_MAPPING[brand] : commerce_indicator + xml.tag!('xid', payment_method.payment_cryptogram) unless commerce_indicator xml.tag!('reconciliationID', options[:reconciliation_id]) if options[:reconciliation_id] end when :master - xml.tag! 'ucaf' do - xml.tag!('authenticationData', payment_method.payment_cryptogram) - xml.tag!('collectionIndicator', DEFAULT_COLLECTION_INDICATOR) - end xml.tag! 'ccAuthService', { 'run' => 'true' } do - xml.tag!('commerceIndicator', ECI_BRAND_MAPPING[brand]) + xml.commerceIndicator commerce_indicator.nil? ? ECI_BRAND_MAPPING[brand] : commerce_indicator xml.tag!('reconciliationID', options[:reconciliation_id]) if options[:reconciliation_id] end when :american_express cryptogram = Base64.decode64(payment_method.payment_cryptogram) xml.tag! 'ccAuthService', { 'run' => 'true' } do xml.tag!('cavv', Base64.encode64(cryptogram[0...20])) xml.tag!('commerceIndicator', ECI_BRAND_MAPPING[brand]) xml.tag!('xid', Base64.encode64(cryptogram[20...40])) if cryptogram.bytes.count > 20 xml.tag!('reconciliationID', options[:reconciliation_id]) if options[:reconciliation_id] end - else - raise ArgumentError.new("Payment method #{brand} is not supported, check https://developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/CreatingOnlineAuth/CreatingAuthReqPNT.html") end end - def add_payment_network_token(xml) + def add_mastercard_network_tokenization_ucaf_data(xml, payment_method, options) + return unless network_tokenization?(payment_method) && card_brand(payment_method).to_sym == :master + return if payment_method.source == :network_token + + commerce_indicator = 'internet' if subsequent_nt_apple_pay_auth(payment_method.source, options) + + xml.tag! 'ucaf' do + xml.tag!('authenticationData', payment_method.payment_cryptogram) unless commerce_indicator + xml.tag!('collectionIndicator', DEFAULT_COLLECTION_INDICATOR) + end + end + + def add_payment_network_token(xml, payment_method, options) + return unless network_tokenization?(payment_method) + + transaction_type = payment_method.source == :network_token ? '3' : '1' xml.tag! 'paymentNetworkToken' do - xml.tag!('transactionType', '1') + xml.tag!('requestorID', options[:trid]) if transaction_type == '3' && options[:trid] + xml.tag!('transactionType', transaction_type) end end def add_capture_service(xml, request_id, request_token, options) xml.tag! 'ccCaptureService', { 'run' => 'true' } do @@ -855,14 +963,23 @@ xml.tag! 'gratuityAmount', options[:gratuity_amount] if options[:gratuity_amount] xml.tag! 'reconciliationID', options[:reconciliation_id] if options[:reconciliation_id] end end + def add_capture_service_fields_with_run_false(xml, options) + return unless options[:gratuity_amount] + + xml.tag! 'ccCaptureService', { 'run' => 'false' } do + xml.tag! 'gratuityAmount', options[:gratuity_amount] + end + end + def add_purchase_service(xml, payment_method, options) add_auth_service(xml, payment_method, options) xml.tag! 'ccCaptureService', { 'run' => 'true' } do xml.tag!('reconciliationID', options[:reconciliation_id]) if options[:reconciliation_id] + xml.tag!('gratuityAmount', options[:gratuity_amount]) if options[:gratuity_amount] end end def add_void_service(xml, request_id, request_token) xml.tag! 'voidService', { 'run' => 'true' } do @@ -1001,23 +1118,39 @@ end def add_stored_credential_options(xml, options = {}) return unless options[:stored_credential] || options[:stored_credential_overrides] - stored_credential_subsequent_auth_first = 'true' if options.dig(:stored_credential, :initial_transaction) + stored_credential_subsequent_auth_first = 'true' if cardholder_or_initiated_transaction?(options) stored_credential_transaction_id = options.dig(:stored_credential, :network_transaction_id) if options.dig(:stored_credential, :initiator) == 'merchant' - stored_credential_subsequent_auth_stored_cred = 'true' if options.dig(:stored_credential, :initiator) == 'cardholder' && !options.dig(:stored_credential, :initial_transaction) || options.dig(:stored_credential, :initiator) == 'merchant' && options.dig(:stored_credential, :reason_type) == 'unscheduled' + stored_credential_subsequent_auth_stored_cred = 'true' if subsequent_cardholder_initiated_transaction?(options) || unscheduled_merchant_initiated_transaction?(options) || threeds_stored_credential_exemption?(options) override_subsequent_auth_first = options.dig(:stored_credential_overrides, :subsequent_auth_first) override_subsequent_auth_transaction_id = options.dig(:stored_credential_overrides, :subsequent_auth_transaction_id) override_subsequent_auth_stored_cred = options.dig(:stored_credential_overrides, :subsequent_auth_stored_credential) xml.subsequentAuthFirst override_subsequent_auth_first.nil? ? stored_credential_subsequent_auth_first : override_subsequent_auth_first xml.subsequentAuthTransactionID override_subsequent_auth_transaction_id.nil? ? stored_credential_transaction_id : override_subsequent_auth_transaction_id xml.subsequentAuthStoredCredential override_subsequent_auth_stored_cred.nil? ? stored_credential_subsequent_auth_stored_cred : override_subsequent_auth_stored_cred end + def cardholder_or_initiated_transaction?(options) + options.dig(:stored_credential, :initiator) == 'cardholder' || options.dig(:stored_credential, :initial_transaction) + end + + def subsequent_cardholder_initiated_transaction?(options) + options.dig(:stored_credential, :initiator) == 'cardholder' && !options.dig(:stored_credential, :initial_transaction) + end + + def unscheduled_merchant_initiated_transaction?(options) + options.dig(:stored_credential, :initiator) == 'merchant' && options.dig(:stored_credential, :reason_type) == 'unscheduled' + end + + def threeds_stored_credential_exemption?(options) + options[:three_ds_exemption_type] == THREEDS_EXEMPTIONS[:stored_credential] + end + def add_partner_solution_id(xml) return unless application_id xml.tag!('partnerSolutionID', application_id) end @@ -1066,16 +1199,20 @@ message = message_from(response) authorization = success || in_fraud_review?(response) ? authorization_from(response, action, amount, options) : nil message = auto_void?(authorization_from(response, action, amount, options), response, message, options) - Response.new(success, message, response, + Response.new( + success, + message, + response, test: test?, authorization: authorization, fraud_review: in_fraud_review?(response), avs_result: { code: response[:avsCode] }, - cvv_result: response[:cvCode]) + cvv_result: response[:cvCode] + ) end def auto_void?(authorization, response, message, options = {}) return message unless response[:reasonCode] == '230' && options[:auto_void_230] @@ -1113,9 +1250,10 @@ if /item/.match?(node.parent.name) parent = node.parent.name parent += '_' + node.parent.attributes['id'] if node.parent.attributes['id'] parent += '_' end + reply[:reconciliationID2] = node.text if node.name == 'reconciliationID' && reply[:reconciliationID] reply["#{parent}#{node.name}".to_sym] ||= node.text end return reply end