Bug: [REFACTOR] Allow deletion of default payment method for a customer if only one pm exists



Currently, we don't delete the default payment method of a customer. But if only one payment method exists for a customer which is also set to default, then we should allow to delete that payment method
diff --git a/crates/diesel_models/src/customers.rs b/crates/diesel_models/src/customers.rs
index cefb0c240ec..bda7af157de 100644
--- a/crates/diesel_models/src/customers.rs
+++ b/crates/diesel_models/src/customers.rs
@@ -52,5 +52,5 @@ pub struct CustomerUpdateInternal {
     pub modified_at: Option<PrimitiveDateTime>,
     pub connector_customer: Option<serde_json::Value>,
     pub address_id: Option<String>,
-    pub default_payment_method_id: Option<String>,
+    pub default_payment_method_id: Option<Option<String>>,
 }
diff --git a/crates/diesel_models/src/query/generics.rs b/crates/diesel_models/src/query/generics.rs
index 8f2e391df6c..a1dd40cd400 100644
--- a/crates/diesel_models/src/query/generics.rs
+++ b/crates/diesel_models/src/query/generics.rs
@@ -40,6 +40,7 @@ pub mod db_metrics {
         DeleteWithResult,
         UpdateWithResults,
         UpdateOne,
+        Count,
     }
 
     #[inline]
diff --git a/crates/diesel_models/src/query/payment_method.rs b/crates/diesel_models/src/query/payment_method.rs
index bed4d079010..a27a2ae8950 100644
--- a/crates/diesel_models/src/query/payment_method.rs
+++ b/crates/diesel_models/src/query/payment_method.rs
@@ -1,4 +1,9 @@
-use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods, Table};
+use async_bb8_diesel::AsyncRunQueryDsl;
+use diesel::{
+    associations::HasTable, debug_query, pg::Pg, BoolExpressionMethods, ExpressionMethods,
+    QueryDsl, Table,
+};
+use error_stack::{IntoReport, ResultExt};
 
 use super::generics;
 use crate::{
@@ -96,6 +101,34 @@ impl PaymentMethod {
         .await
     }
 
+    pub async fn get_count_by_customer_id_merchant_id_status(
+        conn: &PgPooledConn,
+        customer_id: &str,
+        merchant_id: &str,
+        status: common_enums::PaymentMethodStatus,
+    ) -> StorageResult<i64> {
+        let filter = <Self as HasTable>::table()
+            .count()
+            .filter(
+                dsl::customer_id
+                    .eq(customer_id.to_owned())
+                    .and(dsl::merchant_id.eq(merchant_id.to_owned()))
+                    .and(dsl::status.eq(status.to_owned())),
+            )
+            .into_boxed();
+
+        router_env::logger::debug!(query = %debug_query::<Pg, _>(&filter).to_string());
+
+        generics::db_metrics::track_database_call::<<Self as HasTable>::Table, _, _>(
+            filter.get_result_async::<i64>(conn),
+            generics::db_metrics::DatabaseOperation::Count,
+        )
+        .await
+        .into_report()
+        .change_context(errors::DatabaseError::Others)
+        .attach_printable("Failed to get a count of payment methods")
+    }
+
     pub async fn find_by_customer_id_merchant_id_status(
         conn: &PgPooledConn,
         customer_id: &str,
diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs
index fccacfef45d..ad272860717 100644
--- a/crates/router/src/core/payment_methods/cards.rs
+++ b/crates/router/src/core/payment_methods/cards.rs
@@ -3193,7 +3193,7 @@ pub async fn set_default_payment_method(
     )?;
 
     let customer_update = CustomerUpdate::UpdateDefaultPaymentMethod {
-        default_payment_method_id: Some(payment_method_id.to_owned()),
+        default_payment_method_id: Some(Some(payment_method_id.to_owned())),
     };
 
     // update the db with the default payment method id
@@ -3442,6 +3442,16 @@ pub async fn delete_payment_method(
         .await
         .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?;
 
+    let payment_methods_count = db
+        .get_payment_method_count_by_customer_id_merchant_id_status(
+            &key.customer_id,
+            &merchant_account.merchant_id,
+            api_enums::PaymentMethodStatus::Active,
+        )
+        .await
+        .change_context(errors::ApiErrorResponse::InternalServerError)
+        .attach_printable("Failed to get a count of payment methods for a customer")?;
+
     let customer = db
         .find_customer_by_customer_id_merchant_id(
             &key.customer_id,
@@ -3453,7 +3463,8 @@ pub async fn delete_payment_method(
         .attach_printable("Customer not found for the payment method")?;
 
     utils::when(
-        customer.default_payment_method_id.as_ref() == Some(&pm_id.payment_method_id),
+        customer.default_payment_method_id.as_ref() == Some(&pm_id.payment_method_id)
+            && payment_methods_count > 1,
         || Err(errors::ApiErrorResponse::PaymentMethodDeleteFailed),
     )?;
 
@@ -3481,6 +3492,22 @@ pub async fn delete_payment_method(
     .await
     .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?;
 
+    if customer.default_payment_method_id.as_ref() == Some(&pm_id.payment_method_id) {
+        let customer_update = CustomerUpdate::UpdateDefaultPaymentMethod {
+            default_payment_method_id: Some(None),
+        };
+
+        db.update_customer_by_customer_id_merchant_id(
+            key.customer_id,
+            key.merchant_id,
+            customer_update,
+            &key_store,
+        )
+        .await
+        .change_context(errors::ApiErrorResponse::InternalServerError)
+        .attach_printable("Failed to update the default payment method id for the customer")?;
+    };
+
     Ok(services::ApplicationResponse::Json(
         api::PaymentMethodDeleteResponse {
             payment_method_id: key.payment_method_id,
diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs
index 4509da8f0ff..ff036320098 100644
--- a/crates/router/src/db/kafka_store.rs
+++ b/crates/router/src/db/kafka_store.rs
@@ -1299,6 +1299,21 @@ impl PaymentMethodInterface for KafkaStore {
             .await
     }
 
+    async fn get_payment_method_count_by_customer_id_merchant_id_status(
+        &self,
+        customer_id: &str,
+        merchant_id: &str,
+        status: common_enums::PaymentMethodStatus,
+    ) -> CustomResult<i64, errors::StorageError> {
+        self.diesel_store
+            .get_payment_method_count_by_customer_id_merchant_id_status(
+                customer_id,
+                merchant_id,
+                status,
+            )
+            .await
+    }
+
     async fn find_payment_method_by_locker_id(
         &self,
         locker_id: &str,
diff --git a/crates/router/src/db/payment_method.rs b/crates/router/src/db/payment_method.rs
index ef471fbd246..94ebee17893 100644
--- a/crates/router/src/db/payment_method.rs
+++ b/crates/router/src/db/payment_method.rs
@@ -36,6 +36,13 @@ pub trait PaymentMethodInterface {
         limit: Option<i64>,
     ) -> CustomResult<Vec<storage::PaymentMethod>, errors::StorageError>;
 
+    async fn get_payment_method_count_by_customer_id_merchant_id_status(
+        &self,
+        customer_id: &str,
+        merchant_id: &str,
+        status: common_enums::PaymentMethodStatus,
+    ) -> CustomResult<i64, errors::StorageError>;
+
     async fn insert_payment_method(
         &self,
         payment_method_new: storage::PaymentMethodNew,
@@ -80,6 +87,25 @@ impl PaymentMethodInterface for Store {
             .into_report()
     }
 
+    #[instrument(skip_all)]
+    async fn get_payment_method_count_by_customer_id_merchant_id_status(
+        &self,
+        customer_id: &str,
+        merchant_id: &str,
+        status: common_enums::PaymentMethodStatus,
+    ) -> CustomResult<i64, errors::StorageError> {
+        let conn = connection::pg_connection_read(self).await?;
+        storage::PaymentMethod::get_count_by_customer_id_merchant_id_status(
+            &conn,
+            customer_id,
+            merchant_id,
+            status,
+        )
+        .await
+        .map_err(Into::into)
+        .into_report()
+    }
+
     #[instrument(skip_all)]
     async fn insert_payment_method(
         &self,
@@ -204,6 +230,27 @@ impl PaymentMethodInterface for MockDb {
         }
     }
 
+    async fn get_payment_method_count_by_customer_id_merchant_id_status(
+        &self,
+        customer_id: &str,
+        merchant_id: &str,
+        status: common_enums::PaymentMethodStatus,
+    ) -> CustomResult<i64, errors::StorageError> {
+        let payment_methods = self.payment_methods.lock().await;
+        let count = payment_methods
+            .iter()
+            .filter(|pm| {
+                pm.customer_id == customer_id
+                    && pm.merchant_id == merchant_id
+                    && pm.status == status
+            })
+            .count();
+        count
+            .try_into()
+            .into_report()
+            .change_context(errors::StorageError::MockDbError)
+    }
+
     async fn insert_payment_method(
         &self,
         payment_method_new: storage::PaymentMethodNew,
diff --git a/crates/router/src/types/domain/customer.rs b/crates/router/src/types/domain/customer.rs
index 5437d06a2e6..d5f05944d6c 100644
--- a/crates/router/src/types/domain/customer.rs
+++ b/crates/router/src/types/domain/customer.rs
@@ -118,7 +118,7 @@ pub enum CustomerUpdate {
         connector_customer: Option<serde_json::Value>,
     },
     UpdateDefaultPaymentMethod {
-        default_payment_method_id: Option<String>,
+        default_payment_method_id: Option<Option<String>>,
     },
 }
 

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

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

## Description
<!-- Describe your changes in detail -->
Currently, we don't delete the default payment method of a customer. But if only one payment method exists for a customer which is also set to default, then we should allow to delete that payment method

### 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
<!--
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)?
-->
1. Store a new card or any other payment method for a customer. (Ensure only one payment method exists for a customer)
2. Set that payment method as default

```
curl --location --request POST 'http://localhost:8080/customers/cus_aXfFGQOFT7JFU47GYhZK/payment_methods/pm_37h7qElY2aTWBEPSgovJ/default' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: abc' \
--data ''
```

3. Now try to delete that default payment method

```
curl --location --request DELETE 'http://localhost:8080/payment_methods/pm_37h7qElY2aTWBEPSgovJ' \
--header 'Accept: application/json' \
--header 'api-key: abc'
```

You should still be able to delete the default payment method as this is the only payment method that exists for that customer.

## 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
- [ ] I added a [CHANGELOG](/CHANGELOG.md) entry if applicable
