Bug: [FEATURE] extend PSP token provisioning through Adyen Platform



### Feature Description

Enable processing payouts using Adyen stored payment method tokens (storedPaymentMethodId) in the AdyenPlatform connector. Currently, the connector only supports raw card details for payouts. This feature will add support for tokenized card payouts, allowing merchants to process payouts using previously stored payment methods using PSP tokens.

### Possible Implementation

Add support for both raw card details and tokenized payments with proper fallback logic, following Adyen's Balance Platform API specifications for payouts with tokenized card details - https://docs.adyen.com/payouts/payout-service/pay-out-to-cards/card-payout-request/?tab=recurring-payouts_3

### Have you spent some time checking if this feature request has been raised before?

- [x] I checked and didn't find a similar issue

### Have you read the Contributing Guidelines?

- [x] I have read the [Contributing Guidelines](https://github.com/juspay/hyperswitch/blob/main/docs/CONTRIBUTING.md)

### Are you willing to submit a PR?

Yes, I am willing to submit a PR!
diff --git a/crates/hyperswitch_connectors/src/connectors/adyenplatform/transformers/payouts.rs b/crates/hyperswitch_connectors/src/connectors/adyenplatform/transformers/payouts.rs
index b04ca410d39..35636889d71 100644
--- a/crates/hyperswitch_connectors/src/connectors/adyenplatform/transformers/payouts.rs
+++ b/crates/hyperswitch_connectors/src/connectors/adyenplatform/transformers/payouts.rs
@@ -11,7 +11,9 @@ use super::{AdyenPlatformRouterData, Error};
 use crate::{
     connectors::adyen::transformers as adyen,
     types::PayoutsResponseRouterData,
-    utils::{self, AddressDetailsData, PayoutsData as _, RouterData as _},
+    utils::{
+        self, AddressDetailsData, PayoutFulfillRequestData, PayoutsData as _, RouterData as _,
+    },
 };
 
 #[derive(Debug, Default, Serialize, Deserialize)]
@@ -54,8 +56,6 @@ pub enum AdyenPayoutMethod {
 pub enum AdyenPayoutMethodDetails {
     BankAccount(AdyenBankAccountDetails),
     Card(AdyenCardDetails),
-    #[serde(rename = "card")]
-    CardToken(AdyenCardTokenDetails),
 }
 
 #[derive(Debug, Serialize, Deserialize)]
@@ -117,9 +117,16 @@ pub struct AdyenCardDetails {
     card_identification: AdyenCardIdentification,
 }
 
+#[derive(Debug, Serialize)]
+#[serde(untagged)]
+pub enum AdyenCardIdentification {
+    Card(AdyenRawCardIdentification),
+    Stored(AdyenStoredCardIdentification),
+}
+
 #[derive(Debug, Serialize)]
 #[serde(rename_all = "camelCase")]
-pub struct AdyenCardIdentification {
+pub struct AdyenRawCardIdentification {
     #[serde(rename = "number")]
     card_number: cards::CardNumber,
     expiry_month: Secret<String>,
@@ -131,14 +138,7 @@ pub struct AdyenCardIdentification {
 
 #[derive(Debug, Serialize)]
 #[serde(rename_all = "camelCase")]
-pub struct AdyenCardTokenDetails {
-    card_holder: AdyenAccountHolder,
-    card_identification: AdyenCardTokenIdentification,
-}
-
-#[derive(Debug, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct AdyenCardTokenIdentification {
+pub struct AdyenStoredCardIdentification {
     stored_payment_method_id: Secret<String>,
 }
 
@@ -260,7 +260,6 @@ impl<F> TryFrom<(&PayoutsRouterData<F>, &payouts::CardPayout)> for AdyenAccountH
     ) -> Result<Self, Self::Error> {
         let billing_address = router_data.get_optional_billing();
 
-        // Address is required for both card and bank payouts
         let address = billing_address
             .and_then(|billing| billing.address.as_ref().map(|addr| addr.try_into()))
             .transpose()?
@@ -294,13 +293,25 @@ impl<F> TryFrom<(&PayoutsRouterData<F>, &payouts::CardPayout)> for AdyenAccountH
             .into());
         };
 
+        let customer_id_reference = match router_data.get_connector_customer_id() {
+            Ok(connector_customer_id) => connector_customer_id,
+            Err(_) => {
+                let customer_id = router_data.get_customer_id()?;
+                format!(
+                    "{}_{}",
+                    router_data.merchant_id.get_string_repr(),
+                    customer_id.get_string_repr()
+                )
+            }
+        };
+
         Ok(Self {
             address: Some(address),
             first_name,
             last_name,
             full_name: None,
             email: router_data.get_optional_billing_email(),
-            customer_id: Some(router_data.get_customer_id()?.get_string_repr().to_owned()),
+            customer_id: Some(customer_id_reference),
             entity_type: Some(EntityType::from(router_data.request.entity_type)),
         })
     }
@@ -314,7 +325,6 @@ impl<F> TryFrom<(&PayoutsRouterData<F>, &payouts::Bank)> for AdyenAccountHolder
     ) -> Result<Self, Self::Error> {
         let billing_address = router_data.get_optional_billing();
 
-        // Address is required for both card and bank payouts
         let address = billing_address
             .and_then(|billing| billing.address.as_ref().map(|addr| addr.try_into()))
             .transpose()?
@@ -324,46 +334,156 @@ impl<F> TryFrom<(&PayoutsRouterData<F>, &payouts::Bank)> for AdyenAccountHolder
 
         let full_name = router_data.get_billing_full_name()?;
 
+        let customer_id_reference = match router_data.get_connector_customer_id() {
+            Ok(connector_customer_id) => connector_customer_id,
+            Err(_) => {
+                let customer_id = router_data.get_customer_id()?;
+                format!(
+                    "{}_{}",
+                    router_data.merchant_id.get_string_repr(),
+                    customer_id.get_string_repr()
+                )
+            }
+        };
+
         Ok(Self {
             address: Some(address),
             first_name: None,
             last_name: None,
             full_name: Some(full_name),
             email: router_data.get_optional_billing_email(),
-            customer_id: Some(router_data.get_customer_id()?.get_string_repr().to_owned()),
+            customer_id: Some(customer_id_reference),
             entity_type: Some(EntityType::from(router_data.request.entity_type)),
         })
     }
 }
 
