Bug: [FEAT] implement capture and webhooks flow in ACI



implement capture and webhooks flow in ACI
diff --git a/config/deployments/production.toml b/config/deployments/production.toml
index f9b884489ba..e044183a416 100644
--- a/config/deployments/production.toml
+++ b/config/deployments/production.toml
@@ -23,7 +23,7 @@ payout_connector_list = "nomupay,stripe,wise"
 # base urls based on your need.
 # Note: These are not optional attributes. hyperswitch request can fail due to invalid/empty values.
 [connectors]
-aci.base_url = "https://eu-test.oppwa.com/"
+aci.base_url = "https://eu-prod.oppwa.com/"
 adyen.base_url = "https://{{merchant_endpoint_prefix}}-checkout-live.adyenpayments.com/checkout/"
 adyen.payout_base_url = "https://{{merchant_endpoint_prefix}}-pal-live.adyenpayments.com/"
 adyen.dispute_base_url = "https://{{merchant_endpoint_prefix}}-ca-live.adyen.com/"
diff --git a/config/development.toml b/config/development.toml
index c5e56a1b3a3..0f7ea051adf 100644
--- a/config/development.toml
+++ b/config/development.toml
@@ -685,8 +685,8 @@ red_pagos = { country = "UY", currency = "UYU" }
 local_bank_transfer = { country = "CN", currency = "CNY" }
 
 [pm_filters.aci]
-credit = { not_available_flows = { capture_method = "manual" }, country = "AD,AE,AT,BE,BG,CH,CN,CO,CR,CY,CZ,DE,DK,DO,EE,EG,ES,ET,FI,FR,GB,GH,GI,GR,GT,HN,HK,HR,HU,ID,IE,IS,IT,JP,KH,LA,LI,LT,LU,LY,MK,MM,MX,MY,MZ,NG,NZ,OM,PA,PE,PK,PL,PT,QA,RO,SA,SN,SE,SI,SK,SV,TH,UA,US,UY,VN,ZM", currency = "AED,ALL,ARS,BGN,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,EGP,EUR,GBP,GHS,HKD,HNL,HRK,HUF,IDR,ILS,ISK,JPY,KHR,KPW,LAK,LKR,MAD,MKD,MMK,MXN,MYR,MZN,NGN,NOK,NZD,OMR,PAB,PEN,PHP,PKR,PLN,QAR,RON,RSD,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,UYU,VND,ZAR,ZMW" }
-debit = { not_available_flows = { capture_method = "manual" }, country = "AD,AE,AT,BE,BG,CH,CN,CO,CR,CY,CZ,DE,DK,DO,EE,EG,ES,ET,FI,FR,GB,GH,GI,GR,GT,HN,HK,HR,HU,ID,IE,IS,IT,JP,KH,LA,LI,LT,LU,LY,MK,MM,MX,MY,MZ,NG,NZ,OM,PA,PE,PK,PL,PT,QA,RO,SA,SN,SE,SI,SK,SV,TH,UA,US,UY,VN,ZM", currency = "AED,ALL,ARS,BGN,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,EGP,EUR,GBP,GHS,HKD,HNL,HRK,HUF,IDR,ILS,ISK,JPY,KHR,KPW,LAK,LKR,MAD,MKD,MMK,MXN,MYR,MZN,NGN,NOK,NZD,OMR,PAB,PEN,PHP,PKR,PLN,QAR,RON,RSD,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,UYU,VND,ZAR,ZMW" }
+credit = { country = "AD,AE,AT,BE,BG,CH,CN,CO,CR,CY,CZ,DE,DK,DO,EE,EG,ES,ET,FI,FR,GB,GH,GI,GR,GT,HN,HK,HR,HU,ID,IE,IS,IT,JP,KH,LA,LI,LT,LU,LY,MK,MM,MX,MY,MZ,NG,NZ,OM,PA,PE,PK,PL,PT,QA,RO,SA,SN,SE,SI,SK,SV,TH,UA,US,UY,VN,ZM", currency = "AED,ALL,ARS,BGN,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,EGP,EUR,GBP,GHS,HKD,HNL,HRK,HUF,IDR,ILS,ISK,JPY,KHR,KPW,LAK,LKR,MAD,MKD,MMK,MXN,MYR,MZN,NGN,NOK,NZD,OMR,PAB,PEN,PHP,PKR,PLN,QAR,RON,RSD,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,UYU,VND,ZAR,ZMW" }
+debit = { country = "AD,AE,AT,BE,BG,CH,CN,CO,CR,CY,CZ,DE,DK,DO,EE,EG,ES,ET,FI,FR,GB,GH,GI,GR,GT,HN,HK,HR,HU,ID,IE,IS,IT,JP,KH,LA,LI,LT,LU,LY,MK,MM,MX,MY,MZ,NG,NZ,OM,PA,PE,PK,PL,PT,QA,RO,SA,SN,SE,SI,SK,SV,TH,UA,US,UY,VN,ZM", currency = "AED,ALL,ARS,BGN,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,EGP,EUR,GBP,GHS,HKD,HNL,HRK,HUF,IDR,ILS,ISK,JPY,KHR,KPW,LAK,LKR,MAD,MKD,MMK,MXN,MYR,MZN,NGN,NOK,NZD,OMR,PAB,PEN,PHP,PKR,PLN,QAR,RON,RSD,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,UYU,VND,ZAR,ZMW" }
 mb_way = { country = "EE,ES,PT", currency = "EUR" }
 ali_pay = { country = "CN", currency = "CNY" }
 eps = { country = "AT", currency = "EUR" }
diff --git a/crates/common_utils/src/errors.rs b/crates/common_utils/src/errors.rs
index a731a5f4630..c2cddf41bce 100644
--- a/crates/common_utils/src/errors.rs
+++ b/crates/common_utils/src/errors.rs
@@ -101,6 +101,9 @@ pub enum CryptoError {
     /// The provided IV length is invalid for the cryptographic algorithm
     #[error("Invalid IV length")]
     InvalidIvLength,
+    /// The provided authentication tag length is invalid for the cryptographic algorithm
+    #[error("Invalid authentication tag length")]
+    InvalidTagLength,
 }
 
 /// Errors for Qr code handling
