Skip to content

fix(authentication): update payment status in case of 2xx authenticat… - #13735

Open
bsayak03 wants to merge 3 commits into
mainfrom
netcetera-auth-2xx-failure-status-update
Open

fix(authentication): update payment status in case of 2xx authenticat…#13735
bsayak03 wants to merge 3 commits into
mainfrom
netcetera-auth-2xx-failure-status-update

Conversation

@bsayak03

@bsayak03 bsayak03 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

…ion failure

Type of Change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring
  • Dependency updates
  • Documentation
  • CI/CD

Description

Previously, payment_external_authentication only propagated the connector's authentication result back to the caller without inspecting it — if the authentication connector (e.g. Netcetera) returned a transaction status indicating failure, or the call to perform_authentication itself errored out, the payment_attempt and payment_intent were left untouched. This meant a payment could get stuck in a non-terminal state (e.g. requires_customer_action) even though 3DS authentication had already definitively failed, requiring a separate sync/retry to notice and correct the mismatch.

Net effect: whenever external 3DS authentication concludes in failure — whether the connector returns a 2xx with a failing trans_status or the authentication call errors outright — the payment attempt and intent are moved to AuthenticationFailed / Failed synchronously, instead of being left in a stale in-progress state.

Additional Changes

  • This PR modifies the API contract
  • This PR modifies the database schema
  • This PR modifies application configuration/environment variables

Motivation and Context

Authentication connectors can return an HTTP 2xx response that still carries a failed 3DS transaction status (or can fail the request outright), and the payment status wasn't being updated to reflect this. This left payments stuck in requires_customer_action instead of moving to a terminal failed state, which is confusing for merchants/customers and can block correct downstream handling (e.g. retries, webhooks, reconciliation).

How did you test it?

Note: This failure is simulated by hardcoding the threeDsServerTransId to a random value in the Authentication Request

  1. Cybersource payments create
curl --location 'http://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_wuhfaFY6wsE3197o3wv67DJ1zYdX2zWNuuwrO1PErdwZ1jEe3nSTZWQmzhYrw0sU' \
--data-raw '{
    "amount": 10,
    "payment_type": "new_mandate",
    "setup_future_usage": "off_session",
    "customer_acceptance": {
        "acceptance_type": "offline",
        "accepted_at": "1963-05-03T04:07:52.723Z",
        "online": {
            "ip_address": "in sit",
            "user_agent": "amet irure esse"
        }
    },
    
    "force_3ds_challenge": true,
    "currency": "USD",
    "confirm": false,
    "capture_method": "automatic",
    "capture_on": "2022-09-10T10:11:12Z",
    "customer_id": "StripeCustomer",
    "email": "guest@example.com",
    "name": "John Doe",
    "phone": "999999999",
    "phone_country_code": "+65",
    "description": "Its my first payment request",
    "authentication_type": "three_ds",
    "return_url": "https://duck.com",
    "billing": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "CA",
            "zip": "94122",
            "country": "US",
            "first_name": "PiX",
            "last_name": "abc"
        },
        "phone": {
            "number": "123456789",
            "country_code": "12"
        }
    },
    "shipping": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "CA",
            "zip": "94122",
            "country": "US",
            "first_name": "PiX",
            "last_name": "Poddar"
        },
        "phone": {
            "number": "123456789",
            "country_code": "12"
        }
    },
    "request_external_three_ds_authentication": true,
    "statement_descriptor_name": "joseph",
    "statement_descriptor_suffix": "JS",
    "metadata": {
        "udf1": "value1",
        "new_customer": "true",
        "login_date": "2019-09-10T10:11:12Z"
    }
}'

Response

