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

pub mod transformers;

use std::sync::LazyLock;

use api_models::webhooks::IncomingWebhookEvent;
use common_enums::{enums, CallConnectorAction, PaymentAction};
use common_utils::{
    crypto,
    errors::CustomResult,
    ext_traits::{ByteSliceExt, BytesExt, ValueExt},
    request::{Method, Request, RequestBuilder, RequestContent},
    types::{
        AmountConvertor, MinorUnit, StringMajorUnit, StringMajorUnitForConnector, StringMinorUnit,
        StringMinorUnitForConnector,
    },
};
use error_stack::{report, ResultExt};
use hyperswitch_domain_models::{
    router_data::{AccessToken, ErrorResponse, RouterData},
    router_flow_types::{
        access_token_auth::AccessTokenAuth,
        payments::{Authorize, Capture, PSync, PaymentMethodToken, Session, SetupMandate, Void},
        refunds::{Execute, RSync},
        CompleteAuthorize, PreProcessing,
    },
    router_request_types::{
        AccessTokenRequestData, CompleteAuthorizeData, PaymentMethodTokenizationData,
        PaymentsAuthorizeData, PaymentsCancelData, PaymentsCaptureData, PaymentsPreProcessingData,
        PaymentsSessionData, PaymentsSyncData, RefundsData, SetupMandateRequestData,
    },
    router_response_types::{
        ConnectorInfo, PaymentMethodDetails, PaymentsResponseData, RefundsResponseData,
        SupportedPaymentMethods, SupportedPaymentMethodsExt,
    },
    types::{
        PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData,
        PaymentsCompleteAuthorizeRouterData, PaymentsPreProcessingRouterData,
        PaymentsSyncRouterData, RefundSyncRouterData, RefundsRouterData, SetupMandateRouterData,
    },
};
use hyperswitch_interfaces::{
    api::{
        self, ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, ConnectorRedirectResponse,
        ConnectorSpecifications, ConnectorValidation,
    },
    configs::Connectors,
    disputes::DisputePayload,
    errors,
    events::connector_api_logs::ConnectorEvent,
    types::{self, Response},
    webhooks::{IncomingWebhook, IncomingWebhookRequestDetails},
};
use masking::{Mask, PeekInterface};
use router_env::logger;
use transformers as airwallex;

use crate::{
    connectors::airwallex::transformers::AirwallexAuthorizeResponse,
    constants::headers,
    types::{RefreshTokenRouterData, ResponseRouterData},
    utils::{convert_amount, AccessTokenRequestInfo, ForeignTryFrom, RefundsRequestData},
};

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

impl Airwallex {
    pub const fn new() -> &'static Self {
        &Self {
            amount_converter: &StringMajorUnitForConnector,
            amount_converter_to_string_minor: &StringMinorUnitForConnector,
        }
    }
}

impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Airwallex
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 headers = vec![(
            headers::CONTENT_TYPE.to_string(),
            self.get_content_type().to_string().into(),
        )];
        let access_token = req
            .access_token
            .clone()
            .ok_or(errors::ConnectorError::FailedToObtainAuthType)?;

        let auth_header = (
            headers::AUTHORIZATION.to_string(),
            format!("Bearer {}", access_token.token.peek()).into_masked(),
        );

        headers.push(auth_header);
        Ok(headers)
    }
}

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

    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.airwallex.base_url.as_ref()
    }

    fn build_error_response(
        &self,
        res: Response,
        event_builder: Option<&mut ConnectorEvent>,
    ) -> CustomResult<ErrorResponse, errors::ConnectorError> {
        logger::debug!(payu_error_response=?res);
        let response: airwallex::AirwallexErrorResponse = res
            .response
            .parse_struct("Airwallex ErrorResponse")
            .change_context(errors::ConnectorError::ResponseDeserializationFailed)?;

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

        Ok(ErrorResponse {
            status_code: res.status_code,
            code: response.code,
            message: response.message,
            reason: response.source,
            attempt_status: None,
            connector_transaction_id: None,
            network_advice_code: None,
            network_decline_code: None,
            network_error_message: None,
            connector_metadata: None,
        })
    }
}

impl ConnectorValidation for Airwallex {}

impl api::Payment for Airwallex {}
impl api::PaymentsPreProcessing for Airwallex {}
impl api::PaymentsCompleteAuthorize for Airwallex {}
impl api::MandateSetup for Airwallex {}
impl ConnectorIntegration<SetupMandate, SetupMandateRequestData, PaymentsResponseData>
    for Airwallex
{
    fn build_request(
        &self,
        _req: &SetupMandateRouterData,
        _connectors: &Connectors,
    ) -> CustomResult<Option<Request>, errors::ConnectorError> {
        Err(
            errors::ConnectorError::NotImplemented("Setup Mandate flow for Airwallex".to_string())
                .into(),
        )
    }
}

impl api::PaymentToken for Airwallex {}

impl ConnectorIntegration<PaymentMethodToken, PaymentMethodTokenizationData, PaymentsResponseData>
    for Airwallex
{
    // Not Implemented (R)
}

impl api::ConnectorAccessToken for Airwallex {}

impl ConnectorIntegration<AccessTokenAuth, AccessTokenRequestData, AccessToken> for Airwallex {
    fn get_url(
        &self,
        _req: &RefreshTokenRouterData,
        connectors: &Connectors,
    ) -> CustomResult<String, errors::ConnectorError> {
        Ok(format!(
            "{}{}",
            self.base_url(connectors),
            "api/v1/authentication/login"
        ))
    }

    fn get_headers(
        &self,
        req: &RefreshTokenRouterData,
        _connectors: &Connectors,
    ) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
        let headers = vec![
            (
                headers::X_API_KEY.to_string(),
                req.request.app_id.clone().into_masked(),
            ),
            ("Content-Length".to_string(), "0".to_string().into()),
            (
                "x-client-id".to_string(),
                req.get_request_id()?.into_masked(),
            ),
        ];
        Ok(headers)
    }

    fn build_request(
        &self,
        req: &RefreshTokenRouterData,
        connectors: &Connectors,
    ) -> CustomResult<Option<Request>, errors::ConnectorError> {
        let req = Some(
            RequestBuilder::new()
                .method(Method::Post)
                .attach_default_headers()
                .headers(types::RefreshTokenType::get_headers(self, req, connectors)?)
                .url(&types::RefreshTokenType::get_url(self, req, connectors)?)
                .build(),
        );
        logger::debug!(payu_access_token_request=?req);
        Ok(req)
    }
    fn handle_response(
        &self,
        data: &RefreshTokenRouterData,
        event_builder: Option<&mut ConnectorEvent>,
        res: Response,
    ) -> CustomResult<RefreshTokenRouterData, errors::ConnectorError> {
        logger::debug!(access_token_response=?res);
        let response: airwallex::AirwallexAuthUpdateResponse = res
            .response
            .parse_struct("airwallex AirwallexAuthUpdateResponse")
            .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<PreProcessing, PaymentsPreProcessingData, PaymentsResponseData>
    for Airwallex
{
    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!(
            "{}{}",
            self.base_url(connectors),
            "api/v1/pa/payment_intents/create"
        ))
    }

    fn get_request_body(
        &self,
        req: &PaymentsPreProcessingRouterData,
        _connectors: &Connectors,
    ) -> CustomResult<RequestContent, errors::ConnectorError> {
        let amount_in_minor_unit = MinorUnit::new(req.request.amount.ok_or(
            errors::ConnectorError::MissingRequiredField {
                field_name: "amount",
            },
        )?);
        let amount = convert_amount(
            self.amount_converter,
            amount_in_minor_unit,
            req.request
                .currency
                .ok_or(errors::ConnectorError::MissingRequiredField {
                    field_name: "currency",
                })?,
        )?;
        let connector_router_data = airwallex::AirwallexRouterData::try_from((amount, req))?;
        let connector_req = airwallex::AirwallexIntentRequest::try_from(&connector_router_data)?;
        Ok(RequestContent::Json(Box::new(connector_req)))
    }

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

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

#[async_trait::async_trait]
impl ConnectorIntegration<Authorize, PaymentsAuthorizeData, PaymentsResponseData> for Airwallex {
    fn get_headers(
        &self,
        req: &PaymentsAuthorizeRouterData,
        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: &PaymentsAuthorizeRouterData,
        connectors: &Connectors,
    ) -> CustomResult<String, errors::ConnectorError> {
        Ok(format!(
            "{}{}{}{}",
            self.base_url(connectors),
            "api/v1/pa/payment_intents/",
            req.reference_id
                .clone()
                .ok_or(errors::ConnectorError::MissingConnectorTransactionID)?,
            "/confirm"
        ))
    }

