File: crates/hyperswitch_connectors/src/connectors/xendit.rs
 Public functions: 1
 Public structs: 1

pub mod transformers;
use std::sync::LazyLock;

use api_models::webhooks::IncomingWebhookEvent;
use base64::Engine;
use common_enums::{enums, CallConnectorAction, CaptureMethod, PaymentAction, PaymentMethodType};
use common_utils::{
    consts::BASE64_ENGINE,
    crypto,
    errors::CustomResult,
    ext_traits::{ByteSliceExt, BytesExt},
    request::{Method, Request, RequestBuilder, RequestContent},
    types::{AmountConvertor, FloatMajorUnit, FloatMajorUnitForConnector},
};
use error_stack::{report, ResultExt};
use hyperswitch_domain_models::{
    payment_method_data::PaymentMethodData,
    router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData},
    router_flow_types::{
        access_token_auth::AccessTokenAuth,
        payments::{
            Authorize, Capture, PSync, PaymentMethodToken, PreProcessing, Session, SetupMandate,
            Void,
        },
        refunds::{Execute, RSync},
    },
    router_request_types::{
        AccessTokenRequestData, PaymentMethodTokenizationData, PaymentsAuthorizeData,
        PaymentsCancelData, PaymentsCaptureData, PaymentsPreProcessingData, PaymentsSessionData,
        PaymentsSyncData, RefundsData, SetupMandateRequestData, SplitRefundsRequest,
    },
    router_response_types::{
        ConnectorInfo, PaymentMethodDetails, PaymentsResponseData, RefundsResponseData,
        SupportedPaymentMethods, SupportedPaymentMethodsExt,
    },
    types::{
        PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData,
        PaymentsPreProcessingRouterData, PaymentsSyncRouterData, RefundSyncRouterData,
        RefundsRouterData,
    },
};
use hyperswitch_interfaces::{
    api::{
        self, ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, ConnectorRedirectResponse,
        ConnectorSpecifications, ConnectorValidation,
    },
    configs::Connectors,
    errors,
    events::connector_api_logs::ConnectorEvent,
    types::{self, PaymentsAuthorizeType, PaymentsCaptureType, PaymentsSyncType, Response},
    webhooks,
};
use image::EncodableLayout;
use masking::{Mask, PeekInterface};
use transformers::{self as xendit, XenditEventType, XenditWebhookEvent};

use crate::{
    constants::headers,
    types::ResponseRouterData,
    utils as connector_utils,
    utils::{self, PaymentMethodDataType, RefundsRequestData},
};

#[derive(Clone)]
pub struct Xendit {
    amount_converter: &'static (dyn AmountConvertor<Output = FloatMajorUnit> + Sync),
}

impl Xendit {
    pub fn new() -> &'static Self {
        &Self {
            amount_converter: &FloatMajorUnitForConnector,
        }
    }
}
impl api::Payment for Xendit {}
impl api::PaymentsPreProcessing for Xendit {}
impl api::PaymentSession for Xendit {}
impl api::ConnectorAccessToken for Xendit {}
impl api::MandateSetup for Xendit {}
impl api::PaymentAuthorize for Xendit {}
impl api::PaymentSync for Xendit {}
impl api::PaymentCapture for Xendit {}
impl api::PaymentVoid for Xendit {}
impl api::Refund for Xendit {}
impl api::RefundExecute for Xendit {}
impl api::RefundSync for Xendit {}
impl api::PaymentToken for Xendit {}

impl ConnectorIntegration<PaymentMethodToken, PaymentMethodTokenizationData, PaymentsResponseData>
    for Xendit
{
}

impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Xendit
where
    Self: ConnectorIntegration<Flow, Request, Response>,
{
    fn build_headers(
        &self,
        req: &RouterData<Flow, Request, Response>,
        _connectors: &Connectors,
    ) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
        let mut header = vec![(
            headers::CONTENT_TYPE.to_string(),
            self.get_content_type().to_string().into(),
        )];
        let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
        header.append(&mut api_key);
        Ok(header)
    }
}

impl ConnectorCommon for Xendit {
    fn id(&self) -> &'static str {
        "xendit"
    }

    fn get_currency_unit(&self) -> api::CurrencyUnit {
        api::CurrencyUnit::Base
    }

    fn common_get_content_type(&self) -> &'static str {
        "application/json"
    }

    fn base_url<'a>(&self, connectors: &'a Connectors) -> &'a str {
        connectors.xendit.base_url.as_ref()
    }

    fn get_auth_header(
        &self,
        auth_type: &ConnectorAuthType,
    ) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
        let auth = xendit::XenditAuthType::try_from(auth_type)
            .change_context(errors::ConnectorError::FailedToObtainAuthType)?;
        let encoded_api_key = BASE64_ENGINE.encode(format!("{}:", auth.api_key.peek()));

        Ok(vec![(
            headers::AUTHORIZATION.to_string(),
            format!("Basic {encoded_api_key}").into_masked(),
        )])
    }
    fn build_error_response(
        &self,
        res: Response,
        event_builder: Option<&mut ConnectorEvent>,
    ) -> CustomResult<ErrorResponse, errors::ConnectorError> {
        let error_response: xendit::XenditErrorResponse = res
            .response
            .parse_struct("ErrorResponse")
            .change_context(errors::ConnectorError::ResponseDeserializationFailed)?;

        event_builder.map(|i| i.set_error_response_body(&error_response));
        router_env::logger::info!(connector_response=?error_response);
        Ok(ErrorResponse {
            code: error_response.error_code.clone(),
            message: error_response.message.clone(),
            reason: Some(error_response.message.clone()),
            status_code: res.status_code,
            attempt_status: None,
            connector_transaction_id: None,
            network_advice_code: None,
            network_decline_code: None,
            network_error_message: None,
            connector_metadata: None,
        })
    }
}
impl ConnectorIntegration<Void, PaymentsCancelData, PaymentsResponseData> for Xendit {
    fn build_request(
        &self,
        _req: &PaymentsCancelRouterData,
        _connectors: &Connectors,
    ) -> CustomResult<Option<Request>, errors::ConnectorError> {
        Err(errors::ConnectorError::NotSupported {
            message: "Cancel/Void flow".to_string(),
            connector: "Xendit",
        }
        .into())
    }
}

impl ConnectorValidation for Xendit {
    fn validate_mandate_payment(
        &self,
        pm_type: Option<PaymentMethodType>,
        pm_data: PaymentMethodData,
    ) -> CustomResult<(), errors::ConnectorError> {
        let mandate_supported_pmd = std::collections::HashSet::from([PaymentMethodDataType::Card]);
        utils::is_mandate_supported(pm_data, pm_type, mandate_supported_pmd, self.id())
    }
}

impl ConnectorIntegration<AccessTokenAuth, AccessTokenRequestData, AccessToken> for Xendit {}

impl ConnectorIntegration<Session, PaymentsSessionData, PaymentsResponseData> for Xendit {
    //TODO: implement sessions flow
}

impl ConnectorIntegration<SetupMandate, SetupMandateRequestData, PaymentsResponseData> for Xendit {}

impl ConnectorIntegration<Authorize, PaymentsAuthorizeData, PaymentsResponseData> for Xendit {
    fn get_headers(
        &self,
        req: &PaymentsAuthorizeRouterData,
        connectors: &Connectors,
    ) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
        let mut headers = self.build_headers(req, connectors)?;

        match &req.request.split_payments {
            Some(common_types::payments::SplitPaymentsRequest::XenditSplitPayment(
                common_types::payments::XenditSplitRequest::MultipleSplits(_),
            )) => {
                if let Ok(PaymentsResponseData::TransactionResponse {
                    charges:
                        Some(common_types::payments::ConnectorChargeResponseData::XenditSplitPayment(
                            common_types::payments::XenditChargeResponseData::MultipleSplits(
                                xendit_response,
                            ),
                        )),
                    ..
                }) = req.response.as_ref()
                {
                    headers.push((
                        xendit::auth_headers::WITH_SPLIT_RULE.to_string(),
                        xendit_response.split_rule_id.clone().into(),
                    ));
                    if let Some(for_user_id) = &xendit_response.for_user_id {
                        headers.push((
                            xendit::auth_headers::FOR_USER_ID.to_string(),
                            for_user_id.clone().into(),
                        ))
                    };
                };
            }
            Some(common_types::payments::SplitPaymentsRequest::XenditSplitPayment(
                common_types::payments::XenditSplitRequest::SingleSplit(single_split_data),
            )) => {
                headers.push((
                    xendit::auth_headers::FOR_USER_ID.to_string(),
                    single_split_data.for_user_id.clone().into(),
                ));
            }
            _ => (),
        };

