From d579b2423d2abb05cbdd65ce261593f3476b12ae Mon Sep 17 00:00:00 2001 From: Dustin A Haefele <45601251+DustinHaefele@users.noreply.github.com> Date: Thu, 10 Oct 2024 10:22:42 -0400 Subject: [PATCH] WorldPay: Add support for encrypted ApplePay and GooglePay (#5271) updated elevon and global collect to receive an object for payment_data Co-authored-by: Alma Malambo --- CHANGELOG | 1 + lib/active_merchant/billing/credit_card.rb | 12 +++ .../billing/gateways/elavon.rb | 2 +- .../billing/gateways/global_collect.rb | 4 +- .../billing/gateways/worldpay.rb | 59 ++++++++++++-- .../network_tokenization_credit_card.rb | 4 + test/unit/gateways/elavon_test.rb | 8 +- test/unit/gateways/global_collect_test.rb | 8 +- test/unit/gateways/worldpay_test.rb | 76 ++++++++++++++++++- 9 files changed, 153 insertions(+), 21 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4af1e06f6c6..78e11e004e2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -55,6 +55,7 @@ * Worldpay: Add customStringFields [jcreiff] #5284 * Airwallex: truncate descriptor field to 32 characters [jcreiff] #5292 * DLocal: Add the description field for refund [yunnydang] #5296 +* Worldpay: Add support for Worldpay decrypted apple pay and google pay [dustinhaefele] #5271 == Version 1.137.0 (August 2, 2024) * Unlock dependency on `rexml` to allow fixing a CVE (#5181). diff --git a/lib/active_merchant/billing/credit_card.rb b/lib/active_merchant/billing/credit_card.rb index 95e7ae5ce38..d6b0dd6bb1a 100644 --- a/lib/active_merchant/billing/credit_card.rb +++ b/lib/active_merchant/billing/credit_card.rb @@ -363,6 +363,18 @@ def allow_spaces_in_card?(number = nil) BRANDS_WITH_SPACES_IN_NUMBER.include?(self.class.brand?(self.number || number)) end + def network_token? + false + end + + def mobile_wallet? + false + end + + def encrypted_wallet? + false + end + private def filter_number(value) diff --git a/lib/active_merchant/billing/gateways/elavon.rb b/lib/active_merchant/billing/gateways/elavon.rb index 04583338366..d8c0ce5c938 100644 --- a/lib/active_merchant/billing/gateways/elavon.rb +++ b/lib/active_merchant/billing/gateways/elavon.rb @@ -218,7 +218,7 @@ def add_txn_id(xml, authorization) end def add_network_token(xml, payment_method) - payment = payment_method.payment_data&.gsub('=>', ':') + payment = payment_method.payment_data.to_s&.gsub('=>', ':') case payment_method.source when :apple_pay xml.ssl_applepay_web url_encode(payment) diff --git a/lib/active_merchant/billing/gateways/global_collect.rb b/lib/active_merchant/billing/gateways/global_collect.rb index 466b2be2de5..8e68c325267 100644 --- a/lib/active_merchant/billing/gateways/global_collect.rb +++ b/lib/active_merchant/billing/gateways/global_collect.rb @@ -297,7 +297,7 @@ def add_mobile_credit_card(post, payment, options, specifics_inputs, expirydate) post['mobilePaymentMethodSpecificInput'] = specifics_inputs if options[:use_encrypted_payment_data] - post['mobilePaymentMethodSpecificInput']['encryptedPaymentData'] = payment.payment_data + post['mobilePaymentMethodSpecificInput']['encryptedPaymentData'] = payment.payment_data.to_s&.gsub('=>', ':') else add_decrypted_payment_data(post, payment, options, expirydate) end @@ -315,7 +315,7 @@ def add_decrypted_payment_data(post, payment, options, expirydate) 'dpan' => payment.number } when :google_pay - payment.payment_data + payment.payment_data.to_s&.gsub('=>', ':') end post['mobilePaymentMethodSpecificInput']["#{data_type}PaymentData"] = data if data diff --git a/lib/active_merchant/billing/gateways/worldpay.rb b/lib/active_merchant/billing/gateways/worldpay.rb index e36e88fe821..0ee1a7107e5 100644 --- a/lib/active_merchant/billing/gateways/worldpay.rb +++ b/lib/active_merchant/billing/gateways/worldpay.rb @@ -630,6 +630,8 @@ 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 :encrypted_wallet + add_encrypted_wallet(xml, payment_method) when :network_token add_network_tokenization_card(xml, payment_method, options) else @@ -683,6 +685,37 @@ def merchant_initiated?(options) options.dig(:stored_credential, :initiator) == 'merchant' end + def add_encrypted_wallet(xml, payment_method) + source = encrypted_wallet_source(payment_method.source) + + xml.paymentDetails do + xml.tag! "#{source}-SSL" do + if source == 'APPLEPAY' + add_encrypted_apple_pay(xml, payment_method) + else + add_encrypted_google_pay(xml, payment_method) + end + end + end + end + + def add_encrypted_apple_pay(xml, payment_method) + xml.header do + xml.ephemeralPublicKey payment_method.payment_data.dig(:header, :ephemeralPublicKey) + xml.publicKeyHash payment_method.payment_data.dig(:header, :publicKeyHash) + xml.transactionId payment_method.payment_data.dig(:header, :transactionId) + end + xml.signature payment_method.payment_data[:signature] + xml.version payment_method.payment_data[:version] + xml.data payment_method.payment_data[:data] + end + + def add_encrypted_google_pay(xml, payment_method) + xml.protocolVersion payment_method.payment_data[:version] + xml.signature payment_method.payment_data[:signature] + xml.signedMessage payment_method.payment_data[:signed_message] + end + def add_card_or_token(xml, payment_method, options) xml.paymentDetails credit_fund_transfer_attribute(options) do if options[:payment_type] == :token @@ -1066,16 +1099,17 @@ def payment_details(payment_method, options = {}) when String token_type_and_details(payment_method) else - type = network_token?(payment_method) || wallet_type_google_pay?(options) || payment_method_apple_pay?(payment_method) ? :network_token : :credit - - { payment_type: type } + payment_method_type(payment_method, options) end end - def network_token?(payment_method) - payment_method.respond_to?(:source) && - payment_method.respond_to?(:payment_cryptogram) && - payment_method.respond_to?(:eci) + def payment_method_type(payment_method, options) + type = if payment_method.is_a?(NetworkTokenizationCreditCard) + payment_method.encrypted_wallet? ? :encrypted_wallet : :network_token + else + wallet_type_google_pay?(options) ? :network_token : :credit + end + { payment_type: type } end def payment_method_apple_pay?(payment_method) @@ -1129,6 +1163,17 @@ def generate_stored_credential_params(is_initial_transaction, reason = nil, init stored_credential_params[customer_or_merchant] = reason if reason stored_credential_params end + + def encrypted_wallet_source(source) + case source + when :apple_pay + 'APPLEPAY' + when :google_pay + 'PAYWITHGOOGLE' + else + raise ArgumentError, 'Invalid encrypted wallet source' + end + end end end end diff --git a/lib/active_merchant/billing/network_tokenization_credit_card.rb b/lib/active_merchant/billing/network_tokenization_credit_card.rb index 55a2d97cde6..49edd0e4e3c 100644 --- a/lib/active_merchant/billing/network_tokenization_credit_card.rb +++ b/lib/active_merchant/billing/network_tokenization_credit_card.rb @@ -39,6 +39,10 @@ def mobile_wallet? %i[apple_pay android_pay google_pay].include?(source) end + def encrypted_wallet? + payment_data.present? + end + def type 'network_tokenization' end diff --git a/test/unit/gateways/elavon_test.rb b/test/unit/gateways/elavon_test.rb index 3fb341c87bc..f58b4cf05b0 100644 --- a/test/unit/gateways/elavon_test.rb +++ b/test/unit/gateways/elavon_test.rb @@ -36,12 +36,12 @@ def setup @google_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ source: :google_pay, - payment_data: "{ 'version': 'EC_v1', 'data': 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9'}" + payment_data: { 'version' => 'EC_v1', 'data' => 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9' } }) @apple_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ source: :apple_pay, - payment_data: "{ 'version': 'EC_v1', 'data': 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9'}" + payment_data: { 'version' => 'EC_v1', 'data' => 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9' } }) end @@ -163,7 +163,7 @@ def test_successful_purchase_with_apple_pay stub_comms do @gateway.purchase(@amount, @apple_pay, @options) end.check_request do |_endpoint, data, _headers| - assert_match(/%7B %27version%27%3A %27EC_v1%27%2C %27data%27%3A %27QlzLxRFnNP9%2FGTaMhBwgmZ2ywntbr9%27%7D<\/ssl_applepay_web>/, data) + assert_match(/%7B%22version%22%3A%22EC_v1%22%2C %22data%22%3A%22QlzLxRFnNP9%2FGTaMhBwgmZ2ywntbr9%22%7D<\/ssl_applepay_web>/, data) end.respond_with(successful_purchase_response) end @@ -171,7 +171,7 @@ def test_successful_purchase_with_google_pay stub_comms do @gateway.purchase(@amount, @google_pay, @options) end.check_request do |_endpoint, data, _headers| - assert_match(/%7B %27version%27%3A %27EC_v1%27%2C %27data%27%3A %27QlzLxRFnNP9%2FGTaMhBwgmZ2ywntbr9%27%7D<\/ssl_google_pay>/, data) + assert_match(/%7B%22version%22%3A%22EC_v1%22%2C %22data%22%3A%22QlzLxRFnNP9%2FGTaMhBwgmZ2ywntbr9%22%7D<\/ssl_google_pay>/, data) end.respond_with(successful_purchase_response) end diff --git a/test/unit/gateways/global_collect_test.rb b/test/unit/gateways/global_collect_test.rb index 964e2739e83..ce8703e1916 100644 --- a/test/unit/gateways/global_collect_test.rb +++ b/test/unit/gateways/global_collect_test.rb @@ -22,7 +22,7 @@ def setup @google_pay_network_token = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ source: :google_pay, - payment_data: "{ 'version': 'EC_v1', 'data': 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9'}" + payment_data: { 'version' => 'EC_v1', 'data' => 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9' } }) @declined_card = credit_card('5424180279791732') @@ -91,14 +91,14 @@ def test_successful_purchase_with_requires_approval_true def test_purchase_request_with_encrypted_google_pay google_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ source: :google_pay, - payment_data: "{ 'version': 'EC_v1', 'data': 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9'}" + payment_data: { 'version' => 'EC_v1', 'data' => 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9' } }) stub_comms(@gateway, :ssl_request) do @gateway.purchase(@accepted_amount, google_pay, { use_encrypted_payment_data: true }) end.check_request(skip_response: true) do |_method, _endpoint, data, _headers| assert_equal '320', JSON.parse(data)['mobilePaymentMethodSpecificInput']['paymentProductId'] - assert_equal google_pay.payment_data, JSON.parse(data)['mobilePaymentMethodSpecificInput']['encryptedPaymentData'] + assert_equal google_pay.payment_data.to_s&.gsub('=>', ':'), JSON.parse(data)['mobilePaymentMethodSpecificInput']['encryptedPaymentData'] end end @@ -131,7 +131,7 @@ def test_add_payment_for_google_pay assert_includes post.keys.first, 'mobilePaymentMethodSpecificInput' assert_equal post['mobilePaymentMethodSpecificInput']['paymentProductId'], '320' assert_equal post['mobilePaymentMethodSpecificInput']['authorizationMode'], 'FINAL_AUTHORIZATION' - assert_equal post['mobilePaymentMethodSpecificInput']['encryptedPaymentData'], @google_pay_network_token.payment_data + assert_equal post['mobilePaymentMethodSpecificInput']['encryptedPaymentData'], @google_pay_network_token.payment_data.to_s&.gsub('=>', ':') end def test_add_payment_for_apple_pay diff --git a/test/unit/gateways/worldpay_test.rb b/test/unit/gateways/worldpay_test.rb index c4319def030..812b76751c5 100644 --- a/test/unit/gateways/worldpay_test.rb +++ b/test/unit/gateways/worldpay_test.rb @@ -186,9 +186,9 @@ def test_payment_type_for_network_card assert_equal payment, :network_token end - def test_payment_type_returns_network_token_if_the_payment_method_responds_to_source_payment_cryptogram_and_eci - payment_method = mock - payment_method.stubs(source: nil, payment_cryptogram: nil, eci: nil) + def test_payment_type_returns_network_token_if_payment_method_is_nt_credit_card_and_not_encrypted + payment_method = @nt_credit_card + payment_method.stubs(source: nil, payment_cryptogram: nil, eci: nil, payment_data: nil) result = @gateway.send(:payment_details, payment_method) assert_equal({ payment_type: :network_token }, result) end @@ -243,6 +243,76 @@ def test_successful_authorize_without_name assert_equal 'R50704213207145707', response.authorization end + def test_successful_authorize_encrypted_apple_pay + apple_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + source: :apple_pay, + payment_data: { + version: 'EC_v1', + data: 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9', + signature: 'signature', + header: { + ephemeralPublicKey: 'ephemeralPublicKey', + publicKeyHash: 'publicKeyHash', + transactionId: 'transactionId' + } + } + }) + + stub_comms do + @gateway.authorize(@amount, apple_pay, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/APPLEPAY-SSL/, data) + assert_match(%r(EC_v1), data) + assert_match(%r(transactionId), data) + assert_match(%r(QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9), data) + end.respond_with(successful_authorize_response) + end + + def test_successful_authorize_encrypted_google_pay + google_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + source: :google_pay, + payment_data: { + version: 'EC_v1', + data: 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9', + signature: 'signature', + header: { + ephemeralPublicKey: 'ephemeralPublicKey', + publicKeyHash: 'publicKeyHash', + transactionId: 'transactionId' + } + } + }) + + stub_comms do + @gateway.authorize(@amount, google_pay, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/PAYWITHGOOGLE-SSL/, data) + assert_match(%r(EC_v1), data) + assert_match(%r(signature), data) + end.respond_with(successful_authorize_response) + end + + def test_encrypted_payment_with_invalid_source + apple_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + source: :android_pay, + payment_data: { + version: 'EC_v1', + data: 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9', + signature: 'signature', + header: { + ephemeralPublicKey: 'ephemeralPublicKey', + publicKeyHash: 'publicKeyHash', + transactionId: 'transactionId' + } + } + }) + error = assert_raises(ArgumentError) do + @gateway.authorize(@amount, apple_pay, @options) + end + + assert_equal 'Invalid encrypted wallet source', error.message + end + def test_successful_authorize_by_reference response = stub_comms do @gateway.authorize(@amount, @options[:order_id].to_s, @options)