diff --git a/crates/hyperswitch_connectors/src/connectors/aci.rs b/crates/hyperswitch_connectors/src/connectors/aci.rs
index 9218ef3e9fb..16a85965e79 100644
--- a/crates/hyperswitch_connectors/src/connectors/aci.rs
+++ b/crates/hyperswitch_connectors/src/connectors/aci.rs
@@ -6,7 +6,8 @@ use std::sync::LazyLock;
 use api_models::webhooks::IncomingWebhookEvent;
 use common_enums::enums;
 use common_utils::{
-    errors::CustomResult,
+    crypto,
+    errors::{CryptoError, CustomResult},
     ext_traits::BytesExt,
     request::{Method, Request, RequestBuilder, RequestContent},
     types::{AmountConvertor, StringMajorUnit, StringMajorUnitForConnector},
@@ -30,8 +31,8 @@ use hyperswitch_domain_models::{
         SupportedPaymentMethods, SupportedPaymentMethodsExt,
     },
     types::{
-        PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsSyncRouterData,
-        RefundsRouterData,
+        PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData,
+        PaymentsSyncRouterData, RefundsRouterData,
     },
 };
 use hyperswitch_interfaces::{
@@ -42,11 +43,13 @@ use hyperswitch_interfaces::{
     errors,
     events::connector_api_logs::ConnectorEvent,
     types::{
-        PaymentsAuthorizeType, PaymentsSyncType, PaymentsVoidType, RefundExecuteType, Response,
+        PaymentsAuthorizeType, PaymentsCaptureType, PaymentsSyncType, PaymentsVoidType,
+        RefundExecuteType, Response,
     },
     webhooks::{IncomingWebhook, IncomingWebhookRequestDetails},
 };
 use masking::{Mask, PeekInterface};
+use ring::aead::{self, UnboundKey};
 use transformers as aci;
 
 use crate::{
@@ -181,8 +184,101 @@ impl ConnectorIntegration<SetupMandate, SetupMandateRequestData, PaymentsRespons
     }
 }
 
+// TODO: Investigate unexplained error in capture flow from connector.
 impl ConnectorIntegration<Capture, PaymentsCaptureData, PaymentsResponseData> for Aci {
-    // Not Implemented (R)
+    fn get_headers(
+        &self,
+        req: &PaymentsCaptureRouterData,
+        _connectors: &Connectors,
+    ) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
+        let mut header = vec![(
+            headers::CONTENT_TYPE.to_string(),
+            PaymentsCaptureType::get_content_type(self)
+                .to_string()
+                .into(),
+        )];
+        let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
+        header.append(&mut api_key);
+        Ok(header)
+    }
+
+    fn get_content_type(&self) -> &'static str {
+        self.common_get_content_type()
+    }
+
+    fn get_url(
+        &self,
+        req: &PaymentsCaptureRouterData,
+        connectors: &Connectors,
+    ) -> CustomResult<String, errors::ConnectorError> {
+        Ok(format!(
+            "{}{}{}",
+            self.base_url(connectors),
+            "v1/payments/",
+            req.request.connector_transaction_id,
+        ))
+    }
+
+    fn get_request_body(
+        &self,
+        req: &PaymentsCaptureRouterData,
+        _connectors: &Connectors,
+    ) -> CustomResult<RequestContent, errors::ConnectorError> {
+        let amount = convert_amount(
+            self.amount_converter,
+            req.request.minor_amount_to_capture,
+            req.request.currency,
+        )?;
+        let connector_router_data = aci::AciRouterData::from((amount, req));
+        let connector_req = aci::AciCaptureRequest::try_from(&connector_router_data)?;
+        Ok(RequestContent::FormUrlEncoded(Box::new(connector_req)))
+    }
+
+    fn build_request(
+        &self,
+        req: &PaymentsCaptureRouterData,
+        connectors: &Connectors,
+    ) -> CustomResult<Option<Request>, errors::ConnectorError> {
+        Ok(Some(
+            RequestBuilder::new()
+                .method(Method::Post)
+                .url(&PaymentsCaptureType::get_url(self, req, connectors)?)
+                .attach_default_headers()
+                .headers(PaymentsCaptureType::get_headers(self, req, connectors)?)
+                .set_body(PaymentsCaptureType::get_request_body(
+                    self, req, connectors,
+                )?)
+                .build(),
+        ))
+    }
+
+    fn handle_response(
+        &self,
+        data: &PaymentsCaptureRouterData,
+        event_builder: Option<&mut ConnectorEvent>,
+        res: Response,
+    ) -> CustomResult<PaymentsCaptureRouterData, errors::ConnectorError> {
+        let response: aci::AciCaptureResponse = res
+            .response
+            .parse_struct("AciCaptureResponse")
+            .change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
+        event_builder.map(|i| i.set_response_body(&response));
+        router_env::logger::info!(connector_response=?response);
+        RouterData::try_from(ResponseRouterData {
+            response,
+            data: data.clone(),
+            http_code: res.status_code,
+        })
+        .change_context(errors::ConnectorError::ResponseHandlingFailed)
+    }
+
+    fn get_error_response(
+        &self,
+        res: Response,
+        event_builder: Option<&mut ConnectorEvent>,
+    ) -> CustomResult<ErrorResponse, errors::ConnectorError> {
+        self.build_error_response(res, event_builder)
+    }
 }
 
 impl ConnectorIntegration<PSync, PaymentsSyncData, PaymentsResponseData> for Aci {
@@ -555,32 +651,252 @@ impl ConnectorIntegration<Execute, RefundsData, RefundsResponseData> for Aci {
 
 impl ConnectorIntegration<RSync, RefundsData, RefundsResponseData> for Aci {}
 
+/// Decrypts an AES-256-GCM encrypted payload where the IV, auth tag, and ciphertext
+/// are provided separately as hex strings. This is specifically tailored for ACI webhooks.
+///
+/// # Arguments
+/// * `hex_key`: The encryption key as a hex string (must decode to 32 bytes).
+/// * `hex_iv`: The initialization vector (nonce) as a hex string (must decode to 12 bytes).
+/// * `hex_auth_tag`: The authentication tag as a hex string (must decode to 16 bytes).
+/// * `hex_encrypted_body`: The encrypted payload as a hex string.
+fn decrypt_aci_webhook_payload(
+    hex_key: &str,
+    hex_iv: &str,
+    hex_auth_tag: &str,
+    hex_encrypted_body: &str,
+) -> CustomResult<Vec<u8>, CryptoError> {
+    let key_bytes = hex::decode(hex_key)
+        .change_context(CryptoError::DecodingFailed)
+        .attach_printable("Failed to decode hex key")?;
+    let iv_bytes = hex::decode(hex_iv)
+        .change_context(CryptoError::DecodingFailed)
+        .attach_printable("Failed to decode hex IV")?;
+    let auth_tag_bytes = hex::decode(hex_auth_tag)
+        .change_context(CryptoError::DecodingFailed)
+        .attach_printable("Failed to decode hex auth tag")?;
+    let encrypted_body_bytes = hex::decode(hex_encrypted_body)
+        .change_context(CryptoError::DecodingFailed)
+        .attach_printable("Failed to decode hex encrypted body")?;
+    if key_bytes.len() != 32 {
+        return Err(CryptoError::InvalidKeyLength)
+            .attach_printable("Key must be 32 bytes for AES-256-GCM");
+    }
+    if iv_bytes.len() != aead::NONCE_LEN {
+        return Err(CryptoError::InvalidIvLength)
+            .attach_printable(format!("IV must be {} bytes for AES-GCM", aead::NONCE_LEN));
+    }
+    if auth_tag_bytes.len() != 16 {
+        return Err(CryptoError::InvalidTagLength)
+            .attach_printable("Auth tag must be 16 bytes for AES-256-GCM");
+    }
+
+    let unbound_key = UnboundKey::new(&aead::AES_256_GCM, &key_bytes)
+        .change_context(CryptoError::DecodingFailed)
+        .attach_printable("Failed to create unbound key")?;
+
+    let less_safe_key = aead::LessSafeKey::new(unbound_key);
+
+    let nonce_arr: [u8; aead::NONCE_LEN] = iv_bytes
+        .as_slice()
+        .try_into()
+        .map_err(|_| CryptoError::InvalidIvLength)
+        .attach_printable_lazy(|| {
+            format!(
+                "IV length is {} but expected {}",
+                iv_bytes.len(),
+                aead::NONCE_LEN
+            )
+        })?;
+    let nonce = aead::Nonce::assume_unique_for_key(nonce_arr);
+
+    let mut ciphertext_and_tag = encrypted_body_bytes;
+    ciphertext_and_tag.extend_from_slice(&auth_tag_bytes);
+
+    less_safe_key
+        .open_in_place(nonce, aead::Aad::empty(), &mut ciphertext_and_tag)
+        .change_context(CryptoError::DecodingFailed)
+        .attach_printable("Failed to decrypt payload using LessSafeKey")?;
+
+    let original_ciphertext_len = ciphertext_and_tag.len() - auth_tag_bytes.len();
+    ciphertext_and_tag.truncate(original_ciphertext_len);
+
+    Ok(ciphertext_and_tag)
+}
+
+// TODO: Test this webhook flow once dashboard access is available.
 #[async_trait::async_trait]
 impl IncomingWebhook for Aci {
-    fn get_webhook_object_reference_id(
+    fn get_webhook_source_verification_algorithm(
         &self,
         _request: &IncomingWebhookRequestDetails<'_>,
+    ) -> CustomResult<Box<dyn crypto::VerifySignature + Send>, errors::ConnectorError> {
+        Ok(Box::new(crypto::NoAlgorithm))
+    }
+
+    fn get_webhook_source_verification_signature(
+        &self,
+        request: &IncomingWebhookRequestDetails<'_>,
+        _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
+    ) -> CustomResult<Vec<u8>, errors::ConnectorError> {
+        let header_value_str = request
+            .headers
+            .get("X-Authentication-Tag")
+            .ok_or(errors::ConnectorError::WebhookSignatureNotFound)
+            .attach_printable("Missing X-Authentication-Tag header")?
+            .to_str()
+            .map_err(|_| errors::ConnectorError::WebhookSignatureNotFound)
+            .attach_printable("Invalid X-Authentication-Tag header value (not UTF-8)")?;
+        Ok(header_value_str.as_bytes().to_vec())
+    }
+
+    fn get_webhook_source_verification_message(
+        &self,
+        request: &IncomingWebhookRequestDetails<'_>,
+        _merchant_id: &common_utils::id_type::MerchantId,
+        connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
+    ) -> CustomResult<Vec<u8>, errors::ConnectorError> {
+        let webhook_secret_str = String::from_utf8(connector_webhook_secrets.secret.to_vec())
+            .map_err(|_| errors::ConnectorError::WebhookVerificationSecretInvalid)
+            .attach_printable("ACI webhook secret is not a valid UTF-8 string")?;
+
+        let iv_hex_str = request
+            .headers
+            .get("X-Initialization-Vector")
+            .ok_or(errors::ConnectorError::WebhookSourceVerificationFailed)
+            .attach_printable("Missing X-Initialization-Vector header")?
+            .to_str()
+            .map_err(|_| errors::ConnectorError::WebhookSourceVerificationFailed)
+            .attach_printable("Invalid X-Initialization-Vector header value (not UTF-8)")?;
+
+        let auth_tag_hex_str = request
+            .headers
+            .get("X-Authentication-Tag")
+            .ok_or(errors::ConnectorError::WebhookSourceVerificationFailed)
+            .attach_printable("Missing X-Authentication-Tag header")?
+            .to_str()
+            .map_err(|_| errors::ConnectorError::WebhookSourceVerificationFailed)
+            .attach_printable("Invalid X-Authentication-Tag header value (not UTF-8)")?;
+
+        let encrypted_body_hex = String::from_utf8(request.body.to_vec())
+            .map_err(|_| errors::ConnectorError::WebhookBodyDecodingFailed)
+            .attach_printable(
+                "Failed to read encrypted body as UTF-8 string for verification message",
+            )?;
+
+        decrypt_aci_webhook_payload(
+            &webhook_secret_str,
+            iv_hex_str,
+            auth_tag_hex_str,
+            &encrypted_body_hex,
+        )
+        .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
+        .attach_printable("Failed to decrypt ACI webhook payload for verification")
+    }
+
+    fn get_webhook_object_reference_id(
+        &self,
+        request: &IncomingWebhookRequestDetails<'_>,
     ) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
-        Err(report!(errors::ConnectorError::WebhooksNotImplemented))
+        let aci_notification: aci::AciWebhookNotification =
+            serde_json::from_slice(request.body)
+                .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)
+                .attach_printable("Failed to deserialize ACI webhook notification for ID extraction (expected decrypted payload)")?;
+
+        let id_value_str = aci_notification
+            .payload
+            .get("id")
+            .and_then(|id| id.as_str())
+            .ok_or_else(|| {
+                report!(errors::ConnectorError::WebhookResourceObjectNotFound)
+                    .attach_printable("Missing 'id' in webhook payload for ID extraction")
+            })?;
+
+        let payment_type_str = aci_notification
+            .payload
+            .get("paymentType")
+            .and_then(|pt| pt.as_str());
+
+        if payment_type_str.is_some_and(|pt| pt.to_uppercase() == "RF") {
+            Ok(api_models::webhooks::ObjectReferenceId::RefundId(
+                api_models::webhooks::RefundIdType::ConnectorRefundId(id_value_str.to_string()),
+            ))
+        } else {
+            Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
+                api_models::payments::PaymentIdType::ConnectorTransactionId(
+                    id_value_str.to_string(),
+                ),
+            ))
+        }
     }
 
     fn get_webhook_event_type(
         &self,
-        _request: &IncomingWebhookRequestDetails<'_>,
+        request: &IncomingWebhookRequestDetails<'_>,
     ) -> CustomResult<IncomingWebhookEvent, errors::ConnectorError> {
-        Ok(IncomingWebhookEvent::EventNotSupported)
+        let aci_notification: aci::AciWebhookNotification =
+            serde_json::from_slice(request.body)
+                .change_context(errors::ConnectorError::WebhookEventTypeNotFound)
+                 .attach_printable("Failed to deserialize ACI webhook notification for event type (expected decrypted payload)")?;
+
+        match aci_notification.event_type {
+            aci::AciWebhookEventType::Payment => {
+                let payment_payload: aci::AciPaymentWebhookPayload =
+                    serde_json::from_value(aci_notification.payload)
+                        .change_context(errors::ConnectorError::WebhookEventTypeNotFound)
+                        .attach_printable("Could not deserialize ACI payment webhook payload for event type determination")?;
+
+                let code = &payment_payload.result.code;
+                if aci_result_codes::SUCCESSFUL_CODES.contains(&code.as_str()) {
+                    if payment_payload.payment_type.to_uppercase() == "RF" {
+                        Ok(IncomingWebhookEvent::RefundSuccess)
+                    } else {
+                        Ok(IncomingWebhookEvent::PaymentIntentSuccess)
+                    }
+                } else if aci_result_codes::PENDING_CODES.contains(&code.as_str()) {
+                    if payment_payload.payment_type.to_uppercase() == "RF" {
+                        Ok(IncomingWebhookEvent::EventNotSupported)
+                    } else {
+                        Ok(IncomingWebhookEvent::PaymentIntentProcessing)
+                    }
+                } else if aci_result_codes::FAILURE_CODES.contains(&code.as_str()) {
+                    if payment_payload.payment_type.to_uppercase() == "RF" {
+                        Ok(IncomingWebhookEvent::RefundFailure)
+                    } else {
+                        Ok(IncomingWebhookEvent::PaymentIntentFailure)
+                    }
+                } else {
+                    Ok(IncomingWebhookEvent::EventNotSupported)
+                }
+            }
+        }
     }
 
     fn get_webhook_resource_object(
         &self,
-        _request: &IncomingWebhookRequestDetails<'_>,
+        request: &IncomingWebhookRequestDetails<'_>,
     ) -> CustomResult<Box<dyn masking::ErasedMaskSerialize>, errors::ConnectorError> {
-        Err(report!(errors::ConnectorError::WebhooksNotImplemented))
+        let aci_notification: aci::AciWebhookNotification =
+            serde_json::from_slice(request.body)
+                .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)
+                .attach_printable("Failed to deserialize ACI webhook notification for resource object (expected decrypted payload)")?;
+
+        match aci_notification.event_type {
+            aci::AciWebhookEventType::Payment => {
+                let payment_payload: aci::AciPaymentWebhookPayload =
+                    serde_json::from_value(aci_notification.payload)
+                        .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)
+                        .attach_printable("Failed to deserialize ACI payment webhook payload")?;
+                Ok(Box::new(payment_payload))
+            }
+        }
     }
 }
 
 static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLock::new(|| {
-    let supported_capture_methods = vec![enums::CaptureMethod::Automatic];
+    let supported_capture_methods = vec![
+        enums::CaptureMethod::Automatic,
+        enums::CaptureMethod::Manual,
+    ];
 
     let supported_card_network = vec![
         common_enums::CardNetwork::AmericanExpress,
@@ -600,7 +916,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
         enums::PaymentMethodType::MbWay,
         PaymentMethodDetails {
             mandates: enums::FeatureStatus::NotSupported,
-            refunds: enums::FeatureStatus::NotSupported,
+            refunds: enums::FeatureStatus::Supported,
             supported_capture_methods: supported_capture_methods.clone(),
             specific_features: None,
         },
@@ -611,7 +927,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
         enums::PaymentMethodType::AliPay,
         PaymentMethodDetails {
             mandates: enums::FeatureStatus::NotSupported,
-            refunds: enums::FeatureStatus::NotSupported,
+            refunds: enums::FeatureStatus::Supported,
             supported_capture_methods: supported_capture_methods.clone(),
             specific_features: None,
         },
@@ -659,7 +975,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
         enums::PaymentMethodType::Eps,
         PaymentMethodDetails {
             mandates: enums::FeatureStatus::NotSupported,
-            refunds: enums::FeatureStatus::NotSupported,
+            refunds: enums::FeatureStatus::Supported,
             supported_capture_methods: supported_capture_methods.clone(),
             specific_features: None,
         },
@@ -669,7 +985,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
         enums::PaymentMethodType::Eft,
         PaymentMethodDetails {
             mandates: enums::FeatureStatus::NotSupported,
-            refunds: enums::FeatureStatus::NotSupported,
+            refunds: enums::FeatureStatus::Supported,
             supported_capture_methods: supported_capture_methods.clone(),
             specific_features: None,
         },
@@ -679,7 +995,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
         enums::PaymentMethodType::Ideal,
         PaymentMethodDetails {
             mandates: enums::FeatureStatus::NotSupported,
-            refunds: enums::FeatureStatus::NotSupported,
+            refunds: enums::FeatureStatus::Supported,
             supported_capture_methods: supported_capture_methods.clone(),
             specific_features: None,
         },
@@ -689,7 +1005,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
         enums::PaymentMethodType::Giropay,
         PaymentMethodDetails {
             mandates: enums::FeatureStatus::NotSupported,
-            refunds: enums::FeatureStatus::NotSupported,
+            refunds: enums::FeatureStatus::Supported,
             supported_capture_methods: supported_capture_methods.clone(),
             specific_features: None,
         },
@@ -699,7 +1015,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
         enums::PaymentMethodType::Sofort,
         PaymentMethodDetails {
             mandates: enums::FeatureStatus::NotSupported,
-            refunds: enums::FeatureStatus::NotSupported,
+            refunds: enums::FeatureStatus::Supported,
             supported_capture_methods: supported_capture_methods.clone(),
             specific_features: None,
         },
@@ -709,7 +1025,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
         enums::PaymentMethodType::Interac,
         PaymentMethodDetails {
             mandates: enums::FeatureStatus::NotSupported,
-            refunds: enums::FeatureStatus::NotSupported,
+            refunds: enums::FeatureStatus::Supported,
             supported_capture_methods: supported_capture_methods.clone(),
             specific_features: None,
         },
@@ -719,7 +1035,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
         enums::PaymentMethodType::Przelewy24,
         PaymentMethodDetails {
             mandates: enums::FeatureStatus::NotSupported,
-            refunds: enums::FeatureStatus::NotSupported,
+            refunds: enums::FeatureStatus::Supported,
             supported_capture_methods: supported_capture_methods.clone(),
             specific_features: None,
         },
@@ -729,7 +1045,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
         enums::PaymentMethodType::Trustly,
         PaymentMethodDetails {
             mandates: enums::FeatureStatus::NotSupported,
-            refunds: enums::FeatureStatus::NotSupported,
+            refunds: enums::FeatureStatus::Supported,
             supported_capture_methods: supported_capture_methods.clone(),
             specific_features: None,
         },
@@ -739,7 +1055,7 @@ static ACI_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLo
         enums::PaymentMethodType::Klarna,
         PaymentMethodDetails {
             mandates: enums::FeatureStatus::NotSupported,
-            refunds: enums::FeatureStatus::NotSupported,
+            refunds: enums::FeatureStatus::Supported,
             supported_capture_methods: supported_capture_methods.clone(),
             specific_features: None,
         },
diff --git a/crates/hyperswitch_connectors/src/connectors/aci/transformers.rs b/crates/hyperswitch_connectors/src/connectors/aci/transformers.rs
index e5f7ec19b50..b2074cb8dc4 100644
--- a/crates/hyperswitch_connectors/src/connectors/aci/transformers.rs
+++ b/crates/hyperswitch_connectors/src/connectors/aci/transformers.rs
@@ -6,11 +6,16 @@ use error_stack::report;
 use hyperswitch_domain_models::{
     payment_method_data::{BankRedirectData, Card, PayLaterData, PaymentMethodData, WalletData},
     router_data::{ConnectorAuthType, RouterData},
-    router_request_types::ResponseId,
+    router_request_types::{
+        PaymentsAuthorizeData, PaymentsCancelData, PaymentsSyncData, ResponseId,
+    },
     router_response_types::{
         MandateReference, PaymentsResponseData, RedirectForm, RefundsResponseData,
     },
-    types::{PaymentsAuthorizeRouterData, PaymentsCancelRouterData, RefundsRouterData},
+    types::{
+        PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData,
+        RefundsRouterData,
+    },
 };
 use hyperswitch_interfaces::errors;
 use masking::{ExposeInterface, Secret};
@@ -25,6 +30,28 @@ use crate::{
 
 type Error = error_stack::Report<errors::ConnectorError>;
 
+trait GetCaptureMethod {
+    fn get_capture_method(&self) -> Option<enums::CaptureMethod>;
+}
+
+impl GetCaptureMethod for PaymentsAuthorizeData {
+    fn get_capture_method(&self) -> Option<enums::CaptureMethod> {
+        self.capture_method
+    }
+}
+
+impl GetCaptureMethod for PaymentsSyncData {
+    fn get_capture_method(&self) -> Option<enums::CaptureMethod> {
+        self.capture_method
+    }
+}
+
+impl GetCaptureMethod for PaymentsCancelData {
+    fn get_capture_method(&self) -> Option<enums::CaptureMethod> {
+        None
+    }
+}
+
 #[derive(Debug, Serialize)]
 pub struct AciRouterData<T> {
     amount: StringMajorUnit,
@@ -629,14 +656,18 @@ pub enum AciPaymentStatus {
     RedirectShopper,
 }
 
-impl From<AciPaymentStatus> for enums::AttemptStatus {
-    fn from(item: AciPaymentStatus) -> Self {
-        match item {
-            AciPaymentStatus::Succeeded => Self::Charged,
-            AciPaymentStatus::Failed => Self::Failure,
-            AciPaymentStatus::Pending => Self::Authorizing,
-            AciPaymentStatus::RedirectShopper => Self::AuthenticationPending,
+fn map_aci_attempt_status(item: AciPaymentStatus, auto_capture: bool) -> enums::AttemptStatus {
+    match item {
+        AciPaymentStatus::Succeeded => {
+            if auto_capture {
+                enums::AttemptStatus::Charged
+            } else {
+                enums::AttemptStatus::Authorized
+            }
         }
+        AciPaymentStatus::Failed => enums::AttemptStatus::Failure,
+        AciPaymentStatus::Pending => enums::AttemptStatus::Authorizing,
+        AciPaymentStatus::RedirectShopper => enums::AttemptStatus::AuthenticationPending,
     }
 }
 impl FromStr for AciPaymentStatus {
@@ -708,12 +739,14 @@ pub struct ErrorParameters {
     pub(super) message: String,
 }
 
-impl<F, T> TryFrom<ResponseRouterData<F, AciPaymentsResponse, T, PaymentsResponseData>>
-    for RouterData<F, T, PaymentsResponseData>
+impl<F, Req> TryFrom<ResponseRouterData<F, AciPaymentsResponse, Req, PaymentsResponseData>>
+    for RouterData<F, Req, PaymentsResponseData>
+where
+    Req: GetCaptureMethod,
 {
     type Error = error_stack::Report<errors::ConnectorError>;
     fn try_from(
-        item: ResponseRouterData<F, AciPaymentsResponse, T, PaymentsResponseData>,
+        item: ResponseRouterData<F, AciPaymentsResponse, Req, PaymentsResponseData>,
     ) -> Result<Self, Self::Error> {
         let redirection_data = item.response.redirect.map(|data| {
             let form_fields = std::collections::HashMap::<_, _>::from_iter(
@@ -740,16 +773,22 @@ impl<F, T> TryFrom<ResponseRouterData<F, AciPaymentsResponse, T, PaymentsRespons
             connector_mandate_request_reference_id: None,
         });
 
+        let auto_capture = matches!(
+            item.data.request.get_capture_method(),
+            Some(enums::CaptureMethod::Automatic) | None
+        );
+
+        let status = if redirection_data.is_some() {
+            map_aci_attempt_status(AciPaymentStatus::RedirectShopper, auto_capture)
+        } else {
+            map_aci_attempt_status(
+                AciPaymentStatus::from_str(&item.response.result.code)?,
+                auto_capture,
+            )
+        };
+
         Ok(Self {
-            status: {
-                if redirection_data.is_some() {
-                    enums::AttemptStatus::from(AciPaymentStatus::RedirectShopper)
-                } else {
-                    enums::AttemptStatus::from(AciPaymentStatus::from_str(
-                        &item.response.result.code,
-                    )?)
-                }
-            },
+            status,
             response: Ok(PaymentsResponseData::TransactionResponse {
                 resource_id: ResponseId::ConnectorTransactionId(item.response.id.clone()),
                 redirection_data: Box::new(redirection_data),
@@ -765,6 +804,95 @@ impl<F, T> TryFrom<ResponseRouterData<F, AciPaymentsResponse, T, PaymentsRespons
     }
 }
 
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct AciCaptureRequest {
+    #[serde(flatten)]
+    pub txn_details: TransactionDetails,
+}
+
+impl TryFrom<&AciRouterData<&PaymentsCaptureRouterData>> for AciCaptureRequest {
+    type Error = error_stack::Report<errors::ConnectorError>;
+
+    fn try_from(item: &AciRouterData<&PaymentsCaptureRouterData>) -> Result<Self, Self::Error> {
+        let auth = AciAuthType::try_from(&item.router_data.connector_auth_type)?;
+        Ok(Self {
+            txn_details: TransactionDetails {
+                entity_id: auth.entity_id,
+                amount: item.amount.to_owned(),
+                currency: item.router_data.request.currency.to_string(),
+                payment_type: AciPaymentType::Capture,
+            },
+        })
+    }
+}
+
+#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct AciCaptureResponse {
+    id: String,
+    referenced_id: String,
+    payment_type: AciPaymentType,
+    amount: StringMajorUnit,
+    currency: String,
+    descriptor: String,
+    result: AciCaptureResult,
+    result_details: AciCaptureResultDetails,
+    build_number: String,
+    timestamp: String,
+    ndc: Secret<String>,
+    source: Secret<String>,
+    payment_method: String,
+    short_id: String,
+}
+
+#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct AciCaptureResult {
+    code: String,
+    description: String,
+}
+
+#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "PascalCase")]
+pub struct AciCaptureResultDetails {
+    extended_description: String,
+    #[serde(rename = "clearingInstituteName")]
+    clearing_institute_name: String,
+    connector_tx_id1: String,
+    connector_tx_id3: String,
+    connector_tx_id2: String,
+    acquirer_response: String,
+}
+
+impl<F, T> TryFrom<ResponseRouterData<F, AciCaptureResponse, T, PaymentsResponseData>>
+    for RouterData<F, T, PaymentsResponseData>
+{
+    type Error = error_stack::Report<errors::ConnectorError>;
+    fn try_from(
+        item: ResponseRouterData<F, AciCaptureResponse, T, PaymentsResponseData>,
+    ) -> Result<Self, Self::Error> {
+        Ok(Self {
+            status: map_aci_attempt_status(
+                AciPaymentStatus::from_str(&item.response.result.code)?,
+                false,
+            ),
+            reference_id: Some(item.response.referenced_id.clone()),
+            response: Ok(PaymentsResponseData::TransactionResponse {
+                resource_id: ResponseId::ConnectorTransactionId(item.response.id.clone()),
+                redirection_data: Box::new(None),
+                mandate_reference: Box::new(None),
+                connector_metadata: None,
+                network_txn_id: None,
+                connector_response_reference_id: Some(item.response.referenced_id),
+                incremental_authorization_allowed: None,
+                charges: None,
+            }),
+            ..item.data
+        })
+    }
+}
+
 #[derive(Default, Debug, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct AciRefundRequest {
@@ -854,3 +982,89 @@ impl<F> TryFrom<RefundsResponseRouterData<F, AciRefundResponse>> for RefundsRout
         })
     }
 }
+
+#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
+pub enum AciWebhookEventType {
+    Payment,
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
+pub enum AciWebhookAction {
+    Created,
+    Updated,
+    Deleted,
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct AciWebhookCardDetails {
+    pub bin: Option<String>,
+    #[serde(rename = "last4Digits")]
+    pub last4_digits: Option<String>,
+    pub holder: Option<String>,
+    pub expiry_month: Option<Secret<String>>,
+    pub expiry_year: Option<Secret<String>>,
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct AciWebhookCustomerDetails {
+    #[serde(rename = "givenName")]
+    pub given_name: Option<Secret<String>>,
+    pub surname: Option<Secret<String>>,
+    #[serde(rename = "merchantCustomerId")]
+    pub merchant_customer_id: Option<Secret<String>>,
+    pub sex: Option<Secret<String>>,
+    pub email: Option<Email>,
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct AciWebhookAuthenticationDetails {
+    #[serde(rename = "entityId")]
+    pub entity_id: Secret<String>,
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct AciWebhookRiskDetails {
+    pub score: Option<String>,
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct AciPaymentWebhookPayload {
+    pub id: String,
+    pub payment_type: String,
+    pub payment_brand: String,
+    pub amount: StringMajorUnit,
+    pub currency: String,
+    pub presentation_amount: Option<StringMajorUnit>,
+    pub presentation_currency: Option<String>,
+    pub descriptor: Option<String>,
+    pub result: ResultCode,
+    pub authentication: Option<AciWebhookAuthenticationDetails>,
+    pub card: Option<AciWebhookCardDetails>,
+    pub customer: Option<AciWebhookCustomerDetails>,
+    #[serde(rename = "customParameters")]
+    pub custom_parameters: Option<serde_json::Value>,
+    pub risk: Option<AciWebhookRiskDetails>,
+    pub build_number: Option<String>,
+    pub timestamp: String,
+    pub ndc: String,
+    #[serde(rename = "channelName")]
+    pub channel_name: Option<String>,
+    pub source: Option<String>,
+    pub payment_method: Option<String>,
+    #[serde(rename = "shortId")]
+    pub short_id: Option<String>,
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct AciWebhookNotification {
+    #[serde(rename = "type")]
+    pub event_type: AciWebhookEventType,
+    pub action: Option<AciWebhookAction>,
+    pub payload: serde_json::Value,
+}
diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml
index 160e6490efc..bda3eed02aa 100644
--- a/loadtest/config/development.toml
+++ b/loadtest/config/development.toml
@@ -500,8 +500,8 @@ credit = { country = "AD,AT,AU,BE,BG,CA,CH,CY,CZ,DE,DK,EE,ES,FI,FR,GB,GG,GI,GR,H
 debit = { country = "AD,AT,AU,BE,BG,CA,CH,CY,CZ,DE,DK,EE,ES,FI,FR,GB,GG,GI,GR,HK,HR,HU,IE,IM,IT,JE,LI,LT,LU,LV,MT,MC,MY,NL,NO,NZ,PL,PT,RO,SE,SG,SI,SK,SM,US", currency = "AED,AMD,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BIF,BMD,BND,BOB,BRL,BSD,BWP,BYN,BZD,CAD,CHF,CLP,CNY,COP,CRC,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,ISK,JMD,JPY,KES,KGS,KHR,KMF,KRW,KYD,KZT,LAK,LBP,LKR,LRD,LSL,MAD,MDL,MKD,MNT,MOP,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SEK,SGD,SHP,SLL,SOS,SRD,STD,SVC,SYP,SZL,THB,TJS,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"}
 
 [pm_filters.aci]
-credit = { not_available_flows = { capture_method = "manual" }, country = "AD,AE,AT,BE,BG,CH,CN,CO,CR,CY,CZ,DE,DK,DO,EE,EG,ES,ET,FI,FR,GB,GH,GI,GR,GT,HN,HK,HR,HU,ID,IE,IS,IT,JP,KH,LA,LI,LT,LU,LY,MK,MM,MX,MY,MZ,NG,NZ,OM,PA,PE,PK,PL,PT,QA,RO,SA,SN,SE,SI,SK,SV,TH,UA,US,UY,VN,ZM", currency = "AED,ALL,ARS,BGN,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,EGP,EUR,GBP,GHS,HKD,HNL,HRK,HUF,IDR,ILS,ISK,JPY,KHR,KPW,LAK,LKR,MAD,MKD,MMK,MXN,MYR,MZN,NGN,NOK,NZD,OMR,PAB,PEN,PHP,PKR,PLN,QAR,RON,RSD,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,UYU,VND,ZAR,ZMW" }
-debit = { not_available_flows = { capture_method = "manual" }, country = "AD,AE,AT,BE,BG,CH,CN,CO,CR,CY,CZ,DE,DK,DO,EE,EG,ES,ET,FI,FR,GB,GH,GI,GR,GT,HN,HK,HR,HU,ID,IE,IS,IT,JP,KH,LA,LI,LT,LU,LY,MK,MM,MX,MY,MZ,NG,NZ,OM,PA,PE,PK,PL,PT,QA,RO,SA,SN,SE,SI,SK,SV,TH,UA,US,UY,VN,ZM", currency = "AED,ALL,ARS,BGN,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,EGP,EUR,GBP,GHS,HKD,HNL,HRK,HUF,IDR,ILS,ISK,JPY,KHR,KPW,LAK,LKR,MAD,MKD,MMK,MXN,MYR,MZN,NGN,NOK,NZD,OMR,PAB,PEN,PHP,PKR,PLN,QAR,RON,RSD,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,UYU,VND,ZAR,ZMW" }
+credit = { country = "AD,AE,AT,BE,BG,CH,CN,CO,CR,CY,CZ,DE,DK,DO,EE,EG,ES,ET,FI,FR,GB,GH,GI,GR,GT,HN,HK,HR,HU,ID,IE,IS,IT,JP,KH,LA,LI,LT,LU,LY,MK,MM,MX,MY,MZ,NG,NZ,OM,PA,PE,PK,PL,PT,QA,RO,SA,SN,SE,SI,SK,SV,TH,UA,US,UY,VN,ZM", currency = "AED,ALL,ARS,BGN,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,EGP,EUR,GBP,GHS,HKD,HNL,HRK,HUF,IDR,ILS,ISK,JPY,KHR,KPW,LAK,LKR,MAD,MKD,MMK,MXN,MYR,MZN,NGN,NOK,NZD,OMR,PAB,PEN,PHP,PKR,PLN,QAR,RON,RSD,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,UYU,VND,ZAR,ZMW" }
+debit = { country = "AD,AE,AT,BE,BG,CH,CN,CO,CR,CY,CZ,DE,DK,DO,EE,EG,ES,ET,FI,FR,GB,GH,GI,GR,GT,HN,HK,HR,HU,ID,IE,IS,IT,JP,KH,LA,LI,LT,LU,LY,MK,MM,MX,MY,MZ,NG,NZ,OM,PA,PE,PK,PL,PT,QA,RO,SA,SN,SE,SI,SK,SV,TH,UA,US,UY,VN,ZM", currency = "AED,ALL,ARS,BGN,CHF,CLP,CNY,COP,CRC,CZK,DKK,DOP,EGP,EUR,GBP,GHS,HKD,HNL,HRK,HUF,IDR,ILS,ISK,JPY,KHR,KPW,LAK,LKR,MAD,MKD,MMK,MXN,MYR,MZN,NGN,NOK,NZD,OMR,PAB,PEN,PHP,PKR,PLN,QAR,RON,RSD,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,UYU,VND,ZAR,ZMW" }
 mb_way = { country = "EE,ES,PT", currency = "EUR" }
 ali_pay = { country = "CN", currency = "CNY" }
 eps = { country = "AT", currency = "EUR" }

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

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

closes this [issue](https://github.com/juspay/hyperswitch/issues/8348)

## Description
<!-- Describe your changes in detail -->

Implemented capture and webhook flow, fix some issues in ACI

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

**NOTE**: This PR adds test cases for the ACI capture flow. Currently, the capture-related tests are failing with the error:
`Cannot capture (PA value exceeded, PA reverted or invalid workflow?)`
I've already raised this issue with the ACI team and am awaiting their response. Once resolved, we can revalidate the flow accordingly. We will follow up with validation once ACI provides the necessary support and access.

Additionally, we do not have dashboard access for ACI to test webhook functionality end-to-end.

Postman Test (For capture flow):

Payments - Create (Confirm: true): 

Request:
```
curl --location 'http://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_Zc8oIJUK0i4PD7sOckK3GefgrZrPYR7B0yHsazdUVemSff6GLX4O6rhOEDMtcrUI' \
--data-raw '
{
    "amount": 1000,
    "currency": "USD",
    "confirm": true,
    "capture_method": "manual",
    "capture_on": "2022-09-10T10:11:12Z",
    "amount_to_capture": 1000,
    "customer_id": "StripeCustomer",
    "email": "abcdef123@gmail.com",
    "name": "John Doe",
    "phone": "999999999",
    "phone_country_code": "+65",
    "description": "Its my first payment request",
    "authentication_type": "no_three_ds",
    "return_url": "https://duck.com",
    "billing": {
        "address": {
            "first_name": "John",
            "last_name": "Doe",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "California",
            "zip": "94122",
            "country": "AT"
        }
    },
    "payment_method": "card",
    "payment_method_type": "credit",
    "payment_method_data": {
        "card": {
            "card_number": "4111111111111111",
            "card_exp_month": "11",
            "card_exp_year": "2030",
            "card_holder_name": "joseph Doe",
            "card_cvc": "276"
        }
    },
    "browser_info": {
        "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
        "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
        "language": "nl-NL",
        "color_depth": 24,
        "screen_height": 723,
        "screen_width": 1536,
        "time_zone": 0,
        "java_enabled": true,
        "java_script_enabled": true,
        "ip_address": "127.0.0.1"
    },
    "shipping": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "California",
            "zip": "94122",
            "country": "US",
            "first_name": "John",
            "last_name": "Doe"
        }
    },
    "statement_descriptor_name": "joseph",
    "statement_descriptor_suffix": "JS",
    "metadata": {
        "udf1": "value1",
        "new_customer": "true",
        "login_date": "2019-09-10T10:11:12Z"
    }
}'
```

Response:

```
{
    "payment_id": "pay_IiHYDvxclZJ4UZEr9irY",
    "merchant_id": "merchant_1751352542",
    "status": "requires_capture",
    "amount": 1000,
    "net_amount": 1000,
    "shipping_cost": null,
    "amount_capturable": 1000,
    "amount_received": null,
    "connector": "aci",
    "client_secret": "pay_IiHYDvxclZJ4UZEr9irY_secret_kvCV7sR0WZkxpOAFHOgt",
    "created": "2025-07-01T06:51:50.527Z",
    "currency": "USD",
    "customer_id": "StripeCustomer",
    "customer": {
        "id": "StripeCustomer",
        "name": "John Doe",
        "email": "abcdef123@gmail.com",
        "phone": "999999999",
        "phone_country_code": "+65"
    },
    "description": "Its my first payment request",
    "refunds": null,
    "disputes": null,
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": null,
    "off_session": null,
    "capture_on": null,
    "capture_method": "manual",
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "last4": "1111",
            "card_type": null,
            "card_network": null,
            "card_issuer": null,
            "card_issuing_country": null,
            "card_isin": "411111",
            "card_extended_bin": null,
            "card_exp_month": "11",
            "card_exp_year": "2030",
            "card_holder_name": "joseph Doe",
            "payment_checks": null,
            "authentication_data": null
        },
        "billing": null
    },
    "payment_token": null,
    "shipping": {
        "address": {
            "city": "San Fransico",
            "country": "US",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "California",
            "first_name": "John",
            "last_name": "Doe"
        },
        "phone": null,
        "email": null
    },
    "billing": {
        "address": {
            "city": "San Fransico",
            "country": "AT",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "California",
            "first_name": "John",
            "last_name": "Doe"
        },
        "phone": null,
        "email": null
    },
    "order_details": null,
    "email": "abcdef123@gmail.com",
    "name": "John Doe",
    "phone": "999999999",
    "return_url": "https://duck.com/",
    "authentication_type": "no_three_ds",
    "statement_descriptor_name": "joseph",
    "statement_descriptor_suffix": "JS",
    "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": "StripeCustomer",
        "created_at": 1751352710,
        "expires": 1751356310,
        "secret": "epk_27f6d5062c894d2eada4b67c4eae1abc"
    },
    "manual_retry_allowed": false,
    "connector_transaction_id": "8ac7a4a297c2d96a0197c4c19aca4810",
    "frm_message": null,
    "metadata": {
        "udf1": "value1",
        "login_date": "2019-09-10T10:11:12Z",
        "new_customer": "true"
    },
    "connector_metadata": null,
    "feature_metadata": null,
    "reference_id": "8ac7a4a297c2d96a0197c4c19aca4810",
    "payment_link": null,
    "profile_id": "pro_sPevIjlEfalGATQpyx0N",
    "surcharge_details": null,
    "attempt_count": 1,
    "merchant_decision": null,
    "merchant_connector_id": "mca_cBSc84vPxAVKJqfS3Db3",
    "incremental_authorization_allowed": null,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": null,
    "external_3ds_authentication_attempted": false,
    "expires_on": "2025-07-01T07:06:50.527Z",
    "fingerprint": null,
    "browser_info": {
        "language": "nl-NL",
        "time_zone": 0,
        "ip_address": "127.0.0.1",
        "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
        "color_depth": 24,
        "java_enabled": true,
        "screen_width": 1536,
        "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
        "screen_height": 723,
        "java_script_enabled": true
    },
    "payment_method_id": null,
    "payment_method_status": null,
    "updated": "2025-07-01T06:51:52.447Z",
    "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": null,
    "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
}
```

Payments - Capture:

Request:

```
curl --location 'http://localhost:8080/payments/pay_IiHYDvxclZJ4UZEr9irY/capture' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_Zc8oIJUK0i4PD7sOckK3GefgrZrPYR7B0yHsazdUVemSff6GLX4O6rhOEDMtcrUI' \
--data '{
  "amount_to_capture": 1000,
  "statement_descriptor_name": "Joseph",
  "statement_descriptor_prefix" :"joseph",
  "statement_descriptor_suffix": "JS"
}'
```

Response:

```
{
    "payment_id": "pay_IiHYDvxclZJ4UZEr9irY",
    "merchant_id": "merchant_1751352542",
    "status": "failed",
    "amount": 1000,
    "net_amount": 1000,
    "shipping_cost": null,
    "amount_capturable": 1000,
    "amount_received": null,
    "connector": "aci",
    "client_secret": "pay_IiHYDvxclZJ4UZEr9irY_secret_kvCV7sR0WZkxpOAFHOgt",
    "created": "2025-07-01T06:51:50.527Z",
    "currency": "USD",
    "customer_id": "StripeCustomer",
    "customer": {
        "id": "StripeCustomer",
        "name": "John Doe",
        "email": "abcdef123@gmail.com",
        "phone": "999999999",
        "phone_country_code": "+65"
    },
    "description": "Its my first payment request",
    "refunds": null,
    "disputes": null,
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": null,
    "off_session": null,
    "capture_on": null,
    "capture_method": "manual",
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "last4": "1111",
            "card_type": null,
            "card_network": null,
            "card_issuer": null,
            "card_issuing_country": null,
            "card_isin": "411111",
            "card_extended_bin": null,
            "card_exp_month": "11",
            "card_exp_year": "2030",
            "card_holder_name": "joseph Doe",
            "payment_checks": null,
            "authentication_data": null
        },
        "billing": null
    },
    "payment_token": null,
    "shipping": {
        "address": {
            "city": "San Fransico",
            "country": "US",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "California",
            "first_name": "John",
            "last_name": "Doe"
        },
        "phone": null,
        "email": null
    },
    "billing": {
        "address": {
            "city": "San Fransico",
            "country": "AT",
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "zip": "94122",
            "state": "California",
            "first_name": "John",
            "last_name": "Doe"
        },
        "phone": null,
        "email": null
    },
    "order_details": null,
    "email": "abcdef123@gmail.com",
    "name": "John Doe",
    "phone": "999999999",
    "return_url": "https://duck.com/",
    "authentication_type": "no_three_ds",
    "statement_descriptor_name": "joseph",
    "statement_descriptor_suffix": "JS",
    "next_action": null,
    "cancellation_reason": null,
    "error_code": "700.400.100",
    "error_message": "cannot capture (PA value exceeded, PA reverted or invalid workflow?)",
    "unified_code": "UE_9000",
    "unified_message": "Something went wrong",
    "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": null,
    "manual_retry_allowed": true,
    "connector_transaction_id": "8ac7a4a297c2d96a0197c4c19aca4810",
    "frm_message": null,
    "metadata": {
        "udf1": "value1",
        "login_date": "2019-09-10T10:11:12Z",
        "new_customer": "true"
    },
    "connector_metadata": null,
    "feature_metadata": null,
    "reference_id": "8ac7a4a297c2d96a0197c4c19aca4810",
    "payment_link": null,
    "profile_id": "pro_sPevIjlEfalGATQpyx0N",
    "surcharge_details": null,
    "attempt_count": 1,
    "merchant_decision": null,
    "merchant_connector_id": "mca_cBSc84vPxAVKJqfS3Db3",
    "incremental_authorization_allowed": false,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": null,
    "external_3ds_authentication_attempted": false,
    "expires_on": "2025-07-01T07:06:50.527Z",
    "fingerprint": null,
    "browser_info": {
        "language": "nl-NL",
        "time_zone": 0,
        "ip_address": "127.0.0.1",
        "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
        "color_depth": 24,
        "java_enabled": true,
        "screen_width": 1536,
        "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
        "screen_height": 723,
        "java_script_enabled": true
    },
    "payment_method_id": null,
    "payment_method_status": null,
    "updated": "2025-07-01T06:53:15.846Z",
    "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": null,
    "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
}
```

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