        Ok(headers)
    }

    fn get_content_type(&self) -> &'static str {
        self.common_get_content_type()
    }

    fn get_url(
        &self,
        _req: &PaymentsAuthorizeRouterData,
        connectors: &Connectors,
    ) -> CustomResult<String, errors::ConnectorError> {
        Ok(format!("{}/payment_requests", self.base_url(connectors)))
    }

    fn get_request_body(
        &self,
        req: &PaymentsAuthorizeRouterData,
        _connectors: &Connectors,
    ) -> CustomResult<RequestContent, errors::ConnectorError> {
        let amount = utils::convert_amount(
            self.amount_converter,
            req.request.minor_amount,
            req.request.currency,
        )?;
        let connector_router_data = xendit::XenditRouterData::from((amount, req));
        let connector_req = xendit::XenditPaymentsRequest::try_from(connector_router_data)?;
        Ok(RequestContent::Json(Box::new(connector_req)))
    }

    fn build_request(
        &self,
        req: &PaymentsAuthorizeRouterData,
        connectors: &Connectors,
    ) -> CustomResult<Option<Request>, errors::ConnectorError> {
        Ok(Some(
            RequestBuilder::new()
                .method(Method::Post)
                .url(&PaymentsAuthorizeType::get_url(self, req, connectors)?)
                .attach_default_headers()
                .headers(PaymentsAuthorizeType::get_headers(self, req, connectors)?)
                .set_body(PaymentsAuthorizeType::get_request_body(
                    self, req, connectors,
                )?)
                .build(),
        ))
    }

    fn handle_response(
        &self,
        data: &PaymentsAuthorizeRouterData,
        event_builder: Option<&mut ConnectorEvent>,
        res: Response,
    ) -> CustomResult<PaymentsAuthorizeRouterData, errors::ConnectorError> {
        let response: xendit::XenditPaymentResponse = res
            .response
            .parse_struct("XenditPaymentResponse")
            .change_context(errors::ConnectorError::ResponseDeserializationFailed)?;

        let response_integrity_object = connector_utils::get_authorise_integrity_object(
            self.amount_converter,
            response.amount,
            response.currency.to_string().clone(),
        )?;

        event_builder.map(|i| i.set_response_body(&response));
        router_env::logger::info!(connector_response=?response);

        let new_router_data = RouterData::try_from(ResponseRouterData {
            response,
            data: data.clone(),
            http_code: res.status_code,
        })
        .change_context(errors::ConnectorError::ResponseHandlingFailed);

        new_router_data.map(|mut router_data| {
            router_data.request.integrity_object = Some(response_integrity_object);
            router_data
        })
    }

    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<PreProcessing, PaymentsPreProcessingData, PaymentsResponseData>
    for Xendit
{
    fn get_headers(
        &self,
        req: &PaymentsPreProcessingRouterData,
        connectors: &Connectors,
    ) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
        self.build_headers(req, connectors)
    }

    fn get_content_type(&self) -> &'static str {
        self.common_get_content_type()
    }

    fn get_url(
        &self,
        _req: &PaymentsPreProcessingRouterData,
        connectors: &Connectors,
    ) -> CustomResult<String, errors::ConnectorError> {
        Ok(format!("{}/split_rules", self.base_url(connectors)))
    }

    fn get_request_body(
        &self,
        req: &PaymentsPreProcessingRouterData,
        _connectors: &Connectors,
    ) -> CustomResult<RequestContent, errors::ConnectorError> {
        let connector_req = xendit::XenditSplitRequestData::try_from(req)?;
        Ok(RequestContent::Json(Box::new(connector_req)))
    }

    fn build_request(
        &self,
        req: &PaymentsPreProcessingRouterData,
        connectors: &Connectors,
    ) -> CustomResult<Option<Request>, errors::ConnectorError> {
        let req = Some(
            RequestBuilder::new()
                .method(Method::Post)
                .attach_default_headers()
                .headers(types::PaymentsPreProcessingType::get_headers(
                    self, req, connectors,
                )?)
                .url(&types::PaymentsPreProcessingType::get_url(
                    self, req, connectors,
                )?)
                .set_body(types::PaymentsPreProcessingType::get_request_body(
                    self, req, connectors,
                )?)
                .build(),
        );
        Ok(req)
    }

    fn handle_response(
        &self,
        data: &PaymentsPreProcessingRouterData,
        event_builder: Option<&mut ConnectorEvent>,
        res: Response,
    ) -> CustomResult<PaymentsPreProcessingRouterData, errors::ConnectorError> {
        let response: xendit::XenditSplitResponse = res
            .response
            .parse_struct("XenditSplitResponse")
            .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,
        })
    }

    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 Xendit {
    fn get_headers(
        &self,
        req: &PaymentsSyncRouterData,
        connectors: &Connectors,
    ) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
        let mut headers = self.build_headers(req, connectors)?;

        match &req.request.split_payments {
            Some(common_types::payments::SplitPaymentsRequest::XenditSplitPayment(
                common_types::payments::XenditSplitRequest::MultipleSplits(xendit_request),
            )) => {
                if let Some(for_user_id) = &xendit_request.for_user_id {
                    headers.push((
                        xendit::auth_headers::FOR_USER_ID.to_string(),
                        for_user_id.clone().into(),
                    ))
                };
            }
            Some(common_types::payments::SplitPaymentsRequest::XenditSplitPayment(
                common_types::payments::XenditSplitRequest::SingleSplit(single_split_data),
            )) => {
                headers.push((
                    xendit::auth_headers::FOR_USER_ID.to_string(),
                    single_split_data.for_user_id.clone().into(),
                ));
            }
            _ => (),
        };

        Ok(headers)
    }

    fn get_content_type(&self) -> &'static str {
        self.common_get_content_type()
    }

    fn get_url(
        &self,
        req: &PaymentsSyncRouterData,
        connectors: &Connectors,
    ) -> CustomResult<String, errors::ConnectorError> {
        let connector_payment_id = req
            .request
            .connector_transaction_id
            .get_connector_transaction_id()
            .change_context(errors::ConnectorError::MissingConnectorTransactionID)?;
        Ok(format!(
            "{}/payment_requests/{connector_payment_id}",
            self.base_url(connectors),
        ))
    }

    fn build_request(
        &self,
        req: &PaymentsSyncRouterData,
        connectors: &Connectors,
    ) -> CustomResult<Option<Request>, errors::ConnectorError> {
        Ok(Some(
            RequestBuilder::new()
                .method(Method::Get)
                .url(&PaymentsSyncType::get_url(self, req, connectors)?)
                .attach_default_headers()
                .headers(PaymentsSyncType::get_headers(self, req, connectors)?)
                .build(),
        ))
    }

    fn handle_response(
        &self,
        data: &PaymentsSyncRouterData,
        event_builder: Option<&mut ConnectorEvent>,
        res: Response,
    ) -> CustomResult<PaymentsSyncRouterData, errors::ConnectorError> {
        let response: xendit::XenditResponse = res
            .response
            .clone()
            .parse_struct("xendit XenditResponse")
            .change_context(errors::ConnectorError::ResponseDeserializationFailed)?;

        let response_integrity_object = match response.clone() {
            xendit::XenditResponse::Payment(p) => connector_utils::get_sync_integrity_object(
                self.amount_converter,
                p.amount,
                p.currency.to_string().clone(),
            ),
            xendit::XenditResponse::Webhook(p) => connector_utils::get_sync_integrity_object(
                self.amount_converter,
                p.data.amount,
                p.data.currency.to_string().clone(),
            ),
        };

        event_builder.map(|i| i.set_response_body(&response));
        router_env::logger::info!(connector_response=?response);

        let new_router_data = RouterData::try_from(ResponseRouterData {
            response,
            data: data.clone(),
            http_code: res.status_code,
        });
        new_router_data.and_then(|mut router_data| {
            let integrity_object = response_integrity_object?;
            router_data.request.integrity_object = Some(integrity_object);
            Ok(router_data)
        })
    }
}

impl ConnectorIntegration<Capture, PaymentsCaptureData, PaymentsResponseData> for Xendit {
    fn get_headers(
        &self,
        req: &PaymentsCaptureRouterData,
        connectors: &Connectors,
    ) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
        let mut headers = self.build_headers(req, connectors)?;

        match &req.request.split_payments {
            Some(common_types::payments::SplitPaymentsRequest::XenditSplitPayment(
                common_types::payments::XenditSplitRequest::MultipleSplits(xendit_request),
            )) => {
                if let Some(for_user_id) = &xendit_request.for_user_id {
                    headers.push((
                        xendit::auth_headers::FOR_USER_ID.to_string(),
                        for_user_id.clone().into(),
                    ))
                };
            }
            Some(common_types::payments::SplitPaymentsRequest::XenditSplitPayment(
                common_types::payments::XenditSplitRequest::SingleSplit(single_split_data),
            )) => {
                headers.push((
                    xendit::auth_headers::FOR_USER_ID.to_string(),
                    single_split_data.for_user_id.clone().into(),
                ));
            }
            _ => (),
        };

