Bug: Recurring on NMI




### Discussed in https://github.com/juspay/hyperswitch/discussions/8291

<div type='discussions-op-text'>

<sup>Originally posted by **jschrecurly** June  9, 2025</sup>
Add recurring payment support for NMI.</div>
diff --git a/crates/hyperswitch_connectors/src/connectors/nmi.rs b/crates/hyperswitch_connectors/src/connectors/nmi.rs
index b2037c45256..7ae21664e73 100644
--- a/crates/hyperswitch_connectors/src/connectors/nmi.rs
+++ b/crates/hyperswitch_connectors/src/connectors/nmi.rs
@@ -38,7 +38,7 @@ use hyperswitch_interfaces::{
     types::{
         PaymentsAuthorizeType, PaymentsCaptureType, PaymentsCompleteAuthorizeType,
         PaymentsPreProcessingType, PaymentsSyncType, PaymentsVoidType, RefundExecuteType,
-        RefundSyncType, Response,
+        RefundSyncType, Response, SetupMandateType,
     },
     webhooks::{IncomingWebhook, IncomingWebhookRequestDetails},
 };
@@ -48,7 +48,7 @@ use transformers as nmi;
 
 use crate::{
     types::ResponseRouterData,
-    utils::{construct_not_supported_error_report, convert_amount, get_header_key_value},
+    utils::{self, construct_not_supported_error_report, convert_amount, get_header_key_value},
 };
 
 #[derive(Clone)]
@@ -161,6 +161,16 @@ impl ConnectorValidation for Nmi {
         // in case we dont have transaction id, we can make psync using attempt id
         Ok(())
     }
+
+    fn validate_mandate_payment(
+        &self,
+        pm_type: Option<enums::PaymentMethodType>,
+        pm_data: hyperswitch_domain_models::payment_method_data::PaymentMethodData,
+    ) -> CustomResult<(), ConnectorError> {
+        let mandate_supported_pmd =
+            std::collections::HashSet::from([utils::PaymentMethodDataType::Card]);
+        utils::is_mandate_supported(pm_data, pm_type, mandate_supported_pmd, self.id())
+    }
 }
 
 impl ConnectorIntegration<PaymentMethodToken, PaymentMethodTokenizationData, PaymentsResponseData>
