Bug: [FEATURE] Add support for network token migration



Currently, the migration api only supports card raw details migration.

Feature description - 
Add support to migrate raw network token details along with the network token requestor ref id.
diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs
index b1f15188dcb..6fdb7d59b0f 100644
--- a/crates/api_models/src/events/payment.rs
+++ b/crates/api_models/src/events/payment.rs
@@ -18,7 +18,7 @@ use crate::{
         ListCountriesCurrenciesResponse, PaymentMethodCollectLinkRenderRequest,
         PaymentMethodCollectLinkRequest, PaymentMethodCollectLinkResponse,
         PaymentMethodDeleteResponse, PaymentMethodListRequest, PaymentMethodListResponse,
-        PaymentMethodResponse, PaymentMethodUpdate,
+        PaymentMethodMigrateResponse, PaymentMethodResponse, PaymentMethodUpdate,
     },
     payments::{
         self, ExtendedCardInfoResponse, PaymentIdType, PaymentListConstraints,
@@ -218,6 +218,29 @@ impl ApiEventMetric for PaymentMethodResponse {
     }
 }
 
+impl ApiEventMetric for PaymentMethodMigrateResponse {
+    #[cfg(all(
+        any(feature = "v1", feature = "v2"),
+        not(feature = "payment_methods_v2")
+    ))]
+    fn get_api_event_type(&self) -> Option<ApiEventsType> {
+        Some(ApiEventsType::PaymentMethod {
+            payment_method_id: self.payment_method_response.payment_method_id.clone(),
+            payment_method: self.payment_method_response.payment_method,
+            payment_method_type: self.payment_method_response.payment_method_type,
+        })
+    }
+
+    #[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
+    fn get_api_event_type(&self) -> Option<ApiEventsType> {
+        Some(ApiEventsType::PaymentMethod {
+            payment_method_id: self.payment_method_response.payment_method_id.clone(),
+            payment_method: self.payment_method_response.payment_method_type,
+            payment_method_type: self.payment_method_response.payment_method_subtype,
+        })
+    }
+}
+
 impl ApiEventMetric for PaymentMethodUpdate {}
 
 impl ApiEventMetric for DefaultPaymentMethod {
diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs
index 5baf138b090..0343ee02119 100644
--- a/crates/api_models/src/payment_methods.rs
+++ b/crates/api_models/src/payment_methods.rs
@@ -245,6 +245,9 @@ pub struct PaymentMethodMigrate {
     /// Card Details
     pub card: Option<MigrateCardDetail>,
 
+    /// Network token details
+    pub network_token: Option<MigrateNetworkTokenDetail>,
+
     /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.
     pub metadata: Option<pii::SecretSerdeValue>,
 
@@ -276,6 +279,24 @@ pub struct PaymentMethodMigrate {
     pub network_transaction_id: Option<String>,
 }
 
+#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)]
+pub struct PaymentMethodMigrateResponse {
+    //payment method response when payment method entry is created
+    pub payment_method_response: PaymentMethodResponse,
+
+    //card data migration status
+    pub card_migrated: Option<bool>,
+
+    //network token data migration status
+    pub network_token_migrated: Option<bool>,
+
+    //connector mandate details migration status
+    pub connector_mandate_details_migrated: Option<bool>,
+
+    //network transaction id migration status
+    pub network_transaction_id_migrated: Option<bool>,
+}
+
 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
 pub struct PaymentsMandateReference(
     pub HashMap<id_type::MerchantConnectorAccountId, PaymentsMandateReferenceRecord>,
@@ -540,6 +561,53 @@ pub struct MigrateCardDetail {
     pub card_type: Option<String>,
 }
 
+#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)]
+#[serde(deny_unknown_fields)]
+pub struct MigrateNetworkTokenData {
+    /// Network Token Number
+    #[schema(value_type = String,example = "4111111145551142")]
+    pub network_token_number: CardNumber,
+
+    /// Network Token Expiry Month
+    #[schema(value_type = String,example = "10")]
+    pub network_token_exp_month: masking::Secret<String>,
+
+    /// Network Token Expiry Year
+    #[schema(value_type = String,example = "25")]
+    pub network_token_exp_year: masking::Secret<String>,
+
+    /// Card Holder Name
+    #[schema(value_type = String,example = "John Doe")]
+    pub card_holder_name: Option<masking::Secret<String>>,
+
+    /// Card Holder's Nick Name
+    #[schema(value_type = Option<String>,example = "John Doe")]
+    pub nick_name: Option<masking::Secret<String>>,
+
+    /// Card Issuing Country
+    pub card_issuing_country: Option<String>,
+
+    /// Card's Network
+    #[schema(value_type = Option<CardNetwork>)]
+    pub card_network: Option<api_enums::CardNetwork>,
+
+    /// Issuer Bank for Card
+    pub card_issuer: Option<String>,
+
+    /// Card Type
+    pub card_type: Option<String>,
+}
+
+#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)]
+#[serde(deny_unknown_fields)]
+pub struct MigrateNetworkTokenDetail {
+    /// Network token details
+    pub network_token_data: MigrateNetworkTokenData,
+
+    /// Network token requestor reference id
+    pub network_token_requestor_ref_id: String,
+}
+
 #[cfg(all(
     any(feature = "v1", feature = "v2"),
     not(feature = "payment_methods_v2")
@@ -2082,6 +2150,10 @@ pub struct PaymentMethodRecord {
     pub original_transaction_amount: Option<i64>,
     pub original_transaction_currency: Option<common_enums::Currency>,
     pub line_number: Option<i64>,
+    pub network_token_number: Option<CardNumber>,
+    pub network_token_expiry_month: Option<masking::Secret<String>>,
+    pub network_token_expiry_year: Option<masking::Secret<String>>,
+    pub network_token_requestor_ref_id: Option<String>,
 }
 
 #[derive(Debug, Default, serde::Serialize)]
@@ -2098,6 +2170,10 @@ pub struct PaymentMethodMigrationResponse {
     #[serde(skip_serializing_if = "Option::is_none")]
     pub migration_error: Option<String>,
     pub card_number_masked: Option<masking::Secret<String>>,
+    pub card_migrated: Option<bool>,
+    pub network_token_migrated: Option<bool>,
+    pub connector_mandate_details_migrated: Option<bool>,
+    pub network_transaction_id_migrated: Option<bool>,
 }
 
 #[derive(Debug, Default, serde::Serialize)]
@@ -2107,8 +2183,10 @@ pub enum MigrationStatus {
     Failed,
 }
 
-type PaymentMethodMigrationResponseType =
-    (Result<PaymentMethodResponse, String>, PaymentMethodRecord);
+type PaymentMethodMigrationResponseType = (
+    Result<PaymentMethodMigrateResponse, String>,
+    PaymentMethodRecord,
+);
 #[cfg(all(
     any(feature = "v2", feature = "v1"),
     not(feature = "payment_methods_v2")
@@ -2117,14 +2195,18 @@ impl From<PaymentMethodMigrationResponseType> for PaymentMethodMigrationResponse
     fn from((response, record): PaymentMethodMigrationResponseType) -> Self {
         match response {
             Ok(res) => Self {
-                payment_method_id: Some(res.payment_method_id),
-                payment_method: res.payment_method,
-                payment_method_type: res.payment_method_type,
-                customer_id: res.customer_id,
+                payment_method_id: Some(res.payment_method_response.payment_method_id),
+                payment_method: res.payment_method_response.payment_method,
+                payment_method_type: res.payment_method_response.payment_method_type,
+                customer_id: res.payment_method_response.customer_id,
                 migration_status: MigrationStatus::Success,
                 migration_error: None,
                 card_number_masked: Some(record.card_number_masked),
                 line_number: record.line_number,
+                card_migrated: res.card_migrated,
+                network_token_migrated: res.network_token_migrated,
+                connector_mandate_details_migrated: res.connector_mandate_details_migrated,
+                network_transaction_id_migrated: res.network_transaction_id_migrated,
             },
             Err(e) => Self {
                 customer_id: Some(record.customer_id),
@@ -2143,14 +2225,18 @@ impl From<PaymentMethodMigrationResponseType> for PaymentMethodMigrationResponse
     fn from((response, record): PaymentMethodMigrationResponseType) -> Self {
         match response {
             Ok(res) => Self {
-                payment_method_id: Some(res.payment_method_id),
-                payment_method: res.payment_method_type,
-                payment_method_type: res.payment_method_subtype,
-                customer_id: Some(res.customer_id),
+                payment_method_id: Some(res.payment_method_response.payment_method_id),
+                payment_method: res.payment_method_response.payment_method_type,
+                payment_method_type: res.payment_method_response.payment_method_subtype,
+                customer_id: Some(res.payment_method_response.customer_id),
                 migration_status: MigrationStatus::Success,
                 migration_error: None,
                 card_number_masked: Some(record.card_number_masked),
                 line_number: record.line_number,
+                card_migrated: res.card_migrated,
+                network_token_migrated: res.network_token_migrated,
+                connector_mandate_details_migrated: res.connector_mandate_details_migrated,
+                network_transaction_id_migrated: res.network_transaction_id_migrated,
             },
             Err(e) => Self {
                 customer_id: Some(record.customer_id),
@@ -2213,6 +2299,22 @@ impl
                 card_issuing_country: None,
                 nick_name: Some(record.nick_name.clone()),
             }),
+            network_token: Some(MigrateNetworkTokenDetail {
+                network_token_data: MigrateNetworkTokenData {
+                    network_token_number: record.network_token_number.unwrap_or_default(),
+                    network_token_exp_month: record.network_token_expiry_month.unwrap_or_default(),
+                    network_token_exp_year: record.network_token_expiry_year.unwrap_or_default(),
+                    card_holder_name: record.name,
+                    nick_name: Some(record.nick_name.clone()),
+                    card_issuing_country: None,
+                    card_network: None,
+                    card_issuer: None,
+                    card_type: None,
+                },
+                network_token_requestor_ref_id: record
+                    .network_token_requestor_ref_id
+                    .unwrap_or_default(),
+            }),
             payment_method: record.payment_method,
             payment_method_type: record.payment_method_type,
             payment_method_issuer: None,
diff --git a/crates/diesel_models/src/payment_method.rs b/crates/diesel_models/src/payment_method.rs
index d607cd04bff..55460037855 100644
--- a/crates/diesel_models/src/payment_method.rs
+++ b/crates/diesel_models/src/payment_method.rs
@@ -248,6 +248,11 @@ pub enum PaymentMethodUpdate {
     ConnectorMandateDetailsUpdate {
         connector_mandate_details: Option<serde_json::Value>,
     },
+    NetworkTokenDataUpdate {
+        network_token_requestor_reference_id: Option<String>,
+        network_token_locker_id: Option<String>,
+        network_token_payment_method_data: Option<Encryption>,
+    },
 }
 
 #[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
@@ -620,6 +625,27 @@ impl From<PaymentMethodUpdate> for PaymentMethodUpdateInternal {
                 network_token_locker_id: None,
                 network_token_payment_method_data: None,
             },
+            PaymentMethodUpdate::NetworkTokenDataUpdate {
+                network_token_requestor_reference_id,
+                network_token_locker_id,
+                network_token_payment_method_data,
+            } => Self {
+                metadata: None,
+                payment_method_data: None,
+                last_used_at: None,
+                status: None,
+                locker_id: None,
+                payment_method: None,
+                connector_mandate_details: None,
+                updated_by: None,
+                payment_method_issuer: None,
+                payment_method_type: None,
+                last_modified: common_utils::date_time::now(),
+                network_transaction_id: None,
+                network_token_requestor_reference_id,
+                network_token_locker_id,
+                network_token_payment_method_data,
+            },
         }
     }
 }
diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs
index 49a75735ea4..4073b7a4d9b 100644
--- a/crates/router/src/core/payment_methods/cards.rs
+++ b/crates/router/src/core/payment_methods/cards.rs
@@ -57,9 +57,12 @@ use masking::Secret;
 use router_env::{instrument, metrics::add_attributes, tracing};
 use strum::IntoEnumIterator;
 
-use super::surcharge_decision_configs::{
-    perform_surcharge_decision_management_for_payment_method_list,
-    perform_surcharge_decision_management_for_saved_cards,
+use super::{
+    migration::RecordMigrationStatusBuilder,
+    surcharge_decision_configs::{
+        perform_surcharge_decision_management_for_payment_method_list,
+        perform_surcharge_decision_management_for_saved_cards,
+    },
 };
 #[cfg(all(
     any(feature = "v2", feature = "v1"),
@@ -376,7 +379,7 @@ pub async fn migrate_payment_method(
     merchant_id: &id_type::MerchantId,
     merchant_account: &domain::MerchantAccount,
     key_store: &domain::MerchantKeyStore,
-) -> errors::RouterResponse<api::PaymentMethodResponse> {
+) -> errors::RouterResponse<api::PaymentMethodMigrateResponse> {
     let mut req = req;
     let card_details = req.card.as_ref().get_required_value("card")?;
 
@@ -405,34 +408,92 @@ pub async fn migrate_payment_method(
         .await?;
     };
 
-    match card_number_validation_result {
+    let should_require_connector_mandate_details = req.network_token.is_none();
+
+    let mut migration_status = RecordMigrationStatusBuilder::new();
+
+    let resp = match card_number_validation_result {
         Ok(card_number) => {
             let payment_method_create_request =
                 api::PaymentMethodCreate::get_payment_method_create_from_payment_method_migrate(
                     card_number,
                     &req,
                 );
+
+            logger::debug!("Storing the card in locker and migrating the payment method");
             get_client_secret_or_add_payment_method_for_migration(
                 &state,
                 payment_method_create_request,
                 merchant_account,
                 key_store,
+                &mut migration_status,
             )
-            .await
+            .await?
         }
         Err(card_validation_error) => {
             logger::debug!("Card number to be migrated is invalid, skip saving in locker {card_validation_error}");
             skip_locker_call_and_migrate_payment_method(
-                state,
+                &state,
                 &req,
                 merchant_id.to_owned(),
                 key_store,
                 merchant_account,
                 card_bin_details.clone(),
+                should_require_connector_mandate_details,
+                &mut migration_status,
             )
-            .await
+            .await?
         }
-    }
+    };
+    let payment_method_response = match resp {
+        services::ApplicationResponse::Json(response) => response,
+        _ => Err(errors::ApiErrorResponse::InternalServerError)
+            .attach_printable("Failed to fetch the payment method response")?,
+    };
+
+    let pm_id = payment_method_response.payment_method_id.clone();
+
+    let network_token = req.network_token.clone();
+
+    let network_token_migrated = match network_token {
+        Some(nt_detail) => {
+            logger::debug!("Network token migration");
+            let network_token_requestor_ref_id = nt_detail.network_token_requestor_ref_id.clone();
+            let network_token_data = &nt_detail.network_token_data;
+
+            Some(
+                save_network_token_and_update_payment_method(
+                    &state,
+                    &req,
+                    key_store,
+                    merchant_account,
+                    network_token_data,
+                    network_token_requestor_ref_id,
+                    pm_id,
+                )
+                .await
+                .map_err(|err| logger::error!(?err, "Failed to save network token"))
+                .ok()
+                .unwrap_or_default(),
+            )
+        }
+        None => {
+            logger::debug!("Network token data is not available");
+            None
+        }
+    };
+    migration_status.network_token_migrated(network_token_migrated);
+    let migrate_status = migration_status.build();
+
+    Ok(services::ApplicationResponse::Json(
+        api::PaymentMethodMigrateResponse {
+            payment_method_response,
+            card_migrated: migrate_status.card_migrated,
+            network_token_migrated: migrate_status.network_token_migrated,
+            connector_mandate_details_migrated: migrate_status.connector_mandate_details_migrated,
+            network_transaction_id_migrated: migrate_status.network_transaction_migrated,
+        },
+    ))
 }
 
 #[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
@@ -442,7 +503,7 @@ pub async fn migrate_payment_method(
     _merchant_id: &id_type::MerchantId,
     _merchant_account: &domain::MerchantAccount,
     _key_store: &domain::MerchantKeyStore,
-) -> errors::RouterResponse<api::PaymentMethodResponse> {
+) -> errors::RouterResponse<api::PaymentMethodMigrateResponse> {
     todo!()
 }
 
@@ -645,31 +706,49 @@ impl
     not(feature = "payment_methods_v2"),
     not(feature = "customer_v2")
 ))]
+#[allow(clippy::too_many_arguments)]
 pub async fn skip_locker_call_and_migrate_payment_method(
-    state: routes::SessionState,
+    state: &routes::SessionState,
     req: &api::PaymentMethodMigrate,
     merchant_id: id_type::MerchantId,
     key_store: &domain::MerchantKeyStore,
     merchant_account: &domain::MerchantAccount,
     card: api_models::payment_methods::CardDetailFromLocker,
+    should_require_connector_mandate_details: bool,
+    migration_status: &mut RecordMigrationStatusBuilder,
 ) -> errors::RouterResponse<api::PaymentMethodResponse> {
     let db = &*state.store;
     let customer_id = req.customer_id.clone().get_required_value("customer_id")?;
 
     // In this case, since we do not have valid card details, recurring payments can only be done through connector mandate details.
-    let connector_mandate_details_req = req
-        .connector_mandate_details
-        .clone()
-        .get_required_value("connector mandate details")?;
+    //if network token data is present, then connector mandate details are not mandatory
 
-    let connector_mandate_details = serde_json::to_value(&connector_mandate_details_req)
-        .change_context(errors::ApiErrorResponse::InternalServerError)
-        .attach_printable("Failed to parse connector mandate details")?;
-    let key_manager_state = (&state).into();
+    let connector_mandate_details = if should_require_connector_mandate_details {
+        let connector_mandate_details_req = req
+            .connector_mandate_details
+            .clone()
+            .get_required_value("connector mandate details")?;
+
+        Some(
+            serde_json::to_value(&connector_mandate_details_req)
+                .change_context(errors::ApiErrorResponse::InternalServerError)
+                .attach_printable("Failed to parse connector mandate details")?,
+        )
+    } else {
+        req.connector_mandate_details
+            .clone()
+            .map(|connector_mandate_details_req| {
+                serde_json::to_value(&connector_mandate_details_req)
+                    .change_context(errors::ApiErrorResponse::InternalServerError)
+                    .attach_printable("Failed to parse connector mandate details")
+            })
+            .transpose()?
+    };
+    let key_manager_state = &state.into();
     let payment_method_billing_address: Option<Encryptable<Secret<serde_json::Value>>> = req
         .billing
         .clone()
-        .async_map(|billing| create_encrypted_data(&key_manager_state, key_store, billing))
+        .async_map(|billing| create_encrypted_data(key_manager_state, key_store, billing))
         .await
         .transpose()
         .change_context(errors::ApiErrorResponse::InternalServerError)
@@ -677,7 +756,7 @@ pub async fn skip_locker_call_and_migrate_payment_method(
 
     let customer = db
         .find_customer_by_customer_id_merchant_id(
-            &(&state).into(),
+            &state.into(),
             &customer_id,
             &merchant_id,
             key_store,
@@ -690,7 +769,7 @@ pub async fn skip_locker_call_and_migrate_payment_method(
         PaymentMethodsData::Card(CardDetailsPaymentMethod::from(card.clone()));
 
     let payment_method_data_encrypted: Option<Encryptable<Secret<serde_json::Value>>> = Some(
-        create_encrypted_data(&key_manager_state, key_store, payment_method_card_details)
+        create_encrypted_data(key_manager_state, key_store, payment_method_card_details)
             .await
             .change_context(errors::ApiErrorResponse::InternalServerError)
             .attach_printable("Unable to encrypt Payment method card details")?,
@@ -699,13 +778,26 @@ pub async fn skip_locker_call_and_migrate_payment_method(
     let payment_method_metadata: Option<serde_json::Value> =
         req.metadata.as_ref().map(|data| data.peek()).cloned();
 
+    let network_transaction_id = req.network_transaction_id.clone();
+
+    migration_status.network_transaction_id_migrated(network_transaction_id.as_ref().map(|_| true));
+
+    migration_status.connector_mandate_details_migrated(
+        connector_mandate_details
+            .as_ref()
+            .map(|_| true)
+            .or_else(|| req.connector_mandate_details.as_ref().map(|_| false)),
+    );
+
+    migration_status.card_migrated(false);
+
     let payment_method_id = generate_id(consts::ID_LENGTH, "pm");
 
     let current_time = common_utils::date_time::now();
 
     let response = db
         .insert_payment_method(
-            &(&state).into(),
+            &state.into(),
             key_store,
             domain::PaymentMethod {
                 customer_id: customer_id.to_owned(),
@@ -718,11 +810,11 @@ pub async fn skip_locker_call_and_migrate_payment_method(
                 scheme: req.card_network.clone().or(card.scheme.clone()),
                 metadata: payment_method_metadata.map(Secret::new),
                 payment_method_data: payment_method_data_encrypted.map(Into::into),
-                connector_mandate_details: Some(connector_mandate_details),
+                connector_mandate_details,
                 customer_acceptance: None,
                 client_secret: None,
                 status: enums::PaymentMethodStatus::Active,
-                network_transaction_id: None,
+                network_transaction_id,
                 payment_method_issuer_code: None,
                 accepted_currency: None,
                 token: None,
@@ -753,7 +845,7 @@ pub async fn skip_locker_call_and_migrate_payment_method(
 
     if customer.default_payment_method_id.is_none() && req.payment_method.is_some() {
         let _ = set_default_payment_method(
-            &state,
+            state,
             &merchant_id,
             key_store.clone(),
             &customer_id,
@@ -768,6 +860,117 @@ pub async fn skip_locker_call_and_migrate_payment_method(
     ))
 }
 
+#[cfg(all(
+    any(feature = "v1", feature = "v2"),
+    not(feature = "payment_methods_v2"),
+    not(feature = "customer_v2")
+))]
+#[allow(clippy::too_many_arguments)]
+pub async fn save_network_token_and_update_payment_method(
+    state: &routes::SessionState,
+    req: &api::PaymentMethodMigrate,
+    key_store: &domain::MerchantKeyStore,
+    merchant_account: &domain::MerchantAccount,
+    network_token_data: &api_models::payment_methods::MigrateNetworkTokenData,
+    network_token_requestor_ref_id: String,
+    pm_id: String,
+) -> errors::RouterResult<bool> {
+    let payment_method_create_request =
+        api::PaymentMethodCreate::get_payment_method_create_from_payment_method_migrate(
+            network_token_data.network_token_number.clone(),
+            req,
+        );
+    let customer_id = req.customer_id.clone().get_required_value("customer_id")?;
+
+    let network_token_details = api::CardDetail {
+        card_number: network_token_data.network_token_number.clone(),
+        card_exp_month: network_token_data.network_token_exp_month.clone(),
+        card_exp_year: network_token_data.network_token_exp_year.clone(),
+        card_holder_name: network_token_data.card_holder_name.clone(),
+        nick_name: network_token_data.nick_name.clone(),
+        card_issuing_country: network_token_data.card_issuing_country.clone(),
+        card_network: network_token_data.card_network.clone(),
+        card_issuer: network_token_data.card_issuer.clone(),
+        card_type: network_token_data.card_type.clone(),
+    };
+
+    logger::debug!(
+        "Adding network token to locker for customer_id: {:?}",
+        customer_id
+    );
+
+    let token_resp = Box::pin(add_card_to_locker(
+        state,
+        payment_method_create_request.clone(),
+        &network_token_details,
+        &customer_id,
+        merchant_account,
+        None,
+    ))
+    .await
+    .change_context(errors::ApiErrorResponse::InternalServerError)
+    .attach_printable("Add Network Token failed");
+    let key_manager_state = &state.into();
+
+    match token_resp {
+        Ok(resp) => {
+            logger::debug!("Network token added to locker");
+            let (token_pm_resp, _duplication_check) = resp;
+            let pm_token_details = token_pm_resp
+                .card
+                .as_ref()
+                .map(|card| PaymentMethodsData::Card(CardDetailsPaymentMethod::from(card.clone())));
+            let pm_network_token_data_encrypted = pm_token_details
+                .async_map(|pm_card| create_encrypted_data(key_manager_state, key_store, pm_card))
+                .await
+                .transpose()
+                .change_context(errors::ApiErrorResponse::InternalServerError)
+                .attach_printable("Unable to encrypt payment method data")?;
+
+            let pm_update = storage::PaymentMethodUpdate::NetworkTokenDataUpdate {
+                network_token_requestor_reference_id: Some(network_token_requestor_ref_id),
+                network_token_locker_id: Some(token_pm_resp.payment_method_id),
+                network_token_payment_method_data: pm_network_token_data_encrypted.map(Into::into),
+            };
+            let db = &*state.store;
+            let existing_pm = db
+                .find_payment_method(
+                    &state.into(),
+                    key_store,
+                    &pm_id,
+                    merchant_account.storage_scheme,
+                )
+                .await
+                .change_context(errors::ApiErrorResponse::InternalServerError)
+                .attach_printable(format!(
+                    "Failed to fetch payment method for existing pm_id: {:?} in db",
+                    pm_id
+                ))?;
+
+            db.update_payment_method(
+                &state.into(),
+                key_store,
+                existing_pm,
+                pm_update,
+                merchant_account.storage_scheme,
+            )
+            .await
+            .change_context(errors::ApiErrorResponse::InternalServerError)
+            .attach_printable(format!(
+                "Failed to update payment method for existing pm_id: {:?} in db",
+                pm_id
+            ))?;
+
+            logger::debug!("Network token added to locker and payment method updated");
+            Ok(true)
+        }
+        Err(err) => {
+            logger::debug!("Network token added to locker failed {:?}", err);
+            Ok(false)
+        }
+    }
+}
+
 // need to discuss regarding the migration APIs for v2
 #[cfg(all(
     feature = "v2",
@@ -899,6 +1102,7 @@ pub async fn get_client_secret_or_add_payment_method_for_migration(
     req: api::PaymentMethodCreate,
     merchant_account: &domain::MerchantAccount,
     key_store: &domain::MerchantKeyStore,
+    migration_status: &mut RecordMigrationStatusBuilder,
 ) -> errors::RouterResponse<api::PaymentMethodResponse> {
     let merchant_id = merchant_account.get_id();
     let customer_id = req.customer_id.clone().get_required_value("customer_id")?;
@@ -908,6 +1112,7 @@ pub async fn get_client_secret_or_add_payment_method_for_migration(
     #[cfg(feature = "payouts")]
     let condition = req.card.is_some() || req.bank_transfer.is_some() || req.wallet.is_some();
     let key_manager_state = state.into();
+
     let payment_method_billing_address: Option<Encryptable<Secret<serde_json::Value>>> = req
         .billing
         .clone()
@@ -930,6 +1135,7 @@ pub async fn get_client_secret_or_add_payment_method_for_migration(
             req,
             merchant_account,
             key_store,
+            migration_status,
         ))
         .await
     } else {
@@ -946,7 +1152,7 @@ pub async fn get_client_secret_or_add_payment_method_for_migration(
             None,
             None,
             key_store,
-            connector_mandate_details,
+            connector_mandate_details.clone(),
             Some(enums::PaymentMethodStatus::AwaitingData),
             None,
             merchant_account.storage_scheme,
@@ -957,6 +1163,14 @@ pub async fn get_client_secret_or_add_payment_method_for_migration(
             None,
         )
         .await?;
+        migration_status.connector_mandate_details_migrated(
+            connector_mandate_details
+                .clone()
+                .map(|_| true)
+                .or_else(|| req.connector_mandate_details.clone().map(|_| false)),
+        );
+
+        migration_status.card_migrated(false);
 
         if res.status == enums::PaymentMethodStatus::AwaitingData {
             add_payment_method_status_update_task(
@@ -1482,6 +1696,7 @@ pub async fn save_migration_payment_method(
     req: api::PaymentMethodCreate,
     merchant_account: &domain::MerchantAccount,
     key_store: &domain::MerchantKeyStore,
+    migration_status: &mut RecordMigrationStatusBuilder,
 ) -> errors::RouterResponse<api::PaymentMethodResponse> {
     req.validate()?;
     let db = &*state.store;
@@ -1505,6 +1720,17 @@ pub async fn save_migration_payment_method(
         .transpose()
         .change_context(errors::ApiErrorResponse::InternalServerError)?;
 
+    let network_transaction_id = req.network_transaction_id.clone();
+
+    migration_status.network_transaction_id_migrated(network_transaction_id.as_ref().map(|_| true));
+
+    migration_status.connector_mandate_details_migrated(
+        connector_mandate_details
+            .as_ref()
+            .map(|_| true)
+            .or_else(|| req.connector_mandate_details.as_ref().map(|_| false)),
+    );
+
     let response = match payment_method {
         #[cfg(feature = "payouts")]
         api_enums::PaymentMethod::BankTransfer => match req.bank_transfer.clone() {
@@ -1564,6 +1790,7 @@ pub async fn save_migration_payment_method(
 
     let (mut resp, duplication_check) = response?;
 
+    migration_status.card_migrated(true);
     match duplication_check {
         Some(duplication_check) => match duplication_check {
             payment_methods::DataDuplicationCheck::Duplicated => {
@@ -1714,7 +1941,7 @@ pub async fn save_migration_payment_method(
                 None,
                 locker_id,
                 connector_mandate_details,
-                req.network_transaction_id.clone(),
+                network_transaction_id,
                 merchant_account.storage_scheme,
                 payment_method_billing_address.map(Into::into),
                 None,
diff --git a/crates/router/src/core/payment_methods/migration.rs b/crates/router/src/core/payment_methods/migration.rs
index b0aaa767a4e..11bc3a9d74f 100644
--- a/crates/router/src/core/payment_methods/migration.rs
+++ b/crates/router/src/core/payment_methods/migration.rs
@@ -149,3 +149,64 @@ fn validate_card_exp_year(year: String) -> Result<(), errors::ValidationError> {
         })
     }
 }
+
+#[derive(Debug)]
+pub struct RecordMigrationStatus {
+    pub card_migrated: Option<bool>,
+    pub network_token_migrated: Option<bool>,
+    pub connector_mandate_details_migrated: Option<bool>,
+    pub network_transaction_migrated: Option<bool>,
+}
+
+#[derive(Debug)]
+pub struct RecordMigrationStatusBuilder {
+    pub card_migrated: Option<bool>,
+    pub network_token_migrated: Option<bool>,
+    pub connector_mandate_details_migrated: Option<bool>,
+    pub network_transaction_migrated: Option<bool>,
+}
+
+impl RecordMigrationStatusBuilder {
+    pub fn new() -> Self {
+        Self {
+            card_migrated: None,
+            network_token_migrated: None,
+            connector_mandate_details_migrated: None,
+            network_transaction_migrated: None,
+        }
+    }
+
+    pub fn card_migrated(&mut self, card_migrated: bool) {
+        self.card_migrated = Some(card_migrated);
+    }
+
+    pub fn network_token_migrated(&mut self, network_token_migrated: Option<bool>) {
+        self.network_token_migrated = network_token_migrated;
+    }
+
+    pub fn connector_mandate_details_migrated(
+        &mut self,
+        connector_mandate_details_migrated: Option<bool>,
+    ) {
+        self.connector_mandate_details_migrated = connector_mandate_details_migrated;
+    }
+
+    pub fn network_transaction_id_migrated(&mut self, network_transaction_migrated: Option<bool>) {
+        self.network_transaction_migrated = network_transaction_migrated;
+    }
+
+    pub fn build(self) -> RecordMigrationStatus {
+        RecordMigrationStatus {
+            card_migrated: self.card_migrated,
+            network_token_migrated: self.network_token_migrated,
+            connector_mandate_details_migrated: self.connector_mandate_details_migrated,
+            network_transaction_migrated: self.network_transaction_migrated,
+        }
+    }
+}
+
+impl Default for RecordMigrationStatusBuilder {
+    fn default() -> Self {
+        Self::new()
+    }
+}
diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs
index 4bae04e1426..a6f3018ed0d 100644
--- a/crates/router/src/core/payments/tokenization.rs
+++ b/crates/router/src/core/payments/tokenization.rs
@@ -973,7 +973,7 @@ pub async fn save_network_token_in_locker(
         {
             Ok((token_response, network_token_requestor_ref_id)) => {
                 // Only proceed if the tokenization was successful
-                let card_data = api::CardDetail {
+                let network_token_data = api::CardDetail {
                     card_number: token_response.token.clone(),
                     card_exp_month: token_response.token_expiry_month.clone(),
                     card_exp_year: token_response.token_expiry_year.clone(),
@@ -988,7 +988,7 @@ pub async fn save_network_token_in_locker(
                 let (res, dc) = Box::pin(payment_methods::cards::add_card_to_locker(
                     state,
                     payment_method_request,
-                    &card_data,
+                    &network_token_data,
                     &customer_id,
                     merchant_account,
                     None,
diff --git a/crates/router/src/types/api/payment_methods.rs b/crates/router/src/types/api/payment_methods.rs
index 8b1175d5cc6..25227ae5383 100644
--- a/crates/router/src/types/api/payment_methods.rs
+++ b/crates/router/src/types/api/payment_methods.rs
@@ -7,9 +7,10 @@ pub use api_models::payment_methods::{
     PaymentMethodCreateData, PaymentMethodDeleteResponse, PaymentMethodId,
     PaymentMethodIntentConfirm, PaymentMethodIntentConfirmInternal, PaymentMethodIntentCreate,
     PaymentMethodListData, PaymentMethodListRequest, PaymentMethodListResponse,
-    PaymentMethodMigrate, PaymentMethodResponse, PaymentMethodResponseData, PaymentMethodUpdate,
-    PaymentMethodUpdateData, PaymentMethodsData, TokenizePayloadEncrypted, TokenizePayloadRequest,
-    TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, TokenizedWalletValue2,
+    PaymentMethodMigrate, PaymentMethodMigrateResponse, PaymentMethodResponse,
+    PaymentMethodResponseData, PaymentMethodUpdate, PaymentMethodUpdateData, PaymentMethodsData,
+    TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2,
+    TokenizedWalletValue1, TokenizedWalletValue2,
 };
 #[cfg(all(
     any(feature = "v2", feature = "v1"),
@@ -22,9 +23,9 @@ pub use api_models::payment_methods::{
     PaymentMethodCollectLinkRenderRequest, PaymentMethodCollectLinkRequest, PaymentMethodCreate,
     PaymentMethodCreateData, PaymentMethodDeleteResponse, PaymentMethodId,
     PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodMigrate,
-    PaymentMethodResponse, PaymentMethodUpdate, PaymentMethodsData, TokenizePayloadEncrypted,
-    TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1,
-    TokenizedWalletValue2,
+    PaymentMethodMigrateResponse, PaymentMethodResponse, PaymentMethodUpdate, PaymentMethodsData,
+    TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2,
+    TokenizedWalletValue1, TokenizedWalletValue2,
 };
 use error_stack::report;
 

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

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

## Description
<!-- Describe your changes in detail -->
Add support for network token migration.

### Additional Changes

- [x] 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
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->


## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->
test migration api.
req - 
```
curl --location 'http://localhost:8080/payment_methods/migrate' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: api-key' \
--data '{
    "merchant_id": "merchant_id",
    "card": {
        "card_number": "card_number",
        "card_exp_month": "12",
        "card_exp_year": "21",
        "card_holder_name": "joseph Doe"
    },
    "customer_id": "customer_id",
    "network_transaction_id": "nt-transaction-id",
    "payment_method": "card",
    "network_token": {
        "network_token_data": {
            "network_token_number": "network_token_number",
            "network_token_exp_month": "12",
            "network_token_exp_year": "21"
        },
        "network_token_requestor_ref_id": "test-ref-id"
    }
}'
```
expected response - 
```
{
    "payment_method_response": {
        "merchant_id": "merchant_1731400520",
        "customer_id": "cust-1731400905",
        "payment_method_id": "pm_LSk7YuLskFS8aBdWNvzk",
        "payment_method": "card",
        "payment_method_type": null,
        "card": {
            "scheme": null,
            "issuer_country": null,
            "last4_digits": "0000",
            "expiry_month": "12",
            "expiry_year": "21",
            "card_token": null,
            "card_holder_name": "joseph Doe",
            "card_fingerprint": null,
            "nick_name": null,
            "card_network": null,
            "card_isin": "420000",
            "card_issuer": null,
            "card_type": null,
            "saved_to_locker": true
        },
        "recurring_enabled": false,
        "installment_payment_enabled": false,
        "payment_experience": [
            "redirect_to_url"
        ],
        "metadata": null,
        "created": "2024-11-12T09:00:50.948Z",
        "last_used_at": "2024-11-12T09:00:50.949Z",
        "client_secret": "pm_LSk7YuLskFS8aBdWNvzk_secret_6B5AfKanyCDhy5hjeUce"
    },
    "card_migrated": true,
    "network_token_migrated": true,
    "connector_mandate_details_migrated": null,
    "network_transaction_id_migrated": true
}
```
<img width="1148" alt="Screenshot 2024-11-12 at 2 31 12 PM" src="https://github.com/user-attachments/assets/e3661f77-075e-4768-bac1-c2b23e0c12ce">


## 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`
- [x] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
