diff --git a/CHANGELOG b/CHANGELOG index 3bd79adb360..8babce4c02a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -58,6 +58,7 @@ * Worldpay: Add support for Worldpay decrypted apple pay and google pay [dustinhaefele] #5271 * Orbital: Update alternate_ucaf_flow [almalee24] #5282 * Adyen: Remove cryptogram flag [almalee24] #5300 +* Cecabank: Include Apple Pay and Google Pay for recurring payments [gasn150] #5295 == Version 1.137.0 (August 2, 2024) * Unlock dependency on `rexml` to allow fixing a CVE (#5181). diff --git a/lib/active_merchant/billing/gateways/cecabank/cecabank_json.rb b/lib/active_merchant/billing/gateways/cecabank/cecabank_json.rb index e24df79c05b..61014f131dc 100644 --- a/lib/active_merchant/billing/gateways/cecabank/cecabank_json.rb +++ b/lib/active_merchant/billing/gateways/cecabank/cecabank_json.rb @@ -29,6 +29,11 @@ class CecabankJsonGateway < Gateway transaction_risk_analysis_exemption: :TRA }.freeze + WALLET_PAYMENT_METHODS = { + apple_pay: 'A', + google_pay: 'G' + } + self.test_url = 'https://tpv.ceca.es/tpvweb/rest/procesos/' self.live_url = 'https://pgw.ceca.es/tpvweb/rest/procesos/' @@ -113,7 +118,7 @@ def handle_purchase(action, money, creditcard, options) post = { parametros: { accion: CECA_ACTIONS_DICTIONARY[action] } } add_invoice(post, money, options) - add_creditcard(post, creditcard) + add_payment_method(post, creditcard, options) add_stored_credentials(post, creditcard, options) add_three_d_secure(post, options) @@ -159,20 +164,28 @@ def add_invoice(post, money, options) post[:parametros][:exponente] = 2.to_s end - def add_creditcard(post, creditcard) + def add_payment_method(post, payment_method, options) params = post[:parametros] ||= {} + three_d_secure = options.fetch(:three_d_secure, {}) - payment_method = { - pan: creditcard.number, - caducidad: strftime_yyyymm(creditcard) + pm = { + pan: payment_method.number, + caducidad: strftime_yyyymm(payment_method) } - if CreditCard.brand?(creditcard.number) == 'american_express' - payment_method[:csc] = creditcard.verification_value + + if payment_method.is_a?(NetworkTokenizationCreditCard) && WALLET_PAYMENT_METHODS[payment_method.source.to_sym] + pm[:wallet] = { + + # the authentication value should come nil (for recurring cases) or should I remove it? + authentication_value: (payment_method.payment_cryptogram unless options.dig(:stored_credential, :network_transaction_id)), + xid: three_d_secure[:xid] || three_d_secure[:ds_transaction_id] || options[:xid], + walletType: WALLET_PAYMENT_METHODS[payment_method.source.to_sym], + eci: payment_method.eci || (three_d_secure[:eci] if three_d_secure) || '07' + }.compact.to_json else - payment_method[:cvv2] = creditcard.verification_value + pm[CreditCard.brand?(payment_method.number) == 'american_express' ? :csc : :cvv2] = payment_method.verification_value end - - @options[:encryption_key] ? params[:encryptedData] = payment_method : params.merge!(payment_method) + @options[:encryption_key] ? params[:encryptedData] = pm : params.merge!(pm) end def add_stored_credentials(post, creditcard, options) @@ -233,7 +246,6 @@ def commit(action, post) add_encryption(post) add_merchant_data(post) - params_encoded = encode_post_parameters(post) add_signature(post, params_encoded, options) diff --git a/test/remote/gateways/remote_cecabank_rest_json_test.rb b/test/remote/gateways/remote_cecabank_rest_json_test.rb index f0c23f98f7c..1be035da8f3 100644 --- a/test/remote/gateways/remote_cecabank_rest_json_test.rb +++ b/test/remote/gateways/remote_cecabank_rest_json_test.rb @@ -10,7 +10,8 @@ def setup @options = { order_id: generate_unique_id, - three_d_secure: three_d_secure + three_d_secure: three_d_secure, + exemption_type: 'transaction_risk_analysis_exemption' } @cit_options = @options.merge({ @@ -21,6 +22,26 @@ def setup initiator: 'cardholder' } }) + + @apple_pay_network_token = network_tokenization_credit_card( + '4507670001000009', + eci: '05', + payment_cryptogram: 'xgQAAAAAAAAAAAAAAAAAAAAAAAAA', + month: '12', + year: Time.now.year, + source: :apple_pay, + verification_value: '989' + ) + + @google_pay_network_token = network_tokenization_credit_card( + '4507670001000009', + eci: '05', + payment_cryptogram: 'AgAAAAAAAIR8CQrXcIhbQAAAAAA', + month: '10', + year: Time.now.year + 1, + source: :google_pay, + verification_value: nil + ) end def test_successful_authorize @@ -57,6 +78,24 @@ def test_successful_purchase assert_equal %i[codAut numAut referencia], JSON.parse(response.message).symbolize_keys.keys.sort end + def test_successful_purchase_with_apple_pay + assert response = @gateway.purchase(@amount, @apple_pay_network_token, { order_id: generate_unique_id }) + assert_success response + assert_equal %i[codAut numAut referencia], JSON.parse(response.message).symbolize_keys.keys.sort + end + + def test_successful_purchase_with_google_pay + assert response = @gateway.purchase(@amount, @apple_pay_network_token, { order_id: generate_unique_id }) + assert_success response + assert_equal %i[codAut numAut referencia], JSON.parse(response.message).symbolize_keys.keys.sort + end + + def test_failed_purchase_with_apple_pay_sending_three_ds_data + assert response = @gateway.purchase(@amount, @apple_pay_network_token, @options) + assert_failure response + assert_equal response.error_code, '1061' + end + def test_unsuccessful_purchase assert response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response @@ -111,7 +150,7 @@ def test_purchase_using_stored_credential_cit def test_purchase_stored_credential_with_network_transaction_id @cit_options.merge!({ network_transaction_id: '999999999999999' }) - assert purchase = @gateway.purchase(@amount, @credit_card, @cit_options) + assert purchase = @gateway.purchase(@amount, @credit_card, @options) assert_success purchase end @@ -124,6 +163,21 @@ def test_purchase_using_auth_capture_and_stored_credential_cit assert_success capture end + def test_purchase_with_apple_pay_using_stored_credential_recurring_mit + @cit_options[:stored_credential][:reason_type] = 'installment' + assert purchase = @gateway.purchase(@amount, @apple_pay_network_token, @cit_options.except(:three_d_secure)) + assert_success purchase + + options = @cit_options.except(:three_d_secure, :extra_options_for_three_d_secure) + options[:stored_credential][:reason_type] = 'recurring' + options[:stored_credential][:initiator] = 'merchant' + options[:stored_credential][:network_transaction_id] = purchase.network_transaction_id + options[:order_id] = generate_unique_id + + assert purchase2 = @gateway.purchase(@amount, @apple_pay_network_token, options) + assert_success purchase2 + end + def test_purchase_using_stored_credential_recurring_mit @cit_options[:stored_credential][:reason_type] = 'installment' assert purchase = @gateway.purchase(@amount, @credit_card, @cit_options) @@ -133,6 +187,7 @@ def test_purchase_using_stored_credential_recurring_mit options[:stored_credential][:reason_type] = 'recurring' options[:stored_credential][:initiator] = 'merchant' options[:stored_credential][:network_transaction_id] = purchase.network_transaction_id + options[:order_id] = generate_unique_id assert purchase2 = @gateway.purchase(@amount, @credit_card, options) assert_success purchase2 @@ -170,12 +225,14 @@ def get_response_params(transcript) def three_d_secure { version: '2.2.0', - eci: '02', - cavv: '4F80DF50ADB0F9502B91618E9B704790EABA35FDFC972DDDD0BF498C6A75E492', + eci: '07', ds_transaction_id: 'a2bf089f-cefc-4d2c-850f-9153827fe070', acs_transaction_id: '18c353b0-76e3-4a4c-8033-f14fe9ce39dc', - authentication_response_status: 'Y', - three_ds_server_trans_id: '9bd9aa9c-3beb-4012-8e52-214cccb25ec5' + authentication_response_status: 'I', + three_ds_server_trans_id: '9bd9aa9c-3beb-4012-8e52-214cccb25ec5', + enrolled: 'true', + cavv: 'AJkCC1111111111122222222AAAA', + xid: '22222' } end end diff --git a/test/unit/gateways/cecabank_rest_json_test.rb b/test/unit/gateways/cecabank_rest_json_test.rb index 913e171e054..9b9cd524819 100644 --- a/test/unit/gateways/cecabank_rest_json_test.rb +++ b/test/unit/gateways/cecabank_rest_json_test.rb @@ -13,6 +13,13 @@ def setup initiator_vector: '0000000000000000' ) + @no_encrypted_gateway = CecabankJsonGateway.new( + merchant_id: '12345678', + acquirer_bin: '12345678', + terminal_id: '00000003', + cypher_key: 'enc_key' + ) + @credit_card = credit_card @amex_card = credit_card('374245455400001', { month: 10, year: Time.now.year + 1, verification_value: '1234' }) @amount = 100 @@ -31,6 +38,26 @@ def setup authentication_response_status: 'Y', three_ds_server_trans_id: '9bd9aa9c-3beb-4012-8e52-214cccb25ec5' } + + @apple_pay_network_token = network_tokenization_credit_card( + '4507670001000009', + eci: '05', + payment_cryptogram: 'xgQAAAAAAAAAAAAAAAAAAAAAAAAA', + month: '12', + year: Time.now.year, + source: :apple_pay, + verification_value: '989' + ) + + @google_pay_network_token = network_tokenization_credit_card( + '4507670001000009', + eci: '05', + payment_cryptogram: 'xgQAAAAAAAAAAAAAAAAAAAAAAAAA', + month: '12', + year: Time.now.year, + source: :google_pay, + verification_value: '989' + ) end def test_successful_authorize @@ -149,6 +176,38 @@ def test_purchase_without_exemption_type end.respond_with(successful_purchase_response) end + def test_successful_purchase_with_apple_pay + stub_comms(@no_encrypted_gateway, :ssl_post) do + @no_encrypted_gateway.purchase(@amount, @apple_pay_network_token, @options.merge(xid: 'some_xid')) + end.check_request do |_endpoint, data, _headers| + data = JSON.parse(data) + params = JSON.parse(Base64.decode64(data['parametros'])) + common_ap_gp_assertions(params, @apple_pay_network_token, wallet_type: 'A') + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_google_pay + stub_comms(@no_encrypted_gateway, :ssl_post) do + @no_encrypted_gateway.purchase(@amount, @google_pay_network_token, @options.merge(xid: 'some_xid')) + end.check_request do |_endpoint, data, _headers| + data = JSON.parse(data) + params = JSON.parse(Base64.decode64(data['parametros'])) + common_ap_gp_assertions(params, @google_pay_network_token, wallet_type: 'G') + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_apple_pay_encrypted_gateway + stub_comms do + @gateway.purchase(@amount, @apple_pay_network_token, @options.merge(xid: 'some_xid')) + end.check_request do |_endpoint, data, _headers| + data = JSON.parse(data) + encryoted_params = JSON.parse(Base64.decode64(data['parametros'])) + sensitive_json = decrypt_sensitive_fields(@gateway.options, encryoted_params['encryptedData']) + sensitive_params = JSON.parse(sensitive_json) + common_ap_gp_assertions(sensitive_params, @apple_pay_network_token, wallet_type: 'A') + end.respond_with(successful_purchase_response) + end + def test_purchase_with_low_value_exemption @options[:exemption_type] = 'low_value_exemption' @options[:three_d_secure] = @three_d_secure @@ -217,6 +276,15 @@ def decrypt_sensitive_fields(options, data) cipher.update([data].pack('H*')) + cipher.final end + def common_ap_gp_assertions(params, payment_method, wallet_type) + assert_include params, 'wallet' + assert_equal params['pan'], payment_method.number + wallet = JSON.parse(params['wallet']) + assert_equal wallet['authentication_value'], payment_method.payment_cryptogram + assert_equal wallet['xid'], 'some_xid' + assert_equal wallet['eci'], payment_method.eci + end + def transcript <<~RESPONSE "opening connection to tpv.ceca.es:443...\nopened\nstarting SSL for tpv.ceca.es:443...\nSSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384\n<- \"POST /tpvweb/rest/procesos/compra HTTP/1.1\\r\\nContent-Type: application/json\\r\\nHost: tpv.ceca.es\\r\\nConnection: close\\r\\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\\r\\nAccept: */*\\r\\nUser-Agent: Ruby\\r\\nContent-Length: 1397\\r\\n\\r\\n\"\n<- \"{\\\"parametros\\\":\\\"eyJhY2Npb24iOiJSRVNUX0FVVE9SSVpBQ0lPTiIsIm51bU9wZXJhY2lvbiI6ImYxZDdlNjBlMDYzMTJiNjI5NDEzOTUxM2YwMGQ2YWM4IiwiaW1wb3J0ZSI6IjEwMCIsInRpcG9Nb25lZGEiOiI5NzgiLCJleHBvbmVudGUiOiIyIiwiZW5jcnlwdGVkRGF0YSI6IjhlOWZhY2RmMDk5NDFlZTU0ZDA2ODRiNDNmNDNhMmRmOGM4ZWE5ODlmYTViYzYyOTM4ODFiYWVjNDFiYjU4OGNhNDc3MWI4OTFmNTkwMWVjMmJhZmJhOTBmMDNkM2NiZmUwNTJlYjAzMDU4Zjk1MGYyNzY4YTk3OWJiZGQxNmJlZmIyODQ2Zjc2MjkyYTFlODYzMDNhNTVhYTIzNjZkODA5MDEyYzlhNzZmYTZiOTQzOWNlNGQ3MzY5NTYwOTNhMDAwZTk5ZDMzNmVhZDgwMjBmOTk5YjVkZDkyMTFjMjE5ZWRhMjVmYjVkZDY2YzZiOTMxZWY3MjY5ZjlmMmVjZGVlYTc2MWRlMDEyZmFhMzg3MDlkODcyNTI4ODViYjI1OThmZDI2YTQzMzNhNDEwMmNmZTg4YjM1NTJjZWU0Yzc2IiwiZXhlbmNpb25TQ0EiOiJOT05FIiwiVGhyZWVEc1Jlc3BvbnNlIjoie1wiZXhlbXB0aW9uX3R5cGVcIjpudWxsLFwidGhyZWVfZHNfdmVyc2lvblwiOlwiMi4yLjBcIixcImRpcmVjdG9yeV9zZXJ2ZXJfdHJhbnNhY3Rpb25faWRcIjpcImEyYmYwODlmLWNlZmMtNGQyYy04NTBmLTkxNTM4MjdmZTA3MFwiLFwiYWNzX3RyYW5zYWN0aW9uX2lkXCI6XCIxOGMzNTNiMC03NmUzLTRhNGMtODAzMy1mMTRmZTljZTM5ZGNcIixcImF1dGhlbnRpY2F0aW9uX3Jlc3BvbnNlX3N0YXR1c1wiOlwiWVwiLFwidGhyZWVfZHNfc2VydmVyX3RyYW5zX2lkXCI6XCI5YmQ5YWE5Yy0zYmViLTQwMTItOGU1Mi0yMTRjY2NiMjVlYzVcIixcImVjb21tZXJjZV9pbmRpY2F0b3JcIjpcIjAyXCIsXCJlbnJvbGxlZFwiOm51bGwsXCJhbW91bnRcIjpcIjEwMFwifSIsIm1lcmNoYW50SUQiOiIxMDY5MDA2NDAiLCJhY3F1aXJlckJJTiI6IjAwMDA1NTQwMDAiLCJ0ZXJtaW5hbElEIjoiMDAwMDAwMDMifQ==\\\",\\\"cifrado\\\":\\\"SHA2\\\",\\\"firma\\\":\\\"ac7e5eb06b675be6c6f58487bbbaa1ddc07518e216cb0788905caffd911eea87\\\"}\"\n-> \"HTTP/1.1 200 OK\\r\\n\"\n-> \"Date: Thu, 14 Dec 2023 15:52:41 GMT\\r\\n\"\n-> \"Server: Apache\\r\\n\"\n-> \"Strict-Transport-Security: max-age=31536000; includeSubDomains\\r\\n\"\n-> \"X-XSS-Protection: 1; mode=block\\r\\n\"\n-> \"X-Content-Type-Options: nosniff\\r\\n\"\n-> \"Content-Length: 103\\r\\n\"\n-> \"Connection: close\\r\\n\"\n-> \"Content-Type: application/json\\r\\n\"\n-> \"\\r\\n\"\nreading 103 bytes...\n-> \"{\\\"cifrado\\\":\\\"SHA2\\\",\\\"parametros\\\":\\\"eyJudW1BdXQiOiIxMDEwMDAiLCJyZWZlcmVuY2lhIjoiMTIwMDQzOTQ4MzIzMTIxNDE2NDg0NjYwMDcwMDAiLCJjb2RBdXQiOiIwMDAifQ==\\\",\\\"firma\\\":\\\"5ce066be8892839d6aa6da15405c9be8987642f4245fac112292084a8532a538\\\",\\\"fecha\\\":\\\"231214164846089\\\",\\\"idProceso\\\":\\\"106900640-adeda8b09b84630d6247b53748ab9c66\\\"}\"\nread 300 bytes\nConn close\n"