@@ -194,27 +204,23 @@ impl ConnectorIntegration<SetupMandate, SetupMandateRequestData, PaymentsRespons
         req: &SetupMandateRouterData,
         _connectors: &Connectors,
     ) -> CustomResult<RequestContent, ConnectorError> {
-        let connector_req = nmi::NmiPaymentsRequest::try_from(req)?;
+        let connector_req = nmi::NmiValidateRequest::try_from(req)?;
         Ok(RequestContent::FormUrlEncoded(Box::new(connector_req)))
     }
 
     fn build_request(
         &self,
-        _req: &SetupMandateRouterData,
-        _connectors: &Connectors,
+        req: &SetupMandateRouterData,
+        connectors: &Connectors,
     ) -> CustomResult<Option<Request>, ConnectorError> {
-        Err(ConnectorError::NotImplemented("Setup Mandate flow for Nmi".to_string()).into())
-
-        // Ok(Some(
-        //     RequestBuilder::new()
-        //         .method(Method::Post)
-        //         .url(&SetupMandateType::get_url(self, req, connectors)?)
-        //         .headers(SetupMandateType::get_headers(self, req, connectors)?)
-        //         .set_body(SetupMandateType::get_request_body(
-        //             self, req, connectors,
-        //         )?)
-        //         .build(),
-        // ))
+        Ok(Some(
+            RequestBuilder::new()
+                .method(Method::Post)
+                .url(&SetupMandateType::get_url(self, req, connectors)?)
+                .headers(SetupMandateType::get_headers(self, req, connectors)?)
+                .set_body(SetupMandateType::get_request_body(self, req, connectors)?)
+                .build(),
+        ))
     }
 
     fn handle_response(
diff --git a/crates/hyperswitch_connectors/src/connectors/nmi/transformers.rs b/crates/hyperswitch_connectors/src/connectors/nmi/transformers.rs
index ec5ce3b2e65..32436f307af 100644
--- a/crates/hyperswitch_connectors/src/connectors/nmi/transformers.rs
+++ b/crates/hyperswitch_connectors/src/connectors/nmi/transformers.rs
@@ -1,8 +1,6 @@
 use api_models::webhooks::IncomingWebhookEvent;
 use cards::CardNumber;
-use common_enums::{
-    AttemptStatus, AuthenticationType, CaptureMethod, CountryAlpha2, Currency, RefundStatus,
-};
+use common_enums::{AttemptStatus, AuthenticationType, CountryAlpha2, Currency, RefundStatus};
 use common_utils::{errors::CustomResult, ext_traits::XmlExt, pii::Email, types::FloatMajorUnit};
 use error_stack::{report, Report, ResultExt};
 use hyperswitch_domain_models::{
@@ -351,6 +349,7 @@ pub struct NmiCompleteResponse {
     pub cvvresponse: Option<String>,
     pub orderid: String,
     pub response_code: String,
+    customer_vault_id: Option<Secret<String>>,
 }
 
 impl
@@ -377,17 +376,27 @@ impl
                 Ok(PaymentsResponseData::TransactionResponse {
                     resource_id: ResponseId::ConnectorTransactionId(item.response.transactionid),
                     redirection_data: Box::new(None),
-                    mandate_reference: Box::new(None),
+                    mandate_reference: match item.response.customer_vault_id {
+                        Some(vault_id) => Box::new(Some(
+                            hyperswitch_domain_models::router_response_types::MandateReference {
+                                connector_mandate_id: Some(vault_id.expose()),
+                                payment_method_id: None,
+                                mandate_metadata: None,
+                                connector_mandate_request_reference_id: None,
+                            },
+                        )),
+                        None => Box::new(None),
+                    },
                     connector_metadata: None,
                     network_txn_id: None,
                     connector_response_reference_id: Some(item.response.orderid),
                     incremental_authorization_allowed: None,
                     charges: None,
                 }),
-                if let Some(CaptureMethod::Automatic) = item.data.request.capture_method {
-                    AttemptStatus::CaptureInitiated
+                if item.data.request.is_auto_capture()? {
+                    AttemptStatus::Charged
                 } else {
-                    AttemptStatus::Authorizing
+                    AttemptStatus::Authorized
                 },
             ),
             Response::Declined | Response::Error => (
@@ -417,6 +426,18 @@ fn get_nmi_error_response(response: NmiCompleteResponse, http_code: u16) -> Erro
     }
 }
 
+#[derive(Debug, Serialize)]
+pub struct NmiValidateRequest {
+    #[serde(rename = "type")]
+    transaction_type: TransactionType,
+    security_key: Secret<String>,
+    ccnumber: CardNumber,
+    ccexp: Secret<String>,
+    cvv: Secret<String>,
+    orderid: String,
+    customer_vault: CustomerAction,
+}
+
 #[derive(Debug, Serialize)]
 pub struct NmiPaymentsRequest {
     #[serde(rename = "type")]
@@ -429,6 +450,8 @@ pub struct NmiPaymentsRequest {
     #[serde(flatten)]
     merchant_defined_field: Option<NmiMerchantDefinedField>,
     orderid: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    customer_vault: Option<CustomerAction>,
 }
 
 #[derive(Debug, Serialize)]
@@ -462,6 +485,12 @@ pub enum PaymentMethod {
     CardThreeDs(Box<CardThreeDsData>),
     GPay(Box<GooglePayData>),
     ApplePay(Box<ApplePayData>),
+    MandatePayment(Box<MandatePayment>),
+}
+
+#[derive(Debug, Serialize)]
+pub struct MandatePayment {
+    customer_vault_id: Secret<String>,
 }
 
 #[derive(Debug, Serialize)]
@@ -503,25 +532,70 @@ impl TryFrom<&NmiRouterData<&PaymentsAuthorizeRouterData>> for NmiPaymentsReques
         };
         let auth_type: NmiAuthType = (&item.router_data.connector_auth_type).try_into()?;
         let amount = item.amount;
-        let payment_method = PaymentMethod::try_from((
-            &item.router_data.request.payment_method_data,
-            Some(item.router_data),
-        ))?;
 
-        Ok(Self {
-            transaction_type,
-            security_key: auth_type.api_key,
-            amount,
-            currency: item.router_data.request.currency,
-            payment_method,
-            merchant_defined_field: item
-                .router_data
-                .request
-                .metadata
-                .as_ref()
-                .map(NmiMerchantDefinedField::new),
-            orderid: item.router_data.connector_request_reference_id.clone(),
-        })
+        match item
+            .router_data
+            .request
+            .mandate_id
+            .clone()
+            .and_then(|mandate_ids| mandate_ids.mandate_reference_id)
+        {
+            Some(api_models::payments::MandateReferenceId::ConnectorMandateId(
+                connector_mandate_id,
+            )) => Ok(Self {
+                transaction_type,
+                security_key: auth_type.api_key,
+                amount,
+                currency: item.router_data.request.currency,
+                payment_method: PaymentMethod::MandatePayment(Box::new(MandatePayment {
+                    customer_vault_id: Secret::new(
+                        connector_mandate_id
+                            .get_connector_mandate_id()
+                            .ok_or(ConnectorError::MissingConnectorMandateID)?,
+                    ),
+                })),
+                merchant_defined_field: item
+                    .router_data
+                    .request
+                    .metadata
+                    .as_ref()
+                    .map(NmiMerchantDefinedField::new),
+                orderid: item.router_data.connector_request_reference_id.clone(),
+                customer_vault: None,
+            }),
+            Some(api_models::payments::MandateReferenceId::NetworkMandateId(_))
+            | Some(api_models::payments::MandateReferenceId::NetworkTokenWithNTI(_)) => {
+                Err(ConnectorError::NotImplemented(
+                    get_unimplemented_payment_method_error_message("nmi"),
+                ))?
+            }
+            None => {
+                let payment_method = PaymentMethod::try_from((
+                    &item.router_data.request.payment_method_data,
+                    Some(item.router_data),
+                ))?;
+
+                Ok(Self {
+                    transaction_type,
+                    security_key: auth_type.api_key,
+                    amount,
+                    currency: item.router_data.request.currency,
+                    payment_method,
+                    merchant_defined_field: item
+                        .router_data
+                        .request
+                        .metadata
+                        .as_ref()
+                        .map(NmiMerchantDefinedField::new),
+                    orderid: item.router_data.connector_request_reference_id.clone(),
+                    customer_vault: item
+                        .router_data
+                        .request
+                        .is_mandate_payment()
+                        .then_some(CustomerAction::AddCustomer),
+                })
+            }
+        }
     }
 }
 