        Ok(headers)
    }

    fn get_content_type(&self) -> &'static str {
        self.common_get_content_type()
    }

    fn get_url(
        &self,
        req: &PaymentsCaptureRouterData,
        connectors: &Connectors,
    ) -> CustomResult<String, errors::ConnectorError> {
        let connector_payment_id = req.request.connector_transaction_id.clone();
        Ok(format!(
            "{}/payment_requests/{connector_payment_id}/captures",
            self.base_url(connectors),
        ))
    }

    fn get_request_body(
        &self,
        req: &PaymentsCaptureRouterData,
        _connectors: &Connectors,
    ) -> CustomResult<RequestContent, errors::ConnectorError> {
        let amount_to_capture = utils::convert_amount(
            self.amount_converter,
            req.request.minor_amount_to_capture,
            req.request.currency,
        )?;
        let authorized_amount = utils::convert_amount(
            self.amount_converter,
            req.request.minor_payment_amount,
            req.request.currency,
        )?;
        if amount_to_capture != authorized_amount {
            Err(report!(errors::ConnectorError::NotSupported {
                message: "Partial Capture".to_string(),
                connector: "Xendit"
            }))
        } else {
            let connector_router_data = xendit::XenditRouterData::from((amount_to_capture, req));
            let connector_req =
                xendit::XenditPaymentsCaptureRequest::try_from(connector_router_data)?;
            Ok(RequestContent::Json(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(self.get_request_body(req, connectors)?)
                .build(),
        ))
    }

    fn handle_response(
        &self,
        data: &PaymentsCaptureRouterData,
        event_builder: Option<&mut ConnectorEvent>,
        res: Response,
    ) -> CustomResult<PaymentsCaptureRouterData, errors::ConnectorError> {
        let response: xendit::XenditCaptureResponse = res
            .response
            .parse_struct("Xendit PaymentsResponse")
            .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<Execute, RefundsData, RefundsResponseData> for Xendit {
    fn get_headers(
        &self,
        req: &RefundsRouterData<Execute>,
        connectors: &Connectors,
    ) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
        let mut headers = self.build_headers(req, connectors)?;
        if let Some(SplitRefundsRequest::XenditSplitRefund(sub_merchant_data)) =
            &req.request.split_refunds
        {
            headers.push((
                xendit::auth_headers::FOR_USER_ID.to_string(),
                sub_merchant_data.for_user_id.clone().into(),
            ));
        };

        Ok(headers)
    }

    fn get_content_type(&self) -> &'static str {
        self.common_get_content_type()
    }

    fn get_url(
        &self,
        _req: &RefundsRouterData<Execute>,
        connectors: &Connectors,
    ) -> CustomResult<String, errors::ConnectorError> {
        Ok(format!("{}/refunds", self.base_url(connectors),))
    }

    fn get_request_body(
        &self,
        req: &RefundsRouterData<Execute>,
        _connectors: &Connectors,
    ) -> CustomResult<RequestContent, errors::ConnectorError> {
        let amount = utils::convert_amount(
            self.amount_converter,
            req.request.minor_refund_amount,
            req.request.currency,
        )?;
        let connector_router_data = xendit::XenditRouterData::from((amount, req));
        let connector_req = xendit::XenditRefundRequest::try_from(&connector_router_data)?;
        Ok(RequestContent::Json(Box::new(connector_req)))
    }

    fn build_request(
        &self,
        req: &RefundsRouterData<Execute>,
        connectors: &Connectors,
    ) -> CustomResult<Option<Request>, errors::ConnectorError> {
        let request = RequestBuilder::new()
            .method(Method::Post)
            .url(&types::RefundExecuteType::get_url(self, req, connectors)?)
            .attach_default_headers()
            .headers(types::RefundExecuteType::get_headers(
                self, req, connectors,
            )?)
            .set_body(types::RefundExecuteType::get_request_body(
                self, req, connectors,
            )?)
            .build();
        Ok(Some(request))
    }

    fn handle_response(
        &self,
        data: &RefundsRouterData<Execute>,
        event_builder: Option<&mut ConnectorEvent>,
        res: Response,
    ) -> CustomResult<RefundsRouterData<Execute>, errors::ConnectorError> {
        let response: xendit::RefundResponse =
            res.response
                .parse_struct("xendit RefundResponse")
                .change_context(errors::ConnectorError::RequestEncodingFailed)?;

        let response_integrity_object = connector_utils::get_refund_integrity_object(
            self.amount_converter,
            response.amount,
            response.currency.to_string().clone(),
        )?;

        event_builder.map(|i| i.set_response_body(&response));
        router_env::logger::info!(connector_response=?response);

        let new_router_data = RouterData::try_from(ResponseRouterData {
            response,
            data: data.clone(),
            http_code: res.status_code,
        });

        new_router_data
            .map(|mut router_data| {
                router_data.request.integrity_object = Some(response_integrity_object);
                router_data
            })
            .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<RSync, RefundsData, RefundsResponseData> for Xendit {
    fn get_headers(
        &self,
        req: &RefundSyncRouterData,
        connectors: &Connectors,
    ) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
        let mut headers = self.build_headers(req, connectors)?;
        if let Some(SplitRefundsRequest::XenditSplitRefund(sub_merchant_data)) =
            &req.request.split_refunds
        {
            headers.push((
                xendit::auth_headers::FOR_USER_ID.to_string(),
                sub_merchant_data.for_user_id.clone().into(),
            ));
        };

        Ok(headers)
    }

    fn get_content_type(&self) -> &'static str {
        self.common_get_content_type()
    }

    fn get_url(
        &self,
        req: &RefundSyncRouterData,
        connectors: &Connectors,
    ) -> CustomResult<String, errors::ConnectorError> {
        let connector_refund_id = req.request.get_connector_refund_id()?;
        Ok(format!(
            "{}/refunds/{}",
            self.base_url(connectors),
            connector_refund_id
        ))
    }

    fn build_request(
        &self,
        req: &RefundSyncRouterData,
        connectors: &Connectors,
    ) -> CustomResult<Option<Request>, errors::ConnectorError> {
        Ok(Some(
            RequestBuilder::new()
                .method(Method::Get)
                .url(&types::RefundSyncType::get_url(self, req, connectors)?)
                .attach_default_headers()
                .headers(types::RefundSyncType::get_headers(self, req, connectors)?)
                .build(),
        ))
    }

    fn handle_response(
        &self,
        data: &RefundSyncRouterData,
        event_builder: Option<&mut ConnectorEvent>,
        res: Response,
    ) -> CustomResult<RefundSyncRouterData, errors::ConnectorError> {
        let response: xendit::RefundResponse = res
            .response
            .clone()
            .parse_struct("xendit RefundResponse")
            .change_context(errors::ConnectorError::ResponseDeserializationFailed)?;

        let response_integrity_object = connector_utils::get_refund_integrity_object(
            self.amount_converter,
            response.amount,
            response.currency.to_string().clone(),
        )?;

        event_builder.map(|i| i.set_response_body(&response));
        router_env::logger::info!(connector_response=?response);
        let new_router_data = RouterData::try_from(ResponseRouterData {
            response,
            data: data.clone(),
            http_code: res.status_code,
        });

        new_router_data
            .map(|mut router_data| {
                router_data.request.integrity_object = Some(response_integrity_object);
                router_data
            })
            .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)
    }
}

#[async_trait::async_trait]
impl webhooks::IncomingWebhook for Xendit {
    fn get_webhook_source_verification_signature(
        &self,
        request: &webhooks::IncomingWebhookRequestDetails<'_>,
        _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
    ) -> CustomResult<Vec<u8>, errors::ConnectorError> {
        let header_value = utils::get_header_key_value("X-CALLBACK-TOKEN", request.headers)?;
        Ok(header_value.into())
    }
    async fn verify_webhook_source(
        &self,
        request: &webhooks::IncomingWebhookRequestDetails<'_>,
        merchant_id: &common_utils::id_type::MerchantId,
        connector_webhook_details: Option<common_utils::pii::SecretSerdeValue>,
        _connector_account_details: crypto::Encryptable<masking::Secret<serde_json::Value>>,
        connector_label: &str,
    ) -> CustomResult<bool, errors::ConnectorError> {
        let connector_webhook_secrets = self
            .get_webhook_source_verification_merchant_secret(
                merchant_id,
                connector_label,
                connector_webhook_details,
            )
            .await
            .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
        let signature = self
            .get_webhook_source_verification_signature(request, &connector_webhook_secrets)
            .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;

        let secret_key = connector_webhook_secrets.secret;
        Ok(secret_key.as_bytes() == (signature).as_bytes())
    }
    fn get_webhook_source_verification_message(
        &self,
        request: &webhooks::IncomingWebhookRequestDetails<'_>,
        _merchant_id: &common_utils::id_type::MerchantId,
        _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
    ) -> CustomResult<Vec<u8>, errors::ConnectorError> {
        Ok(request.body.to_vec())
    }
    fn get_webhook_object_reference_id(
        &self,
        request: &webhooks::IncomingWebhookRequestDetails<'_>,
    ) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
        let details: XenditWebhookEvent = request
            .body
            .parse_struct("XenditWebhookEvent")
            .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
        match details.event {
            XenditEventType::PaymentSucceeded
            | XenditEventType::PaymentAwaitingCapture
            | XenditEventType::PaymentFailed
            | XenditEventType::CaptureSucceeded
            | XenditEventType::CaptureFailed => {
                Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
                    api_models::payments::PaymentIdType::ConnectorTransactionId(
                        details
                            .data
                            .payment_request_id
                            .ok_or(errors::ConnectorError::WebhookReferenceIdNotFound)?,
                    ),
                ))
            }
        }
    }
    fn get_webhook_event_type(
        &self,
        request: &webhooks::IncomingWebhookRequestDetails<'_>,
    ) -> CustomResult<IncomingWebhookEvent, errors::ConnectorError> {
        let body: XenditWebhookEvent = request
            .body
            .parse_struct("XenditWebhookEvent")
            .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
        match body.event {
            XenditEventType::PaymentSucceeded => Ok(IncomingWebhookEvent::PaymentIntentSuccess),
            XenditEventType::CaptureSucceeded => {
                Ok(IncomingWebhookEvent::PaymentIntentCaptureSuccess)
            }
            XenditEventType::PaymentAwaitingCapture => {
                Ok(IncomingWebhookEvent::PaymentIntentAuthorizationSuccess)
            }
            XenditEventType::PaymentFailed | XenditEventType::CaptureFailed => {
                Ok(IncomingWebhookEvent::PaymentIntentFailure)
            }
        }
    }

    fn get_webhook_resource_object(
        &self,
        request: &webhooks::IncomingWebhookRequestDetails<'_>,
    ) -> CustomResult<Box<dyn masking::ErasedMaskSerialize>, errors::ConnectorError> {
        let body: XenditWebhookEvent = request
            .body
            .parse_struct("XenditWebhookEvent")
            .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
        Ok(Box::new(body))
    }
}

static XENDIT_SUPPORTED_PAYMENT_METHODS: LazyLock<SupportedPaymentMethods> = LazyLock::new(|| {
    let supported_capture_methods = vec![CaptureMethod::Automatic, CaptureMethod::Manual];

    let supported_card_network = vec![
        common_enums::CardNetwork::Mastercard,
        common_enums::CardNetwork::Visa,
        common_enums::CardNetwork::Interac,
        common_enums::CardNetwork::AmericanExpress,
        common_enums::CardNetwork::JCB,
        common_enums::CardNetwork::DinersClub,
        common_enums::CardNetwork::Discover,
        common_enums::CardNetwork::CartesBancaires,
        common_enums::CardNetwork::UnionPay,
    ];

    let mut xendit_supported_payment_methods = SupportedPaymentMethods::new();

    xendit_supported_payment_methods.add(
        enums::PaymentMethod::Card,
        PaymentMethodType::Credit,
        PaymentMethodDetails {
            mandates: enums::FeatureStatus::Supported,
            refunds: enums::FeatureStatus::Supported,
            supported_capture_methods: supported_capture_methods.clone(),
            specific_features: Some(
                api_models::feature_matrix::PaymentMethodSpecificFeatures::Card({
                    api_models::feature_matrix::CardSpecificFeatures {
                        three_ds: common_enums::FeatureStatus::Supported,
                        no_three_ds: common_enums::FeatureStatus::Supported,
                        supported_card_networks: supported_card_network.clone(),
                    }
                }),
            ),
        },
    );

    xendit_supported_payment_methods.add(
        enums::PaymentMethod::Card,
        PaymentMethodType::Debit,
        PaymentMethodDetails {
            mandates: enums::FeatureStatus::Supported,
            refunds: enums::FeatureStatus::Supported,
            supported_capture_methods: supported_capture_methods.clone(),
            specific_features: Some(
                api_models::feature_matrix::PaymentMethodSpecificFeatures::Card({
                    api_models::feature_matrix::CardSpecificFeatures {
                        three_ds: common_enums::FeatureStatus::Supported,
                        no_three_ds: common_enums::FeatureStatus::Supported,
                        supported_card_networks: supported_card_network.clone(),
                    }
                }),
            ),
        },
    );

    xendit_supported_payment_methods
});

static XENDIT_CONNECTOR_INFO: ConnectorInfo = ConnectorInfo {
    display_name: "Xendit",
    description: "Xendit is a financial technology company that provides payment solutions and simplifies the payment process for businesses in Indonesia, the Philippines and Southeast Asia, from SMEs and e-commerce startups to large enterprises.",
    connector_type: enums::HyperswitchConnectorCategory::PaymentGateway,
    integration_status: enums::ConnectorIntegrationStatus::Sandbox,
};