    fn get_request_body(
        &self,
        req: &PaymentsAuthorizeRouterData,
        _connectors: &Connectors,
    ) -> CustomResult<RequestContent, errors::ConnectorError> {
        let amount_in_minor_unit = MinorUnit::new(req.request.amount);
        let amount = convert_amount(
            self.amount_converter,
            amount_in_minor_unit,
            req.request.currency,
        )?;
        let connector_router_data = airwallex::AirwallexRouterData::try_from((amount, req))?;
        let connector_req = airwallex::AirwallexPaymentsRequest::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(&types::PaymentsAuthorizeType::get_url(
                    self, req, connectors,
                )?)
                .attach_default_headers()
                .headers(types::PaymentsAuthorizeType::get_headers(
                    self, req, connectors,
                )?)
                .set_body(types::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: AirwallexAuthorizeResponse = res
            .response
            .parse_struct("AirwallexAuthorizeResponse")
            .change_context(errors::ConnectorError::ResponseDeserializationFailed)?;

        event_builder.map(|i| i.set_response_body(&response));
        RouterData::foreign_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 api::PaymentSync for Airwallex {}
impl ConnectorIntegration<PSync, PaymentsSyncData, PaymentsResponseData> for Airwallex {
    fn get_headers(
        &self,
        req: &PaymentsSyncRouterData,
        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: &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!(
            "{}{}{}",
            self.base_url(connectors),
            "api/v1/pa/payment_intents/",
            connector_payment_id,
        ))
    }

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

    fn handle_response(
        &self,
        data: &PaymentsSyncRouterData,
        event_builder: Option<&mut ConnectorEvent>,
        res: Response,
    ) -> CustomResult<PaymentsSyncRouterData, errors::ConnectorError> {
        logger::debug!(payment_sync_response=?res);
        let response: airwallex::AirwallexPaymentsSyncResponse = res
            .response
            .parse_struct("airwallex AirwallexPaymentsSyncResponse")
            .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<CompleteAuthorize, CompleteAuthorizeData, PaymentsResponseData>
    for Airwallex
{
    fn get_headers(
        &self,
        req: &PaymentsCompleteAuthorizeRouterData,
        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: &PaymentsCompleteAuthorizeRouterData,
        connectors: &Connectors,
    ) -> CustomResult<String, errors::ConnectorError> {
        let connector_payment_id = req
            .request
            .connector_transaction_id
            .clone()
            .ok_or(errors::ConnectorError::MissingConnectorTransactionID)?;
        Ok(format!(
            "{}api/v1/pa/payment_intents/{}/confirm_continue",
            self.base_url(connectors),
            connector_payment_id,
        ))
    }
    fn get_request_body(
        &self,
        req: &PaymentsCompleteAuthorizeRouterData,
        _connectors: &Connectors,
    ) -> CustomResult<RequestContent, errors::ConnectorError> {
        let req_obj = airwallex::AirwallexCompleteRequest::try_from(req)?;

        Ok(RequestContent::Json(Box::new(req_obj)))
    }
    fn build_request(
        &self,
        req: &PaymentsCompleteAuthorizeRouterData,
        connectors: &Connectors,
    ) -> CustomResult<Option<Request>, errors::ConnectorError> {
        Ok(Some(
            RequestBuilder::new()
                .method(Method::Post)
                .url(&types::PaymentsCompleteAuthorizeType::get_url(
                    self, req, connectors,
                )?)
                .headers(types::PaymentsCompleteAuthorizeType::get_headers(
                    self, req, connectors,
                )?)
                .set_body(types::PaymentsCompleteAuthorizeType::get_request_body(
                    self, req, connectors,
                )?)
                .build(),
        ))
    }
    fn handle_response(
        &self,
        data: &PaymentsCompleteAuthorizeRouterData,
        event_builder: Option<&mut ConnectorEvent>,
        res: Response,
    ) -> CustomResult<PaymentsCompleteAuthorizeRouterData, errors::ConnectorError> {
        let response: airwallex::AirwallexPaymentsResponse = res
            .response
            .parse_struct("AirwallexPaymentsResponse")
            .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 api::PaymentCapture for Airwallex {}
impl ConnectorIntegration<Capture, PaymentsCaptureData, PaymentsResponseData> for Airwallex {
    fn get_headers(
        &self,
        req: &PaymentsCaptureRouterData,
        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: &PaymentsCaptureRouterData,
        connectors: &Connectors,
    ) -> CustomResult<String, errors::ConnectorError> {
        Ok(format!(
            "{}{}{}{}",
            self.base_url(connectors),
            "api/v1/pa/payment_intents/",
            req.request.connector_transaction_id,
            "/capture"
        ))
    }

    fn get_request_body(
        &self,
        req: &PaymentsCaptureRouterData,
        _connectors: &Connectors,
    ) -> CustomResult<RequestContent, errors::ConnectorError> {
        let connector_req = airwallex::AirwallexPaymentsCaptureRequest::try_from(req)?;

        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(&types::PaymentsCaptureType::get_url(self, req, connectors)?)
                .attach_default_headers()
                .headers(types::PaymentsCaptureType::get_headers(
                    self, req, connectors,
                )?)
                .set_body(types::PaymentsCaptureType::get_request_body(
                    self, req, connectors,
                )?)
                .build(),
        ))
    }

    fn handle_response(
        &self,
        data: &PaymentsCaptureRouterData,
        event_builder: Option<&mut ConnectorEvent>,
        res: Response,
    ) -> CustomResult<PaymentsCaptureRouterData, errors::ConnectorError> {
        let response: airwallex::AirwallexPaymentsResponse = res
            .response
            .parse_struct("Airwallex 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 api::PaymentSession for Airwallex {}

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

impl api::PaymentVoid for Airwallex {}

impl ConnectorIntegration<Void, PaymentsCancelData, PaymentsResponseData> for Airwallex {
    fn get_headers(
        &self,
        req: &PaymentsCancelRouterData,
        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: &PaymentsCancelRouterData,
        connectors: &Connectors,
    ) -> CustomResult<String, errors::ConnectorError> {
        Ok(format!(
            "{}{}{}{}",
            self.base_url(connectors),
            "api/v1/pa/payment_intents/",
            req.request.connector_transaction_id,
            "/cancel"
        ))
    }
    fn get_request_body(
        &self,
        req: &PaymentsCancelRouterData,
        _connectors: &Connectors,
    ) -> CustomResult<RequestContent, errors::ConnectorError> {
        let connector_req = airwallex::AirwallexPaymentsCancelRequest::try_from(req)?;

        Ok(RequestContent::Json(Box::new(connector_req)))
    }
    fn handle_response(
        &self,
        data: &PaymentsCancelRouterData,
        event_builder: Option<&mut ConnectorEvent>,
        res: Response,
    ) -> CustomResult<PaymentsCancelRouterData, errors::ConnectorError> {
        let response: airwallex::AirwallexPaymentsResponse = res
            .response
            .parse_struct("Airwallex 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 build_request(
        &self,
        req: &PaymentsCancelRouterData,
        connectors: &Connectors,
    ) -> CustomResult<Option<Request>, errors::ConnectorError> {
        Ok(Some(
            RequestBuilder::new()
                .method(Method::Post)
                .url(&types::PaymentsVoidType::get_url(self, req, connectors)?)
                .attach_default_headers()
                .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?)
                .set_body(types::PaymentsVoidType::get_request_body(
                    self, req, connectors,
                )?)
                .build(),
        ))
    }

    fn get_error_response(
        &self,
        res: Response,
        event_builder: Option<&mut ConnectorEvent>,
    ) -> CustomResult<ErrorResponse, errors::ConnectorError> {
        self.build_error_response(res, event_builder)
    }
}

impl api::Refund for Airwallex {}
impl api::RefundExecute for Airwallex {}
impl api::RefundSync for Airwallex {}

impl ConnectorIntegration<Execute, RefundsData, RefundsResponseData> for Airwallex {
    fn get_headers(
        &self,
        req: &RefundsRouterData<Execute>,
        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: &RefundsRouterData<Execute>,
        connectors: &Connectors,
    ) -> CustomResult<String, errors::ConnectorError> {
        Ok(format!(
            "{}{}",
            self.base_url(connectors),
            "api/v1/pa/refunds/create"
        ))
    }

    fn get_request_body(
        &self,
        req: &RefundsRouterData<Execute>,
        _connectors: &Connectors,
    ) -> CustomResult<RequestContent, errors::ConnectorError> {
        let amount_in_minor_unit = MinorUnit::new(req.request.refund_amount);
        let amount = convert_amount(
            self.amount_converter,
            amount_in_minor_unit,
            req.request.currency,
        )?;
        let connector_router_data = airwallex::AirwallexRouterData::try_from((amount, req))?;
        let connector_req = airwallex::AirwallexRefundRequest::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> {
        logger::debug!(target: "router::connector::airwallex", response=?res);
        let response: airwallex::RefundResponse = res
            .response
            .parse_struct("airwallex RefundResponse")
            .change_context(errors::ConnectorError::RequestEncodingFailed)?;
        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<RSync, RefundsData, RefundsResponseData> for Airwallex {
    fn get_headers(
        &self,
        req: &RefundSyncRouterData,
        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: &RefundSyncRouterData,
        connectors: &Connectors,
    ) -> CustomResult<String, errors::ConnectorError> {
        Ok(format!(
            "{}{}{}",
            self.base_url(connectors),
            "/api/v1/pa/refunds/",
            req.request.get_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> {
        logger::debug!(target: "router::connector::airwallex", response=?res);
        let response: airwallex::RefundResponse = res
            .response
            .parse_struct("airwallex RefundResponse")
            .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)
    }
}

#[async_trait::async_trait]
impl IncomingWebhook for Airwallex {
    fn get_webhook_source_verification_algorithm(
        &self,
        _request: &IncomingWebhookRequestDetails<'_>,
    ) -> CustomResult<Box<dyn crypto::VerifySignature + Send>, errors::ConnectorError> {
        Ok(Box::new(crypto::HmacSha256))
    }

    fn get_webhook_source_verification_signature(
        &self,
        request: &IncomingWebhookRequestDetails<'_>,
        _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
    ) -> CustomResult<Vec<u8>, errors::ConnectorError> {
        let security_header = request
            .headers
            .get("x-signature")
            .map(|header_value| {
                header_value
                    .to_str()
                    .map(String::from)
                    .map_err(|_| errors::ConnectorError::WebhookSignatureNotFound)
            })
            .ok_or(errors::ConnectorError::WebhookSignatureNotFound)??;

        hex::decode(security_header)
            .change_context(errors::ConnectorError::WebhookSignatureNotFound)
    }

    fn get_webhook_source_verification_message(
        &self,
        request: &IncomingWebhookRequestDetails<'_>,
        _merchant_id: &common_utils::id_type::MerchantId,
        _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
    ) -> CustomResult<Vec<u8>, errors::ConnectorError> {
        let timestamp = request
            .headers
            .get("x-timestamp")
            .map(|header_value| {
                header_value
                    .to_str()
                    .map(String::from)
                    .map_err(|_| errors::ConnectorError::WebhookSignatureNotFound)
            })
            .ok_or(errors::ConnectorError::WebhookSignatureNotFound)??;

        Ok(format!("{}{}", timestamp, String::from_utf8_lossy(request.body)).into_bytes())
    }

    fn get_webhook_object_reference_id(
        &self,
        request: &IncomingWebhookRequestDetails<'_>,
    ) -> CustomResult<api_models::webhooks::ObjectReferenceId, errors::ConnectorError> {
        let details: airwallex::AirwallexWebhookData = request
            .body
            .parse_struct("airwallexWebhookData")
            .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;

        if airwallex::is_transaction_event(&details.name) {
            Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
                api_models::payments::PaymentIdType::ConnectorTransactionId(
                    details
                        .source_id
                        .ok_or(errors::ConnectorError::WebhookReferenceIdNotFound)?,
                ),
            ))
        } else if airwallex::is_refund_event(&details.name) {
            Ok(api_models::webhooks::ObjectReferenceId::RefundId(
                api_models::webhooks::RefundIdType::ConnectorRefundId(
                    details
                        .source_id
                        .ok_or(errors::ConnectorError::WebhookReferenceIdNotFound)?,
                ),
            ))
        } else if airwallex::is_dispute_event(&details.name) {
            let dispute_details: airwallex::AirwallexDisputeObject = details
                .data
                .object
                .parse_value("AirwallexDisputeObject")
                .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;

            Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
                api_models::payments::PaymentIdType::ConnectorTransactionId(
                    dispute_details.payment_intent_id,
                ),
            ))
        } else {
            Err(report!(errors::ConnectorError::WebhookEventTypeNotFound))
        }
    }

    fn get_webhook_event_type(
        &self,
        request: &IncomingWebhookRequestDetails<'_>,
    ) -> CustomResult<IncomingWebhookEvent, errors::ConnectorError> {
        let details: airwallex::AirwallexWebhookData = request
            .body
            .parse_struct("airwallexWebhookData")
            .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
        Ok(IncomingWebhookEvent::try_from(details.name)?)
    }

    fn get_webhook_resource_object(
        &self,
        request: &IncomingWebhookRequestDetails<'_>,
    ) -> CustomResult<Box<dyn masking::ErasedMaskSerialize>, errors::ConnectorError> {
        let details: airwallex::AirwallexWebhookObjectResource = request
            .body
            .parse_struct("AirwallexWebhookObjectResource")


OpenAPI Block Path: components.schemas.PayoutLinkInitiateRequest
{
  "type": "object",
  "required": [
    "merchant_id",
    "payout_id"
  ],
  "properties": {
    "merchant_id": {
      "type": "string"
    },
    "payout_id": {
      "type": "string"
    }
  }
}


pub trait PaymentDistributionAccumulator {
    type DistributionOutput;

    fn add_distribution_bucket(&mut self, distribution: &PaymentDistributionRow);

    fn collect(self) -> Self::DistributionOutput;
}


pub struct NexinetsPaymentsMetadata {
    pub transaction_id: Option<String>,
    pub order_id: Option<String>,
    pub psync_flow: NexinetsTransactionType,
}


pub fn apply_changeset(self, source: PaymentMethod) -> PaymentMethod {
        let Self {
            metadata,
            payment_method_data,
            last_used_at,
            network_transaction_id,
            status,
            locker_id,
            network_token_requestor_reference_id,
            payment_method,
            connector_mandate_details,
            updated_by,
            payment_method_type,
            payment_method_issuer,
            last_modified,
            network_token_locker_id,
            network_token_payment_method_data,
            scheme,
        } = self;

        PaymentMethod {
            customer_id: source.customer_id,
            merchant_id: source.merchant_id,
            payment_method_id: source.payment_method_id,
            accepted_currency: source.accepted_currency,
            scheme: scheme.or(source.scheme),
            token: source.token,
            cardholder_name: source.cardholder_name,
            issuer_name: source.issuer_name,
            issuer_country: source.issuer_country,
            payer_country: source.payer_country,
            is_stored: source.is_stored,
            swift_code: source.swift_code,
            direct_debit_token: source.direct_debit_token,
            created_at: source.created_at,
            last_modified,
            payment_method: payment_method.or(source.payment_method),
            payment_method_type: payment_method_type.or(source.payment_method_type),
            payment_method_issuer: payment_method_issuer.or(source.payment_method_issuer),
            payment_method_issuer_code: source.payment_method_issuer_code,
            metadata: metadata.map_or(source.metadata, |v| Some(v.into())),
            payment_method_data: payment_method_data.or(source.payment_method_data),
            locker_id: locker_id.or(source.locker_id),
            last_used_at: last_used_at.unwrap_or(source.last_used_at),
            connector_mandate_details: connector_mandate_details
                .or(source.connector_mandate_details),
            customer_acceptance: source.customer_acceptance,
            status: status.unwrap_or(source.status),
            network_transaction_id: network_transaction_id.or(source.network_transaction_id),
            client_secret: source.client_secret,
            payment_method_billing_address: source.payment_method_billing_address,
            updated_by: updated_by.or(source.updated_by),
            version: source.version,
            network_token_requestor_reference_id: network_token_requestor_reference_id
                .or(source.network_token_requestor_reference_id),
            network_token_locker_id: network_token_locker_id.or(source.network_token_locker_id),
            network_token_payment_method_data: network_token_payment_method_data
                .or(source.network_token_payment_method_data),
        }
    }


impl RequiredFieldsAndSurchargeForEnabledPaymentMethodTypes {
    fn populate_pm_subtype_specific_data(
        self,
        bank_config: &settings::BankRedirectConfig,
    ) -> RequiredFieldsAndSurchargeWithExtraInfoForEnabledPaymentMethodTypes {
        let response_payment_methods = self
            .0
            .into_iter()
            .map(|payment_methods_enabled| {
                RequiredFieldsAndSurchargeWithExtraInfoForEnabledPaymentMethodType {
                    payment_method_type: payment_methods_enabled.payment_method_type,
                    payment_method_subtype: payment_methods_enabled.payment_method_subtype,
                    payment_experience: payment_methods_enabled.payment_experience,
                    required_fields: payment_methods_enabled.required_fields,
                    surcharge: payment_methods_enabled.surcharge,
                    pm_subtype_specific_data: get_pm_subtype_specific_data(
                        bank_config,
                        payment_methods_enabled.payment_method_type,
                        payment_methods_enabled.payment_method_subtype,
                        &payment_methods_enabled.connectors,
                    ),
                }
            })
            .collect();

        RequiredFieldsAndSurchargeWithExtraInfoForEnabledPaymentMethodTypes(
            response_payment_methods,
        )
    }
}


impl crate::events::ApiEventMetric for GlobalPaymentMethodId {
    fn get_api_event_type(&self) -> Option<crate::events::ApiEventsType> {
        Some(
            crate::events::ApiEventsType::PaymentMethodListForPaymentMethods {
                payment_method_id: self.clone(),
            },
        )
    }
}

impl crate::events::ApiEventMetric for for GlobalPaymentMethodId


pub async fn payments_get_intent_using_merchant_reference(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    profile: domain::Profile,
    req_state: ReqState,
    merchant_reference_id: &id_type::PaymentReferenceId,
    header_payload: HeaderPayload,
) -> RouterResponse<api::PaymentsIntentResponse> {
    let db = state.store.as_ref();
    let storage_scheme = merchant_context.get_merchant_account().storage_scheme;
    let key_manager_state = &(&state).into();
    let payment_intent = db
        .find_payment_intent_by_merchant_reference_id_profile_id(
            key_manager_state,
            merchant_reference_id,
            profile.get_id(),
            merchant_context.get_merchant_key_store(),
            &storage_scheme,
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;

    let (payment_data, _req, customer) = Box::pin(payments_intent_operation_core::<
        api::PaymentGetIntent,
        _,
        _,
        PaymentIntentData<api::PaymentGetIntent>,
    >(
        &state,
        req_state,
        merchant_context.clone(),
        profile.clone(),
        operations::PaymentGetIntent,
        api_models::payments::PaymentsGetIntentRequest {
            id: payment_intent.get_id().clone(),
        },
        payment_intent.get_id().clone(),
        header_payload.clone(),
    ))
    .await?;

    transformers::ToResponse::<
        api::PaymentGetIntent,
        PaymentIntentData<api::PaymentGetIntent>,
        operations::PaymentGetIntent,
    >::generate_response(
        payment_data,
        customer,
        &state.base_url,
        operations::PaymentGetIntent,
        &state.conf.connector_request_reference_id_config,
        None,
        None,
        header_payload.x_hs_latency,
        &merchant_context,
    )
}


impl State for CardTokenized {}


File: crates/router/src/core/api_keys.rs
 Public functions: 14
 Public structs: 2

use common_utils::date_time;
#[cfg(feature = "email")]
use diesel_models::{api_keys::ApiKey, enums as storage_enums};
use error_stack::{report, ResultExt};
use masking::{PeekInterface, StrongSecret};
use router_env::{instrument, tracing};

use crate::{
    configs::settings,
    consts,
    core::errors::{self, RouterResponse, StorageErrorExt},
    db::domain,
    routes::{metrics, SessionState},
    services::{authentication, ApplicationResponse},
    types::{api, storage, transformers::ForeignInto},
};

#[cfg(feature = "email")]
const API_KEY_EXPIRY_TAG: &str = "API_KEY";
#[cfg(feature = "email")]
const API_KEY_EXPIRY_NAME: &str = "API_KEY_EXPIRY";
#[cfg(feature = "email")]
const API_KEY_EXPIRY_RUNNER: diesel_models::ProcessTrackerRunner =
    diesel_models::ProcessTrackerRunner::ApiKeyExpiryWorkflow;

static HASH_KEY: once_cell::sync::OnceCell<StrongSecret<[u8; PlaintextApiKey::HASH_KEY_LEN]>> =
    once_cell::sync::OnceCell::new();

impl settings::ApiKeys {
    pub fn get_hash_key(
        &self,
    ) -> errors::RouterResult<&'static StrongSecret<[u8; PlaintextApiKey::HASH_KEY_LEN]>> {
        HASH_KEY.get_or_try_init(|| {
            <[u8; PlaintextApiKey::HASH_KEY_LEN]>::try_from(
                hex::decode(self.hash_key.peek())
                    .change_context(errors::ApiErrorResponse::InternalServerError)
                    .attach_printable("API key hash key has invalid hexadecimal data")?
                    .as_slice(),
            )
            .change_context(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("The API hashing key has incorrect length")
            .map(StrongSecret::new)
        })
    }
}

// Defining new types `PlaintextApiKey` and `HashedApiKey` in the hopes of reducing the possibility
// of plaintext API key being stored in the data store.
pub struct PlaintextApiKey(StrongSecret<String>);

#[derive(Debug, PartialEq, Eq)]
pub struct HashedApiKey(String);

impl PlaintextApiKey {
    const HASH_KEY_LEN: usize = 32;

    const PREFIX_LEN: usize = 12;

    pub fn new(length: usize) -> Self {
        let env = router_env::env::prefix_for_env();
        let key = common_utils::crypto::generate_cryptographically_secure_random_string(length);
        Self(format!("{env}_{key}").into())
    }

    pub fn new_key_id() -> common_utils::id_type::ApiKeyId {
        let env = router_env::env::prefix_for_env();
        common_utils::id_type::ApiKeyId::generate_key_id(env)
    }

    pub fn prefix(&self) -> String {
        self.0.peek().chars().take(Self::PREFIX_LEN).collect()
    }

    pub fn peek(&self) -> &str {
        self.0.peek()
    }

    pub fn keyed_hash(&self, key: &[u8; Self::HASH_KEY_LEN]) -> HashedApiKey {
        /*
        Decisions regarding API key hashing algorithm chosen:

        - Since API key hash verification would be done for each request, there is a requirement
          for the hashing to be quick.
        - Password hashing algorithms would not be suitable for this purpose as they're designed to
          prevent brute force attacks, considering that the same password could be shared  across
          multiple sites by the user.
        - Moreover, password hash verification happens once per user session, so the delay involved
          is negligible, considering the security benefits it provides.
          While with API keys (assuming uniqueness of keys across the application), the delay
          involved in hashing (with the use of a password hashing algorithm) becomes significant,
          considering that it must be done per request.
        - Since we are the only ones generating API keys and are able to guarantee their uniqueness,
          a simple hash algorithm is sufficient for this purpose.

        Hash algorithms considered:
        - Password hashing algorithms: Argon2id and PBKDF2
        - Simple hashing algorithms: HMAC-SHA256, HMAC-SHA512, BLAKE3

        After benchmarking the simple hashing algorithms, we decided to go with the BLAKE3 keyed
        hashing algorithm, with a randomly generated key for the hash key.
        */

        HashedApiKey(
            blake3::keyed_hash(key, self.0.peek().as_bytes())
                .to_hex()
                .to_string(),
        )
    }
}

#[instrument(skip_all)]
pub async fn create_api_key(
    state: SessionState,
    api_key: api::CreateApiKeyRequest,
    key_store: domain::MerchantKeyStore,
) -> RouterResponse<api::CreateApiKeyResponse> {
    let api_key_config = state.conf.api_keys.get_inner();
    let store = state.store.as_ref();

    let merchant_id = key_store.merchant_id.clone();

    let hash_key = api_key_config.get_hash_key()?;
    let plaintext_api_key = PlaintextApiKey::new(consts::API_KEY_LENGTH);
    let api_key = storage::ApiKeyNew {
        key_id: PlaintextApiKey::new_key_id(),
        merchant_id: merchant_id.to_owned(),
        name: api_key.name,
        description: api_key.description,
        hashed_api_key: plaintext_api_key.keyed_hash(hash_key.peek()).into(),
        prefix: plaintext_api_key.prefix(),
        created_at: date_time::now(),
        expires_at: api_key.expiration.into(),
        last_used: None,
    };

    let api_key = store
        .insert_api_key(api_key)
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Failed to insert new API key")?;

    let state_inner = state.clone();
    let hashed_api_key = api_key.hashed_api_key.clone();
    let merchant_id_inner = merchant_id.clone();
    let key_id = api_key.key_id.clone();
    let expires_at = api_key.expires_at;

    authentication::decision::spawn_tracked_job(
        async move {
            authentication::decision::add_api_key(
                &state_inner,
                hashed_api_key.into_inner().into(),
                merchant_id_inner,
                key_id,
                expires_at.map(authentication::decision::convert_expiry),
            )
            .await
        },
        authentication::decision::ADD,
    );

    metrics::API_KEY_CREATED.add(
        1,
        router_env::metric_attributes!(("merchant", merchant_id.clone())),
    );

    // Add process to process_tracker for email reminder, only if expiry is set to future date
    // If the `api_key` is set to expire in less than 7 days, the merchant is not notified about it's expiry
    #[cfg(feature = "email")]
    {
        if api_key.expires_at.is_some() {
            let expiry_reminder_days = state.conf.api_keys.get_inner().expiry_reminder_days.clone();

            add_api_key_expiry_task(store, &api_key, expiry_reminder_days)
                .await
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Failed to insert API key expiry reminder to process tracker")?;
        }
    }

    Ok(ApplicationResponse::Json(
        (api_key, plaintext_api_key).foreign_into(),
    ))
}

// Add api_key_expiry task to the process_tracker table.
// Construct ProcessTrackerNew struct with all required fields, and schedule the first email.
// After first email has been sent, update the schedule_time based on retry_count in execute_workflow().
// A task is not scheduled if the time for the first email is in the past.
#[cfg(feature = "email")]
#[instrument(skip_all)]
pub async fn add_api_key_expiry_task(
    store: &dyn crate::db::StorageInterface,
    api_key: &ApiKey,
    expiry_reminder_days: Vec<u8>,
) -> Result<(), errors::ProcessTrackerError> {
    let current_time = date_time::now();

    let schedule_time = expiry_reminder_days
        .first()
        .and_then(|expiry_reminder_day| {
            api_key.expires_at.map(|expires_at| {
                expires_at.saturating_sub(time::Duration::days(i64::from(*expiry_reminder_day)))
            })
        })
        .ok_or(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Failed to obtain initial process tracker schedule time")?;

    if schedule_time <= current_time {
        return Ok(());
    }

    let api_key_expiry_tracker = storage::ApiKeyExpiryTrackingData {
        key_id: api_key.key_id.clone(),
        merchant_id: api_key.merchant_id.clone(),
        api_key_name: api_key.name.clone(),
        prefix: api_key.prefix.clone(),
        // We need API key expiry too, because we need to decide on the schedule_time in
        // execute_workflow() where we won't be having access to the Api key object.
        api_key_expiry: api_key.expires_at,
        expiry_reminder_days: expiry_reminder_days.clone(),
    };

    let process_tracker_id = generate_task_id_for_api_key_expiry_workflow(&api_key.key_id);
    let process_tracker_entry = storage::ProcessTrackerNew::new(
        process_tracker_id,
        API_KEY_EXPIRY_NAME,
        API_KEY_EXPIRY_RUNNER,
        [API_KEY_EXPIRY_TAG],
        api_key_expiry_tracker,
        None,
        schedule_time,
        common_types::consts::API_VERSION,
    )
    .change_context(errors::ApiErrorResponse::InternalServerError)
    .attach_printable("Failed to construct API key expiry process tracker task")?;

    store
        .insert_process(process_tracker_entry)
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable_lazy(|| {
            format!(
                "Failed while inserting API key expiry reminder to process_tracker: {:?}",
                api_key.key_id
            )
        })?;
    metrics::TASKS_ADDED_COUNT.add(1, router_env::metric_attributes!(("flow", "ApiKeyExpiry")));

    Ok(())
}

#[instrument(skip_all)]
pub async fn retrieve_api_key(
    state: SessionState,
    merchant_id: common_utils::id_type::MerchantId,
    key_id: common_utils::id_type::ApiKeyId,
) -> RouterResponse<api::RetrieveApiKeyResponse> {
    let store = state.store.as_ref();
    let api_key = store
        .find_api_key_by_merchant_id_key_id_optional(&merchant_id, &key_id)
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError) // If retrieve failed
        .attach_printable("Failed to retrieve API key")?
        .ok_or(report!(errors::ApiErrorResponse::ApiKeyNotFound))?; // If retrieve returned `None`

    Ok(ApplicationResponse::Json(api_key.foreign_into()))
}

#[instrument(skip_all)]
pub async fn update_api_key(
    state: SessionState,
    api_key: api::UpdateApiKeyRequest,
) -> RouterResponse<api::RetrieveApiKeyResponse> {
    let merchant_id = api_key.merchant_id.clone();
    let key_id = api_key.key_id.clone();
    let store = state.store.as_ref();

    let api_key = store
        .update_api_key(
            merchant_id.to_owned(),
            key_id.to_owned(),
            api_key.foreign_into(),
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::ApiKeyNotFound)?;

    let state_inner = state.clone();
    let hashed_api_key = api_key.hashed_api_key.clone();
    let key_id_inner = api_key.key_id.clone();
    let expires_at = api_key.expires_at;

    authentication::decision::spawn_tracked_job(
        async move {
            authentication::decision::add_api_key(
                &state_inner,
                hashed_api_key.into_inner().into(),
                merchant_id.clone(),
                key_id_inner,
                expires_at.map(authentication::decision::convert_expiry),
            )
            .await
        },
        authentication::decision::ADD,
    );

    #[cfg(feature = "email")]
    {
        let expiry_reminder_days = state.conf.api_keys.get_inner().expiry_reminder_days.clone();

        let task_id = generate_task_id_for_api_key_expiry_workflow(&key_id);
        // In order to determine how to update the existing process in the process_tracker table,
        // we need access to the current entry in the table.
        let existing_process_tracker_task = store
            .find_process_by_id(task_id.as_str())
            .await
            .change_context(errors::ApiErrorResponse::InternalServerError) // If retrieve failed
            .attach_printable(
                "Failed to retrieve API key expiry reminder task from process tracker",
            )?;

        // If process exist
        if existing_process_tracker_task.is_some() {
            if api_key.expires_at.is_some() {
                // Process exist in process, update the process with new schedule_time
                update_api_key_expiry_task(store, &api_key, expiry_reminder_days)
                    .await
                    .change_context(errors::ApiErrorResponse::InternalServerError)
                    .attach_printable(
                        "Failed to update API key expiry reminder task in process tracker",
                    )?;
            }
            // If an expiry is set to 'never'
            else {
                // Process exist in process, revoke it
                revoke_api_key_expiry_task(store, &key_id)
                    .await
                    .change_context(errors::ApiErrorResponse::InternalServerError)
                    .attach_printable(
                        "Failed to revoke API key expiry reminder task in process tracker",
                    )?;
            }
        }
        // This case occurs if the expiry for an API key is set to 'never' during its creation. If so,
        // process in tracker was not created.
        else if api_key.expires_at.is_some() {
            // Process doesn't exist in process_tracker table, so create new entry with
            // schedule_time based on new expiry set.
            add_api_key_expiry_task(store, &api_key, expiry_reminder_days)
                .await
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable(
                    "Failed to insert API key expiry reminder task to process tracker",
                )?;
        }
    }

    Ok(ApplicationResponse::Json(api_key.foreign_into()))
}

// Update api_key_expiry task in the process_tracker table.
// Construct Update variant of ProcessTrackerUpdate with new tracking_data.
// A task is not scheduled if the time for the first email is in the past.
#[cfg(feature = "email")]
#[instrument(skip_all)]
pub async fn update_api_key_expiry_task(
    store: &dyn crate::db::StorageInterface,
    api_key: &ApiKey,
    expiry_reminder_days: Vec<u8>,
) -> Result<(), errors::ProcessTrackerError> {
    let current_time = date_time::now();

    let schedule_time = expiry_reminder_days
        .first()
        .and_then(|expiry_reminder_day| {
            api_key.expires_at.map(|expires_at| {
                expires_at.saturating_sub(time::Duration::days(i64::from(*expiry_reminder_day)))
            })
        });

    if let Some(schedule_time) = schedule_time {
        if schedule_time <= current_time {
            return Ok(());
        }
    }

    let task_id = generate_task_id_for_api_key_expiry_workflow(&api_key.key_id);

    let task_ids = vec![task_id.clone()];

    let updated_tracking_data = &storage::ApiKeyExpiryTrackingData {
        key_id: api_key.key_id.clone(),
        merchant_id: api_key.merchant_id.clone(),
        api_key_name: api_key.name.clone(),
        prefix: api_key.prefix.clone(),
        api_key_expiry: api_key.expires_at,
        expiry_reminder_days,
    };

    let updated_api_key_expiry_workflow_model = serde_json::to_value(updated_tracking_data)
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable_lazy(|| {
            format!("unable to serialize API key expiry tracker: {updated_tracking_data:?}")
        })?;

    let updated_process_tracker_data = storage::ProcessTrackerUpdate::Update {
        name: None,
        retry_count: Some(0),
        schedule_time,
        tracking_data: Some(updated_api_key_expiry_workflow_model),
        business_status: Some(String::from(
            diesel_models::process_tracker::business_status::PENDING,
        )),
        status: Some(storage_enums::ProcessTrackerStatus::New),
        updated_at: Some(current_time),
    };
    store
        .process_tracker_update_process_status_by_ids(task_ids, updated_process_tracker_data)
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)?;

    Ok(())
}

#[instrument(skip_all)]
pub async fn revoke_api_key(
    state: SessionState,
    merchant_id: common_utils::id_type::MerchantId,
    key_id: &common_utils::id_type::ApiKeyId,
) -> RouterResponse<api::RevokeApiKeyResponse> {
    let store = state.store.as_ref();

    let api_key = store
        .find_api_key_by_merchant_id_key_id_optional(&merchant_id, key_id)
        .await
        .to_not_found_response(errors::ApiErrorResponse::ApiKeyNotFound)?;

    let revoked = store
        .revoke_api_key(&merchant_id, key_id)
        .await
        .to_not_found_response(errors::ApiErrorResponse::ApiKeyNotFound)?;

    if let Some(api_key) = api_key {
        let hashed_api_key = api_key.hashed_api_key;
        let state = state.clone();

        authentication::decision::spawn_tracked_job(
            async move {
                authentication::decision::revoke_api_key(&state, hashed_api_key.into_inner().into())
                    .await
            },
            authentication::decision::REVOKE,
        );
    }

    metrics::API_KEY_REVOKED.add(1, &[]);

    #[cfg(feature = "email")]
    {
        let task_id = generate_task_id_for_api_key_expiry_workflow(key_id);
        // In order to determine how to update the existing process in the process_tracker table,
        // we need access to the current entry in the table.
        let existing_process_tracker_task = store
            .find_process_by_id(task_id.as_str())
            .await
            .change_context(errors::ApiErrorResponse::InternalServerError) // If retrieve failed
            .attach_printable(
                "Failed to retrieve API key expiry reminder task from process tracker",
            )?;

        // If process exist, then revoke it
        if existing_process_tracker_task.is_some() {
            revoke_api_key_expiry_task(store, key_id)
                .await
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable(
                    "Failed to revoke API key expiry reminder task in process tracker",
                )?;
        }
    }

    Ok(ApplicationResponse::Json(api::RevokeApiKeyResponse {
        merchant_id: merchant_id.to_owned(),
        key_id: key_id.to_owned(),
        revoked,
    }))
}

// Function to revoke api_key_expiry task in the process_tracker table when API key is revoked.
// Construct StatusUpdate variant of ProcessTrackerUpdate by setting status to 'finish'.
#[cfg(feature = "email")]
#[instrument(skip_all)]
pub async fn revoke_api_key_expiry_task(
    store: &dyn crate::db::StorageInterface,
    key_id: &common_utils::id_type::ApiKeyId,
) -> Result<(), errors::ProcessTrackerError> {
    let task_id = generate_task_id_for_api_key_expiry_workflow(key_id);
    let task_ids = vec![task_id];
    let updated_process_tracker_data = storage::ProcessTrackerUpdate::StatusUpdate {
        status: storage_enums::ProcessTrackerStatus::Finish,
        business_status: Some(String::from(diesel_models::business_status::REVOKED)),
    };

    store
        .process_tracker_update_process_status_by_ids(task_ids, updated_process_tracker_data)
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)?;

    Ok(())
}

#[instrument(skip_all)]
pub async fn list_api_keys(
    state: SessionState,
    merchant_id: common_utils::id_type::MerchantId,
    limit: Option<i64>,
    offset: Option<i64>,
) -> RouterResponse<Vec<api::RetrieveApiKeyResponse>> {
    let store = state.store.as_ref();
    let api_keys = store
        .list_api_keys_by_merchant_id(&merchant_id, limit, offset)
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Failed to list merchant API keys")?;
    let api_keys = api_keys
        .into_iter()
        .map(ForeignInto::foreign_into)
        .collect();

    Ok(ApplicationResponse::Json(api_keys))
}

#[cfg(feature = "email")]
fn generate_task_id_for_api_key_expiry_workflow(
    key_id: &common_utils::id_type::ApiKeyId,
) -> String {
    format!(
        "{API_KEY_EXPIRY_RUNNER}_{API_KEY_EXPIRY_NAME}_{}",
        key_id.get_string_repr()
    )
}

impl From<&str> for PlaintextApiKey {
    fn from(s: &str) -> Self {
        Self(s.to_owned().into())
    }
}

impl From<String> for PlaintextApiKey {
    fn from(s: String) -> Self {
        Self(s.into())
    }
}

impl From<HashedApiKey> for storage::HashedApiKey {
    fn from(hashed_api_key: HashedApiKey) -> Self {
        hashed_api_key.0.into()
    }
}

impl From<storage::HashedApiKey> for HashedApiKey {
    fn from(hashed_api_key: storage::HashedApiKey) -> Self {
        Self(hashed_api_key.into_inner())
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::expect_used, clippy::unwrap_used)]
    use super::*;

    #[tokio::test]
    async fn test_hashing_and_verification() {
        let settings = settings::Settings::new().expect("invalid settings");

        let plaintext_api_key = PlaintextApiKey::new(consts::API_KEY_LENGTH);
        let hash_key = settings.api_keys.get_inner().get_hash_key().unwrap();
        let hashed_api_key = plaintext_api_key.keyed_hash(hash_key.peek());

        assert_ne!(
            plaintext_api_key.0.peek().as_bytes(),
            hashed_api_key.0.as_bytes()
        );

        let new_hashed_api_key = plaintext_api_key.keyed_hash(hash_key.peek());
        assert_eq!(hashed_api_key, new_hashed_api_key)
    }
}


pub struct GrpcRecoveryHeaders {
    /// Request id
    pub request_id: Option<String>,
}


pub struct RetrieveCardResp {
    pub status: String,
    pub error_message: Option<String>,
    pub error_code: Option<String>,
    pub payload: Option<RetrieveCardRespPayload>,
}


pub fn construct_connector_invoke_hidden_frame(
    connector_three_ds_invoke_data: api_models::payments::PaymentsConnectorThreeDsInvokeData,
) -> RouterResult<api_models::payments::NextActionData> {
    let iframe_data = api_models::payments::IframeData::ThreedsInvokeAndCompleteAutorize {
        three_ds_method_data_submission: connector_three_ds_invoke_data
            .three_ds_method_data_submission,
        three_ds_method_data: Some(connector_three_ds_invoke_data.three_ds_method_data),
        three_ds_method_url: connector_three_ds_invoke_data.three_ds_method_url,
        directory_server_id: connector_three_ds_invoke_data.directory_server_id,
        message_version: connector_three_ds_invoke_data.message_version,
    };

    Ok(api_models::payments::NextActionData::InvokeHiddenIframe { iframe_data })
}


pub fn is_separate_version_call_required(self) -> bool {
        match self {
            Self::Threedsecureio
            | Self::Netcetera
            | Self::CtpMastercard
            | Self::UnifiedAuthenticationService
            | Self::Juspaythreedsserver
            | Self::CtpVisa => false,
            Self::Gpayments => true,
        }
    }


File: crates/payment_methods/src/core/errors.rs

pub use common_utils::errors::{CustomResult, ParsingError, ValidationError};
pub use hyperswitch_domain_models::{
    api,
    errors::api_error_response::{self, *},
};

pub type PmResult<T> = CustomResult<T, ApiErrorResponse>;
pub type PmResponse<T> = CustomResult<api::ApplicationResponse<T>, ApiErrorResponse>;
pub type VaultResult<T> = CustomResult<T, VaultError>;

#[derive(Debug, thiserror::Error)]
pub enum VaultError {
    #[error("Failed to save card in card vault")]
    SaveCardFailed,
    #[error("Failed to fetch card details from card vault")]
    FetchCardFailed,
    #[error("Failed to delete card in card vault")]
    DeleteCardFailed,
    #[error("Failed to encode card vault request")]
    RequestEncodingFailed,
    #[error("Failed to deserialize card vault response")]
    ResponseDeserializationFailed,
    #[error("Failed to create payment method")]
    PaymentMethodCreationFailed,
    #[error("The given payment method is currently not supported in vault")]
    PaymentMethodNotSupported,
    #[error("The given payout method is currently not supported in vault")]
    PayoutMethodNotSupported,
    #[error("Missing required field: {field_name}")]
    MissingRequiredField { field_name: &'static str },
    #[error("The card vault returned an unexpected response: {0:?}")]
    UnexpectedResponseError(bytes::Bytes),
    #[error("Failed to update in PMD table")]
    UpdateInPaymentMethodDataTableFailed,
    #[error("Failed to fetch payment method in vault")]
    FetchPaymentMethodFailed,
    #[error("Failed to save payment method in vault")]
    SavePaymentMethodFailed,
    #[error("Failed to generate fingerprint")]
    GenerateFingerprintFailed,
    #[error("Failed to encrypt vault request")]
    RequestEncryptionFailed,
    #[error("Failed to decrypt vault response")]
    ResponseDecryptionFailed,
    #[error("Failed to call vault")]
    VaultAPIError,
    #[error("Failed while calling locker API")]
    ApiError,
}


pub struct VoidResponse {
    response_code: AuthorizedotnetVoidStatus,
    #[serde(rename = "transId")]
    transaction_id: String,
    network_trans_id: Option<Secret<String>>,
    pub account_number: Option<Secret<String>>,
    pub errors: Option<Vec<ErrorMessage>>,
}


pub struct DeleteCardToken {
    pub card_reference: String, //network token requestor ref id
    pub customer_id: id_type::GlobalCustomerId,
}


Documentation: api-reference/v1/poll/poll--retrieve-poll-status.mdx
# Type: Doc File

---
openapi: get /poll/status/{poll_id}
---


impl fmt::Display for BokuRefundReasonCode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::NonFulfillment => write!(f, "8"),
        }
    }
}

impl fmt::Display for for BokuRefundReasonCode


pub struct OpennodeRouterData<T> {
    pub amount: MinorUnit,
    pub router_data: T,
}


pub struct Level(pub(super) tracing::Level);


impl api::Refund for Katapult {}


impl api::MandateSetup for Phonepe {}


pub struct SignifydPaymentsRecordReturnResponse {
    return_id: String,
    order_id: String,
}


pub async fn list_payments(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    constraints: api::PaymentListConstraints,
) -> RouterResponse<payments_api::PaymentListResponse> {
    common_utils::metrics::utils::record_operation_time(
        async {
            let limit = &constraints.limit;
            helpers::validate_payment_list_request_for_joins(*limit)?;
            let db: &dyn StorageInterface = state.store.as_ref();
            let fetch_constraints = constraints.clone().into();
            let list: Vec<(storage::PaymentIntent, Option<storage::PaymentAttempt>)> = db
                .get_filtered_payment_intents_attempt(
                    &(&state).into(),
                    merchant_context.get_merchant_account().get_id(),
                    &fetch_constraints,
                    merchant_context.get_merchant_key_store(),
                    merchant_context.get_merchant_account().storage_scheme,
                )
                .await
                .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
            let data: Vec<api_models::payments::PaymentsListResponseItem> =
                list.into_iter().map(ForeignFrom::foreign_from).collect();

            let active_attempt_ids = db
                .get_filtered_active_attempt_ids_for_total_count(
                    merchant_context.get_merchant_account().get_id(),
                    &fetch_constraints,
                    merchant_context.get_merchant_account().storage_scheme,
                )
                .await
                .to_not_found_response(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Error while retrieving active_attempt_ids for merchant")?;

            let total_count = if constraints.has_no_attempt_filters() {
                i64::try_from(active_attempt_ids.len())
                    .change_context(errors::ApiErrorResponse::InternalServerError)
                    .attach_printable("Error while converting from usize to i64")
            } else {
                let active_attempt_ids = active_attempt_ids
                    .into_iter()
                    .flatten()
                    .collect::<Vec<String>>();

                db.get_total_count_of_filtered_payment_attempts(
                    merchant_context.get_merchant_account().get_id(),
                    &active_attempt_ids,
                    constraints.connector,
                    constraints.payment_method_type,
                    constraints.payment_method_subtype,
                    constraints.authentication_type,
                    constraints.merchant_connector_id,
                    constraints.card_network,
                    merchant_context.get_merchant_account().storage_scheme,
                )
                .await
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Error while retrieving total count of payment attempts")
            }?;

            Ok(services::ApplicationResponse::Json(
                api_models::payments::PaymentListResponse {
                    count: data.len(),
                    total_count,
                    data,
                },
            ))
        },
        &metrics::PAYMENT_LIST_LATENCY,
        router_env::metric_attributes!((
            "merchant_id",
            merchant_context.get_merchant_account().get_id().clone()
        )),
    )
    .await
}


File: crates/analytics/src/disputes/accumulators.rs
 Public functions: 1
 Public structs: 3

use api_models::analytics::disputes::DisputeMetricsBucketValue;
use diesel_models::enums as storage_enums;

use super::metrics::DisputeMetricRow;
#[derive(Debug, Default)]
pub struct DisputeMetricsAccumulator {
    pub disputes_status_rate: RateAccumulator,
    pub disputed_amount: DisputedAmountAccumulator,
    pub dispute_lost_amount: DisputedAmountAccumulator,
}
#[derive(Debug, Default)]
pub struct RateAccumulator {
    pub won_count: i64,
    pub challenged_count: i64,
    pub lost_count: i64,
    pub total: i64,
}
#[derive(Debug, Default)]
#[repr(transparent)]
pub struct DisputedAmountAccumulator {
    pub total: Option<i64>,
}

pub trait DisputeMetricAccumulator {
    type MetricOutput;

    fn add_metrics_bucket(&mut self, metrics: &DisputeMetricRow);

    fn collect(self) -> Self::MetricOutput;
}

impl DisputeMetricAccumulator for DisputedAmountAccumulator {
    type MetricOutput = Option<u64>;
    #[inline]
    fn add_metrics_bucket(&mut self, metrics: &DisputeMetricRow) {
        self.total = match (
            self.total,
            metrics
                .total
                .as_ref()
                .and_then(bigdecimal::ToPrimitive::to_i64),
        ) {
            (None, None) => None,
            (None, i @ Some(_)) | (i @ Some(_), None) => i,
            (Some(a), Some(b)) => Some(a + b),
        }
    }
    #[inline]
    fn collect(self) -> Self::MetricOutput {
        self.total.and_then(|i| u64::try_from(i).ok())
    }
}

impl DisputeMetricAccumulator for RateAccumulator {
    type MetricOutput = Option<(Option<u64>, Option<u64>, Option<u64>, Option<u64>)>;

    fn add_metrics_bucket(&mut self, metrics: &DisputeMetricRow) {
        if let Some(ref dispute_status) = metrics.dispute_status {
            if dispute_status.as_ref() == &storage_enums::DisputeStatus::DisputeChallenged {
                self.challenged_count += metrics.count.unwrap_or_default();
            }
            if dispute_status.as_ref() == &storage_enums::DisputeStatus::DisputeWon {
                self.won_count += metrics.count.unwrap_or_default();
            }
            if dispute_status.as_ref() == &storage_enums::DisputeStatus::DisputeLost {
                self.lost_count += metrics.count.unwrap_or_default();
            }
        };

        self.total += metrics.count.unwrap_or_default();
    }

    fn collect(self) -> Self::MetricOutput {
        if self.total <= 0 {
            Some((None, None, None, None))
        } else {
            Some((
                u64::try_from(self.challenged_count).ok(),
                u64::try_from(self.won_count).ok(),
                u64::try_from(self.lost_count).ok(),
                u64::try_from(self.total).ok(),
            ))
        }
    }
}

impl DisputeMetricsAccumulator {
    pub fn collect(self) -> DisputeMetricsBucketValue {
        let (challenge_rate, won_rate, lost_rate, total_dispute) =
            self.disputes_status_rate.collect().unwrap_or_default();
        DisputeMetricsBucketValue {
            disputes_challenged: challenge_rate,
            disputes_won: won_rate,
            disputes_lost: lost_rate,
            disputed_amount: self.disputed_amount.collect(),
            dispute_lost_amount: self.dispute_lost_amount.collect(),
            total_dispute,
        }
    }
}


impl api::Refund for Rapyd {}


File: crates/analytics/src/api_event/metrics.rs
 Public structs: 1

use api_models::analytics::{
    api_event::{
        ApiEventDimensions, ApiEventFilters, ApiEventMetrics, ApiEventMetricsBucketIdentifier,
    },
    Granularity, TimeRange,
};
use time::PrimitiveDateTime;

use crate::{
    query::{Aggregate, GroupByClause, ToSql, Window},
    types::{AnalyticsCollection, AnalyticsDataSource, LoadRow, MetricsResult},
};

mod api_count;
pub mod latency;
mod status_code_count;
use std::collections::HashSet;

use api_count::ApiCount;
use latency::MaxLatency;
use status_code_count::StatusCodeCount;

use self::latency::LatencyAvg;

#[derive(Debug, PartialEq, Eq, serde::Deserialize, Hash)]
pub struct ApiEventMetricRow {
    pub latency: Option<u64>,
    pub api_count: Option<u64>,
    pub status_code_count: Option<u64>,
    #[serde(with = "common_utils::custom_serde::iso8601::option")]
    pub start_bucket: Option<PrimitiveDateTime>,
    #[serde(with = "common_utils::custom_serde::iso8601::option")]
    pub end_bucket: Option<PrimitiveDateTime>,
}

pub trait ApiEventMetricAnalytics: LoadRow<ApiEventMetricRow> + LoadRow<LatencyAvg> {}

#[async_trait::async_trait]
pub trait ApiEventMetric<T>
where
    T: AnalyticsDataSource + ApiEventMetricAnalytics,
{
    async fn load_metrics(
        &self,
        dimensions: &[ApiEventDimensions],
        merchant_id: &common_utils::id_type::MerchantId,
        filters: &ApiEventFilters,
        granularity: Option<Granularity>,
        time_range: &TimeRange,
        pool: &T,
    ) -> MetricsResult<HashSet<(ApiEventMetricsBucketIdentifier, ApiEventMetricRow)>>;
}

#[async_trait::async_trait]
impl<T> ApiEventMetric<T> for ApiEventMetrics
where
    T: AnalyticsDataSource + ApiEventMetricAnalytics,
    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: &[ApiEventDimensions],
        merchant_id: &common_utils::id_type::MerchantId,
        filters: &ApiEventFilters,
        granularity: Option<Granularity>,
        time_range: &TimeRange,
        pool: &T,
    ) -> MetricsResult<HashSet<(ApiEventMetricsBucketIdentifier, ApiEventMetricRow)>> {
        match self {
            Self::Latency => {
                MaxLatency
                    .load_metrics(
                        dimensions,
                        merchant_id,
                        filters,
                        granularity,
                        time_range,
                        pool,
                    )
                    .await
            }
            Self::ApiCount => {
                ApiCount
                    .load_metrics(
                        dimensions,
                        merchant_id,
                        filters,
                        granularity,
                        time_range,
                        pool,
                    )
                    .await
            }
            Self::StatusCodeCount => {
                StatusCodeCount
                    .load_metrics(
                        dimensions,
                        merchant_id,
                        filters,
                        granularity,
                        time_range,
                        pool,
                    )
                    .await
            }
        }
    }
}


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

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

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

impl ConnectorSpecifications for for Globepay


pub struct FlexitiErrorResponse {
    pub message: String,
    pub error: String,
}


impl api::PaymentVoid for Dlocal {}


pub fn get_metadata_as_secret(
        &self,
    ) -> CustomResult<Option<pii::SecretSerdeValue>, errors::ParsingError> {
        self.metadata
            .as_ref()
            .map(|metadata| metadata.encode_to_value().map(Secret::new))
            .transpose()
    }


pub struct EventContext<T, A>
where
    A: MessagingInterface<MessageClass = T>,
{
    message_sink: Arc<A>,
    metadata: HashMap<String, Value>,
}


impl api::PaymentAuthorize for Razorpay {}

impl api::PaymentAuthorize for for Razorpay


pub fn is_deletable(&self) -> bool {
        self.is_deletable
    }


pub struct ApplePayPaymentInformation {
    tokenized_card: TokenizedCard,
}


pub async fn list_initial_webhook_delivery_attempts(
    state: web::Data<AppState>,
    req: HttpRequest,
    path: web::Path<common_utils::id_type::MerchantId>,
    json_payload: web::Json<EventListConstraints>,
) -> impl Responder {
    let flow = Flow::WebhookEventInitialDeliveryAttemptList;
    let merchant_id = path.into_inner();
    let constraints = json_payload.into_inner();

    let request_internal = EventListRequestInternal {
        merchant_id: merchant_id.clone(),
        constraints,
    };

    Box::pin(api::server_wrap(
        flow,
        state,
        &req,
        request_internal,
        |state, _, request_internal, _| {
            webhook_events::list_initial_delivery_attempts(
                state,
                request_internal.merchant_id,
                request_internal.constraints,
            )
        },
        auth::auth_type(
            &auth::AdminApiAuth,
            &auth::JWTAuthMerchantFromRoute {
                merchant_id,
                required_permission: Permission::MerchantWebhookEventRead,
            },
            req.headers(),
        ),
        api_locking::LockAction::NotApplicable,
    ))
    .await
}


pub trait DecisionEngineApiHandler {
    async fn send_decision_engine_request<Req, Res>(
        state: &SessionState,
        http_method: services::Method,
        path: &str,
        request_body: Option<Req>, // Option to handle GET/DELETE requests without body
        timeout: Option<u64>,
        events_wrapper: Option<RoutingEventsWrapper<Req>>,
    ) -> RoutingResult<RoutingEventsResponse<Res>>
    where
        Req: Serialize + Send + Sync + 'static + Clone,
        Res: Serialize + serde::de::DeserializeOwned + Send + 'static + std::fmt::Debug + Clone;
}


pub struct GetSearchResponse {
    pub count: u64,
    pub index: SearchIndex,
    pub hits: Vec<Value>,
    pub status: SearchStatus,
}


pub struct ApiRequest {
    merchant_transaction_id: String,
    payment_channel: ZenPaymentChannels,
    amount: String,
    currency: enums::Currency,
    payment_specific_data: ZenPaymentSpecificData,
    customer: ZenCustomerDetails,
    custom_ipn_url: String,
    items: Vec<ZenItemObject>,
}


pub async fn insert(self, conn: &PgPooledConn) -> StorageResult<Authorization> {
        generics::generic_insert(conn, self).await
    }


impl OutgoingWebhookEventMetric for OutgoingWebhookContent {
    fn get_outgoing_webhook_event_content(&self) -> Option<OutgoingWebhookEventContent> {
        match self {
            Self::PaymentDetails(payment_payload) => Some(OutgoingWebhookEventContent::Payment {
                payment_id: payment_payload.payment_id.clone(),
                content: masking::masked_serialize(&payment_payload)
                    .unwrap_or(serde_json::json!({"error":"failed to serialize"})),
            }),
            Self::RefundDetails(refund_payload) => Some(OutgoingWebhookEventContent::Refund {
                payment_id: refund_payload.payment_id.clone(),
                refund_id: refund_payload.get_refund_id_as_string(),
                content: masking::masked_serialize(&refund_payload)
                    .unwrap_or(serde_json::json!({"error":"failed to serialize"})),
            }),
            Self::DisputeDetails(dispute_payload) => Some(OutgoingWebhookEventContent::Dispute {
                payment_id: dispute_payload.payment_id.clone(),
                attempt_id: dispute_payload.attempt_id.clone(),
                dispute_id: dispute_payload.dispute_id.clone(),
                content: masking::masked_serialize(&dispute_payload)
                    .unwrap_or(serde_json::json!({"error":"failed to serialize"})),
            }),
            Self::MandateDetails(mandate_payload) => Some(OutgoingWebhookEventContent::Mandate {
                payment_method_id: mandate_payload.payment_method_id.clone(),
                mandate_id: mandate_payload.mandate_id.clone(),
                content: masking::masked_serialize(&mandate_payload)
                    .unwrap_or(serde_json::json!({"error":"failed to serialize"})),
            }),
            #[cfg(feature = "payouts")]
            Self::PayoutDetails(payout_payload) => Some(OutgoingWebhookEventContent::Payout {
                payout_id: payout_payload.payout_id.clone(),
                content: masking::masked_serialize(&payout_payload)
                    .unwrap_or(serde_json::json!({"error":"failed to serialize"})),
            }),
        }
    }
}

impl OutgoingWebhookEventMetric for for OutgoingWebhookContent


pub fn validate(&self) -> Result<(), ApplicationError> {
        use common_utils::fp_utils::when;

        when(self.jwt_secret.is_default_or_empty(), || {
            Err(ApplicationError::InvalidConfigurationValueError(
                "JWT secret must not be empty".into(),
            ))
        })?;

        when(self.admin_api_key.is_default_or_empty(), || {
            Err(ApplicationError::InvalidConfigurationValueError(
                "admin API key must not be empty".into(),
            ))
        })?;

        when(self.master_enc_key.is_default_or_empty(), || {
            Err(ApplicationError::InvalidConfigurationValueError(
                "Master encryption key must not be empty".into(),
            ))
        })
    }


pub struct BluecodeQrRedirect {}


Documentation: api-reference/v2/connector-account/connector-account--retrieve.mdx
# Type: Doc File

---
openapi: get /v2/connector-accounts/{id}
---


OpenAPI Block Path: components.schemas.MultibancoTransferInstructions
{
  "type": "object",
  "required": [
    "reference",
    "entity"
  ],
  "properties": {
    "reference": {
      "type": "string",
      "example": "122385736258"
    },
    "entity": {
      "type": "string",
      "example": "12345"
    }
  }
}


pub fn get_payment_methods_supporting_extended_authorization(self) -> HashSet<PaymentMethod> {
        HashSet::from([PaymentMethod::Card])
    }


pub struct PaymentDmnNotification {
    #[serde(rename = "PPP_TransactionID")]
    pub ppp_transaction_id: Option<String>,
    #[serde(rename = "TransactionID")]
    pub transaction_id: Option<String>,
    pub status: Option<DmnStatus>,
    #[serde(rename = "ErrCode")]
    pub err_code: Option<String>,
    #[serde(rename = "ExErrCode")]
    pub ex_err_code: Option<String>,
    pub desc: Option<String>,
    pub merchant_unique_id: Option<String>,
    pub custom_data: Option<String>,
    pub product_id: Option<String>,
    pub first_name: Option<String>,
    pub last_name: Option<String>,
    pub email: Option<String>,
    pub total_amount: Option<String>,
    pub currency: Option<String>,
    pub fee: Option<String>,
    #[serde(rename = "AuthCode")]
    pub auth_code: Option<String>,
    pub transaction_status: Option<TransactionStatus>,
    pub transaction_type: Option<PaymentTransactionType>,
    #[serde(rename = "user_token_id")]
    pub user_token_id: Option<String>,
    #[serde(rename = "payment_method")]
    pub payment_method: Option<String>,
    #[serde(rename = "responseTimeStamp")]
    pub response_time_stamp: Option<String>,
    #[serde(rename = "invoice_id")]
    pub invoice_id: Option<String>,
    #[serde(rename = "merchant_id")]
    pub merchant_id: Option<String>,
    #[serde(rename = "merchant_site_id")]
    pub merchant_site_id: Option<String>,
    #[serde(rename = "responsechecksum")]
    pub response_checksum: Option<String>,
    #[serde(rename = "advanceResponseChecksum")]
    pub advance_response_checksum: Option<String>,
}