@@ -662,20 +736,36 @@ impl From<&ApplePayWalletData> for PaymentMethod {
     }
 }
 
-impl TryFrom<&SetupMandateRouterData> for NmiPaymentsRequest {
+impl TryFrom<&SetupMandateRouterData> for NmiValidateRequest {
     type Error = Error;
     fn try_from(item: &SetupMandateRouterData) -> Result<Self, Self::Error> {
-        let auth_type: NmiAuthType = (&item.connector_auth_type).try_into()?;
-        let payment_method = PaymentMethod::try_from((&item.request.payment_method_data, None))?;
-        Ok(Self {
-            transaction_type: TransactionType::Validate,
-            security_key: auth_type.api_key,
-            amount: FloatMajorUnit::zero(),
-            currency: item.request.currency,
-            payment_method,
-            merchant_defined_field: None,
-            orderid: item.connector_request_reference_id.clone(),
-        })
+        match item.request.amount {
+            Some(amount) if amount > 0 => Err(ConnectorError::FlowNotSupported {
+                flow: "Setup Mandate with non zero amount".to_string(),
+                connector: "NMI".to_string(),
+            }
+            .into()),
+            _ => {
+                if let PaymentMethodData::Card(card_details) = &item.request.payment_method_data {
+                    let auth_type: NmiAuthType = (&item.connector_auth_type).try_into()?;
+                    Ok(Self {
+                        transaction_type: TransactionType::Validate,
+                        security_key: auth_type.api_key,
+                        ccnumber: card_details.card_number.clone(),
+                        ccexp: card_details
+                            .get_card_expiry_month_year_2_digit_with_delimiter("".to_string())?,
+                        cvv: card_details.card_cvc.clone(),
+                        orderid: item.connector_request_reference_id.clone(),
+                        customer_vault: CustomerAction::AddCustomer,
+                    })
+                } else {
+                    Err(ConnectorError::NotImplemented(
+                        get_unimplemented_payment_method_error_message("Nmi"),
+                    )
+                    .into())
+                }
+            }
+        }
     }
 }
 
@@ -746,7 +836,7 @@ impl
                     incremental_authorization_allowed: None,
                     charges: None,
                 }),