static XENDIT_SUPPORTED_WEBHOOK_FLOWS: [enums::EventClass; 1] = [enums::EventClass::Payments];

impl ConnectorSpecifications for Xendit {
    fn get_connector_about(&self) -> Option<&'static ConnectorInfo> {
        Some(&XENDIT_CONNECTOR_INFO)
    }

    fn get_supported_payment_methods(&self) -> Option<&'static SupportedPaymentMethods> {
        Some(&*XENDIT_SUPPORTED_PAYMENT_METHODS)
    }

    fn get_supported_webhook_flows(&self) -> Option<&'static [enums::EventClass]> {
        Some(&XENDIT_SUPPORTED_WEBHOOK_FLOWS)
    }
}

impl ConnectorRedirectResponse for Xendit {
    fn get_flow_type(
        &self,
        _query_params: &str,
        _json_payload: Option<serde_json::Value>,
        action: PaymentAction,
    ) -> CustomResult<CallConnectorAction, errors::ConnectorError> {
        match action {
            PaymentAction::PSync
            | PaymentAction::PaymentAuthenticateCompleteAuthorize
            | PaymentAction::CompleteAuthorize => Ok(CallConnectorAction::Trigger),
        }
    }
}


pub struct PayoutOutput {
    amount_of_money: AmountOfMoney,
}


        })
    }
}

#[cfg(feature = "v2")]
impl transformers::ForeignTryFrom<domain::PaymentMethod> for PaymentMethodResponseItem {
    type Error = error_stack::Report<errors::ValidationError>;

    fn foreign_try_from(item: domain::PaymentMethod) -> Result<Self, Self::Error> {
        // For payment methods that are active we should always have the payment method subtype
        let payment_method_subtype =
            item.payment_method_subtype
                .ok_or(errors::ValidationError::MissingRequiredField {
                    field_name: "payment_method_subtype".to_string(),
                })?;

        // For payment methods that are active we should always have the payment method type
        let payment_method_type =
            item.payment_method_type
                .ok_or(errors::ValidationError::MissingRequiredField {
                    field_name: "payment_method_type".to_string(),
                })?;

        let payment_method_data = item
            .payment_method_data
            .map(|payment_method_data| payment_method_data.into_inner())
            .map(|payment_method_data| match payment_method_data {
                api_models::payment_methods::PaymentMethodsData::Card(
                    card_details_payment_method,
                ) => {
                    let card_details = api::CardDetailFromLocker::from(card_details_payment_method);
                    api_models::payment_methods::PaymentMethodListData::Card(card_details)
                }
                api_models::payment_methods::PaymentMethodsData::BankDetails(..) => todo!(),
                api_models::payment_methods::PaymentMethodsData::WalletDetails(..) => {
                    todo!()
                }
            });

        let payment_method_billing = item
            .payment_method_billing_address
            .clone()
            .map(|billing| billing.into_inner())
            .map(From::from);

        let network_token_pmd = item
            .network_token_payment_method_data
            .clone()
            .map(|data| data.into_inner())
            .and_then(|data| match data {
                domain::PaymentMethodsData::NetworkToken(token) => {
                    Some(api::NetworkTokenDetailsPaymentMethod::from(token))
                }
                _ => None,
            });

        let network_token_resp = network_token_pmd.map(|pmd| api::NetworkTokenResponse {
            payment_method_data: pmd,
        });

        // TODO: check how we can get this field
        let recurring_enabled = Some(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,
            payment_method_type,
            payment_method_subtype,
            created: item.created_at,
            last_used_at: item.last_used_at,
            recurring_enabled,
            payment_method_data,
            bank: None,
            requires_cvv: true,
            is_default: false,
            billing: payment_method_billing,
            network_tokenization: network_token_resp,
            psp_tokenization_enabled: psp_tokenization_enabled.unwrap_or(false),
        })
    }
}

#[cfg(feature = "v2")]
pub fn generate_payment_method_session_response(
    payment_method_session: hyperswitch_domain_models::payment_methods::PaymentMethodSession,
    client_secret: Secret<String>,
    associated_payment: Option<api_models::payments::PaymentsResponse>,
    tokenization_service_response: Option<api_models::tokenization::GenericTokenizationResponse>,
) -> api_models::payment_methods::PaymentMethodSessionResponse {
    let next_action = associated_payment
        .as_ref()
        .and_then(|payment| payment.next_action.clone());

    let authentication_details =
        associated_payment.map(
            |payment| api_models::payment_methods::AuthenticationDetails {
                status: payment.status,
                error: payment.error,
            },
        );

    let token_id = tokenization_service_response
        .as_ref()
        .map(|tokenization_service_response| tokenization_service_response.id.clone());

    api_models::payment_methods::PaymentMethodSessionResponse {
        id: payment_method_session.id,
        customer_id: payment_method_session.customer_id,
        billing: payment_method_session
            .billing
            .map(|address| address.into_inner())
            .map(From::from),
        psp_tokenization: payment_method_session.psp_tokenization,
        network_tokenization: payment_method_session.network_tokenization,
        tokenization_data: payment_method_session.tokenization_data,
        expires_at: payment_method_session.expires_at,
        client_secret,
        next_action,
        return_url: payment_method_session.return_url,
        associated_payment_methods: payment_method_session.associated_payment_methods,
        authentication_details,
        associated_token_id: token_id,
    }
}

#[cfg(feature = "v2")]
impl transformers::ForeignFrom<api_models::payment_methods::ConnectorTokenDetails>
    for hyperswitch_domain_models::mandates::ConnectorTokenReferenceRecord
{
    fn foreign_from(item: api_models::payment_methods::ConnectorTokenDetails) -> Self {
        let api_models::payment_methods::ConnectorTokenDetails {
            status,
            connector_token_request_reference_id,
            original_payment_authorized_amount,
            original_payment_authorized_currency,
            metadata,
            token,
            ..
        } = item;

        Self {
            connector_token: token.expose().clone(),
            // TODO: check why do we need this field
            payment_method_subtype: None,
            original_payment_authorized_amount,
            original_payment_authorized_currency,
            metadata,
            connector_token_status: status,
            connector_token_request_reference_id,
        }
    }
}

#[cfg(feature = "v2")]
impl
    transformers::ForeignFrom<(
        id_type::MerchantConnectorAccountId,
        hyperswitch_domain_models::mandates::ConnectorTokenReferenceRecord,
    )> for api_models::payment_methods::ConnectorTokenDetails
{
    fn foreign_from(
        (connector_id, mandate_reference_record): (
            id_type::MerchantConnectorAccountId,
            hyperswitch_domain_models::mandates::ConnectorTokenReferenceRecord,
        ),
    ) -> Self {
        let hyperswitch_domain_models::mandates::ConnectorTokenReferenceRecord {
            connector_token_request_reference_id,
            original_payment_authorized_amount,
            original_payment_authorized_currency,
            metadata,
            connector_token,
            connector_token_status,
            ..
        } = mandate_reference_record;

        Self {
            connector_id,
            status: connector_token_status,
            connector_token_request_reference_id,
            original_payment_authorized_amount,
            original_payment_authorized_currency,
            metadata,
            token: Secret::new(connector_token),
            // Token that is derived from payments mandate reference will always be multi use token
            token_type: common_enums::TokenizationType::MultiUse,
        }
    }
}

#[cfg(feature = "v2")]
impl transformers::ForeignFrom<&payment_method_data::SingleUsePaymentMethodToken>
    for api_models::payment_methods::ConnectorTokenDetails
{
    fn foreign_from(token: &payment_method_data::SingleUsePaymentMethodToken) -> Self {
        Self {
            connector_id: token.clone().merchant_connector_id,
            token_type: common_enums::TokenizationType::SingleUse,
            status: api_enums::ConnectorTokenStatus::Active,
            connector_token_request_reference_id: None,
            original_payment_authorized_amount: None,
            original_payment_authorized_currency: None,
            metadata: None,
            token: token.clone().token,
        }
    }
}


File: crates/router/src/core/disputes.rs
 Public functions: 17

use std::{collections::HashMap, ops::Deref, str::FromStr};

use api_models::{
    admin::MerchantConnectorInfo, disputes as dispute_models, files as files_api_models,
};
use common_utils::ext_traits::{Encode, ValueExt};
use error_stack::ResultExt;
use router_env::{
    instrument, logger,
    tracing::{self, Instrument},
};
use strum::IntoEnumIterator;
pub mod transformers;

use super::{
    errors::{self, ConnectorErrorExt, RouterResponse, StorageErrorExt},
    metrics,
};
use crate::{
    core::{files, payments, utils as core_utils, webhooks},
    routes::{app::StorageInterface, metrics::TASKS_ADDED_COUNT, SessionState},
    services,
    types::{
        api::{self, disputes},
        domain,
        storage::enums as storage_enums,
        transformers::{ForeignFrom, ForeignInto},
        AcceptDisputeRequestData, AcceptDisputeResponse, DefendDisputeRequestData,
        DefendDisputeResponse, DisputePayload, DisputeSyncData, DisputeSyncResponse,
        FetchDisputesRequestData, FetchDisputesResponse, SubmitEvidenceRequestData,
        SubmitEvidenceResponse,
    },
    workflows::process_dispute,
};

pub(crate) fn should_call_connector_for_dispute_sync(
    force_sync: Option<bool>,
    dispute_status: storage_enums::DisputeStatus,
) -> bool {
    force_sync == Some(true)
        && matches!(
            dispute_status,
            common_enums::DisputeStatus::DisputeAccepted
                | common_enums::DisputeStatus::DisputeChallenged
                | common_enums::DisputeStatus::DisputeOpened
        )
}

