Bug: feat(router): Add `psp_tokenization_enabled` field in Customer Saved Payment Methods List



Required to show whether `psp_tokenization` is enabled for a saved payment method
diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json
index dbe13e3776e..0ae089c5ea0 100644
--- a/api-reference-v2/openapi_spec.json
+++ b/api-reference-v2/openapi_spec.json
@@ -2834,7 +2834,7 @@
       }
     },
     "/v2/customers/{id}/saved-payment-methods": {
-      "delete": {
+      "get": {
         "tags": [
           "Payment Methods"
         ],
@@ -7666,6 +7666,7 @@
           "prophetpay",
           "rapyd",
           "razorpay",
+          "recurly",
           "shift4",
           "square",
           "stax",
@@ -8651,7 +8652,8 @@
           "recurring_enabled",
           "created",
           "requires_cvv",
-          "is_default"
+          "is_default",
+          "psp_tokenization_enabled"
         ],
         "properties": {
           "id": {
@@ -8730,6 +8732,10 @@
               }
             ],
             "nullable": true
+          },
+          "psp_tokenization_enabled": {
+            "type": "boolean",
+            "description": "Whether psp_tokenization is enabled for the payment_method, this will be true when at least\none multi-use token with status `Active` is available for the payment method"
           }
         }
       },
@@ -11603,7 +11609,7 @@
           "product_type": {
             "allOf": [
               {
-                "$ref": "#/components/schemas/api_enums.MerchantProductType"
+                "$ref": "#/components/schemas/MerchantProductType"
               }
             ],
             "nullable": true
@@ -11745,7 +11751,7 @@
           "product_type": {
             "allOf": [
               {
-                "$ref": "#/components/schemas/api_enums.MerchantProductType"
+                "$ref": "#/components/schemas/MerchantProductType"
               }
             ],
             "nullable": true
@@ -12359,6 +12365,17 @@
         },
         "additionalProperties": false
       },
+      "MerchantProductType": {
+        "type": "string",
+        "enum": [
+          "orchestration",
+          "vault",
+          "recon",
+          "recovery",
+          "cost_observability",
+          "dynamic_routing"
+        ]
+      },
       "MerchantRecipientData": {
         "oneOf": [
           {
@@ -12581,6 +12598,69 @@
           }
         }
       },