-                AttemptStatus::CaptureInitiated,
+                AttemptStatus::Charged,
             ),
             Response::Declined | Response::Error => (
                 Err(get_standard_error_response(item.response, item.http_code)),
@@ -827,6 +917,7 @@ pub struct StandardResponse {
     pub cvvresponse: Option<String>,
     pub orderid: String,
     pub response_code: String,
+    pub customer_vault_id: Option<Secret<String>>,
 }
 
 impl<T> TryFrom<ResponseRouterData<SetupMandate, StandardResponse, T, PaymentsResponseData>>
@@ -843,7 +934,17 @@ impl<T> TryFrom<ResponseRouterData<SetupMandate, StandardResponse, T, PaymentsRe
                         item.response.transactionid.clone(),
                     ),
                     redirection_data: Box::new(None),
-                    mandate_reference: Box::new(None),
+                    mandate_reference: match item.response.customer_vault_id {
+                        Some(vault_id) => Box::new(Some(
+                            hyperswitch_domain_models::router_response_types::MandateReference {
+                                connector_mandate_id: Some(vault_id.expose()),
+                                payment_method_id: None,
+                                mandate_metadata: None,
+                                connector_mandate_request_reference_id: None,
+                            },
+                        )),
+                        None => Box::new(None),
+                    },
                     connector_metadata: None,
                     network_txn_id: None,
                     connector_response_reference_id: Some(item.response.orderid),
@@ -897,15 +998,25 @@ impl TryFrom<PaymentsResponseRouterData<StandardResponse>>
                         item.response.transactionid.clone(),
                     ),
                     redirection_data: Box::new(None),
-                    mandate_reference: Box::new(None),
+                    mandate_reference: match item.response.customer_vault_id {
+                        Some(vault_id) => Box::new(Some(
+                            hyperswitch_domain_models::router_response_types::MandateReference {
+                                connector_mandate_id: Some(vault_id.expose()),
+                                payment_method_id: None,
+                                mandate_metadata: None,
+                                connector_mandate_request_reference_id: None,
+                            },
+                        )),
+                        None => Box::new(None),
+                    },
                     connector_metadata: None,
                     network_txn_id: None,
                     connector_response_reference_id: Some(item.response.orderid),
                     incremental_authorization_allowed: None,
                     charges: None,
                 }),