#[instrument(skip(state))]
pub async fn retrieve_dispute(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    profile_id: Option<common_utils::id_type::ProfileId>,
    req: dispute_models::DisputeRetrieveRequest,
) -> RouterResponse<api_models::disputes::DisputeResponse> {
    let dispute = state
        .store
        .find_dispute_by_merchant_id_dispute_id(
            merchant_context.get_merchant_account().get_id(),
            &req.dispute_id,
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::DisputeNotFound {
            dispute_id: req.dispute_id,
        })?;
    core_utils::validate_profile_id_from_auth_layer(profile_id.clone(), &dispute)?;

    #[cfg(feature = "v1")]
    let dispute_response =
        if should_call_connector_for_dispute_sync(req.force_sync, dispute.dispute_status) {
            let db = &state.store;
            core_utils::validate_profile_id_from_auth_layer(profile_id.clone(), &dispute)?;
            let payment_intent = db
                .find_payment_intent_by_payment_id_merchant_id(
                    &(&state).into(),
                    &dispute.payment_id,
                    merchant_context.get_merchant_account().get_id(),
                    merchant_context.get_merchant_key_store(),
                    merchant_context.get_merchant_account().storage_scheme,
                )
                .await
                .change_context(errors::ApiErrorResponse::PaymentNotFound)?;

            let payment_attempt = db
                .find_payment_attempt_by_attempt_id_merchant_id(
                    &dispute.attempt_id,
                    merchant_context.get_merchant_account().get_id(),
                    merchant_context.get_merchant_account().storage_scheme,
                )
                .await
                .change_context(errors::ApiErrorResponse::PaymentNotFound)?;

            let connector_data = api::ConnectorData::get_connector_by_name(
                &state.conf.connectors,
                &dispute.connector,
                api::GetToken::Connector,
                dispute.merchant_connector_id.clone(),
            )?;

            let connector_integration: services::BoxedDisputeConnectorIntegrationInterface<
                api::Dsync,
                DisputeSyncData,
                DisputeSyncResponse,
            > = connector_data.connector.get_connector_integration();
            let router_data = core_utils::construct_dispute_sync_router_data(
                &state,
                &payment_intent,
                &payment_attempt,
                &merchant_context,
                &dispute,
            )
            .await?;
            let response = services::execute_connector_processing_step(
                &state,
                connector_integration,
                &router_data,
                payments::CallConnectorAction::Trigger,
                None,
                None,
            )
            .await
            .to_dispute_failed_response()
            .attach_printable("Failed while calling accept dispute connector api")?;

            let dispute_sync_response = response.response.map_err(|err| {
                errors::ApiErrorResponse::ExternalConnectorError {
                    code: err.code,
                    message: err.message,
                    connector: dispute.connector.clone(),
                    status_code: err.status_code,
                    reason: err.reason,
                }
            })?;

            let business_profile = state
                .store
                .find_business_profile_by_profile_id(
                    &(&state).into(),
                    merchant_context.get_merchant_key_store(),
                    &payment_attempt.profile_id,
                )
                .await
                .to_not_found_response(errors::ApiErrorResponse::ProfileNotFound {
                    id: payment_attempt.profile_id.get_string_repr().to_owned(),
                })?;

            update_dispute_data(
                &state,
                merchant_context,
                business_profile,
                Some(dispute.clone()),
                dispute_sync_response,
                payment_attempt,
                dispute.connector.as_str(),
            )
            .await
            .attach_printable("Dispute update failed")?
        } else {
            api_models::disputes::DisputeResponse::foreign_from(dispute)
        };

    #[cfg(not(feature = "v1"))]
    let dispute_response = api_models::disputes::DisputeResponse::foreign_from(dispute);

    Ok(services::ApplicationResponse::Json(dispute_response))
}