{
    "payment_id": "pay_gqbZmtDMLxQvXgTa9UCm",
    "merchant_id": "merchant_1786963900",
    "status": "requires_payment_method",
    "amount": 10,
    "net_amount": 10,
    "shipping_cost": null,
    "amount_capturable": 0,
    "amount_received": null,
    "processor_merchant_id": "merchant_1786963900",
    "initiator": null,
    "sdk_authorization": "cHJvZmlsZV9pZD1wcm9fSTUwVEtQYjE5THRWQVVGYkx4MWsscHVibGlzaGFibGVfa2V5PXBrX2Rldl9hNmNmZjQwZWI4Yjc0YWE3OGZhNjc0Y2M5YmE2OGM4YSxjbGllbnRfc2VjcmV0PXBheV9ncWJabXRETUx4UXZYZ1RhOVVDbV9zZWNyZXRfdTlkZXlBSER2TkVETEJEdlEwbTgsY3VzdG9tZXJfaWQ9U3RyaXBlQ3VzdG9tZXIsY2xpZW50X3Nlc3Npb25faWQ9Y2xpZW50X3Nlc3NfWTFXM1I1d0N3TVlQSnNIUnYxc0cscGF5bWVudF9pZD1wYXlfZ3FiWm10RE1MeFF2WGdUYTlVQ20=",
    "connector": null,
    "state_metadata": null,
    "client_secret": "pay_gqbZmtDMLxQvXgTa9UCm_secret_u9deyAHDvNEDLBDvQ0m8",
    "created": "2026-08-17T11:02:30.185Z",
    "modified_at": "2026-08-17T11:02:30.292Z",
    "connector_customer_id": null,
    "currency": "USD",
    "customer_id": "StripeCustomer",
    "customer": {
        "id": "StripeCustomer",
        "name": "John Doe",
        "email": "guest@example.com",
        "phone": "999999999",
        "phone_country_code": "+65",
        "customer_document_details": null
    },
    "description": "Its my first payment request",
    "refunds": null,
    "disputes": null,
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": "off_session",
    "off_session": null,
    "capture_on": null,
    "capture_method": "automatic",
    "payment_method": null,
    "payment_method_data": null,
    "payment_token": null,
    "shipping": {
        "address": {
            "city": "San Fransico",
            "country": "US",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "CA",
            "first_name": "PiX",
            "last_name": "Poddar",
            "origin_zip": null
        },
        "phone": {
            "number": "123456789",
            "country_code": "12"
        },
        "email": null
    },
    "billing": {
        "address": {
            "city": "San Fransico",
            "country": "US",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "CA",
            "first_name": "PiX",
            "last_name": "abc",
            "origin_zip": null
        },
        "phone": {
            "number": "123456789",
            "country_code": "12"
        },
        "email": null
    },
    "order_details": null,
    "email": "guest@example.com",
    "name": "John Doe",
    "phone": "999999999",
    "return_url": "https://duck.com/",
    "authentication_type": "three_ds",
    "statement_descriptor_name": "joseph",
    "statement_descriptor_suffix": "JS",
    "next_action": null,
    "cancellation_reason": null,
    "error_code": null,
    "error_message": null,
    "unified_code": null,
    "unified_message": null,
    "error_details": null,
    "payment_experience": null,
    "payment_method_type": null,
    "connector_label": null,
    "business_country": null,
    "business_label": "default",
    "business_sub_label": null,
    "allowed_payment_method_types": null,
    "manual_retry_allowed": null,
    "connector_transaction_id": null,
    "frm_message": null,
    "metadata": {
        "udf1": "value1",
        "login_date": "2019-09-10T10:11:12Z",
        "new_customer": "true"
    },
    "connector_metadata": null,
    "connector_response_metadata": null,
    "feature_metadata": null,
    "reference_id": null,
    "payment_link": null,
    "profile_id": "pro_I50TKPb19LtVAUFbLx1k",
    "surcharge_details": null,
    "attempt_count": 1,
    "merchant_decision": null,
    "merchant_connector_id": null,
    "incremental_authorization_allowed": null,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": null,
    "external_3ds_authentication_attempted": false,
    "expires_on": "2026-08-17T11:17:30.185Z",
    "fingerprint": null,
    "browser_info": null,
    "payment_channel": null,
    "payment_method_id": null,
    "network_transaction_id": null,
    "network_transaction_link_id": null,
    "payment_method_status": null,
    "updated": "2026-08-17T11:02:30.292Z",
    "split_payments": null,
    "frm_metadata": null,
    "extended_authorization_applied": null,
    "extended_authorization_last_applied_at": null,
    "request_extended_authorization": null,
    "capture_before": null,
    "merchant_order_reference_id": null,
    "order_tax_amount": null,
    "connector_mandate_id": null,
    "card_discovery": null,
    "force_3ds_challenge": true,
    "force_3ds_challenge_trigger": true,
    "issuer_error_code": null,
    "issuer_error_message": null,
    "is_iframe_redirection_enabled": null,
    "whole_connector_response": null,
    "enable_partial_authorization": null,
    "enable_overcapture": null,
    "is_overcapture_enabled": null,
    "network_details": null,
    "is_stored_credential": null,
    "mit_category": null,
    "billing_descriptor": null,
    "tokenization": null,
    "partner_merchant_identifier_details": null,
    "payment_method_tokenization_details": null,
    "installment_options": null,
    "installment_data": null,
    "sender_payment_instrument_id": null
}
  1. Cybersource payments confirm