-impl<F> TryFrom<&AdyenPlatformRouterData<&PayoutsRouterData<F>>> for AdyenTransferRequest {
+#[derive(Debug)]
+pub struct StoredPaymentCounterparty<'a, F> {
+    pub item: &'a AdyenPlatformRouterData<&'a PayoutsRouterData<F>>,
+    pub stored_payment_method_id: String,
+}
+
+#[derive(Debug)]
+pub struct RawPaymentCounterparty<'a, F> {
+    pub item: &'a AdyenPlatformRouterData<&'a PayoutsRouterData<F>>,
+    pub raw_payout_method_data: payouts::PayoutMethodData,
+}
+
+impl<F> TryFrom<StoredPaymentCounterparty<'_, F>>
+    for (AdyenPayoutMethodDetails, Option<AdyenPayoutPriority>)
+{
     type Error = Error;
-    fn try_from(
-        item: &AdyenPlatformRouterData<&PayoutsRouterData<F>>,
-    ) -> Result<Self, Self::Error> {
-        let request = &item.router_data.request;
-        let (counterparty, priority) = match item.router_data.get_payout_method_data()? {
+
+    fn try_from(stored_payment: StoredPaymentCounterparty<'_, F>) -> Result<Self, Self::Error> {
+        let request = &stored_payment.item.router_data.request;
+        let payout_type = request.get_payout_type()?;
+
+        match payout_type {
+            enums::PayoutType::Card => {
+                let billing_address = stored_payment.item.router_data.get_optional_billing();
+                let address = billing_address
+                    .and_then(|billing| billing.address.as_ref())
+                    .ok_or(ConnectorError::MissingRequiredField {
+                        field_name: "address",
+                    })?
+                    .try_into()?;
+
+                let customer_id_reference =
+                    match stored_payment.item.router_data.get_connector_customer_id() {
+                        Ok(connector_customer_id) => connector_customer_id,
+                        Err(_) => {
+                            let customer_id = stored_payment.item.router_data.get_customer_id()?;
+                            format!(
+                                "{}_{}",
+                                stored_payment
+                                    .item
+                                    .router_data
+                                    .merchant_id
+                                    .get_string_repr(),
+                                customer_id.get_string_repr()
+                            )
+                        }
+                    };
+
+                let card_holder = AdyenAccountHolder {
+                    address: Some(address),
+                    first_name: stored_payment
+                        .item
+                        .router_data
+                        .get_optional_billing_first_name(),
+                    last_name: stored_payment
+                        .item
+                        .router_data
+                        .get_optional_billing_last_name(),
+                    full_name: stored_payment
+                        .item
+                        .router_data
+                        .get_optional_billing_full_name(),
+                    email: stored_payment.item.router_data.get_optional_billing_email(),
+                    customer_id: Some(customer_id_reference),
+                    entity_type: Some(EntityType::from(request.entity_type)),
+                };
+
+                let card_identification =
+                    AdyenCardIdentification::Stored(AdyenStoredCardIdentification {
+                        stored_payment_method_id: Secret::new(
+                            stored_payment.stored_payment_method_id,
+                        ),
+                    });
+
+                let counterparty = AdyenPayoutMethodDetails::Card(AdyenCardDetails {
+                    card_holder,
+                    card_identification,
+                });
+
+                Ok((counterparty, None))
+            }
+            _ => Err(ConnectorError::NotSupported {
+                message: "Stored payment method is only supported for card payouts".to_string(),
+                connector: "Adyenplatform",
+            }
+            .into()),
+        }
+    }
+}
+
+impl<F> TryFrom<RawPaymentCounterparty<'_, F>>
+    for (AdyenPayoutMethodDetails, Option<AdyenPayoutPriority>)
+{
+    type Error = Error;
+
+    fn try_from(raw_payment: RawPaymentCounterparty<'_, F>) -> Result<Self, Self::Error> {
+        let request = &raw_payment.item.router_data.request;
+
+        match raw_payment.raw_payout_method_data {
             payouts::PayoutMethodData::Wallet(_) => Err(ConnectorError::NotImplemented(
                 utils::get_unimplemented_payment_method_error_message("Adyenplatform"),
             ))?,
             payouts::PayoutMethodData::Card(c) => {
-                let card_holder: AdyenAccountHolder = (item.router_data, &c).try_into()?;
-                let card_identification = AdyenCardIdentification {
-                    card_number: c.card_number,
-                    expiry_month: c.expiry_month,
-                    expiry_year: c.expiry_year,
-                    issue_number: None,
-                    start_month: None,
-                    start_year: None,
-                };
+                let card_holder: AdyenAccountHolder =
+                    (raw_payment.item.router_data, &c).try_into()?;
+
+                let card_identification =
+                    AdyenCardIdentification::Card(AdyenRawCardIdentification {
+                        card_number: c.card_number,
+                        expiry_month: c.expiry_month,
+                        expiry_year: c.expiry_year,
+                        issue_number: None,
+                        start_month: None,
+                        start_year: None,
+                    });
+
                 let counterparty = AdyenPayoutMethodDetails::Card(AdyenCardDetails {
                     card_holder,
                     card_identification,
                 });
-                (counterparty, None)
+
+                Ok((counterparty, None))
             }
             payouts::PayoutMethodData::Bank(bd) => {
-                let account_holder: AdyenAccountHolder = (item.router_data, &bd).try_into()?;
+                let account_holder: AdyenAccountHolder =
+                    (raw_payment.item.router_data, &bd).try_into()?;
                 let bank_details = match bd {
                     payouts::Bank::Sepa(b) => AdyenBankAccountIdentification {
                         bank_type: "iban".to_string(),
@@ -393,9 +513,42 @@ impl<F> TryFrom<&AdyenPlatformRouterData<&PayoutsRouterData<F>>> for AdyenTransf
                     .ok_or(ConnectorError::MissingRequiredField {
                         field_name: "priority",
                     })?;
-                (counterparty, Some(AdyenPayoutPriority::from(priority)))
+
+                Ok((counterparty, Some(AdyenPayoutPriority::from(priority))))
             }
-        };
+        }
+    }
+}
+
+impl<F> TryFrom<&AdyenPlatformRouterData<&PayoutsRouterData<F>>> for AdyenTransferRequest {
+    type Error = Error;
+    fn try_from(
+        item: &AdyenPlatformRouterData<&PayoutsRouterData<F>>,
+    ) -> Result<Self, Self::Error> {
+        let request = &item.router_data.request;
+        let stored_payment_method_result =
+            item.router_data.request.get_connector_transfer_method_id();
+        let raw_payout_method_result = item.router_data.get_payout_method_data();
+
+        let (counterparty, priority) =
+            if let Ok(stored_payment_method_id) = stored_payment_method_result {
+                StoredPaymentCounterparty {
+                    item,
+                    stored_payment_method_id,
+                }
+                .try_into()?
+            } else if let Ok(raw_payout_method_data) = raw_payout_method_result {
+                RawPaymentCounterparty {
+                    item,
+                    raw_payout_method_data,
+                }
+                .try_into()?
+            } else {
+                return Err(ConnectorError::MissingRequiredField {
+                    field_name: "payout_method_data or stored_payment_method_id",
+                }
+                .into());
+            };
         let adyen_connector_metadata_object =
             AdyenPlatformConnectorMetadataObject::try_from(&item.router_data.connector_meta_data)?;
         let balance_account_id = adyen_connector_metadata_object
@@ -570,22 +723,11 @@ pub fn get_adyen_webhook_event(
             AdyenplatformWebhookStatus::Authorised | AdyenplatformWebhookStatus::Received => {
                 webhooks::IncomingWebhookEvent::PayoutCreated
             }
-            AdyenplatformWebhookStatus::Booked => {
-                match category {
-                    Some(AdyenPayoutMethod::Card) => {
-                        // For card payouts, "booked" is the final success state
-                        webhooks::IncomingWebhookEvent::PayoutSuccess
-                    }
-                    Some(AdyenPayoutMethod::Bank) => {
-                        // For bank payouts, "booked" is intermediate - wait for final confirmation
-                        webhooks::IncomingWebhookEvent::PayoutProcessing
-                    }
-                    None => {
-                        // Default to processing if category is unknown
-                        webhooks::IncomingWebhookEvent::PayoutProcessing
-                    }
-                }
-            }
+            AdyenplatformWebhookStatus::Booked => match category {
+                Some(AdyenPayoutMethod::Card) => webhooks::IncomingWebhookEvent::PayoutSuccess,
+                Some(AdyenPayoutMethod::Bank) => webhooks::IncomingWebhookEvent::PayoutProcessing,
+                None => webhooks::IncomingWebhookEvent::PayoutProcessing,
+            },
             AdyenplatformWebhookStatus::Pending => webhooks::IncomingWebhookEvent::PayoutProcessing,
             AdyenplatformWebhookStatus::Failed => webhooks::IncomingWebhookEvent::PayoutFailure,
             AdyenplatformWebhookStatus::Returned => webhooks::IncomingWebhookEvent::PayoutReversed,
diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs
index a06b6140da4..c7784118847 100644
--- a/crates/router/src/core/payments/routing.rs
+++ b/crates/router/src/core/payments/routing.rs
@@ -188,7 +188,19 @@ pub fn make_dsl_input_for_payouts(
         payment_method_type: payout_data
             .payout_method_data
             .as_ref()
-            .map(api_enums::PaymentMethodType::foreign_from),
+            .map(api_enums::PaymentMethodType::foreign_from)
+            .or_else(|| {
+                payout_data.payment_method.as_ref().and_then(|pm| {
+                    #[cfg(feature = "v1")]
+                    {
+                        pm.payment_method_type
+                    }
+                    #[cfg(feature = "v2")]
+                    {
+                        pm.payment_method_subtype
+                    }
+                })
+            }),
         card_network: None,
     };
     Ok(dsl_inputs::BackendInput {
diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs
index 66a824fc01b..dee0d3f130a 100644
--- a/crates/router/src/core/payouts.rs
+++ b/crates/router/src/core/payouts.rs
@@ -58,7 +58,7 @@ use crate::{
         api::{self, payments as payment_api_types, payouts},
         domain,
         storage::{self, PaymentRoutingInfo},
-        transformers::ForeignFrom,
+        transformers::{ForeignFrom, ForeignTryFrom},
     },
     utils::{self, OptionExt},
 };
@@ -78,6 +78,7 @@ pub struct PayoutData {
     pub payout_link: Option<PayoutLink>,
     pub current_locale: String,
     pub payment_method: Option<PaymentMethod>,
+    pub connector_transfer_method_id: Option<String>,
 }
 
 // ********************************************** CORE FLOWS **********************************************
@@ -693,27 +694,8 @@ pub async fn payouts_fulfill_core(
         .attach_printable("Connector not found for payout fulfillment")?,
     };
 
-    // Trigger fulfillment
-    let customer_id = payout_data
-        .payouts
-        .customer_id
-        .clone()
-        .get_required_value("customer_id")?;
-    payout_data.payout_method_data = Some(
-        helpers::make_payout_method_data(
-            &state,
-            None,
-            payout_attempt.payout_token.as_deref(),
-            &customer_id,
-            &payout_attempt.merchant_id,
-            payout_data.payouts.payout_type,
-            merchant_context.get_merchant_key_store(),
-            Some(&mut payout_data),
-            merchant_context.get_merchant_account().storage_scheme,
-        )
-        .await?
-        .get_required_value("payout_method_data")?,
-    );
+    helpers::fetch_payout_method_data(&state, &mut payout_data, &connector_data, &merchant_context)
+        .await?;
     Box::pin(fulfill_payout(
         &state,
         &merchant_context,
@@ -1072,25 +1054,8 @@ pub async fn call_connector_payout(
 
     // Fetch / store payout_method_data
     if payout_data.payout_method_data.is_none() || payout_attempt.payout_token.is_none() {
-        let customer_id = payouts
-            .customer_id
-            .clone()
-            .get_required_value("customer_id")?;
-        payout_data.payout_method_data = Some(
-            helpers::make_payout_method_data(
-                state,
-                payout_data.payout_method_data.to_owned().as_ref(),
-                payout_attempt.payout_token.as_deref(),
-                &customer_id,
-                &payout_attempt.merchant_id,
-                payouts.payout_type,
-                merchant_context.get_merchant_key_store(),
-                Some(payout_data),
-                merchant_context.get_merchant_account().storage_scheme,
-            )
-            .await?
-            .get_required_value("payout_method_data")?,
-        );
+        helpers::fetch_payout_method_data(state, payout_data, connector_data, merchant_context)
+            .await?;
     }
     // Eligibility flow
     Box::pin(complete_payout_eligibility(
@@ -2002,8 +1967,7 @@ pub async fn complete_create_recipient_disburse_account(
         && connector_data
             .connector_name
             .supports_vendor_disburse_account_create_for_payout()
-        && helpers::should_create_connector_transfer_method(&*payout_data, connector_data)?
-            .is_none()
+        && helpers::should_create_connector_transfer_method(payout_data, connector_data)?.is_none()
     {
         Box::pin(create_recipient_disburse_account(
             state,
@@ -2706,12 +2670,29 @@ pub async fn payout_create_db_entries(
                 .map(|pm| pm.payment_method_id.clone()),
             Some(api_enums::PayoutType::foreign_from(payout_method_data)),
         ),
-        None => (
-            payment_method
-                .as_ref()
-                .map(|pm| pm.payment_method_id.clone()),
-            req.payout_type.to_owned(),
-        ),
+        None => {
+            (
+                payment_method
+                    .as_ref()
+                    .map(|pm| pm.payment_method_id.clone()),
+                match req.payout_type {
+                    None => payment_method
+                        .as_ref()
+                        .and_then(|pm| pm.payment_method)
+                        .map(|payment_method_enum| {
+                            api_enums::PayoutType::foreign_try_from(payment_method_enum)
+                                .change_context(errors::ApiErrorResponse::InvalidRequestData {
+                                    message: format!(
+                                        "PaymentMethod {payment_method_enum:?} is not supported for payouts"
+                                    ),
+                                })
+                                .attach_printable("Failed to convert PaymentMethod to PayoutType")
+                        })
+                        .transpose()?,
+                    payout_type => payout_type,
+                },
+            )
+        }
     };
 
     let client_secret = utils::generate_id(
@@ -2837,6 +2818,7 @@ pub async fn payout_create_db_entries(
         payout_link,
         current_locale: locale.to_string(),
         payment_method,
+        connector_transfer_method_id: None,
     })
 }
 
@@ -3031,11 +3013,10 @@ pub async fn make_payout_data(
         .await
         .transpose()?;
 
-    let payout_method_id = payouts.payout_method_id.clone();
-    let mut payment_method: Option<PaymentMethod> = None;
-
-    if let Some(pm_id) = payout_method_id {
-        payment_method = Some(
+    let payment_method = payouts
+        .payout_method_id
+        .clone()
+        .async_map(|pm_id| async move {
             db.find_payment_method(
                 &(state.into()),
                 merchant_context.get_merchant_key_store(),
@@ -3044,9 +3025,10 @@ pub async fn make_payout_data(
             )
             .await
             .change_context(errors::ApiErrorResponse::PaymentMethodNotFound)
-            .attach_printable("Unable to find payment method")?,
-        );
-    }
+            .attach_printable("Unable to find payment method")
+        })
+        .await
+        .transpose()?;
 
     Ok(PayoutData {
         billing_address,
@@ -3061,6 +3043,7 @@ pub async fn make_payout_data(
         payout_link,
         current_locale: locale.to_string(),
         payment_method,
+        connector_transfer_method_id: None,
     })
 }
 
diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs
index d74baa2011d..8f104451be5 100644
--- a/crates/router/src/core/payouts/helpers.rs
+++ b/crates/router/src/core/payouts/helpers.rs
@@ -222,6 +222,49 @@ pub fn should_create_connector_transfer_method(
     Ok(connector_transfer_method_id)
 }
 
+pub async fn fetch_payout_method_data(
+    state: &SessionState,
+    payout_data: &mut PayoutData,
+    connector_data: &api::ConnectorData,
+    merchant_context: &domain::MerchantContext,
+) -> RouterResult<()> {
+    let connector_transfer_method_id =
+        should_create_connector_transfer_method(payout_data, connector_data)?;
+
+    if connector_transfer_method_id.is_some() {
+        logger::info!("Using stored transfer_method_id, skipping payout_method_data fetch");
+    } else {
+        let customer_id = payout_data
+            .payouts
+            .customer_id
+            .clone()
+            .get_required_value("customer_id")?;
+
+        let payout_method_data_clone = payout_data.payout_method_data.clone();
+        let payout_token = payout_data.payout_attempt.payout_token.clone();
+        let merchant_id = payout_data.payout_attempt.merchant_id.clone();
+        let payout_type = payout_data.payouts.payout_type;
+
+        let payout_method_data = make_payout_method_data(
+            state,
+            payout_method_data_clone.as_ref(),
+            payout_token.as_deref(),
+            &customer_id,
+            &merchant_id,
+            payout_type,
+            merchant_context.get_merchant_key_store(),
+            Some(payout_data),
+            merchant_context.get_merchant_account().storage_scheme,
+        )
+        .await?
+        .get_required_value("payout_method_data")?;
+
+        payout_data.payout_method_data = Some(payout_method_data);
+    }
+
+    Ok(())
+}
+
 #[cfg(feature = "v1")]
 pub async fn save_payout_data_to_locker(
     state: &SessionState,
diff --git a/crates/router/src/core/payouts/validator.rs b/crates/router/src/core/payouts/validator.rs
index 198caa4b893..87a0748e5f4 100644
--- a/crates/router/src/core/payouts/validator.rs
+++ b/crates/router/src/core/payouts/validator.rs
@@ -223,34 +223,48 @@ pub async fn validate_create_request(
             .await
         }
         (_, Some(_), Some(payment_method)) => {
-            match get_pm_list_context(
-                state,
-                payment_method
-                    .payment_method
-                    .as_ref()
-                    .get_required_value("payment_method_id")?,
-                merchant_context.get_merchant_key_store(),
-                payment_method,
-                None,
-                false,
-                true,
-                merchant_context,
-            )
-            .await?
+            // Check if we have a stored transfer_method_id first
+            if payment_method
+                .get_common_mandate_reference()
+                .ok()
+                .and_then(|common_mandate_ref| common_mandate_ref.payouts)
+                .map(|payouts_mandate_ref| !payouts_mandate_ref.0.is_empty())
+                .unwrap_or(false)
             {
-                Some(pm) => match (pm.card_details, pm.bank_transfer_details) {
-                    (Some(card), _) => Ok(Some(payouts::PayoutMethodData::Card(
-                        api_models::payouts::CardPayout {
-                            card_number: card.card_number.get_required_value("card_number")?,
-                            card_holder_name: card.card_holder_name,
-                            expiry_month: card.expiry_month.get_required_value("expiry_month")?,
-                            expiry_year: card.expiry_year.get_required_value("expiry_year")?,
-                        },
-                    ))),
-                    (_, Some(bank)) => Ok(Some(payouts::PayoutMethodData::Bank(bank))),
-                    _ => Ok(None),
-                },
-                None => Ok(None),
+                Ok(None)
+            } else {
+                // No transfer_method_id available, proceed with vault fetch for raw card details
+                match get_pm_list_context(
+                    state,
+                    payment_method
+                        .payment_method
+                        .as_ref()
+                        .get_required_value("payment_method_id")?,
+                    merchant_context.get_merchant_key_store(),
+                    payment_method,
+                    None,
+                    false,
+                    true,
+                    merchant_context,
+                )
+                .await?
+                {
+                    Some(pm) => match (pm.card_details, pm.bank_transfer_details) {
+                        (Some(card), _) => Ok(Some(payouts::PayoutMethodData::Card(
+                            api_models::payouts::CardPayout {
+                                card_number: card.card_number.get_required_value("card_number")?,
+                                card_holder_name: card.card_holder_name,
+                                expiry_month: card
+                                    .expiry_month
+                                    .get_required_value("expiry_month")?,
+                                expiry_year: card.expiry_year.get_required_value("expiry_year")?,
+                            },
+                        ))),
+                        (_, Some(bank)) => Ok(Some(payouts::PayoutMethodData::Bank(bank))),
+                        _ => Ok(None),
+                    },
+                    None => Ok(None),
+                }
             }
         }
         _ => Ok(None),
diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs
index 8109df07554..81d72101fe4 100644
--- a/crates/router/src/types/transformers.rs
+++ b/crates/router/src/types/transformers.rs
@@ -1251,6 +1251,24 @@ impl ForeignFrom<api_models::enums::PayoutType> for api_enums::PaymentMethod {
     }
 }
 
+#[cfg(feature = "payouts")]
+impl ForeignTryFrom<api_enums::PaymentMethod> for api_models::enums::PayoutType {
+    type Error = error_stack::Report<errors::ApiErrorResponse>;
+
+    fn foreign_try_from(value: api_enums::PaymentMethod) -> Result<Self, Self::Error> {
+        match value {
+            api_enums::PaymentMethod::Card => Ok(Self::Card),
+            api_enums::PaymentMethod::BankTransfer => Ok(Self::Bank),
+            api_enums::PaymentMethod::Wallet => Ok(Self::Wallet),
+            _ => Err(errors::ApiErrorResponse::InvalidRequestData {
+                message: format!("PaymentMethod {value:?} is not supported for payouts"),
+            })
+            .change_context(errors::ApiErrorResponse::InternalServerError)
+            .attach_printable("Failed to convert PaymentMethod to PayoutType"),
+        }
+    }
+}
+
 #[cfg(feature = "v1")]
 impl ForeignTryFrom<&HeaderMap> for hyperswitch_domain_models::payments::HeaderPayload {
     type Error = error_stack::Report<errors::ApiErrorResponse>;
diff --git a/nix/modules/flake/devshell.nix b/nix/modules/flake/devshell.nix
index 6df0f5951f1..2fdcdd78627 100644
--- a/nix/modules/flake/devshell.nix
+++ b/nix/modules/flake/devshell.nix
@@ -35,6 +35,8 @@
           shellHook = ''
             echo 1>&2 "Ready to work on hyperswitch!"
             rustc --version
+            export OPENSSL_DIR="${pkgs.openssl.dev}"
+            export OPENSSL_LIB_DIR="${pkgs.openssl.out}/lib"
           '';
         };
         qa = pkgs.mkShell {

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [x] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
This PR adds support for Adyen tokenized card payouts in the AdyenPlatform connector. This enables processing payouts using stored PSP tokens

- Restructured `AdyenCardIdentification` as an enum supporting both raw card data and stored payment method IDs
- Added `StoredPaymentCounterparty` and `RawPaymentCounterparty` for handling different payment method types
- Implemented fallback logic: stored payment method ID → raw card details
- Added `fetch_payout_method_data` helper function to centralize payout method data retrieval
- Enhanced validator to check for stored transfer method IDs before fetching raw card details



### Additional Changes

- [ ] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
Enables merchants to process Adyen payouts using previously stored PSP tokens without requiring raw card data. This is helpful for migrated PSP tokens (created outside of HS).

## How did you test it?
<details>
    <summary>1. Create a payment CIT</summary>

cURL

    curl --location --request POST 'http://localhost:8080/payments' \
        --header 'Content-Type: application/json' \
        --header 'Accept: application/json' \
        --header 'api-key: dev_ACto5kMwzvZvAZCM5LMfFoDaBxAKHNeQkYVRuitCrf2Q30DrqxbPr6YFOe8c2f3l' \
        --data-raw '{"amount":4500,"currency":"EUR","confirm":true,"profile_id":"pro_S5yvh1TfnZi2Gjv7uSUQ","capture_method":"automatic","authentication_type":"three_ds","setup_future_usage":"off_session","customer_id":"cus_payment_cit_1","email":"abc@example.com","return_url":"https://google.com","payment_method":"card","payment_method_type":"debit","payment_method_data":{"card":{"card_number":"4111111111111111","card_exp_month":"03","card_exp_year":"2030","card_cvc":"737"}},"billing":{"address":{"line1":"1467","line2":"Harrison Street","line3":"Harrison Street","city":"San Fransico","zip":"94122","country":"SG","first_name":"joseph","last_name":"Doe"},"phone":{"number":"8056594427","country_code":"+91"},"email":"guest@example.com"},"metadata":{"udf1":"value1","new_customer":"true","login_date":"2019-09-10T10:11:12Z"},"browser_info":{"user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36","accept_header":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8","language":"en-US","color_depth":32,"screen_height":1117,"screen_width":1728,"time_zone":-330,"java_enabled":true,"java_script_enabled":true,"ip_address":"127.0.0.1"},"customer_acceptance":{"acceptance_type":"online","accepted_at":"1963-05-03T04:07:52.723Z","online":{"ip_address":"127.0.0.1","user_agent":"amet irure esse"}},"session_expiry":2890}'

Response

    {"payment_id":"pay_funVtY7kQ3avj2SN3Lzm","merchant_id":"merchant_1755863240","status":"succeeded","amount":4500,"net_amount":4500,"shipping_cost":null,"amount_capturable":0,"amount_received":4500,"connector":"adyen","client_secret":"pay_funVtY7kQ3avj2SN3Lzm_secret_cR7UTM6j2h1jabVHAFHq","created":"2025-08-22T12:57:49.884Z","currency":"EUR","customer_id":"cus_uRtCHhDU0C2hafZMVHx1","customer":{"id":"cus_uRtCHhDU0C2hafZMVHx1","name":"Albert Klaassen","email":"abc@example.com","phone":"6168205362","phone_country_code":"+1"},"description":null,"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":"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":null,"payment_checks":null,"authentication_data":null},"billing":null},"payment_token":null,"shipping":null,"billing":{"address":{"city":"San Fransico","country":"SG","line1":"1467","line2":"Harrison Street","line3":"Harrison Street","zip":"94122","state":null,"first_name":"joseph","last_name":"Doe","origin_zip":null},"phone":{"number":"8056594427","country_code":"+91"},"email":"guest@example.com"},"order_details":null,"email":"abc@example.com","name":"Albert Klaassen","phone":"6168205362","return_url":"https://google.com/","authentication_type":"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":"debit","connector_label":null,"business_country":null,"business_label":"default","business_sub_label":null,"allowed_payment_method_types":null,"ephemeral_key":{"customer_id":"cus_uRtCHhDU0C2hafZMVHx1","created_at":1755867469,"expires":1755871069,"secret":"epk_fdc03a4797ef451a98b2929bcfc398ca"},"manual_retry_allowed":false,"connector_transaction_id":"VCV7F35F2L3C37V5","frm_message":null,"metadata":{"udf1":"value1","login_date":"2019-09-10T10:11:12Z","new_customer":"true"},"connector_metadata":null,"feature_metadata":{"redirect_response":null,"search_tags":null,"apple_pay_recurring_details":null,"gateway_system":"direct"},"reference_id":"pay_funVtY7kQ3avj2SN3Lzm_1","payment_link":null,"profile_id":"pro_sHtakSPxfG6JnK0wpCKG","surcharge_details":null,"attempt_count":1,"merchant_decision":null,"merchant_connector_id":"mca_A3zlBnD9y8WvakW0ratr","incremental_authorization_allowed":false,"authorization_count":null,"incremental_authorizations":null,"external_authentication_details":null,"external_3ds_authentication_attempted":false,"expires_on":"2025-08-22T13:45:59.884Z","fingerprint":null,"browser_info":{"language":"en-US","time_zone":-330,"ip_address":"127.0.0.1","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36","color_depth":32,"java_enabled":true,"screen_width":1728,"accept_header":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8","screen_height":1117,"java_script_enabled":true},"payment_channel":null,"payment_method_id":"pm_L6rQCpfqi5f1OAG4DHIG","network_transaction_id":"056847192959811","payment_method_status":"active","updated":"2025-08-22T12:57:50.457Z","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":"X99D3DNTHP447NV5","card_discovery":"manual","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,"enable_partial_authorization":null}

Manually update `connector_mandate_detail` to include payout MCA as well

    {
        "mca_A3zlBnD9y8WvakW0ratr": {
            "mandate_metadata": null,
            "payment_method_type": "debit",
            "connector_mandate_id": "X99D3DNTHP447NV5",
            "connector_mandate_status": "active",
            "original_payment_authorized_amount": 4500,
            "original_payment_authorized_currency": "EUR",
            "connector_mandate_request_reference_id": "GOSHRSEHXi2emhKCLC"
        }
    }

Updated to

    {
        "mca_A3zlBnD9y8WvakW0ratr": {
            "mandate_metadata": null,
            "payment_method_type": "debit",
            "connector_mandate_id": "X99D3DNTHP447NV5",
            "connector_mandate_status": "active",
            "original_payment_authorized_amount": 4500,
            "original_payment_authorized_currency": "EUR",
            "connector_mandate_request_reference_id": "GOSHRSEHXi2emhKCLC"
        },
        "payouts": {
            "mca_NsHlFe2GsWvK9CdftrLt": {
                "transfer_method_id": "X99D3DNTHP447NV5"
            }
        }
    }

</details>

<details>
    <summary>2. Create a payout using payout_method_id</summary>

cURL

    curl --location --request POST 'http://localhost:8080/payouts/create' \
        --header 'Content-Type: application/json' \
        --header 'api-key: dev_XKQaI34jBT7ZKVNZtpbrc1feIuijxd7MgPf8GMFvqLxniEGUdbNoKPcm0r8aHZhg' \
        --data '{"payout_method_id":"pm_L6rQCpfqi5f1OAG4DHIG","amount":100,"currency":"EUR","customer_id":"cus_CzI1yN5av9n1nICj0ofl","connector":["adyenplatform"],"billing":{"address":{"line1":"1467","line2":"Harrison Street","line3":"Harrison Street","city":"San Fransico","state":"CA","zip":"94122","country":"DE","first_name":"Albert","last_name":"Klassen"},"phone":{"number":"8056594427","country_code":"+91"}},"entity_type":"Individual","recurring":true,"metadata":{"ref":"123"},"confirm":true,"auto_fulfill":true}'

Response

    {"payout_id":"payout_wJvyBtv3lJXXYe2f2Zlo","merchant_id":"merchant_1755867774","merchant_order_reference_id":null,"amount":100,"currency":"EUR","connector":"adyenplatform","payout_type":"card","payout_method_data":null,"billing":{"address":{"city":"San Fransico","country":"DE","line1":"1467","line2":"Harrison Street","line3":"Harrison Street","zip":"94122","state":"CA","first_name":"Albert","last_name":"Klassen","origin_zip":null},"phone":{"number":"8056594427","country_code":"+91"},"email":null},"auto_fulfill":true,"customer_id":"cus_CzI1yN5av9n1nICj0ofl","customer":{"id":"cus_CzI1yN5av9n1nICj0ofl","name":"Albert Klaassen","email":"abc@example.com","phone":"6168205362","phone_country_code":"+1"},"client_secret":"payout_payout_wJvyBtv3lJXXYe2f2Zlo_secret_jgJ0TCCA8exlXEeh5KsK","return_url":null,"business_country":null,"business_label":null,"description":null,"entity_type":"Individual","recurring":true,"metadata":{"ref":"123"},"merchant_connector_id":"mca_8gi7OlijF22Wv9ImMR4w","status":"initiated","error_message":null,"error_code":null,"profile_id":"pro_8lrTNA9EX88OGfPXiElF","created":"2025-08-22T14:57:13.628Z","connector_transaction_id":"38EBH7682DK7VMVK","priority":null,"payout_link":null,"email":"abc@example.com","name":"Albert Klaassen","phone":"6168205362","phone_country_code":"+1","unified_code":null,"unified_message":null,"payout_method_id":"pm_L6rQCpfqi5f1OAG4DHIG"}

</details>

## Checklist
<!-- Put an `x` in the boxes that apply -->

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