#[instrument(skip(state))]
pub async fn retrieve_disputes_list(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    profile_id_list: Option<Vec<common_utils::id_type::ProfileId>>,
    constraints: api_models::disputes::DisputeListGetConstraints,
) -> RouterResponse<Vec<api_models::disputes::DisputeResponse>> {
    let dispute_list_constraints = &(constraints.clone(), profile_id_list.clone()).try_into()?;
    let disputes = state
        .store
        .find_disputes_by_constraints(
            merchant_context.get_merchant_account().get_id(),
            dispute_list_constraints,
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Unable to retrieve disputes")?;
    let disputes_list = disputes
        .into_iter()
        .map(api_models::disputes::DisputeResponse::foreign_from)
        .collect();
    Ok(services::ApplicationResponse::Json(disputes_list))
}

#[cfg(feature = "v2")]
#[instrument(skip(state))]
pub async fn accept_dispute(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    profile_id: Option<common_utils::id_type::ProfileId>,
    req: disputes::DisputeId,
) -> RouterResponse<dispute_models::DisputeResponse> {
    todo!()
}

#[cfg(feature = "v1")]
#[instrument(skip(state))]
pub async fn get_filters_for_disputes(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    profile_id_list: Option<Vec<common_utils::id_type::ProfileId>>,
) -> RouterResponse<api_models::disputes::DisputeListFilters> {
    let merchant_connector_accounts = if let services::ApplicationResponse::Json(data) =
        super::admin::list_payment_connectors(
            state,
            merchant_context.get_merchant_account().get_id().to_owned(),
            profile_id_list,
        )
        .await?
    {
        data
    } else {
        return Err(error_stack::report!(
            errors::ApiErrorResponse::InternalServerError
        ))
        .attach_printable(
            "Failed to retrieve merchant connector accounts while fetching dispute list filters.",
        );
    };

    let connector_map = merchant_connector_accounts
        .into_iter()
        .filter_map(|merchant_connector_account| {
            merchant_connector_account
                .connector_label
                .clone()
                .map(|label| {
                    let info = merchant_connector_account.to_merchant_connector_info(&label);
                    (merchant_connector_account.connector_name, info)
                })
        })
        .fold(
            HashMap::new(),
            |mut map: HashMap<String, Vec<MerchantConnectorInfo>>, (connector_name, info)| {
                map.entry(connector_name).or_default().push(info);
                map
            },
        );

    Ok(services::ApplicationResponse::Json(
        api_models::disputes::DisputeListFilters {
            connector: connector_map,
            currency: storage_enums::Currency::iter().collect(),
            dispute_status: storage_enums::DisputeStatus::iter().collect(),
            dispute_stage: storage_enums::DisputeStage::iter().collect(),
        },
    ))
}

#[cfg(feature = "v1")]
#[instrument(skip(state))]
pub async fn accept_dispute(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    profile_id: Option<common_utils::id_type::ProfileId>,
    req: disputes::DisputeId,
) -> RouterResponse<dispute_models::DisputeResponse> {
    let db = &state.store;
    let dispute = state
        .store
        .find_dispute_by_merchant_id_dispute_id(
            merchant_context.get_merchant_account().get_id(),
            &req.dispute_id,
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::DisputeNotFound {
            dispute_id: req.dispute_id,
        })?;
    core_utils::validate_profile_id_from_auth_layer(profile_id, &dispute)?;
    let dispute_id = dispute.dispute_id.clone();
    common_utils::fp_utils::when(
        !core_utils::should_proceed_with_accept_dispute(
            dispute.dispute_stage,
            dispute.dispute_status,
        ),
        || {
            metrics::ACCEPT_DISPUTE_STATUS_VALIDATION_FAILURE_METRIC.add(1, &[]);
            Err(errors::ApiErrorResponse::DisputeStatusValidationFailed {
            reason: format!(
                "This dispute cannot be accepted because the dispute is in {} stage and has {} status",
                dispute.dispute_stage, dispute.dispute_status
            ),
        })
        },
    )?;

    let payment_intent = db
        .find_payment_intent_by_payment_id_merchant_id(
            &(&state).into(),
            &dispute.payment_id,
            merchant_context.get_merchant_account().get_id(),
            merchant_context.get_merchant_key_store(),
            merchant_context.get_merchant_account().storage_scheme,
        )
        .await
        .change_context(errors::ApiErrorResponse::PaymentNotFound)?;

    let payment_attempt = db
        .find_payment_attempt_by_attempt_id_merchant_id(
            &dispute.attempt_id,
            merchant_context.get_merchant_account().get_id(),
            merchant_context.get_merchant_account().storage_scheme,
        )
        .await
        .change_context(errors::ApiErrorResponse::PaymentNotFound)?;
    let connector_data = api::ConnectorData::get_connector_by_name(
        &state.conf.connectors,
        &dispute.connector,
        api::GetToken::Connector,
        dispute.merchant_connector_id.clone(),
    )?;
    let connector_integration: services::BoxedDisputeConnectorIntegrationInterface<
        api::Accept,
        AcceptDisputeRequestData,
        AcceptDisputeResponse,
    > = connector_data.connector.get_connector_integration();
    let router_data = core_utils::construct_accept_dispute_router_data(
        &state,
        &payment_intent,
        &payment_attempt,
        &merchant_context,
        &dispute,
    )
    .await?;
    let response = services::execute_connector_processing_step(
        &state,
        connector_integration,
        &router_data,
        payments::CallConnectorAction::Trigger,
        None,
        None,
    )
    .await
    .to_dispute_failed_response()
    .attach_printable("Failed while calling accept dispute connector api")?;
    let accept_dispute_response =
        response
            .response
            .map_err(|err| errors::ApiErrorResponse::ExternalConnectorError {
                code: err.code,
                message: err.message,
                connector: dispute.connector.clone(),
                status_code: err.status_code,
                reason: err.reason,
            })?;
    let update_dispute = diesel_models::dispute::DisputeUpdate::StatusUpdate {
        dispute_status: accept_dispute_response.dispute_status,
        connector_status: accept_dispute_response.connector_status.clone(),
    };
    let updated_dispute = db
        .update_dispute(dispute.clone(), update_dispute)
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable_lazy(|| {
            format!("Unable to update dispute with dispute_id: {dispute_id}")
        })?;
    let dispute_response = api_models::disputes::DisputeResponse::foreign_from(updated_dispute);
    Ok(services::ApplicationResponse::Json(dispute_response))
}

#[cfg(feature = "v2")]
#[instrument(skip(state))]
pub async fn submit_evidence(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    profile_id: Option<common_utils::id_type::ProfileId>,
    req: dispute_models::SubmitEvidenceRequest,
) -> RouterResponse<dispute_models::DisputeResponse> {
    todo!()
}

#[cfg(feature = "v1")]
#[instrument(skip(state))]
pub async fn submit_evidence(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    profile_id: Option<common_utils::id_type::ProfileId>,
    req: dispute_models::SubmitEvidenceRequest,
) -> RouterResponse<dispute_models::DisputeResponse> {
    let db = &state.store;
    let dispute = state
        .store
        .find_dispute_by_merchant_id_dispute_id(
            merchant_context.get_merchant_account().get_id(),
            &req.dispute_id,
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::DisputeNotFound {
            dispute_id: req.dispute_id.clone(),
        })?;
    core_utils::validate_profile_id_from_auth_layer(profile_id, &dispute)?;
    let dispute_id = dispute.dispute_id.clone();
    common_utils::fp_utils::when(
        !core_utils::should_proceed_with_submit_evidence(
            dispute.dispute_stage,
            dispute.dispute_status,
        ),
        || {
            metrics::EVIDENCE_SUBMISSION_DISPUTE_STATUS_VALIDATION_FAILURE_METRIC.add(1, &[]);
            Err(errors::ApiErrorResponse::DisputeStatusValidationFailed {
                reason: format!(
                "Evidence cannot be submitted because the dispute is in {} stage and has {} status",
                dispute.dispute_stage, dispute.dispute_status
            ),
            })
        },
    )?;
    let submit_evidence_request_data =
        transformers::get_evidence_request_data(&state, &merchant_context, req, &dispute).await?;

    let payment_intent = db
        .find_payment_intent_by_payment_id_merchant_id(
            &(&state).into(),
            &dispute.payment_id,
            merchant_context.get_merchant_account().get_id(),
            merchant_context.get_merchant_key_store(),
            merchant_context.get_merchant_account().storage_scheme,
        )
        .await
        .change_context(errors::ApiErrorResponse::PaymentNotFound)?;

    let payment_attempt = db
        .find_payment_attempt_by_attempt_id_merchant_id(
            &dispute.attempt_id,
            merchant_context.get_merchant_account().get_id(),
            merchant_context.get_merchant_account().storage_scheme,
        )
        .await
        .change_context(errors::ApiErrorResponse::PaymentNotFound)?;
    let connector_data = api::ConnectorData::get_connector_by_name(
        &state.conf.connectors,
        &dispute.connector,
        api::GetToken::Connector,
        dispute.merchant_connector_id.clone(),
    )?;

    let connector_integration: services::BoxedDisputeConnectorIntegrationInterface<
        api::Evidence,
        SubmitEvidenceRequestData,
        SubmitEvidenceResponse,
    > = connector_data.connector.get_connector_integration();
    let router_data = core_utils::construct_submit_evidence_router_data(
        &state,
        &payment_intent,
        &payment_attempt,
        &merchant_context,
        &dispute,
        submit_evidence_request_data,
    )
    .await?;
    let response = services::execute_connector_processing_step(
        &state,
        connector_integration,
        &router_data,
        payments::CallConnectorAction::Trigger,
        None,
        None,
    )
    .await
    .to_dispute_failed_response()
    .attach_printable("Failed while calling submit evidence connector api")?;
    let submit_evidence_response =
        response
            .response
            .map_err(|err| errors::ApiErrorResponse::ExternalConnectorError {
                code: err.code,
                message: err.message,
                connector: dispute.connector.clone(),
                status_code: err.status_code,
                reason: err.reason,
            })?;
    //Defend Dispute Optionally if connector expects to defend / submit evidence in a separate api call
    let (dispute_status, connector_status) = if connector_data
        .connector_name
        .requires_defend_dispute()
    {
        let connector_integration_defend_dispute: services::BoxedDisputeConnectorIntegrationInterface<
                api::Defend,
                DefendDisputeRequestData,
                DefendDisputeResponse,
            > = connector_data.connector.get_connector_integration();
        let defend_dispute_router_data = core_utils::construct_defend_dispute_router_data(
            &state,
            &payment_intent,
            &payment_attempt,
            &merchant_context,
            &dispute,
        )
        .await?;
        let defend_response = services::execute_connector_processing_step(
            &state,
            connector_integration_defend_dispute,
            &defend_dispute_router_data,
            payments::CallConnectorAction::Trigger,
            None,
            None,
        )
        .await
        .to_dispute_failed_response()
        .attach_printable("Failed while calling defend dispute connector api")?;
        let defend_dispute_response = defend_response.response.map_err(|err| {
            errors::ApiErrorResponse::ExternalConnectorError {
                code: err.code,
                message: err.message,
                connector: dispute.connector.clone(),
                status_code: err.status_code,
                reason: err.reason,
            }
        })?;
        (
            defend_dispute_response.dispute_status,
            defend_dispute_response.connector_status,
        )
    } else {
        (
            submit_evidence_response.dispute_status,
            submit_evidence_response.connector_status,
        )
    };
    let update_dispute = diesel_models::dispute::DisputeUpdate::StatusUpdate {
        dispute_status,
        connector_status,
    };
    let updated_dispute = db
        .update_dispute(dispute.clone(), update_dispute)
        .await
        .to_not_found_response(errors::ApiErrorResponse::DisputeNotFound {
            dispute_id: dispute_id.to_owned(),
        })
        .attach_printable_lazy(|| {
            format!("Unable to update dispute with dispute_id: {dispute_id}")
        })?;
    let dispute_response = api_models::disputes::DisputeResponse::foreign_from(updated_dispute);
    Ok(services::ApplicationResponse::Json(dispute_response))
}

pub async fn attach_evidence(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    profile_id: Option<common_utils::id_type::ProfileId>,
    attach_evidence_request: api::AttachEvidenceRequest,
) -> RouterResponse<files_api_models::CreateFileResponse> {
    let db = &state.store;
    let dispute_id = attach_evidence_request
        .create_file_request
        .dispute_id
        .clone()
        .ok_or(errors::ApiErrorResponse::MissingDisputeId)?;
    let dispute = db
        .find_dispute_by_merchant_id_dispute_id(
            merchant_context.get_merchant_account().get_id(),
            &dispute_id,
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::DisputeNotFound {
            dispute_id: dispute_id.clone(),
        })?;
    core_utils::validate_profile_id_from_auth_layer(profile_id, &dispute)?;
    common_utils::fp_utils::when(
        !(dispute.dispute_stage == storage_enums::DisputeStage::Dispute
            && dispute.dispute_status == storage_enums::DisputeStatus::DisputeOpened),
        || {
            metrics::ATTACH_EVIDENCE_DISPUTE_STATUS_VALIDATION_FAILURE_METRIC.add(1, &[]);
            Err(errors::ApiErrorResponse::DisputeStatusValidationFailed {
                reason: format!(
                "Evidence cannot be attached because the dispute is in {} stage and has {} status",
                dispute.dispute_stage, dispute.dispute_status
            ),
            })
        },
    )?;
    let create_file_response = Box::pin(files::files_create_core(
        state.clone(),
        merchant_context,
        attach_evidence_request.create_file_request,
    ))
    .await?;
    let file_id = match &create_file_response {
        services::ApplicationResponse::Json(res) => res.file_id.clone(),
        _ => Err(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("Unexpected response received from files create core")?,
    };
    let dispute_evidence: api::DisputeEvidence = dispute
        .evidence
        .clone()
        .parse_value("DisputeEvidence")
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Error while parsing dispute evidence record")?;
    let updated_dispute_evidence = transformers::update_dispute_evidence(
        dispute_evidence,
        attach_evidence_request.evidence_type,
        file_id,
    );
    let update_dispute = diesel_models::dispute::DisputeUpdate::EvidenceUpdate {
        evidence: updated_dispute_evidence
            .encode_to_value()
            .change_context(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("Error while encoding dispute evidence")?
            .into(),
    };
    db.update_dispute(dispute, update_dispute)
        .await
        .to_not_found_response(errors::ApiErrorResponse::DisputeNotFound {
            dispute_id: dispute_id.to_owned(),
        })
        .attach_printable_lazy(|| {
            format!("Unable to update dispute with dispute_id: {dispute_id}")
        })?;
    Ok(create_file_response)
}

#[instrument(skip(state))]
pub async fn retrieve_dispute_evidence(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    profile_id: Option<common_utils::id_type::ProfileId>,
    req: disputes::DisputeId,
) -> RouterResponse<Vec<api_models::disputes::DisputeEvidenceBlock>> {
    let dispute = state
        .store
        .find_dispute_by_merchant_id_dispute_id(
            merchant_context.get_merchant_account().get_id(),
            &req.dispute_id,
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::DisputeNotFound {
            dispute_id: req.dispute_id,
        })?;
    core_utils::validate_profile_id_from_auth_layer(profile_id, &dispute)?;
    let dispute_evidence: api::DisputeEvidence = dispute
        .evidence
        .clone()
        .parse_value("DisputeEvidence")
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Error while parsing dispute evidence record")?;
    let dispute_evidence_vec =
        transformers::get_dispute_evidence_vec(&state, merchant_context, dispute_evidence).await?;
    Ok(services::ApplicationResponse::Json(dispute_evidence_vec))
}

pub async fn delete_evidence(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    delete_evidence_request: dispute_models::DeleteEvidenceRequest,
) -> RouterResponse<serde_json::Value> {
    let dispute_id = delete_evidence_request.dispute_id.clone();
    let dispute = state
        .store
        .find_dispute_by_merchant_id_dispute_id(
            merchant_context.get_merchant_account().get_id(),
            &dispute_id,
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::DisputeNotFound {
            dispute_id: dispute_id.clone(),
        })?;
    let dispute_evidence: api::DisputeEvidence = dispute
        .evidence
        .clone()
        .parse_value("DisputeEvidence")
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Error while parsing dispute evidence record")?;
    let updated_dispute_evidence =
        transformers::delete_evidence_file(dispute_evidence, delete_evidence_request.evidence_type);
    let update_dispute = diesel_models::dispute::DisputeUpdate::EvidenceUpdate {
        evidence: updated_dispute_evidence
            .encode_to_value()
            .change_context(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("Error while encoding dispute evidence")?
            .into(),
    };
    state
        .store
        .update_dispute(dispute, update_dispute)
        .await
        .to_not_found_response(errors::ApiErrorResponse::DisputeNotFound {
            dispute_id: dispute_id.to_owned(),
        })
        .attach_printable_lazy(|| {
            format!("Unable to update dispute with dispute_id: {dispute_id}")
        })?;
    Ok(services::ApplicationResponse::StatusOk)
}

#[instrument(skip(state))]
pub async fn get_aggregates_for_disputes(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    profile_id_list: Option<Vec<common_utils::id_type::ProfileId>>,
    time_range: common_utils::types::TimeRange,
) -> RouterResponse<dispute_models::DisputesAggregateResponse> {
    let db = state.store.as_ref();
    let dispute_status_with_count = db
        .get_dispute_status_with_count(
            merchant_context.get_merchant_account().get_id(),
            profile_id_list,
            &time_range,
        )
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Unable to retrieve disputes aggregate")?;

    let mut status_map: HashMap<storage_enums::DisputeStatus, i64> =
        dispute_status_with_count.into_iter().collect();

    for status in storage_enums::DisputeStatus::iter() {
        status_map.entry(status).or_default();
    }

    Ok(services::ApplicationResponse::Json(
        dispute_models::DisputesAggregateResponse {
            status_with_count: status_map,
        },
    ))
}

#[cfg(feature = "v1")]
#[instrument(skip(state))]
pub async fn connector_sync_disputes(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    merchant_connector_id: String,
    payload: disputes::DisputeFetchQueryData,
) -> RouterResponse<FetchDisputesResponse> {
    let connector_id =
        common_utils::id_type::MerchantConnectorAccountId::wrap(merchant_connector_id)
            .change_context(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("Failed to parse merchant connector account id format")?;
    let format = time::format_description::parse("[year]-[month]-[day]T[hour]:[minute]:[second]")
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Failed to parse the date-time format")?;
    let created_from = time::PrimitiveDateTime::parse(&payload.fetch_from, &format)
        .change_context(errors::ApiErrorResponse::InvalidDataFormat {
            field_name: "fetch_from".to_string(),
            expected_format: "YYYY-MM-DDTHH:MM:SS".to_string(),
        })?;
    let created_till = time::PrimitiveDateTime::parse(&payload.fetch_till, &format)
        .change_context(errors::ApiErrorResponse::InvalidDataFormat {
            field_name: "fetch_till".to_string(),
            expected_format: "YYYY-MM-DDTHH:MM:SS".to_string(),
        })?;
    let fetch_dispute_request = FetchDisputesRequestData {
        created_from,
        created_till,
    };
    Box::pin(fetch_disputes_from_connector(
        state,
        merchant_context,
        connector_id,
        fetch_dispute_request,
    ))
    .await
}

#[cfg(feature = "v1")]
#[instrument(skip(state))]
pub async fn fetch_disputes_from_connector(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    merchant_connector_id: common_utils::id_type::MerchantConnectorAccountId,
    req: FetchDisputesRequestData,
) -> RouterResponse<FetchDisputesResponse> {
    let db = &*state.store;
    let key_manager_state = &(&state).into();
    let merchant_id = merchant_context.get_merchant_account().get_id();
    let merchant_connector_account = db
        .find_by_merchant_connector_account_merchant_id_merchant_connector_id(
            key_manager_state,
            merchant_id,
            &merchant_connector_id,
            merchant_context.get_merchant_key_store(),
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
            id: merchant_connector_id.get_string_repr().to_string(),
        })?;
    let connector_name = merchant_connector_account.connector_name.clone();
    let connector_data = api::ConnectorData::get_connector_by_name(
        &state.conf.connectors,
        &connector_name,
        api::GetToken::Connector,
        Some(merchant_connector_id.clone()),
    )?;
    let connector_integration: services::BoxedDisputeConnectorIntegrationInterface<
        api::Fetch,
        FetchDisputesRequestData,
        FetchDisputesResponse,
    > = connector_data.connector.get_connector_integration();

    let router_data =
        core_utils::construct_dispute_list_router_data(&state, merchant_connector_account, req)
            .await?;

    let response = services::execute_connector_processing_step(
        &state,
        connector_integration,
        &router_data,
        payments::CallConnectorAction::Trigger,
        None,
        None,
    )
    .await
    .to_dispute_failed_response()
    .attach_printable("Failed while calling accept dispute connector api")?;
    let fetch_dispute_response =
        response
            .response
            .map_err(|err| errors::ApiErrorResponse::ExternalConnectorError {
                code: err.code,
                message: err.message,
                connector: connector_name.clone(),
                status_code: err.status_code,
                reason: err.reason,
            })?;

    for dispute in &fetch_dispute_response {
        // check if payment already exist
        let payment_attempt = webhooks::incoming::get_payment_attempt_from_object_reference_id(
            &state,
            dispute.object_reference_id.clone(),
            &merchant_context,
        )
        .await;

        if payment_attempt.is_ok() {
            let schedule_time = process_dispute::get_sync_process_schedule_time(
                &*state.store,
                &connector_name,
                merchant_id,
                0,
            )
            .await
            .change_context(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("Failed while getting process schedule time")?;

            let response = add_process_dispute_task_to_pt(
                db,
                &connector_name,
                dispute,
                merchant_id.clone(),
                schedule_time,
            )
            .await;

            match response {
                Err(report)
                    if report
                        .downcast_ref::<errors::StorageError>()
                        .is_some_and(|error| {
                            matches!(error, errors::StorageError::DuplicateValue { .. })
                        }) =>
                {
                    Ok(())
                }
                Ok(_) => Ok(()),
                Err(_) => Err(errors::ApiErrorResponse::InternalServerError)
                    .attach_printable("Failed while adding task to process tracker"),
            }?;
        } else {
            router_env::logger::info!("Disputed payment does not exist in our records");
        }
    }

    Ok(services::ApplicationResponse::Json(fetch_dispute_response))
}

#[cfg(feature = "v1")]
#[instrument(skip_all)]
pub async fn update_dispute_data(
    state: &SessionState,
    merchant_context: domain::MerchantContext,
    business_profile: domain::Profile,
    option_dispute: Option<diesel_models::dispute::Dispute>,
    dispute_details: DisputeSyncResponse,
    payment_attempt: domain::PaymentAttempt,
    connector_name: &str,
) -> errors::CustomResult<api_models::disputes::DisputeResponse, errors::ApiErrorResponse> {
    let dispute_data = DisputePayload::from(dispute_details.clone());
    let dispute_object = webhooks::incoming::get_or_update_dispute_object(
        state.clone(),
        option_dispute,
        dispute_data,
        merchant_context.get_merchant_account().get_id(),
        &merchant_context.get_merchant_account().organization_id,
        &payment_attempt,
        dispute_details.dispute_status,
        &business_profile,
        connector_name,
    )
    .await?;
    let disputes_response: dispute_models::DisputeResponse = dispute_object.clone().foreign_into();
    let event_type: storage_enums::EventType = dispute_details.dispute_status.into();

    Box::pin(webhooks::create_event_and_trigger_outgoing_webhook(
        state.clone(),
        merchant_context,
        business_profile,
        event_type,
        storage_enums::EventClass::Disputes,
        dispute_object.dispute_id.clone(),
        storage_enums::EventObjectType::DisputeDetails,
        api::OutgoingWebhookContent::DisputeDetails(Box::new(disputes_response.clone())),
        Some(dispute_object.created_at),
    ))
    .await?;
    Ok(disputes_response)
}

#[cfg(feature = "v1")]
pub async fn add_process_dispute_task_to_pt(
    db: &dyn StorageInterface,
    connector_name: &str,
    dispute_payload: &DisputeSyncResponse,
    merchant_id: common_utils::id_type::MerchantId,
    schedule_time: Option<time::PrimitiveDateTime>,
) -> common_utils::errors::CustomResult<(), errors::StorageError> {
    match schedule_time {
        Some(time) => {
            TASKS_ADDED_COUNT.add(
                1,
                router_env::metric_attributes!(("flow", "dispute_process")),
            );
            let tracking_data = disputes::ProcessDisputePTData {
                connector_name: connector_name.to_string(),
                dispute_payload: dispute_payload.clone(),
                merchant_id: merchant_id.clone(),
            };
            let runner = common_enums::ProcessTrackerRunner::ProcessDisputeWorkflow;
            let task = "DISPUTE_PROCESS";
            let tag = ["PROCESS", "DISPUTE"];
            let process_tracker_id = scheduler::utils::get_process_tracker_id(
                runner,
                task,
                &dispute_payload.connector_dispute_id.clone(),
                &merchant_id,
            );
            let process_tracker_entry = diesel_models::ProcessTrackerNew::new(
                process_tracker_id,
                task,
                runner,
                tag,
                tracking_data,
                None,
                time,
                common_types::consts::API_VERSION,
            )
            .map_err(errors::StorageError::from)?;
            db.insert_process(process_tracker_entry).await?;
            Ok(())
        }
        None => Ok(()),
    }
}

#[cfg(feature = "v1")]
pub async fn add_dispute_list_task_to_pt(
    db: &dyn StorageInterface,
    connector_name: &str,
    merchant_id: common_utils::id_type::MerchantId,
    merchant_connector_id: common_utils::id_type::MerchantConnectorAccountId,
    profile_id: common_utils::id_type::ProfileId,
    fetch_request: FetchDisputesRequestData,
) -> common_utils::errors::CustomResult<(), errors::StorageError> {
    TASKS_ADDED_COUNT.add(1, router_env::metric_attributes!(("flow", "dispute_list")));
    let tracking_data = disputes::DisputeListPTData {
        connector_name: connector_name.to_string(),
        merchant_id: merchant_id.clone(),
        merchant_connector_id: merchant_connector_id.clone(),
        created_from: fetch_request.created_from,
        created_till: fetch_request.created_till,
        profile_id,
    };
    let runner = common_enums::ProcessTrackerRunner::DisputeListWorkflow;
    let task = "DISPUTE_LIST";
    let tag = ["LIST", "DISPUTE"];
    let process_tracker_id = scheduler::utils::get_process_tracker_id_for_dispute_list(
        runner,
        &merchant_connector_id,
        fetch_request.created_from,
        &merchant_id,
    );
    let process_tracker_entry = diesel_models::ProcessTrackerNew::new(
        process_tracker_id,
        task,
        runner,
        tag,
        tracking_data,
        None,
        fetch_request.created_from,
        common_types::consts::API_VERSION,
    )
    .map_err(errors::StorageError::from)?;
    db.insert_process(process_tracker_entry).await?;
    Ok(())
}

#[cfg(feature = "v1")]
pub async fn schedule_dispute_sync_task(
    state: &SessionState,
    business_profile: &domain::Profile,
    mca: &domain::MerchantConnectorAccount,
) -> common_utils::errors::CustomResult<(), errors::ApiErrorResponse> {
    let connector = api::enums::Connector::from_str(&mca.connector_name).change_context(
        errors::ApiErrorResponse::InvalidDataValue {
            field_name: "connector",
        },
    )?;

    if core_utils::should_add_dispute_sync_task_to_pt(state, connector) {
        let offset_date_time = time::OffsetDateTime::now_utc();
        let created_from =
            time::PrimitiveDateTime::new(offset_date_time.date(), offset_date_time.time());
        let dispute_polling_interval = *business_profile
            .dispute_polling_interval
            .unwrap_or_default()
            .deref();

        let created_till = created_from
            .checked_add(time::Duration::hours(i64::from(dispute_polling_interval)))
            .ok_or(errors::ApiErrorResponse::InternalServerError)?;

        let m_db = state.clone().store;
        let connector_name = mca.connector_name.clone();
        let merchant_id = mca.merchant_id.clone();
        let merchant_connector_id = mca.merchant_connector_id.clone();
        let business_profile_id = business_profile.get_id().clone();

        tokio::spawn(
            async move {
                add_dispute_list_task_to_pt(
                    &*m_db,
                    &connector_name,
                    merchant_id.clone(),
                    merchant_connector_id.clone(),
                    business_profile_id,
                    FetchDisputesRequestData {
                        created_from,
                        created_till,
                    },
                )
                .await
                .map_err(|error| {
                    logger::error!("Failed to add dispute list task to process tracker: {error}")
                })
            }
            .in_current_span(),
        );
    }
    Ok(())
}


File: crates/analytics/src/payment_intents/metrics/sessionized_metrics/total_smart_retries.rs

use std::collections::HashSet;

use api_models::analytics::{
    payment_intents::{
        PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetricsBucketIdentifier,
    },
    Granularity, TimeRange,
};
use common_utils::errors::ReportSwitchExt;
use error_stack::ResultExt;
use time::PrimitiveDateTime;

use super::PaymentIntentMetricRow;
use crate::{
    enums::AuthInfo,
    query::{
        Aggregate, FilterTypes, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql,
        Window,
    },
    types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult},
};

#[derive(Default)]
pub(crate) struct TotalSmartRetries;

#[async_trait::async_trait]
impl<T> super::PaymentIntentMetric<T> for TotalSmartRetries
where
    T: AnalyticsDataSource + super::PaymentIntentMetricAnalytics,
    PrimitiveDateTime: ToSql<T>,
    AnalyticsCollection: ToSql<T>,
    Granularity: GroupByClause<T>,
    Aggregate<&'static str>: ToSql<T>,
    Window<&'static str>: ToSql<T>,
{
    async fn load_metrics(
        &self,
        dimensions: &[PaymentIntentDimensions],
        auth: &AuthInfo,
        filters: &PaymentIntentFilters,
        granularity: Option<Granularity>,
        time_range: &TimeRange,
        pool: &T,
    ) -> MetricsResult<HashSet<(PaymentIntentMetricsBucketIdentifier, PaymentIntentMetricRow)>>
    {
        let mut query_builder: QueryBuilder<T> =
            QueryBuilder::new(AnalyticsCollection::PaymentIntentSessionized);

        for dim in dimensions.iter() {
            query_builder.add_select_column(dim).switch()?;
        }
        query_builder
            .add_select_column(Aggregate::Count {
                field: None,
                alias: Some("count"),
            })
            .switch()?;
        query_builder
            .add_select_column(Aggregate::Min {
                field: "created_at",
                alias: Some("start_bucket"),
            })
            .switch()?;
        query_builder
            .add_select_column(Aggregate::Max {
                field: "created_at",
                alias: Some("end_bucket"),
            })
            .switch()?;

        filters.set_filter_clause(&mut query_builder).switch()?;

        auth.set_filter_clause(&mut query_builder).switch()?;

        query_builder
            .add_custom_filter_clause("attempt_count", "1", FilterTypes::Gt)
            .switch()?;
        time_range
            .set_filter_clause(&mut query_builder)
            .attach_printable("Error filtering time range")
            .switch()?;

        for dim in dimensions.iter() {
            query_builder
                .add_group_by_clause(dim)
                .attach_printable("Error grouping by dimensions")
                .switch()?;
        }

        if let Some(granularity) = granularity {
            granularity
                .set_group_by_clause(&mut query_builder)
                .attach_printable("Error adding granularity")
                .switch()?;
        }

        query_builder
            .execute_query::<PaymentIntentMetricRow, _>(pool)
            .await
            .change_context(MetricsError::QueryBuildingError)?
            .change_context(MetricsError::QueryExecutionFailure)?
            .into_iter()
            .map(|i| {
                Ok((
                    PaymentIntentMetricsBucketIdentifier::new(
                        i.status.as_ref().map(|i| i.0),
                        i.currency.as_ref().map(|i| i.0),
                        i.profile_id.clone(),
                        i.connector.clone(),
                        i.authentication_type.as_ref().map(|i| i.0),
                        i.payment_method.clone(),
                        i.payment_method_type.clone(),
                        i.card_network.clone(),
                        i.merchant_id.clone(),
                        i.card_last_4.clone(),
                        i.card_issuer.clone(),
                        i.error_reason.clone(),
                        TimeRange {
                            start_time: match (granularity, i.start_bucket) {
                                (Some(g), Some(st)) => g.clip_to_start(st)?,
                                _ => time_range.start_time,
                            },
                            end_time: granularity.as_ref().map_or_else(
                                || Ok(time_range.end_time),
                                |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(),
                            )?,
                        },
                    ),
                    i,
                ))
            })
            .collect::<error_stack::Result<
                HashSet<(PaymentIntentMetricsBucketIdentifier, PaymentIntentMetricRow)>,
                crate::query::PostProcessingError,
            >>()
            .change_context(MetricsError::PostProcessingFailure)
    }
}


impl api::PaymentCapture for Cryptopay {}

impl api::PaymentCapture for for Cryptopay


pub struct ChargeRefunds {
    pub charge_id: String,
    pub transfer_account_id: String,
    pub charge_type: api_models::enums::PaymentChargeType,
    pub options: ChargeRefundsOptions,
}


pub async fn create_encrypted_data<T>(
    key_manager_state: &KeyManagerState,
    key_store: &domain::MerchantKeyStore,
    data: T,
) -> Result<Encryptable<Secret<serde_json::Value>>, error_stack::Report<errors::StorageError>>
where
    T: Debug + serde::Serialize,
{
    let key = key_store.key.get_inner().peek();
    let identifier = Identifier::Merchant(key_store.merchant_id.clone());

    let encoded_data = Encode::encode_to_value(&data)
        .change_context(errors::StorageError::SerializationFailed)
        .attach_printable("Unable to encode data")?;

    let secret_data = Secret::<_, masking::WithType>::new(encoded_data);

    let encrypted_data = domain::types::crypto_operation(
        key_manager_state,
        type_name!(payment_method::PaymentMethod),
        domain::types::CryptoOperation::Encrypt(secret_data),
        identifier.clone(),
        key,
    )
    .await
    .and_then(|val| val.try_into_operation())
    .change_context(errors::StorageError::EncryptionError)
    .attach_printable("Unable to encrypt data")?;

    Ok(encrypted_data)
}


pub struct TrustpayAuthUpdateResponse {
    pub access_token: Option<Secret<String>>,
    pub token_type: Option<String>,
    pub expires_in: Option<i64>,
    #[serde(rename = "ResultInfo")]
    pub result_info: ResultInfo,
}


pub struct Adyen {
    amount_converter: &'static (dyn AmountConvertor<Output = MinorUnit> + Sync),
    amount_converter_webhooks: &'static (dyn AmountConvertor<Output = StringMinorUnit> + Sync),
}


pub struct MandateNew {
    pub mandate_id: String,
    pub customer_id: common_utils::id_type::CustomerId,
    pub merchant_id: common_utils::id_type::MerchantId,
    pub payment_method_id: String,
    pub mandate_status: storage_enums::MandateStatus,
    pub mandate_type: storage_enums::MandateType,
    pub customer_accepted_at: Option<PrimitiveDateTime>,
    pub customer_ip_address: Option<Secret<String, pii::IpAddress>>,
    pub customer_user_agent: Option<String>,
    pub network_transaction_id: Option<String>,
    pub previous_attempt_id: Option<String>,
    pub created_at: Option<PrimitiveDateTime>,
    pub mandate_amount: Option<i64>,
    pub mandate_currency: Option<storage_enums::Currency>,
    pub amount_captured: Option<i64>,
    pub connector: String,
    pub connector_mandate_id: Option<String>,
    pub start_date: Option<PrimitiveDateTime>,
    pub end_date: Option<PrimitiveDateTime>,
    pub metadata: Option<pii::SecretSerdeValue>,
    pub connector_mandate_ids: Option<pii::SecretSerdeValue>,
    pub original_payment_id: Option<common_utils::id_type::PaymentId>,
    pub merchant_connector_id: Option<common_utils::id_type::MerchantConnectorAccountId>,
    pub updated_by: Option<String>,
    pub customer_user_agent_extended: Option<String>,
}


pub struct TsysResponse {
    pub status: TsysPaymentStatus,
    pub response_code: String,
    pub response_message: String,
    #[serde(rename = "transactionID")]
    pub transaction_id: String,
}


pub trait Stop {
    async fn stop_server(&mut self);
}


File: crates/router/src/routes/poll.rs
 Public functions: 1

use actix_web::{web, HttpRequest, HttpResponse};
use router_env::{instrument, tracing, Flow};

use super::app::AppState;
use crate::{
    core::{api_locking, poll},
    services::{api, authentication as auth},
    types::{api::PollId, domain},
};

#[cfg(feature = "v1")]
/// Poll - Retrieve Poll Status
#[utoipa::path(
    get,
    path = "/poll/status/{poll_id}",
    params(
        ("poll_id" = String, Path, description = "The identifier for poll")
    ),
    responses(
        (status = 200, description = "The poll status was retrieved successfully", body = PollResponse),
        (status = 404, description = "Poll not found")
    ),
    tag = "Poll",
    operation_id = "Retrieve Poll Status",
    security(("publishable_key" = []))
)]
#[instrument(skip_all, fields(flow = ?Flow::RetrievePollStatus))]
pub async fn retrieve_poll_status(
    state: web::Data<AppState>,
    req: HttpRequest,
    path: web::Path<String>,
) -> HttpResponse {
    let flow = Flow::RetrievePollStatus;
    let poll_id = PollId {
        poll_id: path.into_inner(),
    };
    Box::pin(api::server_wrap(
        flow,
        state,
        &req,
        poll_id,
        |state, auth, req, _| {
            let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
                domain::Context(auth.merchant_account, auth.key_store),
            ));
            poll::retrieve_poll_status(state, req, merchant_context)
        },
        &auth::HeaderAuth(auth::PublishableKeyAuth),
        api_locking::LockAction::NotApplicable,
    ))
    .await
}


impl ConnectorError {
    /// fn is_connector_timeout
    pub fn is_connector_timeout(&self) -> bool {
        self == &Self::RequestTimeoutReceived
    }
}


/// Get the inner String
    pub fn into_inner(self) -> String {
        self.0
    }


impl RequestPaymentMethodTypes {
    ///Get payment_method_subtype
    pub fn get_payment_method_type(&self) -> Option<common_enums::PaymentMethodType> {
        Some(self.payment_method_subtype)
    }
}