curl --location 'http://localhost:8080/payments/pay_gqbZmtDMLxQvXgTa9UCm/confirm' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_wuhfaFY6wsE3197o3wv67DJ1zYdX2zWNuuwrO1PErdwZ1jEe3nSTZWQmzhYrw0sU' \
--data '{
    "browser_info": {
        "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
        "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
        "language": "nl-NL",
        "color_depth": 24,
        "screen_height": 723,
        "screen_width": 1536,
        "time_zone": 0,
        "java_enabled": true,
        "java_script_enabled": true,
        "ip_address": "115.99.183.2"
    },
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "card_number": "5105105105105100",
            "card_exp_month": "01",
            "card_exp_year": "2099",
            "card_holder_name": "joseph Doe",
            "card_cvc": "123",
            "card_network": "Mastercard"
        }
    }
}

'

Response

{
    "payment_id": "pay_gqbZmtDMLxQvXgTa9UCm",
    "merchant_id": "merchant_1786963900",
    "status": "requires_customer_action",
    "amount": 10,
    "net_amount": 10,
    "shipping_cost": null,
    "amount_capturable": 0,
    "amount_received": null,
    "processor_merchant_id": "merchant_1786963900",
    "initiator": null,
    "sdk_authorization": "cHJvZmlsZV9pZD1wcm9fSTUwVEtQYjE5THRWQVVGYkx4MWsscHVibGlzaGFibGVfa2V5PXBrX2Rldl9hNmNmZjQwZWI4Yjc0YWE3OGZhNjc0Y2M5YmE2OGM4YSxjbGllbnRfc2VjcmV0PXBheV9ncWJabXRETUx4UXZYZ1RhOVVDbV9zZWNyZXRfdTlkZXlBSER2TkVETEJEdlEwbTgsY3VzdG9tZXJfaWQ9U3RyaXBlQ3VzdG9tZXIscGF5bWVudF9pZD1wYXlfZ3FiWm10RE1MeFF2WGdUYTlVQ20=",
    "connector": "cybersource",
    "state_metadata": null,
    "client_secret": "pay_gqbZmtDMLxQvXgTa9UCm_secret_u9deyAHDvNEDLBDvQ0m8",
    "created": "2026-08-17T11:02:30.185Z",
    "modified_at": "2026-08-17T11:02:36.250Z",
    "connector_customer_id": null,
    "currency": "USD",
    "customer_id": "StripeCustomer",
    "customer": {
        "id": "StripeCustomer",
        "name": "John Doe",
        "email": "guest@example.com",
        "phone": "999999999",
        "phone_country_code": "+65",
        "customer_document_details": null
    },
    "description": "Its my first payment request",
    "refunds": null,
    "disputes": null,
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": "off_session",
    "off_session": null,
    "capture_on": null,
    "capture_method": "automatic",
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "last4": "5100",
            "card_type": "CREDIT",
            "card_network": "Mastercard",
            "card_issuer": "BANK OF HAWAII",
            "card_issuing_country": "UNITEDSTATES",
            "card_isin": "510510",
            "card_extended_bin": null,
            "card_exp_month": "01",
            "card_exp_year": "2099",
            "card_holder_name": "joseph Doe",
            "payment_checks": null,
            "authentication_data": null,
            "auth_code": null
        },
        "billing": null
    },
    "payment_token": "token_JcbD23hJnfOR9hduF6kg",
    "shipping": {
        "address": {
            "city": "San Fransico",
            "country": "US",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "CA",
            "first_name": "PiX",
            "last_name": "Poddar",
            "origin_zip": null
        },
        "phone": {
            "number": "123456789",
            "country_code": "12"
        },
        "email": null
    },
    "billing": {
        "address": {
            "city": "San Fransico",
            "country": "US",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "CA",
            "first_name": "PiX",
            "last_name": "abc",
            "origin_zip": null
        },
        "phone": {
            "number": "123456789",
            "country_code": "12"
        },
        "email": null
    },
    "order_details": null,
    "email": "guest@example.com",
    "name": "John Doe",
    "phone": "999999999",
    "return_url": "https://duck.com/",
    "authentication_type": "three_ds",
    "statement_descriptor_name": "joseph",
    "statement_descriptor_suffix": "JS",
    "next_action": {
        "type": "three_ds_invoke",
        "three_ds_data": {
            "three_ds_authentication_url": "http://localhost:8080/payments/pay_gqbZmtDMLxQvXgTa9UCm/3ds/authentication",
            "three_ds_authorize_url": "http://localhost:8080/payments/pay_gqbZmtDMLxQvXgTa9UCm/merchant_1786963900/authorize/cybersource",
            "three_ds_method_details": {
                "three_ds_method_data_submission": false,
                "three_ds_method_data": null,
                "three_ds_method_url": null,
                "three_ds_method_key": null,
                "consume_post_message_for_three_ds_method_completion": false
            },
            "poll_config": {
                "poll_id": "external_authentication_pay_gqbZmtDMLxQvXgTa9UCm",
                "delay_in_secs": 2,
                "frequency": 5
            },
            "message_version": "2.3.1",
            "directory_server_id": "A000000004",
            "card_network": "Mastercard",
            "three_ds_connector": "netcetera"
        }
    },
    "cancellation_reason": null,
    "error_code": null,
    "error_message": null,
    "unified_code": null,
    "unified_message": null,
    "error_details": null,
    "payment_experience": null,
    "payment_method_type": "credit",
    "connector_label": null,
    "business_country": null,
    "business_label": "default",
    "business_sub_label": null,
    "allowed_payment_method_types": null,
    "manual_retry_allowed": null,
    "connector_transaction_id": null,
    "frm_message": null,
    "metadata": {
        "udf1": "value1",
        "login_date": "2019-09-10T10:11:12Z",
        "new_customer": "true"
    },
    "connector_metadata": null,
    "connector_response_metadata": null,
    "feature_metadata": null,
    "reference_id": null,
    "payment_link": null,
    "profile_id": "pro_I50TKPb19LtVAUFbLx1k",
    "surcharge_details": null,
    "attempt_count": 1,
    "merchant_decision": null,
    "merchant_connector_id": "mca_LFKswgar2YdXvYurkd2B",
    "incremental_authorization_allowed": null,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": {
        "authentication_flow": null,
        "electronic_commerce_indicator": null,
        "status": "pending",
        "ds_transaction_id": "87b845ee-0f6f-4663-8d5a-195516b50897",
        "version": "2.3.1",
        "error_code": null,
        "error_message": null,
        "challenge_cancel_code": null,
        "trans_status_reason": null
    },
    "external_3ds_authentication_attempted": true,
    "expires_on": "2026-08-17T11:17:30.185Z",
    "fingerprint": null,
    "browser_info": {
        "os_type": null,
        "referer": null,
        "language": "nl-NL",
        "time_zone": 0,
        "ip_address": "115.99.183.2",
        "os_version": null,
        "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
        "color_depth": 24,
        "device_model": null,
        "java_enabled": true,
        "screen_width": 1536,
        "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
        "screen_height": 723,
        "accept_language": "en",
        "java_script_enabled": true
    },
    "payment_channel": null,
    "payment_method_id": null,
    "network_transaction_id": null,
    "network_transaction_link_id": null,
    "payment_method_status": null,
    "updated": "2026-08-17T11:02:36.250Z",
    "split_payments": null,
    "frm_metadata": null,
    "extended_authorization_applied": null,
    "extended_authorization_last_applied_at": null,
    "request_extended_authorization": null,
    "capture_before": null,
    "merchant_order_reference_id": null,
    "order_tax_amount": null,
    "connector_mandate_id": null,
    "card_discovery": "manual",
    "force_3ds_challenge": true,
    "force_3ds_challenge_trigger": true,
    "issuer_error_code": null,
    "issuer_error_message": null,
    "is_iframe_redirection_enabled": null,
    "whole_connector_response": null,
    "enable_partial_authorization": null,
    "enable_overcapture": null,
    "is_overcapture_enabled": null,
    "network_details": null,
    "is_stored_credential": null,
    "mit_category": null,
    "billing_descriptor": null,
    "tokenization": null,
    "partner_merchant_identifier_details": null,
    "payment_method_tokenization_details": null,
    "installment_options": null,
    "installment_data": null,
    "sender_payment_instrument_id": null
}
  1. Authenticate Browser
