Issue:
Bug: Batch update payment methods API



We need to make update payment methods API which can update the fields of the records in payment methods table. It will consume a CSV file which will have batch update entries. Currently it should only update connector_mandate_id, network_transaction_id and mandate_status.

Pull request description:
## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [x] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
<!-- Describe your changes in detail -->
We need to make update payment methods API which can update the fields of the records in payment methods table. It will consume a CSV file which will have batch update entries. Currently it should only update connector_mandate_id, network_transaction_id and mandate_status.

### 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).
-->
Frequent DB updates on payment methods table due to ongoing migrations of merchants.

## 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)?
-->
```
curl --location 'http://localhost:8080/payment_methods/update-batch' \
--header 'api-key: test_admin' \
--form 'file=@"/Users/mrudul.vajpayee/Downloads/update_sample.csv"' \
--form 'merchant_id="merchant_1756355489"'
```
Sample output:
```
[
    {
        "payment_method_id": "pm_RsWnXeiOhlaVseKdk0Mm",
        "status": "active",
        "network_transaction_id": "063AF598E0A5C5",
        "connector_mandate_details": {
            "payouts": {
                "mca_SbUFb1h4NfixAWFaLQeT": {
                    "transfer_method_id": "ARM-678ab3997b16cb7cd"
                }
            },
            "mca_Qwuqfidvy2Lk2y0PbvFa": {
                "mandate_metadata": null,
                "payment_method_type": "google_pay",
                "connector_mandate_id": "2D5D6F0E961E639BE063AF598E0A9A32",
                "connector_mandate_status": "active",
                "original_payment_authorized_amount": 20000,
                "original_payment_authorized_currency": "EUR",
                "connector_mandate_request_reference_id": "ytlaTEEjkONzieVl0k"
            },
            "mca_xHIYFXMCUsYTZIZ1rwgY": {
                "mandate_metadata": null,
                "payment_method_type": null,
                "connector_mandate_id": "6D981A0542DDF0EBE063AF598E0A5C5E",
                "connector_mandate_status": null,
                "original_payment_authorized_amount": null,
                "original_payment_authorized_currency": null,
                "connector_mandate_request_reference_id": null
            }
        },
        "update_status": "Success",
        "line_number": 1
    },
    {
        "payment_method_id": "pm_RsWnXeiOhlaVseKdk0Mm",
        "status": "active",
        "network_transaction_id": "063AF598E0A5C5",
        "connector_mandate_details": {
            "payouts": {
                "mca_SbUFb1h4NfixAWFaLQeT": {
                    "transfer_method_id": "ARM-678ab3997b16cb7cd"
                }
            },
            "mca_Qwuqfidvy2Lk2y0PbvFa": {
                "mandate_metadata": null,
                "payment_method_type": "google_pay",
                "connector_mandate_id": "2D5D6F0E961E639BE063AF598E0A9A32",
                "connector_mandate_status": "active",
                "original_payment_authorized_amount": 20000,
                "original_payment_authorized_currency": "EUR",
                "connector_mandate_request_reference_id": "ytlaTEEjkONzieVl0k"
            },
            "mca_xHIYFXMCUsYTZIZ1rwgY": {
                "mandate_metadata": null,
                "payment_method_type": null,
                "connector_mandate_id": "6D981A0542DDF0EBE063AF598E0A5C5E",
                "connector_mandate_status": null,
                "original_payment_authorized_amount": null,
                "original_payment_authorized_currency": null,
                "connector_mandate_request_reference_id": null
            }
        },
        "update_status": "Success",
        "line_number": 2
    },
    {
        "payment_method_id": "pm_RsWnXeiOhlaVseKdk0Mm",
        "status": "active",
        "network_transaction_id": "063AF598E0A5C5",
        "connector_mandate_details": {
            "payouts": {
                "mca_SbUFb1h4NfixAWFaLQeT": {
                    "transfer_method_id": "ARM-678ab3997b16cb7cd"
                }
            },
            "mca_Qwuqfidvy2Lk2y0PbvFa": {
                "mandate_metadata": null,
                "payment_method_type": "google_pay",
                "connector_mandate_id": "2D5D6F0E961E639BE063AF598E0A9A32",
                "connector_mandate_status": "active",
                "original_payment_authorized_amount": 20000,
                "original_payment_authorized_currency": "EUR",
                "connector_mandate_request_reference_id": "ytlaTEEjkONzieVl0k"
            },
            "mca_xHIYFXMCUsYTZIZ1rwgY": {
                "mandate_metadata": null,
                "payment_method_type": null,
                "connector_mandate_id": "6D981A0542DDF0EBE063AF598E0A5C5E",
                "connector_mandate_status": null,
                "original_payment_authorized_amount": null,
                "original_payment_authorized_currency": null,
                "connector_mandate_request_reference_id": null
            }
        },
        "update_status": "Success",
        "line_number": 3
    }
]
```
[update_sample.csv](https://github.com/user-attachments/files/22072833/update_sample.csv)

## 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


Testing:

```
curl --location 'http://localhost:8080/payment_methods/update-batch' \
--header 'api-key: test_admin' \
--form 'file=@"/Users/mrudul.vajpayee/Downloads/update_sample.csv"' \
--form 'merchant_id="merchant_1756355489"'
```
Sample output:
```
[
    {
        "payment_method_id": "pm_RsWnXeiOhlaVseKdk0Mm",
        "status": "active",
        "network_transaction_id": "063AF598E0A5C5",
        "connector_mandate_details": {
            "payouts": {
                "mca_SbUFb1h4NfixAWFaLQeT": {
                    "transfer_method_id": "ARM-678ab3997b16cb7cd"
                }
            },
            "mca_Qwuqfidvy2Lk2y0PbvFa": {
                "mandate_metadata": null,
                "payment_method_type": "google_pay",
                "connector_mandate_id": "2D5D6F0E961E639BE063AF598E0A9A32",
                "connector_mandate_status": "active",
                "original_payment_authorized_amount": 20000,
                "original_payment_authorized_currency": "EUR",
                "connector_mandate_request_reference_id": "ytlaTEEjkONzieVl0k"
            },
            "mca_xHIYFXMCUsYTZIZ1rwgY": {
                "mandate_metadata": null,
                "payment_method_type": null,
                "connector_mandate_id": "6D981A0542DDF0EBE063AF598E0A5C5E",
                "connector_mandate_status": null,
                "original_payment_authorized_amount": null,
                "original_payment_authorized_currency": null,
                "connector_mandate_request_reference_id": null
            }
        },
        "update_status": "Success",
        "line_number": 1
    },
    {
        "payment_method_id": "pm_RsWnXeiOhlaVseKdk0Mm",
        "status": "active",
        "network_transaction_id": "063AF598E0A5C5",
        "connector_mandate_details": {
            "payouts": {
                "mca_SbUFb1h4NfixAWFaLQeT": {
                    "transfer_method_id": "ARM-678ab3997b16cb7cd"
                }
            },
            "mca_Qwuqfidvy2Lk2y0PbvFa": {
                "mandate_metadata": null,
                "payment_method_type": "google_pay",
                "connector_mandate_id": "2D5D6F0E961E639BE063AF598E0A9A32",
                "connector_mandate_status": "active",
                "original_payment_authorized_amount": 20000,
                "original_payment_authorized_currency": "EUR",
                "connector_mandate_request_reference_id": "ytlaTEEjkONzieVl0k"
            },
            "mca_xHIYFXMCUsYTZIZ1rwgY": {
                "mandate_metadata": null,
                "payment_method_type": null,
                "connector_mandate_id": "6D981A0542DDF0EBE063AF598E0A5C5E",
                "connector_mandate_status": null,
                "original_payment_authorized_amount": null,
                "original_payment_authorized_currency": null,
                "connector_mandate_request_reference_id": null
            }
        },
        "update_status": "Success",
        "line_number": 2
    },
    {
        "payment_method_id": "pm_RsWnXeiOhlaVseKdk0Mm",
        "status": "active",
        "network_transaction_id": "063AF598E0A5C5",
        "connector_mandate_details": {
            "payouts": {
                "mca_SbUFb1h4NfixAWFaLQeT": {
                    "transfer_method_id": "ARM-678ab3997b16cb7cd"
                }
            },
            "mca_Qwuqfidvy2Lk2y0PbvFa": {
                "mandate_metadata": null,
                "payment_method_type": "google_pay",
                "connector_mandate_id": "2D5D6F0E961E639BE063AF598E0A9A32",
                "connector_mandate_status": "active",
                "original_payment_authorized_amount": 20000,
                "original_payment_authorized_currency": "EUR",
                "connector_mandate_request_reference_id": "ytlaTEEjkONzieVl0k"
            },
            "mca_xHIYFXMCUsYTZIZ1rwgY": {
                "mandate_metadata": null,
                "payment_method_type": null,
                "connector_mandate_id": "6D981A0542DDF0EBE063AF598E0A5C5E",
                "connector_mandate_status": null,
                "original_payment_authorized_amount": null,
                "original_payment_authorized_currency": null,
                "connector_mandate_request_reference_id": null
            }
        },
        "update_status": "Success",
        "line_number": 3
    }
]
```
[update_sample.csv](https://github.com/user-attachments/files/22072833/update_sample.csv)

Patch:
diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs
index 135e0ddc248..5803a5c6876 100644
--- a/crates/api_models/src/payment_methods.rs
+++ b/crates/api_models/src/payment_methods.rs
@@ -285,6 +285,14 @@ pub struct PaymentMethodMigrateResponse {
     pub network_transaction_id_migrated: Option<bool>,
 }
 
+#[derive(Debug, serde::Serialize, ToSchema)]
+pub struct PaymentMethodRecordUpdateResponse {
+    pub payment_method_id: String,
+    pub status: common_enums::PaymentMethodStatus,
+    pub network_transaction_id: Option<String>,
+    pub connector_mandate_details: Option<pii::SecretSerdeValue>,
+}
+
 #[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
 pub struct PaymentsMandateReference(
     pub HashMap<id_type::MerchantConnectorAccountId, PaymentsMandateReferenceRecord>,
@@ -2640,6 +2648,28 @@ pub struct PaymentMethodRecord {
     pub network_token_requestor_ref_id: Option<String>,
 }
 
+#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
+pub struct UpdatePaymentMethodRecord {
+    pub payment_method_id: String,
+    pub status: Option<common_enums::PaymentMethodStatus>,
+    pub network_transaction_id: Option<String>,
+    pub line_number: Option<i64>,
+    pub payment_instrument_id: Option<masking::Secret<String>>,
+    pub merchant_connector_id: Option<id_type::MerchantConnectorAccountId>,
+}
+
+#[derive(Debug, serde::Serialize)]
+pub struct PaymentMethodUpdateResponse {
+    pub payment_method_id: String,
+    pub status: Option<common_enums::PaymentMethodStatus>,
+    pub network_transaction_id: Option<String>,
+    pub connector_mandate_details: Option<pii::SecretSerdeValue>,
+    pub update_status: UpdateStatus,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub update_error: Option<String>,
+    pub line_number: Option<i64>,
+}
+
 #[derive(Debug, Default, serde::Serialize)]
 pub struct PaymentMethodMigrationResponse {
     pub line_number: Option<i64>,
@@ -2667,6 +2697,13 @@ pub enum MigrationStatus {
     Failed,
 }
 
+#[derive(Debug, Default, serde::Serialize)]
+pub enum UpdateStatus {
+    Success,
+    #[default]
+    Failed,
+}
+
 impl PaymentMethodRecord {
     fn create_address(&self) -> Option<payments::AddressDetails> {
         if self.billing_address_first_name.is_some()
@@ -2725,6 +2762,12 @@ type PaymentMethodMigrationResponseType = (
     PaymentMethodRecord,
 );
 
+#[cfg(feature = "v1")]
+type PaymentMethodUpdateResponseType = (
+    Result<PaymentMethodRecordUpdateResponse, String>,
+    UpdatePaymentMethodRecord,
+);
+
 #[cfg(feature = "v1")]
 impl From<PaymentMethodMigrationResponseType> for PaymentMethodMigrationResponse {
     fn from((response, record): PaymentMethodMigrationResponseType) -> Self {
@@ -2755,6 +2798,32 @@ impl From<PaymentMethodMigrationResponseType> for PaymentMethodMigrationResponse
     }
 }
 
+#[cfg(feature = "v1")]
+impl From<PaymentMethodUpdateResponseType> for PaymentMethodUpdateResponse {
+    fn from((response, record): PaymentMethodUpdateResponseType) -> Self {
+        match response {
+            Ok(res) => Self {
+                payment_method_id: res.payment_method_id,
+                status: Some(res.status),
+                network_transaction_id: res.network_transaction_id,
+                connector_mandate_details: res.connector_mandate_details,
+                update_status: UpdateStatus::Success,
+                update_error: None,
+                line_number: record.line_number,
+            },
+            Err(e) => Self {
+                payment_method_id: record.payment_method_id,
+                status: record.status,
+                network_transaction_id: record.network_transaction_id,
+                connector_mandate_details: None,
+                update_status: UpdateStatus::Failed,
+                update_error: Some(e),
+                line_number: record.line_number,
+            },
+        }
+    }
+}
+
 impl
     TryFrom<(
         &PaymentMethodRecord,
diff --git a/crates/diesel_models/src/payment_method.rs b/crates/diesel_models/src/payment_method.rs
index 79d6865b727..34fff64e7d9 100644
--- a/crates/diesel_models/src/payment_method.rs
+++ b/crates/diesel_models/src/payment_method.rs
@@ -245,6 +245,11 @@ pub enum PaymentMethodUpdate {
         connector_mandate_details: Option<pii::SecretSerdeValue>,
         network_transaction_id: Option<Secret<String>>,
     },
+    ConnectorNetworkTransactionIdStatusAndMandateDetailsUpdate {
+        connector_mandate_details: Option<pii::SecretSerdeValue>,
+        network_transaction_id: Option<String>,
+        status: Option<storage_enums::PaymentMethodStatus>,
+    },
 }
 
 #[cfg(feature = "v2")]
@@ -673,6 +678,29 @@ impl From<PaymentMethodUpdate> for PaymentMethodUpdateInternal {
                 network_token_payment_method_data: None,
                 scheme: None,
             },
+            PaymentMethodUpdate::ConnectorNetworkTransactionIdStatusAndMandateDetailsUpdate {
+                connector_mandate_details,
+                network_transaction_id,
+                status,
+            } => Self {
+                metadata: None,
+                payment_method_data: None,
+                last_used_at: None,
+                status,
+                locker_id: None,
+                network_token_requestor_reference_id: None,
+                payment_method: None,
+                connector_mandate_details: connector_mandate_details
+                    .map(|mandate_details| mandate_details.expose()),
+                network_transaction_id,
+                updated_by: None,
+                payment_method_issuer: None,
+                payment_method_type: None,
+                last_modified: common_utils::date_time::now(),
+                network_token_locker_id: None,
+                network_token_payment_method_data: None,
+                scheme: None,
+            },
         }
     }
 }
diff --git a/crates/hyperswitch_domain_models/src/payment_methods.rs b/crates/hyperswitch_domain_models/src/payment_methods.rs
index 0fa3d5b4e07..003998d9ccc 100644
--- a/crates/hyperswitch_domain_models/src/payment_methods.rs
+++ b/crates/hyperswitch_domain_models/src/payment_methods.rs
@@ -12,7 +12,7 @@ use common_utils::{
     id_type, pii, type_name,
     types::keymanager,
 };
-use diesel_models::{enums as storage_enums, PaymentMethodUpdate};
+pub use diesel_models::{enums as storage_enums, PaymentMethodUpdate};
 use error_stack::ResultExt;
 #[cfg(feature = "v1")]
 use masking::ExposeInterface;
diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs
index 3a072e12cc7..06622842281 100644
--- a/crates/router/src/core/payment_methods.rs
+++ b/crates/router/src/core/payment_methods.rs
@@ -1,4 +1,5 @@
 pub mod cards;
+pub mod migration;
 pub mod network_tokenization;
 pub mod surcharge_decision_configs;
 #[cfg(feature = "v1")]
diff --git a/crates/router/src/core/payment_methods/migration.rs b/crates/router/src/core/payment_methods/migration.rs
new file mode 100644
index 00000000000..d802e432ec1
--- /dev/null
+++ b/crates/router/src/core/payment_methods/migration.rs
@@ -0,0 +1,257 @@
+use actix_multipart::form::{self, bytes, text};
+use api_models::payment_methods as pm_api;
+use common_utils::{errors::CustomResult, id_type};
+use csv::Reader;
+use error_stack::ResultExt;
+use hyperswitch_domain_models::{
+    api::ApplicationResponse, errors::api_error_response as errors, merchant_context,
+    payment_methods::PaymentMethodUpdate,
+};
+use masking::PeekInterface;
+use rdkafka::message::ToBytes;
+use router_env::logger;
+
+use crate::{core::errors::StorageErrorExt, routes::SessionState};
+
+type PmMigrationResult<T> = CustomResult<ApplicationResponse<T>, errors::ApiErrorResponse>;
+
+#[cfg(feature = "v1")]
+pub async fn update_payment_methods(
+    state: &SessionState,
+    payment_methods: Vec<pm_api::UpdatePaymentMethodRecord>,
+    merchant_id: &id_type::MerchantId,
+    merchant_context: &merchant_context::MerchantContext,
+) -> PmMigrationResult<Vec<pm_api::PaymentMethodUpdateResponse>> {
+    let mut result = Vec::with_capacity(payment_methods.len());
+
+    for record in payment_methods {
+        let update_res =
+            update_payment_method_record(state, record.clone(), merchant_id, merchant_context)
+                .await;
+        let res = match update_res {
+            Ok(ApplicationResponse::Json(response)) => Ok(response),
+            Err(e) => Err(e.to_string()),
+            _ => Err("Failed to update payment method".to_string()),
+        };
+
+        result.push(pm_api::PaymentMethodUpdateResponse::from((res, record)));
+    }
+    Ok(ApplicationResponse::Json(result))
+}
+
+#[cfg(feature = "v1")]
+pub async fn update_payment_method_record(
+    state: &SessionState,
+    req: pm_api::UpdatePaymentMethodRecord,
+    merchant_id: &id_type::MerchantId,
+    merchant_context: &merchant_context::MerchantContext,
+) -> CustomResult<
+    ApplicationResponse<pm_api::PaymentMethodRecordUpdateResponse>,
+    errors::ApiErrorResponse,
+> {
+    use std::collections::HashMap;
+
+    use common_enums::enums;
+    use common_utils::pii;
+    use hyperswitch_domain_models::mandates::{
+        CommonMandateReference, PaymentsMandateReference, PaymentsMandateReferenceRecord,
+        PayoutsMandateReference, PayoutsMandateReferenceRecord,
+    };
+
+    let db = &*state.store;
+    let payment_method_id = req.payment_method_id.clone();
+    let network_transaction_id = req.network_transaction_id.clone();
+    let status = req.status;
+
+    let payment_method = db
+        .find_payment_method(
+            &state.into(),
+            merchant_context.get_merchant_key_store(),
+            &payment_method_id,
+            merchant_context.get_merchant_account().storage_scheme,
+        )
+        .await
+        .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?;
+
+    if payment_method.merchant_id != *merchant_id {
+        return Err(errors::ApiErrorResponse::InvalidRequestData {
+                    message: "Merchant ID in the request does not match the Merchant ID in the payment method record.".to_string(),
+                }.into());
+    }
+
+    // Process mandate details when both payment_instrument_id and merchant_connector_id are present
+    let pm_update = match (&req.payment_instrument_id, &req.merchant_connector_id) {
+        (Some(payment_instrument_id), Some(merchant_connector_id)) => {
+            let mandate_details = payment_method
+                .get_common_mandate_reference()
+                .change_context(errors::ApiErrorResponse::InternalServerError)
+                .attach_printable("Failed to deserialize to Payment Mandate Reference ")?;
+
+            let mca = db
+                .find_by_merchant_connector_account_merchant_id_merchant_connector_id(
+                    &state.into(),
+                    merchant_context.get_merchant_account().get_id(),
+                    merchant_connector_id,
+                    merchant_context.get_merchant_key_store(),
+                )
+                .await
+                .to_not_found_response(
+                    errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
+                        id: merchant_connector_id.get_string_repr().to_string(),
+                    },
+                )?;
+
+            let updated_connector_mandate_details = match mca.connector_type {
+                enums::ConnectorType::PayoutProcessor => {
+                    // Handle PayoutsMandateReference
+                    let mut existing_payouts_mandate = mandate_details
+                        .payouts
+                        .unwrap_or_else(|| PayoutsMandateReference(HashMap::new()));
+
+                    // Create new payout mandate record
+                    let new_payout_record = PayoutsMandateReferenceRecord {
+                        transfer_method_id: Some(payment_instrument_id.peek().to_string()),
+                    };
+
+                    // Check if record exists for this merchant_connector_id
+                    if let Some(existing_record) =
+                        existing_payouts_mandate.0.get_mut(merchant_connector_id)
+                    {
+                        if let Some(transfer_method_id) = &new_payout_record.transfer_method_id {
+                            existing_record.transfer_method_id = Some(transfer_method_id.clone());
+                        }
+                    } else {
+                        // Insert new record in connector_mandate_details
+                        existing_payouts_mandate
+                            .0
+                            .insert(merchant_connector_id.clone(), new_payout_record);
+                    }
+
+                    // Create updated CommonMandateReference preserving payments section
+                    CommonMandateReference {
+                        payments: mandate_details.payments,
+                        payouts: Some(existing_payouts_mandate),
+                    }
+                }
+                _ => {
+                    // Handle PaymentsMandateReference
+                    let mut existing_payments_mandate = mandate_details
+                        .payments
+                        .unwrap_or_else(|| PaymentsMandateReference(HashMap::new()));
+
+                    // Check if record exists for this merchant_connector_id
+                    if let Some(existing_record) =
+                        existing_payments_mandate.0.get_mut(merchant_connector_id)
+                    {
+                        existing_record.connector_mandate_id =
+                            payment_instrument_id.peek().to_string();
+                    } else {
+                        // Insert new record in connector_mandate_details
+                        existing_payments_mandate.0.insert(
+                            merchant_connector_id.clone(),
+                            PaymentsMandateReferenceRecord {
+                                connector_mandate_id: payment_instrument_id.peek().to_string(),
+                                payment_method_type: None,
+                                original_payment_authorized_amount: None,
+                                original_payment_authorized_currency: None,
+                                mandate_metadata: None,
+                                connector_mandate_status: None,
+                                connector_mandate_request_reference_id: None,
+                            },
+                        );
+                    }
+
+                    // Create updated CommonMandateReference preserving payouts section
+                    CommonMandateReference {
+                        payments: Some(existing_payments_mandate),
+                        payouts: mandate_details.payouts,
+                    }
+                }
+            };
+
+            let connector_mandate_details_value = updated_connector_mandate_details
+                .get_mandate_details_value()
+                .map_err(|err| {
+                    logger::error!("Failed to get get_mandate_details_value : {:?}", err);
+                    errors::ApiErrorResponse::MandateUpdateFailed
+                })?;
+
+            PaymentMethodUpdate::ConnectorNetworkTransactionIdStatusAndMandateDetailsUpdate {
+                connector_mandate_details: Some(pii::SecretSerdeValue::new(
+                    connector_mandate_details_value,
+                )),
+                network_transaction_id,
+                status,
+            }
+        }
+        _ => {
+            // Update only network_transaction_id and status
+            PaymentMethodUpdate::NetworkTransactionIdAndStatusUpdate {
+                network_transaction_id,
+                status,
+            }
+        }
+    };
+
+    let response = db
+        .update_payment_method(
+            &state.into(),
+            merchant_context.get_merchant_key_store(),
+            payment_method,
+            pm_update,
+            merchant_context.get_merchant_account().storage_scheme,
+        )
+        .await
+        .change_context(errors::ApiErrorResponse::InternalServerError)
+        .attach_printable(format!(
+            "Failed to update payment method for existing pm_id: {payment_method_id:?} in db",
+        ))?;
+
+    logger::debug!("Payment method updated in db");
+
+    Ok(ApplicationResponse::Json(
+        pm_api::PaymentMethodRecordUpdateResponse {
+            payment_method_id: response.payment_method_id,
+            status: response.status,
+            network_transaction_id: response.network_transaction_id,
+            connector_mandate_details: response
+                .connector_mandate_details
+                .map(pii::SecretSerdeValue::new),
+        },
+    ))
+}
+
+#[derive(Debug, form::MultipartForm)]
+pub struct PaymentMethodsUpdateForm {
+    #[multipart(limit = "1MB")]
+    pub file: bytes::Bytes,
+
+    pub merchant_id: text::Text<id_type::MerchantId>,
+}
+
+fn parse_update_csv(data: &[u8]) -> csv::Result<Vec<pm_api::UpdatePaymentMethodRecord>> {
+    let mut csv_reader = Reader::from_reader(data);
+    let mut records = Vec::new();
+    let mut id_counter = 0;
+    for result in csv_reader.deserialize() {
+        let mut record: pm_api::UpdatePaymentMethodRecord = result?;
+        id_counter += 1;
+        record.line_number = Some(id_counter);
+        records.push(record);
+    }
+    Ok(records)
+}
+
+type UpdateValidationResult =
+    Result<(id_type::MerchantId, Vec<pm_api::UpdatePaymentMethodRecord>), errors::ApiErrorResponse>;
+
+impl PaymentMethodsUpdateForm {
+    pub fn validate_and_get_payment_method_records(self) -> UpdateValidationResult {
+        let records = parse_update_csv(self.file.data.to_bytes()).map_err(|e| {
+            errors::ApiErrorResponse::PreconditionFailed {
+                message: e.to_string(),
+            }
+        })?;
+        Ok((self.merchant_id.clone(), records))
+    }
+}
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index 690a4337351..c48fcd1fec1 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -1429,6 +1429,10 @@ impl PaymentMethods {
                     web::resource("/migrate-batch")
                         .route(web::post().to(payment_methods::migrate_payment_methods)),
                 )
+                .service(
+                    web::resource("/update-batch")
+                        .route(web::post().to(payment_methods::update_payment_methods)),
+                )
                 .service(
                     web::resource("/tokenize-card")
                         .route(web::post().to(payment_methods::tokenize_card_api)),
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index 27b66e5bb54..0c42909b457 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -121,6 +121,7 @@ impl From<Flow> for ApiIdentifier {
 
             Flow::PaymentMethodsCreate
             | Flow::PaymentMethodsMigrate
+            | Flow::PaymentMethodsBatchUpdate
             | Flow::PaymentMethodsList
             | Flow::CustomerPaymentMethodsList
             | Flow::GetPaymentMethodTokenData
diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs
index 08b84e40977..7aa1ba949ba 100644
--- a/crates/router/src/routes/payment_methods.rs
+++ b/crates/router/src/routes/payment_methods.rs
@@ -21,7 +21,7 @@ use crate::{
     core::{
         api_locking,
         errors::{self, utils::StorageErrorExt},
-        payment_methods::{self as payment_methods_routes, cards},
+        payment_methods::{self as payment_methods_routes, cards, migration as update_migration},
     },
     services::{self, api, authentication as auth, authorization::permissions::Permission},
     types::{
@@ -413,6 +413,46 @@ pub async fn migrate_payment_methods(
     .await
 }
 
+#[cfg(all(feature = "v1", any(feature = "olap", feature = "oltp")))]
+#[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsBatchUpdate))]
+pub async fn update_payment_methods(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    MultipartForm(form): MultipartForm<update_migration::PaymentMethodsUpdateForm>,
+) -> HttpResponse {
+    let flow = Flow::PaymentMethodsBatchUpdate;
+    let (merchant_id, records) = match form.validate_and_get_payment_method_records() {
+        Ok((merchant_id, records)) => (merchant_id, records),
+        Err(e) => return api::log_and_return_error_response(e.into()),
+    };
+    Box::pin(api::server_wrap(
+        flow,
+        state,
+        &req,
+        records,
+        |state, _, req, _| {
+            let merchant_id = merchant_id.clone();
+            async move {
+                let (key_store, merchant_account) =
+                    get_merchant_account(&state, &merchant_id).await?;
+                let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
+                    domain::Context(merchant_account.clone(), key_store.clone()),
+                ));
+                Box::pin(update_migration::update_payment_methods(
+                    &state,
+                    req,
+                    &merchant_id,
+                    &merchant_context,
+                ))
+                .await
+            }
+        },
+        &auth::AdminApiAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
 #[cfg(feature = "v1")]
 #[instrument(skip_all, fields(flow = ?Flow::PaymentMethodSave))]
 pub async fn save_payment_method_api(
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index b758d98f498..ae5f34fd78f 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -118,6 +118,8 @@ pub enum Flow {
     PaymentMethodsCreate,
     /// Payment methods migrate flow.
     PaymentMethodsMigrate,
+    /// Payment methods batch update flow.
+    PaymentMethodsBatchUpdate,
     /// Payment methods list flow.
     PaymentMethodsList,
     /// Payment method save flow