-                if let Some(CaptureMethod::Automatic) = item.data.request.capture_method {
-                    AttemptStatus::CaptureInitiated
+                if item.data.request.is_auto_capture()? {
+                    AttemptStatus::Charged
                 } else {
                     AttemptStatus::Authorized
                 },
@@ -1094,7 +1205,7 @@ impl TryFrom<RefundsResponseRouterData<Capture, StandardResponse>> for RefundsRo
 impl From<Response> for RefundStatus {
     fn from(item: Response) -> Self {
         match item {
-            Response::Approved => Self::Pending,
+            Response::Approved => Self::Success,
             Response::Declined | Response::Error => Self::Failure,
         }
     }

## 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 -->
Mandate payments support has been added to NMI.
https://support.nmi.com/hc/en-gb/articles/13923047779729-Using-the-Customer-Vault

### 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).
-->
https://github.com/juspay/hyperswitch/discussions/8291 https://github.com/juspay/hyperswitch/issues/8653

## 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)?
-->
Tested and added relevant cypress tests.
CIT:
Request:
```
curl --location 'http://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_6Qh8tlhJxGnzCnYSgZLYUcZg3se94a12xetjLpMLCE3srm2F6E3szTR3vJ8IRzxA' \
--data-raw '{
    "amount": 1122,
    "currency": "USD",
    "confirm": true,
    "profile_id": "pro_prQtzjS2YNKFt8NTnPhS",
    "authentication_type": "no_three_ds",
    "email": "vaily@gmail.com",
    "customer_id": "vaily",
    "return_url": "https://google.com",
    "payment_method": "card",
    "payment_method_type": "credit",
    "payment_method_data": {
        "card": {
            "card_number": "4532111111111112",
            "card_exp_month": "10",
            "card_exp_year": "25",
            "card_cvc": "123"
        }
    },
    "setup_future_usage": "off_session",
    "customer_acceptance": {
        "acceptance_type": "online"
    }
}'
```
Response:
```
{
    "payment_id": "pay_VosL2oOd5lEpfI5PFdAF",
    "merchant_id": "merchant_1754040245",
    "status": "succeeded",
    "amount": 1122,
    "net_amount": 1122,
    "shipping_cost": null,
    "amount_capturable": 0,
    "amount_received": 1122,
    "connector": "nmi",
    "client_secret": "pay_VosL2oOd5lEpfI5PFdAF_secret_pwdKA6SfrJQA90LvmvQD",
    "created": "2025-08-01T10:36:17.871Z",
    "currency": "USD",
    "customer_id": "vaily",
    "customer": {
        "id": "vaily",
        "name": null,
        "email": "vaily@gmail.com",
        "phone": null,
        "phone_country_code": null
    },
    "description": null,
    "refunds": null,
    "disputes": null,
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": "off_session",
    "off_session": null,
    "capture_on": null,
    "capture_method": null,
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "last4": "1112",
            "card_type": null,
            "card_network": null,
            "card_issuer": null,
            "card_issuing_country": null,
            "card_isin": "453211",
            "card_extended_bin": null,
            "card_exp_month": "10",
            "card_exp_year": "25",
            "card_holder_name": null,
            "payment_checks": null,
            "authentication_data": null
        },
        "billing": null
    },
    "payment_token": "token_4z27khJu7gCT1bLHEHse",
    "shipping": null,
    "billing": null,
    "order_details": null,
    "email": "vaily@gmail.com",
    "name": null,
    "phone": null,
    "return_url": "https://google.com/",
    "authentication_type": "no_three_ds",
    "statement_descriptor_name": null,
    "statement_descriptor_suffix": null,
    "next_action": null,
    "cancellation_reason": null,
    "error_code": null,
    "error_message": null,
    "unified_code": null,
    "unified_message": null,
    "payment_experience": null,
    "payment_method_type": "credit",
    "connector_label": null,
    "business_country": null,
    "business_label": "default",
    "business_sub_label": null,
    "allowed_payment_method_types": null,
    "ephemeral_key": {
        "customer_id": "vaily",
        "created_at": 1754044577,
        "expires": 1754048177,
        "secret": "epk_554d3664984344ca9fe39c48ff4ff5ef"
    },
    "manual_retry_allowed": false,
    "connector_transaction_id": "10979926051",
    "frm_message": null,
    "metadata": null,
    "connector_metadata": null,
    "feature_metadata": null,
    "reference_id": "pay_VosL2oOd5lEpfI5PFdAF_1",
    "payment_link": null,
    "profile_id": "pro_prQtzjS2YNKFt8NTnPhS",
    "surcharge_details": null,
    "attempt_count": 1,
    "merchant_decision": null,
    "merchant_connector_id": "mca_PbTIQbItUvYnnqZyRRrC",
    "incremental_authorization_allowed": null,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": null,
    "external_3ds_authentication_attempted": false,
    "expires_on": "2025-08-01T10:51:17.871Z",
    "fingerprint": null,
    "browser_info": null,
    "payment_method_id": "pm_MFTGiuZQ7jQOCCsyzy8p",
    "payment_method_status": "active",
    "updated": "2025-08-01T10:36:20.930Z",
    "split_payments": null,
    "frm_metadata": null,
    "extended_authorization_applied": null,
    "capture_before": null,
    "merchant_order_reference_id": null,
    "order_tax_amount": null,
    "connector_mandate_id": "212727464",
    "card_discovery": "manual",
    "force_3ds_challenge": false,
    "force_3ds_challenge_trigger": false,
    "issuer_error_code": null,
    "issuer_error_message": null,
    "is_iframe_redirection_enabled": null,
    "whole_connector_response": null
}
```

MIT:
Request:
```
curl --location 'http://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_6Qh8tlhJxGnzCnYSgZLYUcZg3se94a12xetjLpMLCE3srm2F6E3szTR3vJ8IRzxA' \
--data '{
    "amount": 1212,
    "currency": "USD",
    "confirm": true,
    "capture_method": "manual",
    "customer_id": "vaily2",
    "recurring_details": {
        "type": "payment_method_id",
        "data": "pm_A6hbrqOFGArsNKgjkC9g"
    },
    "off_session": true
}'
```
Response:
```
{
    "payment_id": "pay_vi4nShu5q4lHUEMgOnr3",
    "merchant_id": "merchant_1754040245",
    "status": "requires_capture",
    "amount": 1212,
    "net_amount": 1212,
    "shipping_cost": null,
    "amount_capturable": 1212,
    "amount_received": null,
    "connector": "nmi",
    "client_secret": "pay_vi4nShu5q4lHUEMgOnr3_secret_j3hvcXvZE1amCwsXIEqZ",
    "created": "2025-08-01T11:40:10.511Z",
    "currency": "USD",
    "customer_id": "vaily2",
    "customer": {
        "id": "vaily2",
        "name": null,
        "email": "vaily2@gmail.com",
        "phone": null,
        "phone_country_code": null
    },
    "description": null,
    "refunds": null,
    "disputes": null,
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": null,
    "off_session": true,
    "capture_on": null,
    "capture_method": "manual",
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "last4": "1112",
            "card_type": null,
            "card_network": null,
            "card_issuer": null,
            "card_issuing_country": null,
            "card_isin": "453211",
            "card_extended_bin": null,
            "card_exp_month": "10",
            "card_exp_year": "25",
            "card_holder_name": null,
            "payment_checks": null,
            "authentication_data": null
        },
        "billing": null
    },
    "payment_token": null,
    "shipping": null,
    "billing": null,
    "order_details": null,
    "email": "vaily2@gmail.com",
    "name": null,
    "phone": null,
    "return_url": null,
    "authentication_type": "no_three_ds",
    "statement_descriptor_name": null,
    "statement_descriptor_suffix": null,
    "next_action": null,
    "cancellation_reason": null,
    "error_code": null,
    "error_message": null,
    "unified_code": null,
    "unified_message": null,
    "payment_experience": null,
    "payment_method_type": "credit",
    "connector_label": null,
    "business_country": null,
    "business_label": "default",
    "business_sub_label": null,
    "allowed_payment_method_types": null,
    "ephemeral_key": {
        "customer_id": "vaily2",
        "created_at": 1754048410,
        "expires": 1754052010,
        "secret": "epk_a9715d624aea41ec9f9428eaaaf8a6a6"
    },
    "manual_retry_allowed": false,
    "connector_transaction_id": "10980179170",
    "frm_message": null,
    "metadata": null,
    "connector_metadata": null,
    "feature_metadata": null,
    "reference_id": "pay_vi4nShu5q4lHUEMgOnr3_1",
    "payment_link": null,
    "profile_id": "pro_prQtzjS2YNKFt8NTnPhS",
    "surcharge_details": null,
    "attempt_count": 1,
    "merchant_decision": null,
    "merchant_connector_id": "mca_PbTIQbItUvYnnqZyRRrC",
    "incremental_authorization_allowed": null,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": null,
    "external_3ds_authentication_attempted": false,
    "expires_on": "2025-08-01T11:55:10.511Z",
    "fingerprint": null,
    "browser_info": null,
    "payment_method_id": "pm_A6hbrqOFGArsNKgjkC9g",
    "payment_method_status": "active",
    "updated": "2025-08-01T11:40:13.459Z",
    "split_payments": null,
    "frm_metadata": null,
    "extended_authorization_applied": null,
    "capture_before": null,
    "merchant_order_reference_id": null,
    "order_tax_amount": null,
    "connector_mandate_id": "1452310032",
    "card_discovery": "manual",
    "force_3ds_challenge": false,
    "force_3ds_challenge_trigger": false,
    "issuer_error_code": null,
    "issuer_error_message": null,
    "is_iframe_redirection_enabled": null,
    "whole_connector_response": null
}
```

Zero Dollar CIT:
Request:
```
curl --location 'http://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_uOYRh8w3a91uu4wD3LFPEXGKB1TqAk6Vi1weDW4da6jde8CnAIK9pjlmixcwDqmk' \
--data-raw '{
    "amount": 0,
    "currency": "USD",
    "confirm": true,
    "profile_id": "null",
    "authentication_type": "no_three_ds",
    "email": "email@gmail.com",
    "customer_id": "email",
    "payment_method": "card",
    "payment_method_type": "credit",
    "payment_method_data": {
        "card": {
            "card_number": "4532111111111112",
            "card_exp_month": "10",
            "card_exp_year": "25",
            "card_cvc": "123"
        }
    },
    "setup_future_usage": "off_session",
    "customer_acceptance": {
        "acceptance_type": "online"
    },
    "payment_type": "setup_mandate"
}'
```

Response:
```
{
    "payment_id": "pay_ZMLElkI1TmhSqv7gIOO5",
    "merchant_id": "merchant_1754412298",
    "status": "succeeded",
    "amount": 0,
    "net_amount": 0,
    "shipping_cost": null,
    "amount_capturable": 0,
    "amount_received": null,
    "connector": "nmi",
    "client_secret": "pay_ZMLElkI1TmhSqv7gIOO5_secret_u5yYt4m3R8cDkutUIEV5",
    "created": "2025-08-05T16:46:39.306Z",
    "currency": "USD",
    "customer_id": "email",
    "customer": {
        "id": "email",
        "name": null,
        "email": "email@gmail.com",
        "phone": null,
        "phone_country_code": null
    },
    "description": null,
    "refunds": null,
    "disputes": null,
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": "off_session",
    "off_session": null,
    "capture_on": null,
    "capture_method": null,
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "last4": "1112",
            "card_type": null,
            "card_network": null,
            "card_issuer": null,
            "card_issuing_country": null,
            "card_isin": "453211",
            "card_extended_bin": null,
            "card_exp_month": "10",
            "card_exp_year": "25",
            "card_holder_name": null,
            "payment_checks": null,
            "authentication_data": null
        },
        "billing": null
    },
    "payment_token": "token_QK0QtaO95lHS7dKGUTDd",
    "shipping": null,
    "billing": null,
    "order_details": null,
    "email": "email@gmail.com",
    "name": null,
    "phone": null,
    "return_url": null,
    "authentication_type": "no_three_ds",
    "statement_descriptor_name": null,
    "statement_descriptor_suffix": null,
    "next_action": null,
    "cancellation_reason": null,
    "error_code": null,
    "error_message": null,
    "unified_code": null,
    "unified_message": null,
    "payment_experience": null,
    "payment_method_type": "credit",
    "connector_label": null,
    "business_country": null,
    "business_label": "default",
    "business_sub_label": null,
    "allowed_payment_method_types": null,
    "ephemeral_key": {
        "customer_id": "email",
        "created_at": 1754412399,
        "expires": 1754415999,
        "secret": "epk_3d19001aea854585ac6ac597d4c31854"
    },
    "manual_retry_allowed": false,
    "connector_transaction_id": "10996495844",
    "frm_message": null,
    "metadata": null,
    "connector_metadata": null,
    "feature_metadata": null,
    "reference_id": "pay_ZMLElkI1TmhSqv7gIOO5_1",
    "payment_link": null,
    "profile_id": "pro_IR3qBTXCy9Mf2JnwgGx9",
    "surcharge_details": null,
    "attempt_count": 1,
    "merchant_decision": null,
    "merchant_connector_id": "mca_a2TxMLhdzJcO8xG0qDSD",
    "incremental_authorization_allowed": null,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": null,
    "external_3ds_authentication_attempted": false,
    "expires_on": "2025-08-05T17:01:39.305Z",
    "fingerprint": null,
    "browser_info": null,
    "payment_channel": null,
    "payment_method_id": "pm_w2iyEnG6GTv00S35UfKn",
    "network_transaction_id": null,
    "payment_method_status": "active",
    "updated": "2025-08-05T16:46:42.940Z",
    "split_payments": null,
    "frm_metadata": null,
    "extended_authorization_applied": null,
    "capture_before": null,
    "merchant_order_reference_id": null,
    "order_tax_amount": null,
    "connector_mandate_id": "231615292",
    "card_discovery": "manual",
    "force_3ds_challenge": false,
    "force_3ds_challenge_trigger": false,
    "issuer_error_code": null,
    "issuer_error_message": null,
    "is_iframe_redirection_enabled": null,
    "whole_connector_response": null
}
```

Cypress Tests:
<img width="561" height="861" alt="image" src="https://github.com/user-attachments/assets/16711370-5966-47aa-8f55-f553edbc5370" />

<img width="342" height="674" alt="image" src="https://github.com/user-attachments/assets/268e8139-4d0c-4ab3-be4d-beadcae15835" />


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