curl --location 'http://localhost:8080/payments/pay_gqbZmtDMLxQvXgTa9UCm/3ds/authentication' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: pk_dev_a6cff40eb8b74aa78fa674cc9ba68c8a' \
--data '{
    "client_secret": "pay_gqbZmtDMLxQvXgTa9UCm_secret_u9deyAHDvNEDLBDvQ0m8",
    "device_channel": "BRW",
    "threeds_method_comp_ind": "Y",
    "force_3ds_challenge": true
}'

Response

{
    "error": {
        "type": "connector",
        "message": "203: Validation of 3DS Requestor Authentication data failed. Data element not in the required format or value is invalid. { threeDSServerTransID: invalid UUID string has wrong length. Expected 36 but got 7 }",
        "code": "CE_00",
        "connector": "netcetera",
        "reason": "threeDSServerTransID"
    }
}

Checklist

  • I formatted the code cargo +nightly fmt --all
  • I addressed lints thrown by cargo clippy
  • I reviewed the submitted code
  • I added unit tests for my changes where possible

@bsayak03
bsayak03 requested a review from a team as a code owner August 17, 2026 11:16
@semanticdiff-com

semanticdiff-com Bot commented Aug 17, 2026

Copy link
Copy Markdown

Review changes with  SemanticDiff