+      "NetworkTokenDetailsPaymentMethod": {
+        "type": "object",
+        "properties": {
+          "last4_digits": {
+            "type": "string",
+            "nullable": true
+          },
+          "issuer_country": {
+            "type": "string",
+            "nullable": true
+          },
+          "network_token_expiry_month": {
+            "type": "string",
+            "nullable": true
+          },
+          "network_token_expiry_year": {
+            "type": "string",
+            "nullable": true
+          },
+          "nick_name": {
+            "type": "string",
+            "nullable": true
+          },
+          "card_holder_name": {
+            "type": "string",
+            "nullable": true
+          },
+          "card_isin": {
+            "type": "string",
+            "nullable": true
+          },
+          "card_issuer": {
+            "type": "string",
+            "nullable": true
+          },
+          "card_network": {
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/CardNetwork"
+              }
+            ],
+            "nullable": true
+          },
+          "card_type": {
+            "type": "string",
+            "nullable": true
+          },
+          "saved_to_locker": {
+            "type": "boolean"
+          }
+        }
+      },
+      "NetworkTokenResponse": {
+        "type": "object",
+        "required": [
+          "payment_method_data"
+        ],
+        "properties": {
+          "payment_method_data": {
+            "$ref": "#/components/schemas/NetworkTokenDetailsPaymentMethod"
+          }
+        }
+      },
       "NetworkTokenization": {
         "type": "object",
         "description": "The network tokenization configuration for creating the payment method session",
@@ -13968,7 +14048,7 @@
       "PaymentConnectorTransmission": {
         "type": "string",
         "enum": [
-          "ConnectorCallFailed",
+          "ConnectorCallUnsuccessful",
           "ConnectorCallSucceeded"
         ]
       },
@@ -20385,6 +20465,7 @@
           "prophetpay",
           "rapyd",
           "razorpay",
+          "recurly",
           "riskified",
           "shift4",
           "signifyd",
diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs
index b6f1e0a8b96..b3d21c5a0b6 100644
--- a/crates/api_models/src/admin.rs
+++ b/crates/api_models/src/admin.rs
@@ -113,7 +113,7 @@ pub struct MerchantAccountCreate {
     pub pm_collect_link_config: Option<BusinessCollectLinkConfig>,
 
     /// Product Type of this merchant account
-    #[schema(value_type = Option<api_enums::MerchantProductType>)]
+    #[schema(value_type = Option<MerchantProductType>)]
     pub product_type: Option<api_enums::MerchantProductType>,
 }
 
@@ -198,6 +198,7 @@ pub struct MerchantAccountCreateWithoutOrgId {
     #[schema(value_type = Option<Object>, example = r#"{ "city": "NY", "unit": "245" }"#)]
     pub metadata: Option<pii::SecretSerdeValue>,
 
+    #[schema(value_type = Option<MerchantProductType>)]
     pub product_type: Option<api_enums::MerchantProductType>,
 }
 
@@ -560,7 +561,7 @@ pub struct MerchantAccountResponse {
     pub pm_collect_link_config: Option<BusinessCollectLinkConfig>,
 
     /// Product Type of this merchant account
-    #[schema(value_type = Option<api_enums::MerchantProductType>)]
+    #[schema(value_type = Option<MerchantProductType>)]
     pub product_type: Option<api_enums::MerchantProductType>,
 }
 
@@ -596,7 +597,7 @@ pub struct MerchantAccountResponse {
     pub recon_status: api_enums::ReconStatus,
 
     /// Product Type of this merchant account
-    #[schema(value_type = Option<api_enums::MerchantProductType>)]
+    #[schema(value_type = Option<MerchantProductType>)]
     pub product_type: Option<api_enums::MerchantProductType>,
 }
 
diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs
index 6b9b35ac2c0..d665337fda0 100644
--- a/crates/api_models/src/payment_methods.rs
+++ b/crates/api_models/src/payment_methods.rs
@@ -980,16 +980,21 @@ pub struct CardDetailsPaymentMethod {
     pub saved_to_locker: bool,
 }
 
-#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
+#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)]
 pub struct NetworkTokenDetailsPaymentMethod {
     pub last4_digits: Option<String>,
     pub issuer_country: Option<String>,
+    #[schema(value_type = Option<String>)]
     pub network_token_expiry_month: Option<masking::Secret<String>>,
+    #[schema(value_type = Option<String>)]
     pub network_token_expiry_year: Option<masking::Secret<String>>,
+    #[schema(value_type = Option<String>)]
     pub nick_name: Option<masking::Secret<String>>,
+    #[schema(value_type = Option<String>)]
     pub card_holder_name: Option<masking::Secret<String>>,
     pub card_isin: Option<String>,
     pub card_issuer: Option<String>,
+    #[schema(value_type = Option<CardNetwork>)]
     pub card_network: Option<api_enums::CardNetwork>,
     pub card_type: Option<String>,
     #[serde(default = "saved_in_locker_default")]
@@ -2019,6 +2024,10 @@ pub struct CustomerPaymentMethod {
 
     ///The network token details for the payment method
     pub network_tokenization: Option<NetworkTokenResponse>,
+
+    /// Whether psp_tokenization is enabled for the payment_method, this will be true when at least
+    /// one multi-use token with status `Active` is available for the payment method
+    pub psp_tokenization_enabled: bool,
 }
 
 #[cfg(all(feature = "v2", feature = "payment_methods_v2"))]
diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs
index 1109c0ca75e..20d1aa64745 100644
--- a/crates/openapi/src/openapi.rs
+++ b/crates/openapi/src/openapi.rs
@@ -320,6 +320,7 @@ Never share your secret api keys. Keep them guarded and secure.
         api_models::enums::PaymentLinkDetailsLayout,
         api_models::enums::PaymentMethodStatus,
         api_models::enums::UIWidgetFormLayout,
+        api_models::enums::MerchantProductType,
         api_models::enums::PaymentConnectorCategory,
         api_models::enums::CardDiscovery,
         api_models::enums::FeatureStatus,
diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs
index 8a122b5fe8b..5bbd8dee4b4 100644
--- a/crates/openapi/src/openapi_v2.rs
+++ b/crates/openapi/src/openapi_v2.rs
@@ -259,6 +259,7 @@ Never share your secret api keys. Keep them guarded and secure.
         api_models::enums::PaymentMethod,
         api_models::enums::PaymentMethodIssuerCode,
         api_models::enums::MandateStatus,
+        api_models::enums::MerchantProductType,
         api_models::enums::PaymentExperience,
         api_models::enums::BankNames,
         api_models::enums::BankType,
@@ -527,6 +528,8 @@ Never share your secret api keys. Keep them guarded and secure.
         api_models::payment_methods::PaymentMethodSessionRequest,
         api_models::payment_methods::PaymentMethodSessionResponse,
         api_models::payment_methods::PaymentMethodsSessionUpdateRequest,
+        api_models::payment_methods::NetworkTokenResponse,
+        api_models::payment_methods::NetworkTokenDetailsPaymentMethod,
         api_models::payment_methods::TokenizeCardRequest,
         api_models::payment_methods::TokenizeDataRequest,
         api_models::payment_methods::TokenizePaymentMethodRequest,
diff --git a/crates/openapi/src/routes/payment_method.rs b/crates/openapi/src/routes/payment_method.rs
index 9304afc69d3..cf1cd15346f 100644
--- a/crates/openapi/src/routes/payment_method.rs
+++ b/crates/openapi/src/routes/payment_method.rs
@@ -326,7 +326,7 @@ pub async fn payment_method_delete_api() {}
 ///
 /// List the payment methods saved for a customer
 #[utoipa::path(
-    delete,
+    get,
     path = "/v2/customers/{id}/saved-payment-methods",
     params (
         ("id" = String, Path, description = "The unique identifier for the customer"),
diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs
index 87f7e38c935..a7817329607 100644
--- a/crates/router/src/core/payment_methods/transformers.rs
+++ b/crates/router/src/core/payment_methods/transformers.rs
@@ -985,6 +985,15 @@ impl transformers::ForeignTryFrom<domain::PaymentMethod> for api::CustomerPaymen
         // TODO: check how we can get this field
         let recurring_enabled = true;
 
+        let psp_tokenization_enabled = item.connector_mandate_details.and_then(|details| {
+            details.payments.map(|payments| {
+                payments.values().any(|connector_token_reference| {
+                    connector_token_reference.connector_token_status
+                        == api_enums::ConnectorTokenStatus::Active
+                })
+            })
+        });
+
         Ok(Self {
             id: item.id,
             customer_id: item.customer_id,
@@ -999,6 +1008,7 @@ impl transformers::ForeignTryFrom<domain::PaymentMethod> for api::CustomerPaymen
             is_default: false,
             billing: payment_method_billing,
             network_tokenization: network_token_resp,
+            psp_tokenization_enabled: psp_tokenization_enabled.unwrap_or(false),
         })
     }
 }

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

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

## Description
<!-- Describe your changes in detail -->
Added `psp_tokenization_enabled` field to `CustomerPaymentMethod` that denotes whether `psp_tokenization` is enabled for a saved payment method

### 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).
-->
Closes #7518 

## 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)?
-->

- Create a Payment Method Session

NOTE: `psp_tokenization` field is required to generate token (Set `psp_tokenization_enabled` to `true`). Its value can either be `multi_use` or `single_use`.
 
```
curl --location 'http://localhost:8080/v2/payment-methods-session' \
--header 'x-profile-id: pro_piGRXWnlda2RVykbLnUU' \
--header 'Authorization: api-key=dev_m7AYHTfChsNPrDsCJodKhqKiRVwgW5dzawo2T3lHGrmkrfkesXVvI3LExORXwRcW' \
--header 'Content-Type: application/json' \
--header 'api-key: dev_m7AYHTfChsNPrDsCJodKhqKiRVwgW5dzawo2T3lHGrmkrfkesXVvI3LExORXwRcW' \
--data-raw '{
    "customer_id": "12345_cus_0195ae660fb270e28ea754519806b702",
     "billing": {
        "address": {
            "first_name": "John",
            "last_name": "Dough"
        },
        "email": "example@example.com"
    },
    "psp_tokenization": {
        "tokenization_type": "multi_use"
    }
}'
```

- Confirm the Payment Method Session

```
curl --location 'http://localhost:8080/v2/payment-methods-session/12345_pms_0195ae661bbc7d60a35f95ab5d29fe52/confirm' \
--header 'x-profile-id: pro_piGRXWnlda2RVykbLnUU' \
--header 'Authorization: publishable-key=pk_dev_04fccc9893564ead968e58bff3b8068b,client-secret=cs_0195ae661bbd70e39efd4f332f062b35' \
--header 'Content-Type: application/json' \
--header 'api-key: dev_m7AYHTfChsNPrDsCJodKhqKiRVwgW5dzawo2T3lHGrmkrfkesXVvI3LExORXwRcW' \
--data '{
    "payment_method_data": {
        "card": {
            "card_number": "4242424242424242",
            "card_exp_month": "05",
            "card_exp_year": "25",
            "card_holder_name": "John Doe",
            "card_cvc": "100"
        }
    },
    "payment_method_type": "card",
    "payment_method_subtype": "credit"
}'
```

Response:

```json
{
    "id": "12345_pms_0195ae661bbc7d60a35f95ab5d29fe52",
    "customer_id": "12345_cus_0195ae660fb270e28ea754519806b702",
    "billing": {
        "address": {
            "city": null,
            "country": null,
            "line1": null,
            "line2": null,
            "line3": null,
            "zip": null,
            "state": null,
            "first_name": "John",
            "last_name": "Dough"
        },
        "phone": null,
        "email": "example@example.com"
    },
    "psp_tokenization": {
        "tokenization_type": "multi_use",
        "connector_id": null
    },
    "network_tokenization": null,
    "expires_at": "2025-03-19T12:49:42.237Z",
    "client_secret": "CLIENT_SECRET_REDACTED",
    "return_url": null,
    "next_action": null,
    "authentication_details": {
        "status": "succeeded",
        "error": null
    },
    "associated_payment_methods": null
}
```

- Retrieve Payment Method:

```
curl --location 'http://localhost:8080/v2/payment-methods/12345_pm_0195ae669d8476d1bdcb54a3b266b5a6' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'X-Profile-Id: pro_piGRXWnlda2RVykbLnUU' \
--header 'X-Resource-Type: payment_method' \
--header 'api-key: dev_m7AYHTfChsNPrDsCJodKhqKiRVwgW5dzawo2T3lHGrmkrfkesXVvI3LExORXwRcW' \
--header 'Authorization: api-key=dev_m7AYHTfChsNPrDsCJodKhqKiRVwgW5dzawo2T3lHGrmkrfkesXVvI3LExORXwRcW'
```

Response:

```json
{
    "id": "12345_pm_0195ae669d8476d1bdcb54a3b266b5a6",
    "merchant_id": "cloth_seller_ofmnd07CcVEfqVvCSgAJ",
    "customer_id": "12345_cus_0195ae660fb270e28ea754519806b702",
    "payment_method_type": "card",
    "payment_method_subtype": "credit",
    "recurring_enabled": false,
    "created": "2025-03-19T12:35:15.461Z",
    "last_used_at": "2025-03-19T12:35:15.461Z",
    "payment_method_data": {
        "card": {
            "issuer_country": null,
            "last4_digits": "4242",
            "expiry_month": "05",
            "expiry_year": "25",
            "card_holder_name": "John Doe",
            "card_fingerprint": null,
            "nick_name": null,
            "card_network": null,
            "card_isin": null,
            "card_issuer": null,
            "card_type": null,
            "saved_to_locker": true
        }
    },
    "connector_tokens": [
        {
            "connector_id": "mca_8nVPkBONTcH6HgSf3u2W",
            "token_type": "multi_use",
            "status": "active",
            "connector_token_request_reference_id": "jRkI9KrD9ndJsl4YUi",
            "original_payment_authorized_amount": 0,
            "original_payment_authorized_currency": "USD",
            "metadata": null,
            "token": "pm_1R4Lq4D5R7gDAGffpqWfb9lx"
        }
    ],
    "network_token": null
}
```

Customer Payment Methods List:
```
curl --location 'http://localhost:8080/v2/customers/12345_cus_0195ae660fb270e28ea754519806b702/saved-payment-methods' \
--header 'api-key: dev_m7AYHTfChsNPrDsCJodKhqKiRVwgW5dzawo2T3lHGrmkrfkesXVvI3LExORXwRcW' \
--header 'Content-Type: application/json' \
--header 'x-profile-id: pro_piGRXWnlda2RVykbLnUU' \
--header 'Authorization: api-key=dev_m7AYHTfChsNPrDsCJodKhqKiRVwgW5dzawo2T3lHGrmkrfkesXVvI3LExORXwRcW'
```


Response:
```json
{
    "customer_payment_methods": [
        {
            "id": "12345_pm_0195ae669d8476d1bdcb54a3b266b5a6",
            "customer_id": "12345_cus_0195ae660fb270e28ea754519806b702",
            "payment_method_type": "card",
            "payment_method_subtype": "credit",
            "recurring_enabled": true,
            "payment_method_data": {
                "card": {
                    "issuer_country": null,
                    "last4_digits": "4242",
                    "expiry_month": "05",
                    "expiry_year": "25",
                    "card_holder_name": "John Doe",
                    "card_fingerprint": null,
                    "nick_name": null,
                    "card_network": null,
                    "card_isin": null,
                    "card_issuer": null,
                    "card_type": null,
                    "saved_to_locker": true
                }
            },
            "bank": null,
            "created": "2025-03-19T12:35:15.461Z",
            "requires_cvv": true,
            "last_used_at": "2025-03-19T12:35:15.461Z",
            "is_default": false,
            "billing": {
                "address": {
                    "city": null,
                    "country": null,
                    "line1": null,
                    "line2": null,
                    "line3": null,
                    "zip": null,
                    "state": null,
                    "first_name": "John",
                    "last_name": "Dough"
                },
                "phone": null,
                "email": "example@example.com"
            },
            "network_tokenization": null,
            "psp_tokenization_enabled": true
        }
    ]
}
```

## 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
