Skip to content

Conversation

@ImSagnik007
Copy link
Contributor

@ImSagnik007 ImSagnik007 commented Jul 13, 2025

Type of Change

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

Description

Added action type in ConnectorRoutingData (NT with ntid, Card with ntid, Connector mandate).
We are storing all the instances of connector routing data with different action types.
During retry it will pick the top element in the list and will try to do payment with the mandate_reference_id that will be fetched from action type.

Solution doc: link

pub struct ConnectorRoutingData {
    pub connector_data: ConnectorData,
    pub network: Option<common_enums::CardNetwork>,
    pub action_type: Option<ActionType>,
}

pub enum ActionType {
    NetworkTokenWithNetworkTransactionId(NTWithNTIRef),
    CardWithNetworkTransactionId(String), // Network Transaction Id
    #[cfg(feature = "v1")]
    ConnectorMandate(Option<diesel_models::PaymentsMandateReference>),
}

Retryable Connectors = [Adyen, Cybersource, Stripe]

Adyen - 1. Connector Mandate
        2. CardWithNetworkTransactionId

Cybersource - 1. NetworkTokenWithNetworkTransactionId
              2. CardWithNetworkTransactionId

Stripe - 1. CardWithNetworkTransactionId

After Processing - 

Retryable Connectors = [Adyen(Connector Mandate), Adyen(CardWithNetworkTransactionId), Cybersource(NetworkTokenWithNetworkTransactionId), Cybersource(CardWithNetworkTransactionId), Stripe(CardWithNetworkTransactionId)]

do_retry:
    let current_connector_routing_data = retryable_connectors.next()
    let mandate_reference_id = get_mandate_reference_id(current_connector_routing_data.action_type)
    payment_data.set_mandate_id(api_models::payments::MandateIds {
        mandate_id: None,
        mandate_reference_id, //mandate_ref_id
    });

Additional Changes

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

Motivation and Context

How did you test it?

  1. Connector create Adyen:
curl --location 'http://localhost:8080/account/merchant_1753897495/connectors' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_BO0MXRwyIFS7jImumSCBpO7VkdFjNZ46uX506zER3sA1rBbnFn3P8OTOS1Zr606h' \
--data '{
    "connector_type": "payment_processor",
    "connector_name": "adyen",
    "connector_account_details": {
        "auth_type": "SignatureKey",
        "api_secret": "abc",
        "api_key": "abc",
        "key1": ""
    },
    "test_mode": true,
    "disabled": false,
    "payment_methods_enabled": [
        {
            "payment_method": "wallet",
            "payment_method_types": [
                {
                    "payment_method_type": "google_pay",
                    "payment_experience": "invoke_sdk_client",
                    "card_networks": null,
                    "accepted_currencies": null,
                    "accepted_countries": null,
                    "minimum_amount": 1,
                    "maximum_amount": 68607706,
                    "recurring_enabled": true,
                    "installment_payment_enabled": true
                }
            ]
        },
        {
            "payment_method": "card",
            "payment_method_types": [
                {
                    "payment_method_type": "credit",
                    "payment_experience": null,
                    "card_networks": [
                        "Visa",
                        "Mastercard",
                        "AmericanExpress"
                    ],
                    "accepted_currencies": null,
                    "accepted_countries": null,
                    "minimum_amount": -1,
                    "maximum_amount": 68607706,
                    "recurring_enabled": true,
                    "installment_payment_enabled": true
                },
                {
                    "payment_method_type": "debit",
                    "payment_experience": null,
                    "card_networks": [
                        "Visa",
                        "Mastercard"
                    ],
                    "accepted_currencies": null,
                    "accepted_countries": null,
                    "minimum_amount": -1,
                    "maximum_amount": 68607706,
                    "recurring_enabled": true,
                    "installment_payment_enabled": true
                }
            ]
        }
    ],
    "metadata": {
        "status_url": "https://2753-2401-4900-1cb8-2ff9-24dd-1ccf-ed12-b464.in.ngrok.io/webhooks/merchant_1678699058/globalpay",
        "account_name": "transaction_processing",
        "pricing_type": "fixed_price",
        "acquirer_bin": "438309",
        "acquirer_merchant_id": "00002000000"
    },
    "business_country": "US",
    "business_label": "default",
    "frm_configs": null,
    "connector_webhook_details": {
        "merchant_secret": ""
    }
}'
  1. Connector Create Cybersource:
curl --location 'http://localhost:8080/account/merchant_1753897495/connectors' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_BO0MXRwyIFS7jImumSCBpO7VkdFjNZ46uX506zER3sA1rBbnFn3P8OTOS1Zr606h' \
--data '{
    "connector_type": "payment_processor",
    "connector_name": "cybersource",
    "connector_account_details": {
        "auth_type": "SignatureKey",
        "api_secret": "abc",
        "api_key": "abc",
        "key1": ""
    },
    "test_mode": true,
    "disabled": false,
    "payment_methods_enabled": [
        {
            "payment_method": "wallet",
            "payment_method_types": [
                {
                    "payment_method_type": "google_pay",
                    "payment_experience": "invoke_sdk_client",
                    "card_networks": null,
                    "accepted_currencies": null,
                    "accepted_countries": null,
                    "minimum_amount": 1,
                    "maximum_amount": 68607706,
                    "recurring_enabled": true,
                    "installment_payment_enabled": true
                }
            ]
        },
        {
            "payment_method": "card",
            "payment_method_types": [
                {
                    "payment_method_type": "credit",
                    "payment_experience": null,
                    "card_networks": [
                        "Visa",
                        "Mastercard",
                        "AmericanExpress"
                    ],
                    "accepted_currencies": null,
                    "accepted_countries": null,
                    "minimum_amount": -1,
                    "maximum_amount": 68607706,
                    "recurring_enabled": true,
                    "installment_payment_enabled": true
                },
                {
                    "payment_method_type": "debit",
                    "payment_experience": null,
                    "card_networks": [
                        "Visa",
                        "Mastercard"
                    ],
                    "accepted_currencies": null,
                    "accepted_countries": null,
                    "minimum_amount": -1,
                    "maximum_amount": 68607706,
                    "recurring_enabled": true,
                    "installment_payment_enabled": true
                }
            ]
        }
    ],
    "metadata": {
        "status_url": "https://2753-2401-4900-1cb8-2ff9-24dd-1ccf-ed12-b464.in.ngrok.io/webhooks/merchant_1678699058/globalpay",
        "account_name": "transaction_processing",
        "pricing_type": "fixed_price",
        "acquirer_bin": "438309",
        "acquirer_merchant_id": "00002000000"
    },
    "business_country": "US",
    "business_label": "default",
    "frm_configs": null,
    "connector_webhook_details": {
        "merchant_secret": ""
    }
}'
  1. Business profile update:
curl --location 'http://localhost:8080/account/merchant_1754988842/business_profile/pro_0ApGdp5GH660dy5AGft5' \
--header 'Content-Type: application/json' \
--header 'api-key: dev_SIJmEz8xbGVEybdUspVKhl4QVfAyelalMWYp52puDIG4OWzk6ewgVGGHTm7gS1cK' \
--data '{
  "is_connector_agnostic_mit_enabled": true,
  "is_auto_retries_enabled": true,
  "max_auto_retries_enabled": 5
}'
  1. CIT payments create:
curl --location 'http://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_BO0MXRwyIFS7jImumSCBpO7VkdFjNZ46uX506zER3sA1rBbnFn3P8OTOS1Zr606h' \
--data-raw '{
    "amount": 10,
    "currency": "USD",
    "confirm": true,
    "capture_method": "automatic",
    "capture_on": "2022-09-10T10:11:12Z",
    "customer_id": "cu_1753904658",
    "email": "[email protected]",
    "name": "John Doe",
    "phone": "999999999",
    "phone_country_code": "+65",
    "description": "Its my first payment request",
    "authentication_type": "no_three_ds",
    "return_url": "https://google.com",
    "payment_method": "card",
    "payment_method_type": "credit",
    "payment_method_data": {
        "card": {
            "card_number": "4111111111111111",
            "card_exp_month": "03",
            "card_exp_year": "2030",
            "card_holder_name": "name name",
            "card_cvc": "737"
        }
    },
    "setup_future_usage": "off_session",
    "payment_type": "setup_mandate",
    "customer_acceptance": {
        "acceptance_type": "offline",
        "accepted_at": "1963-05-03T04:07:52.723Z",
        "online": {
            "ip_address": "in sit",
            "user_agent": "amet irure esse"
        }
    },
    "billing": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "California",
            "zip": "94122",
            "country": "US",
            "first_name": "PiX",
            "last_name": "ss"
        }
    },
    "shipping": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "California",
            "zip": "94122",
            "country": "US",
            "first_name": "John",
            "last_name": "Doe"
        },
        "phone": {
            "number": "8056594427",
            "country_code": "+91"
        }
    },
    "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": "125.0.0.1"
    },
    "statement_descriptor_name": "joseph",
    "statement_descriptor_suffix": "JS",
    "metadata": {},
    "order_details": [
        {
            "product_name": "Apple iphone 15",
            "quantity": 1,
            "amount": 0,
            "account_name": "transaction_processing"
        }
    ]
}'
  1. Change the value of connector_mandate_id to some wrong value in db.

  2. GSM error:

curl --location 'localhost:8080/gsm' \
--header 'api-key: test_admin' \
--header 'Content-Type: application/json' \
--data '{
    "connector": "adyen",
    "flow": "Authorize",
    "sub_flow": "sub_flow",
    "code": "803",
    "message": "PaymentDetail not found",
    "status": "failed",
    "router_error": "Something went wrong",
    "decision": "retry",
    "step_up_possible": false,
    "clear_pan_possible": false,
    "unified_code": "5xx",
    "unified_message": "Error",
    "error_category": "frm_decline"
}'
  1. MIT payments create:
curl --location 'http://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_BO0MXRwyIFS7jImumSCBpO7VkdFjNZ46uX506zER3sA1rBbnFn3P8OTOS1Zr606h' \
--data '{
    "amount": 999,
    "currency": "USD",
    "confirm": true,
    "profile_id": "pro_ZeFGow0L5Y1TzhzffSK7",
    "customer_id": "customer098765",
    "return_url": "https://google.com/",
    "recurring_details": {
        "type": "payment_method_id",
        "data": "successful CIT payment id"
    },
    "off_session": true,
    "billing": {
        "address": {
            "first_name":"John",
            "last_name":"Doe",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "California",
            "zip": "94122",
            "country": "US"
        },
         "phone": {
            "number": "8056594427",
            "country_code": "+91"
        }
    }  
    
}'

Response (attempt_count = 2):

{
    "payment_id": "pay_DjvhVKgjBOEDU6nFkalO",
    "merchant_id": "merchant_1754046615",
    "status": "succeeded",
    "amount": 999,
    "net_amount": 999,
    "shipping_cost": null,
    "amount_capturable": 0,
    "amount_received": 999,
    "connector": "adyen",
    "client_secret": "pay_DjvhVKgjBOEDU6nFkalO_secret_682mdlC6mQ2xCf0Wf0A4",
    "created": "2025-08-01T12:42:04.553Z",
    "currency": "USD",
    "customer_id": "cu_1754050231",
    "customer": {
        "id": "cu_1754050231",
        "name": "John Doe",
        "email": "[email protected]",
        "phone": "999999999",
        "phone_country_code": "+65"
    },
    "description": null,
    "refunds": null,
    "disputes": null,
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": null,
    "off_session": true,
    "capture_on": null,
    "capture_method": null,
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "last4": "1111",
            "card_type": null,
            "card_network": null,
            "card_issuer": null,
            "card_issuing_country": null,
            "card_isin": "411111",
            "card_extended_bin": null,
            "card_exp_month": "03",
            "card_exp_year": "2030",
            "card_holder_name": "name name",
            "payment_checks": null,
            "authentication_data": null
        },
        "billing": null
    },
    "payment_token": null,
    "shipping": null,
    "billing": {
        "address": {
            "city": "San Fransico",
            "country": "US",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "California",
            "first_name": "John",
            "last_name": "Doe"
        },
        "phone": {
            "number": "8056594427",
            "country_code": "+91"
        },
        "email": null
    },
    "order_details": null,
    "email": "[email protected]",
    "name": "John Doe",
    "phone": "999999999",
    "return_url": "https://google.com/",
    "authentication_type": "no_three_ds",
    "statement_descriptor_name": null,
    "statement_descriptor_suffix": null,
    "next_action": null,
    "cancellation_reason": null,
    "error_code": null,
    "error_message": null,
    "unified_code": null,
    "unified_message": 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,
    "ephemeral_key": {
        "customer_id": "cu_1754050231",
        "created_at": 1754052124,
        "expires": 1754055724,
        "secret": "epk_378607c699c9493ba67d398ba4069d5b"
    },
    "manual_retry_allowed": false,
    "connector_transaction_id": "J3LV3LZB9JTHNS75",
    "frm_message": null,
    "metadata": null,
    "connector_metadata": null,
    "feature_metadata": null,
    "reference_id": "pay_DjvhVKgjBOEDU6nFkalO_2",
    "payment_link": null,
    "profile_id": "pro_wPWBpxTXWAgouZVHOP4P",
    "surcharge_details": null,
    "attempt_count": 2,
    "merchant_decision": null,
    "merchant_connector_id": "mca_2MjRpgYbkH8U92tH1huS",
    "incremental_authorization_allowed": null,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": null,
    "external_3ds_authentication_attempted": false,
    "expires_on": "2025-08-01T12:57:04.553Z",
    "fingerprint": null,
    "browser_info": {
        "language": "nl-NL",
        "time_zone": 0,
        "ip_address": "125.0.0.1",
        "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,
        "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,
        "java_script_enabled": true
    },
    "payment_method_id": "pm_eslwOh7cYf7sFsNWN1Iq",
    "payment_method_status": "active",
    "updated": "2025-08-01T12:42:08.581Z",
    "split_payments": null,
    "frm_metadata": null,
    "extended_authorization_applied": null,
    "capture_before": null,
    "merchant_order_reference_id": null,
    "order_tax_amount": null,
    "connector_mandate_id": null,
    "card_discovery": "saved_card",
    "force_3ds_challenge": false,
    "force_3ds_challenge_trigger": false,
    "issuer_error_code": null,
    "issuer_error_message": null,
    "is_iframe_redirection_enabled": null,
    "whole_connector_response": null
}
  1. Payments Retrieve:
curl --location 'http://localhost:8080/payments/pay_DjvhVKgjBOEDU6nFkalO?force_sync=true&expand_attempts=true' \
--header 'Accept: application/json' \
--header 'api-key: dev_hfUb5b3thW9LuPyco462Eu5KxYbWF9GK0SldV3joK018qcsZhBG7SQbSUMPyl3ZC'

Payments Retrieve Response:

{
    "payment_id": "pay_DjvhVKgjBOEDU6nFkalO",
    "merchant_id": "merchant_1754046615",
    "status": "succeeded",
    "amount": 999,
    "net_amount": 999,
    "shipping_cost": null,
    "amount_capturable": 0,
    "amount_received": 999,
    "connector": "adyen",
    "client_secret": "pay_DjvhVKgjBOEDU6nFkalO_secret_682mdlC6mQ2xCf0Wf0A4",
    "created": "2025-08-01T12:42:04.553Z",
    "currency": "USD",
    "customer_id": "cu_1754050231",
    "customer": {
        "id": "cu_1754050231",
        "name": "John Doe",
        "email": "[email protected]",
        "phone": "999999999",
        "phone_country_code": "+65"
    },
    "description": null,
    "refunds": null,
    "disputes": null,
    "attempts": [
        {
            "attempt_id": "pay_DjvhVKgjBOEDU6nFkalO_2",
            "status": "charged",
            "amount": 999,
            "order_tax_amount": null,
            "currency": "USD",
            "connector": "adyen",
            "error_message": null,
            "payment_method": "card",
            "connector_transaction_id": "J3LV3LZB9JTHNS75",
            "capture_method": null,
            "authentication_type": "no_three_ds",
            "created_at": "2025-08-01T12:42:06.553Z",
            "modified_at": "2025-08-01T12:42:08.581Z",
            "cancellation_reason": null,
            "mandate_id": null,
            "error_code": null,
            "payment_token": null,
            "connector_metadata": null,
            "payment_experience": null,
            "payment_method_type": "credit",
            "reference_id": "pay_DjvhVKgjBOEDU6nFkalO_2",
            "unified_code": null,
            "unified_message": null,
            "client_source": null,
            "client_version": null
        },
        {
            "attempt_id": "pay_DjvhVKgjBOEDU6nFkalO_1",
            "status": "failure",
            "amount": 999,
            "order_tax_amount": null,
            "currency": "USD",
            "connector": "adyen",
            "error_message": "PaymentDetail not found",
            "payment_method": "card",
            "connector_transaction_id": "H5BKWV6FGQ2VJT65",
            "capture_method": null,
            "authentication_type": "no_three_ds",
            "created_at": "2025-08-01T12:42:04.554Z",
            "modified_at": "2025-08-01T12:42:06.613Z",
            "cancellation_reason": null,
            "mandate_id": null,
            "error_code": "803",
            "payment_token": null,
            "connector_metadata": null,
            "payment_experience": null,
            "payment_method_type": "credit",
            "reference_id": null,
            "unified_code": "5xx",
            "unified_message": "Error",
            "client_source": null,
            "client_version": null
        }
    ],
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": null,
    "off_session": true,
    "capture_on": null,
    "capture_method": null,
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "last4": "1111",
            "card_type": null,
            "card_network": null,
            "card_issuer": null,
            "card_issuing_country": null,
            "card_isin": "411111",
            "card_extended_bin": null,
            "card_exp_month": "03",
            "card_exp_year": "2030",
            "card_holder_name": "name name",
            "payment_checks": null,
            "authentication_data": null
        },
        "billing": null
    },
    "payment_token": null,
    "shipping": null,
    "billing": {
        "address": {
            "city": "San Fransico",
            "country": "US",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "California",
            "first_name": "John",
            "last_name": "Doe"
        },
        "phone": {
            "number": "8056594427",
            "country_code": "+91"
        },
        "email": null
    },
    "order_details": null,
    "email": "[email protected]",
    "name": "John Doe",
    "phone": "999999999",
    "return_url": "https://google.com/",
    "authentication_type": "no_three_ds",
    "statement_descriptor_name": null,
    "statement_descriptor_suffix": null,
    "next_action": null,
    "cancellation_reason": null,
    "error_code": null,
    "error_message": null,
    "unified_code": null,
    "unified_message": 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,
    "ephemeral_key": null,
    "manual_retry_allowed": false,
    "connector_transaction_id": "J3LV3LZB9JTHNS75",
    "frm_message": null,
    "metadata": null,
    "connector_metadata": null,
    "feature_metadata": null,
    "reference_id": "pay_DjvhVKgjBOEDU6nFkalO_2",
    "payment_link": null,
    "profile_id": "pro_wPWBpxTXWAgouZVHOP4P",
    "surcharge_details": null,
    "attempt_count": 2,
    "merchant_decision": null,
    "merchant_connector_id": "mca_2MjRpgYbkH8U92tH1huS",
    "incremental_authorization_allowed": null,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": null,
    "external_3ds_authentication_attempted": false,
    "expires_on": "2025-08-01T12:57:04.553Z",
    "fingerprint": null,
    "browser_info": {
        "language": "nl-NL",
        "time_zone": 0,
        "ip_address": "125.0.0.1",
        "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,
        "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,
        "java_script_enabled": true
    },
    "payment_method_id": "pm_eslwOh7cYf7sFsNWN1Iq",
    "payment_method_status": "active",
    "updated": "2025-08-01T12:42:08.581Z",
    "split_payments": null,
    "frm_metadata": null,
    "extended_authorization_applied": null,
    "capture_before": null,
    "merchant_order_reference_id": null,
    "order_tax_amount": null,
    "connector_mandate_id": null,
    "card_discovery": "saved_card",
    "force_3ds_challenge": false,
    "force_3ds_challenge_trigger": false,
    "issuer_error_code": null,
    "issuer_error_message": null,
    "is_iframe_redirection_enabled": null,
    "whole_connector_response": null
}

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

@ImSagnik007 ImSagnik007 requested review from a team as code owners July 13, 2025 19:43
@semanticdiff-com
Copy link

semanticdiff-com bot commented Jul 13, 2025

Review changes with  SemanticDiff

Changed Files
File Status
  crates/router/src/types/api/fraud_check.rs  68% smaller
  crates/router/src/core/payments.rs  30% smaller
  crates/router/src/core/payments/retry.rs  11% smaller
  crates/hyperswitch_domain_models/src/mandates.rs  0% smaller
  crates/router/src/core/debit_routing.rs  0% smaller
  crates/router/src/types/api.rs  0% smaller

Copy link
Contributor

@ShankarSinghC ShankarSinghC left a comment

Choose a reason for hiding this comment

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

Can you please add some test cases and also provide flow solution documentation ?

@ImSagnik007 ImSagnik007 force-pushed the mit_retries branch 3 times, most recently from 03ad3f1 to 1287b2f Compare August 1, 2025 13:03
types::RouterData<F, FData, types::PaymentsResponseData>: Feature<F, FData>,
dyn api::Connector: services::api::ConnectorIntegration<F, FData, types::PaymentsResponseData>,
{
use hyperswitch_domain_models::ext_traits::OptionExt;
Copy link
Contributor

Choose a reason for hiding this comment

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

can move this to top

pub struct ConnectorRoutingData {
pub connector_data: ConnectorData,
pub network: Option<common_enums::CardNetwork>,
pub action_type: Option<ActionType>,
Copy link
Contributor

Choose a reason for hiding this comment

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

please add a comment that this is used for mandates currently. since action_type is not self-explanatory.

Copy link
Contributor

Choose a reason for hiding this comment

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

this can be mandate_type or some other similar name.
Are we not modifying this?

payment_data.get_payment_intent().setup_future_usage,
payment_data.get_payment_intent().off_session,
) {
(Some(storage_enums::FutureUsage::OffSession), _) | (_, Some(true)) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

if setup future usage is off-session - then it is considered as CIT, then we dont want to go through this snippet.

transaction is considered as Merchant Initiated transaction only when off-session is set to true which mean customer is not present.


if is_mandate_flow {
if let Ok(connector_mandate_details) = connector_mandate_details {
let action_type = ActionType::ConnectorMandate(connector_mandate_details.to_owned());
Copy link
Contributor

Choose a reason for hiding this comment

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

can we use extend() to minimize this nested id-else?

Comment on lines 8668 to 8691
if is_mandate_flow {
action_types.extend(
connector_mandate_details
.as_ref()
.ok()
.map(|details| ActionType::ConnectorMandate(details.to_owned())),
);
}

// Collect network token with network transaction ID action types
if let IsNtWithNtiFlow::NtWithNtiSupported(network_transaction_id) =
is_network_token_with_ntid_flow
{
let connector_supports_both = ntid_supported_connectors.contains(&connector.connector_name)
&& network_tokenization_supported_connectors.contains(&connector.connector_name);

if connector_supports_both {
action_types.extend(
network_tokenization::do_status_check_for_network_token(state, payment_method_info)
.await
{
Some(ActionType::NetworkTokenWithNetworkTransactionId(
NTWithNTIRef {
token_exp_month,
token_exp_year,
network_transaction_id,
},
))
} else {
None
}
.ok()
.map(|(token_exp_month, token_exp_year)| {
ActionType::NetworkTokenWithNetworkTransactionId(NTWithNTIRef {
token_exp_month,
token_exp_year,
network_transaction_id,
})
}),
);
Copy link
Contributor

Choose a reason for hiding this comment

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

instead of these many if-else, can we use builder pattern for the code optimization?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

refactored these with match statement with guards

@ImSagnik007 ImSagnik007 force-pushed the mit_retries branch 2 times, most recently from 83c54a2 to df9f0ab Compare August 11, 2025 08:31
Comment on lines 8662 to 8681
if is_mandate_flow {
action_types.extend(
connector_mandate_details
.as_ref()
.ok()
.map(|details| ActionType::ConnectorMandate(details.to_owned())),
);
}

let is_nt_with_ntid_supported_connector = ntid_supported_connectors
.contains(&connector.connector_name)
&& network_tokenization_supported_connectors.contains(&connector.connector_name);

// Collect network token with network transaction ID action types
match is_network_token_with_ntid_flow {
IsNtWithNtiFlow::NtWithNtiSupported(network_transaction_id)
if is_nt_with_ntid_supported_connector =>
{
action_types.extend(
network_tokenization::do_status_check_for_network_token(state, payment_method_info)
Copy link
Contributor

Choose a reason for hiding this comment

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

instead of if-else(s), use builder pattern for it

Copy link
Contributor

@prasunna09 prasunna09 Aug 11, 2025

Choose a reason for hiding this comment

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

for example -

let action_types = ActionTypesBuilder::new()
    .with_mandate_flow(is_mandate_flow, connector_mandate_details)
    .with_network_tokenization(is_network_token_with_ntid_flow, is_nt_with_ntid_supported_connector)
    .build();
    ```

prasunna09
prasunna09 previously approved these changes Aug 12, 2025
Copy link
Contributor

@ShankarSinghC ShankarSinghC left a comment

Choose a reason for hiding this comment

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

Clippy is failing on this pr, please check

NetworkTokenWithNetworkTransactionId(NTWithNTIRef),
CardWithNetworkTransactionId(String), // Network Transaction Id
#[cfg(feature = "v1")]
ConnectorMandate(Option<diesel_models::PaymentsMandateReference>),
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is PaymentsMandateReference optional in ConnectorMandate ?

pub struct ConnectorRoutingData {
pub connector_data: ConnectorData,
pub network: Option<common_enums::CardNetwork>,
pub action_type: Option<ActionType>, // action_type is used for mandates currently
Copy link
Contributor

Choose a reason for hiding this comment

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

If required add the comment above the filed

Suggested change
pub action_type: Option<ActionType>, // action_type is used for mandates currently
// action_type is used for mandates currently
pub action_type: Option<ActionType>,

Comment on lines 184 to 171
let current_connector_routing_data =
super::get_connector_data(&mut connector_routing_data)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

The connector data is already fetched in the above code right ?
It is stored in the connector variable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

connector_data is strored in the connector variable not connector_routing _data


let is_mandate_flow = connector_mandate_details
.as_ref()
.ok()
Copy link
Contributor

Choose a reason for hiding this comment

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

If there is a parsing error should we log it here ?

.as_ref()
.ok()
.zip(merchant_connector_id)
.and_then(|(details, merchant_id)| details.clone().map(|d| d.contains_key(merchant_id)))
Copy link
Contributor

Choose a reason for hiding this comment

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

The naming here is misleading, is it merchant id or merchant connector id ?

Comment on lines 8728 to 8735
let connector_mandate_details = &payment_method_info
.connector_mandate_details
.clone()
.map(|details| {
details
.parse_value::<diesel_models::PaymentsMandateReference>("connector_mandate_details")
})
.transpose();
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can use get_common_mandate_reference function here.


let is_mandate_flow = connector_mandate_details
.as_ref()
.ok()
Copy link
Contributor

Choose a reason for hiding this comment

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

You can do flatten here to avoid details.clone().map(|d| d.contains_key(merchant_id))

(connector_routing_data.connector_data, routing_decision)
};

if payment_data
Copy link
Contributor

Choose a reason for hiding this comment

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

Correct me if I am wrong
If the code is reaching this place that means the payment has failed and it can be retried, which our auto retry. In such cases aren't we suppose to chose a different connector each time ?

For example
Retryable Connectors = [Adyen(Connector Mandate), Adyen(CardWithNetworkTransactionId), Cybersource(NetworkTokenWithNetworkTransactionId), Cybersource(CardWithNetworkTransactionId), Stripe(CardWithNetworkTransactionId)]

If the first payment happened -> Adyen(Connector Mandate) and failed
And the GSM record says retry then it would because of connector error, right ?
If yes, in such cases we can not use get_connector_data as it just gives the next element from the list. In this case it is Adyen(CardWithNetworkTransactionId). But as it is connector error we should retry with Cybersource(NetworkTokenWithNetworkTransactionId).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If the connector mandate id is wrong for that connector, we should proceed with the payment with the other two action types right? (network token + nti or card+nti)
cc: @prasunna09

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For 5xx type of errors we can have do_default mapping in gsm to stop retrying. Other than that we are just retrying with the next item in the retryable connector list in this PR. The logic to change the current connector and proceed with the next connector, and other incremental changes if any will be done in the next following PR.

Comment on lines 8335 to 8349
#[cfg(feature = "v2")]
#[allow(clippy::too_many_arguments)]
pub async fn decide_connector_for_normal_or_recurring_payment<F: Clone, D>(
state: &SessionState,
payment_data: &mut D,
routing_data: &mut storage::RoutingData,
connectors: Vec<api::ConnectorData>,
is_connector_agnostic_mit_enabled: Option<bool>,
payment_method_info: &domain::PaymentMethod,
) -> RouterResult<ConnectorCallType>
where
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
{
todo!()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this required ?
Are we calling decide_connector_for_normal_or_recurring_payment some where other than v2 ?

Comment on lines 8391 to 8393
let connector_details = connector_mandate_details
.clone()
.get_required_value("connector_mandate_details")
Copy link
Contributor

Choose a reason for hiding this comment

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

As mentioned in the enum definition, the connector_mandate_details inside ActionType::ConnectorMandate is currently an Option, but its usage here assumes it is always present. To avoid runtime errors and improve clarity, can we make PaymentsMandateReference mandatory in the enum variant ?

{
let mandate_reference_id = match action_type {
Some(ActionType::NetworkTokenWithNetworkTransactionId(nt_data)) => {
logger::info!("using network_tokenization with network_transaction_id for MIT flow");
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit

Suggested change
logger::info!("using network_tokenization with network_transaction_id for MIT flow");
logger::info!("using network token with network_transaction_id for MIT flow");

D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
{
let mandate_reference_id = match action_type {
Some(ActionType::NetworkTokenWithNetworkTransactionId(nt_data)) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit

Suggested change
Some(ActionType::NetworkTokenWithNetworkTransactionId(nt_data)) => {
Some(ActionType::NetworkTokenWithNetworkTransactionId(network_token_data)) => {

Comment on lines 8945 to 9107
Some(payments_api::MandateReferenceId::NetworkTokenWithNTI(
payments_api::NetworkTokenWithNTIRef {
network_transaction_id: nt_data.network_transaction_id.to_string(),
token_exp_month: nt_data.token_exp_month,
token_exp_year: nt_data.token_exp_year,
},
))
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be an from conversation on network_token_data

Comment on lines 8989 to 8996
hyperswitch_domain_models::router_data::RecurringMandatePaymentData {
payment_method_type: mandate_reference_record.payment_method_type,
original_payment_authorized_amount: mandate_reference_record
.original_payment_authorized_amount,
original_payment_authorized_currency: mandate_reference_record
.original_payment_authorized_currency,
mandate_metadata: mandate_reference_record.mandate_metadata.clone(),
},
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be from conversation

is_nt_with_ntid_supported_connector: bool,
payment_method_info: &domain::PaymentMethod,
) -> Self {
match is_network_token_with_ntid_flow {
Copy link
Contributor

Choose a reason for hiding this comment

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

As we are taking some action only in case of NtWithNtiSupported and not returning anything from the match block, this can be made as if let

Ex:
if let IsNtWithNtiFlow::NtWithNtiSupported(network_transaction_id) = is_network_token_with_ntid_flow

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have used match cases with guards here, if I do this with if let case, then there will be nesting of ifs, I guess that is less readable.

payment_method_info,
)
.await
.ok()
Copy link
Contributor

Choose a reason for hiding this comment

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

@prasunna09 should we log the error that occurred during do_status_check_for_network_token ?

Copy link
Contributor

Choose a reason for hiding this comment

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

we can, but we have done attach_printable for every response/error returns.

ig its better to log before we do .ok()

ShankarSinghC
ShankarSinghC previously approved these changes Aug 29, 2025
@ImSagnik007 ImSagnik007 requested a review from jarnura August 29, 2025 08:32
pub connector_mandate_request_reference_id: Option<String>,
}

impl From<&PaymentsMandateReferenceRecord> for crate::router_data::RecurringMandatePaymentData {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
impl From<&PaymentsMandateReferenceRecord> for crate::router_data::RecurringMandatePaymentData {
impl From<&PaymentsMandateReferenceRecord> for router_data::RecurringMandatePaymentData {

#[cfg(feature = "v1")]
impl Default for ActionTypesBuilder {
fn default() -> Self {
Self::new()
Copy link
Member

Choose a reason for hiding this comment

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

Vec::new()

jarnura
jarnura previously approved these changes Sep 2, 2025
ShankarSinghC
ShankarSinghC previously approved these changes Sep 3, 2025
@ImSagnik007 ImSagnik007 dismissed stale reviews from ShankarSinghC and jarnura via 2553552 September 9, 2025 13:43
@likhinbopanna likhinbopanna added this pull request to the merge queue Sep 10, 2025
Merged via the queue into main with commit c0e31d3 Sep 10, 2025
21 of 25 checks passed
@likhinbopanna likhinbopanna deleted the mit_retries branch September 10, 2025 14:03
pixincreate added a commit that referenced this pull request Sep 11, 2025
…ee-ds

* 'main' of github.com:juspay/hyperswitch:
  feat(webhooks): Provide outgoing webhook support for revenue recovery (#9294)
  feat(connector): Add Peachpayments Template Code (#9363)
  feat(connector): [Paysafe] Implement card 3ds flow (#9305)
  feat(router): Add Connector changes for 3ds (v2) (#9117)
  feat(connector): [ADYEN] Add support to ideal Mandate Webhook (#9347)
  refactor(core): accept manual retry from profile  (#9302)
  fix(nuvei): nuvei 3ds fix + psync fix (#9279)
  fix(connector): [checkout] Add US Support for Apple Pay and Google Pay + Enhanced Checkout Response Data (#9356)
  fix(router): adding connector_customer_id for external vault proxy (#9263)
  feat(core): Add first_name and last_name as Secret<String> Types.  (#9326)
  feat(injector): injector request formation changes (#9306)
  fix(revenue-recovery): Update Redis TTL for customer locks after token selection (#9282)
  chore(version): 2025.09.11.0
  refactor(connector): [Paysafe] fix wasm (#9349)
  refactor(connector): rename RevenueRecoveryRecordBack as InvoiceRecordBack (#9321)
  feat(connector): [checkout] add support for MOTO payments (#9327)
  feat(connector): enhance ACI connector with comprehensive 3DS support - DRAFT (#8986)
  feat(core): [Retry] MIT Retries (#8628)
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.

5 participants