Changed Files
File Status
  crates/router/src/core/payments.rs  2% smaller
  crates/common_enums/src/transformers.rs  0% smaller

@bsayak03 bsayak03 self-assigned this Aug 17, 2026
@bsayak03 bsayak03 added the S-test-ready Status: This PR is ready for cypress-tests label Aug 17, 2026
@github-actions github-actions Bot removed the S-test-ready Status: This PR is ready for cypress-tests label Aug 17, 2026
@bsayak03
bsayak03 requested a review from a team as a code owner August 17, 2026 14:31
@bsayak03 bsayak03 added the S-test-ready Status: This PR is ready for cypress-tests label Aug 17, 2026
@github-actions github-actions Bot removed the S-test-ready Status: This PR is ready for cypress-tests label Aug 17, 2026
@bsayak03 bsayak03 added the S-test-ready Status: This PR is ready for cypress-tests label Aug 17, 2026
@github-actions github-actions Bot removed the S-test-ready Status: This PR is ready for cypress-tests label Aug 17, 2026
@XyneSpaces

Copy link
Copy Markdown
Contributor

⚠️ Swallowing the original authentication connector error when the failure-status helper also fails.

crates/router/src/core/payments.rs in the added Err(error) => { ... } arm around line 13600: if update_payment_status_for_external_authentication_failure(...) returns an error, the ? on its result propagates that helper error and drops the original error from authentication_core::perform_authentication. When the auth connector times out or returns a 5xx and the helper also fails (for example, because payment_attempt.authentication_id is missing), operators lose the real root cause.

Log the helper failure but always return the original connector error:

Err(error) => {
    if let Err(update_err) = update_payment_status_for_external_authentication_failure(...).await {
        router_env::logger::error!(?update_err, "failed to update payment status after external auth failure");
    }
    return Err(error);
}

@bsayak03 bsayak03 added the S-test-ready Status: This PR is ready for cypress-tests label Aug 18, 2026
@bsayak03
bsayak03 force-pushed the netcetera-auth-2xx-failure-status-update branch from 9587b3f to b218589 Compare August 18, 2026 08:25
@github-actions github-actions Bot removed the S-test-ready Status: This PR is ready for cypress-tests label Aug 18, 2026
error_message: response.error_message,
};

let failure_attempt_status: Option<storage_enums::AttemptStatus> =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is the variable named failure_attempt_status it should be just attempt_status right


let failure_attempt_status: Option<storage_enums::AttemptStatus> =
authentication_response.trans_status.clone().into();
if failure_attempt_status == Some(storage_enums::AttemptStatus::AuthenticationFailed) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Comment on lines +13613 to +13624
let failure_attempt_status: Option<storage_enums::AttemptStatus> =
authentication_response.trans_status.clone().into();
if failure_attempt_status == Some(storage_enums::AttemptStatus::AuthenticationFailed) {
update_payment_status_for_failed_external_authentication(
&state,
&platform,
payment_intent,
&payment_attempt,
&authentication_connector,
)
.await?;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

u can fork this recurring step out of conditional statement

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants