// File: crates/router/src/core/utils/refunds_validator.rs
// Module: router
// Public functions: 10

use diesel_models::refund as diesel_refund;
use error_stack::report;
use router_env::{instrument, tracing};
use time::PrimitiveDateTime;

use crate::{
    core::errors::{self, CustomResult, RouterResult},
    types::{
        self,
        api::enums as api_enums,
        storage::{self, enums},
    },
    utils::{self, OptionExt},
};

// Limit constraints for refunds list flow
pub const LOWER_LIMIT: i64 = 1;
pub const UPPER_LIMIT: i64 = 100;
pub const DEFAULT_LIMIT: i64 = 10;

#[derive(Debug, thiserror::Error)]
pub enum RefundValidationError {
    #[error("The payment attempt was not successful")]
    UnsuccessfulPaymentAttempt,
    #[error("The refund amount exceeds the amount captured")]
    RefundAmountExceedsPaymentAmount,
    #[error("The order has expired")]
    OrderExpired,
    #[error("The maximum refund count for this payment attempt")]
    MaxRefundCountReached,
    #[error("There is already another refund request for this payment attempt")]
    DuplicateRefund,
}

#[instrument(skip_all)]
pub fn validate_success_transaction(
    transaction: &storage::PaymentAttempt,
) -> CustomResult<(), RefundValidationError> {
    if transaction.status != enums::AttemptStatus::Charged {
        Err(report!(RefundValidationError::UnsuccessfulPaymentAttempt))?
    }

    Ok(())
}

#[instrument(skip_all)]
pub fn validate_refund_amount(
    amount_captured: i64,
    all_refunds: &[diesel_refund::Refund],
    refund_amount: i64,
) -> CustomResult<(), RefundValidationError> {
    let total_refunded_amount: i64 = all_refunds
        .iter()
        .filter_map(|refund| {
            if refund.refund_status != enums::RefundStatus::Failure
                && refund.refund_status != enums::RefundStatus::TransactionFailure
            {
                Some(refund.refund_amount.get_amount_as_i64())
            } else {
                None
            }
        })
        .sum();

    utils::when(
        refund_amount > (amount_captured - total_refunded_amount),
        || {
            Err(report!(
                RefundValidationError::RefundAmountExceedsPaymentAmount
            ))
        },
    )
}

#[instrument(skip_all)]
pub fn validate_payment_order_age(
    created_at: &PrimitiveDateTime,
    refund_max_age: i64,
) -> CustomResult<(), RefundValidationError> {
    let current_time = common_utils::date_time::now();

    utils::when(
        (current_time - *created_at).whole_days() > refund_max_age,
        || Err(report!(RefundValidationError::OrderExpired)),
    )
}

#[instrument(skip_all)]
pub fn validate_maximum_refund_against_payment_attempt(
    all_refunds: &[diesel_refund::Refund],
    refund_max_attempts: usize,
) -> CustomResult<(), RefundValidationError> {
    utils::when(all_refunds.len() > refund_max_attempts, || {
        Err(report!(RefundValidationError::MaxRefundCountReached))
    })
}

pub fn validate_refund_list(limit: Option<i64>) -> CustomResult<i64, errors::ApiErrorResponse> {
    match limit {
        Some(limit_val) => {
            if !(LOWER_LIMIT..=UPPER_LIMIT).contains(&limit_val) {
                Err(errors::ApiErrorResponse::InvalidRequestData {
                    message: "limit should be in between 1 and 100".to_string(),
                }
                .into())
            } else {
                Ok(limit_val)
            }
        }
        None => Ok(DEFAULT_LIMIT),
    }
}

#[cfg(feature = "v1")]
pub fn validate_for_valid_refunds(
    payment_attempt: &hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt,
    connector: api_models::enums::Connector,
) -> RouterResult<()> {
    let payment_method = payment_attempt
        .payment_method
        .as_ref()
        .get_required_value("payment_method")?;

    match payment_method {
        diesel_models::enums::PaymentMethod::PayLater
        | diesel_models::enums::PaymentMethod::Wallet => {
            let payment_method_type = payment_attempt
                .payment_method_type
                .get_required_value("payment_method_type")?;

            utils::when(
                matches!(
                    (connector, payment_method_type),
                    (
                        api_models::enums::Connector::Braintree,
                        diesel_models::enums::PaymentMethodType::Paypal,
                    )
                ),
                || {
                    Err(errors::ApiErrorResponse::RefundNotPossible {
                        connector: connector.to_string(),
                    }
                    .into())
                },
            )
        }
        _ => Ok(()),
    }
}

#[cfg(feature = "v2")]
pub fn validate_for_valid_refunds(
    payment_attempt: &hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt,
    connector: api_models::enums::Connector,
) -> RouterResult<()> {
    let payment_method_type = payment_attempt.payment_method_type;

    match payment_method_type {
        diesel_models::enums::PaymentMethod::PayLater
        | diesel_models::enums::PaymentMethod::Wallet => {
            let payment_method_subtype = payment_attempt.payment_method_subtype;

            utils::when(
                matches!(
                    (connector, payment_method_subtype),
                    (
                        api_models::enums::Connector::Braintree,
                        diesel_models::enums::PaymentMethodType::Paypal,
                    )
                ),
                || {
                    Err(errors::ApiErrorResponse::RefundNotPossible {
                        connector: connector.to_string(),
                    }
                    .into())
                },
            )
        }
        _ => Ok(()),
    }
}

pub fn validate_stripe_charge_refund(
    charge_type_option: Option<api_enums::PaymentChargeType>,
    split_refund_request: &Option<common_types::refunds::SplitRefund>,
) -> RouterResult<types::ChargeRefundsOptions> {
    let charge_type = charge_type_option.ok_or_else(|| {
        report!(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("Missing `charge_type` in PaymentAttempt.")
    })?;

    let refund_request = match split_refund_request {
        Some(common_types::refunds::SplitRefund::StripeSplitRefund(stripe_split_refund)) => {
            stripe_split_refund
        }
        _ => Err(errors::ApiErrorResponse::MissingRequiredField {
            field_name: "stripe_split_refund",
        })?,
    };

    let options = match charge_type {
        api_enums::PaymentChargeType::Stripe(api_enums::StripeChargeType::Direct) => {
            types::ChargeRefundsOptions::Direct(types::DirectChargeRefund {
                revert_platform_fee: refund_request
                    .revert_platform_fee
                    .get_required_value("revert_platform_fee")?,
            })
        }
        api_enums::PaymentChargeType::Stripe(api_enums::StripeChargeType::Destination) => {
            types::ChargeRefundsOptions::Destination(types::DestinationChargeRefund {
                revert_platform_fee: refund_request
                    .revert_platform_fee
                    .get_required_value("revert_platform_fee")?,
                revert_transfer: refund_request
                    .revert_transfer
                    .get_required_value("revert_transfer")?,
            })
        }
    };

    Ok(options)
}

pub fn validate_adyen_charge_refund(
    adyen_split_payment_response: &common_types::domain::AdyenSplitData,
    adyen_split_refund_request: &common_types::domain::AdyenSplitData,
) -> RouterResult<()> {
    if adyen_split_refund_request.store != adyen_split_payment_response.store {
        return Err(report!(errors::ApiErrorResponse::InvalidDataValue {
            field_name: "split_payments.adyen_split_payment.store",
        }));
    };

    for refund_split_item in adyen_split_refund_request.split_items.iter() {
        let refund_split_reference = refund_split_item.reference.clone();
        let matching_payment_split_item = adyen_split_payment_response
            .split_items
            .iter()
            .find(|payment_split_item| refund_split_reference == payment_split_item.reference);

        if let Some(payment_split_item) = matching_payment_split_item {
            if let Some((refund_amount, payment_amount)) =
                refund_split_item.amount.zip(payment_split_item.amount)
            {
                if refund_amount > payment_amount {
                    return Err(report!(errors::ApiErrorResponse::InvalidRequestData {
                        message: format!(
                            "Invalid refund amount for split item, reference: {refund_split_reference}",

                        ),
                    }));
                }
            }

            if let Some((refund_account, payment_account)) = refund_split_item
                .account
                .as_ref()
                .zip(payment_split_item.account.as_ref())
            {
                if !refund_account.eq(payment_account) {
                    return Err(report!(errors::ApiErrorResponse::InvalidRequestData {
                        message: format!(
                            "Invalid refund account for split item, reference: {refund_split_reference}",

                        ),
                    }));
                }
            }

            if refund_split_item.split_type != payment_split_item.split_type {
                return Err(report!(errors::ApiErrorResponse::InvalidRequestData {
                    message: format!(
                        "Invalid refund split_type for split item, reference: {refund_split_reference}",

                    ),
                }));
            }
        } else {
            return Err(report!(errors::ApiErrorResponse::InvalidRequestData {
                message: format!(
                    "No matching payment split item found for reference: {refund_split_reference}",
                ),
            }));
        }
    }
    Ok(())
}
pub fn validate_xendit_charge_refund(
    xendit_split_payment_response: &common_types::payments::XenditChargeResponseData,
    xendit_split_refund_request: &common_types::domain::XenditSplitSubMerchantData,
) -> RouterResult<Option<String>> {
    match xendit_split_payment_response {
        common_types::payments::XenditChargeResponseData::MultipleSplits(
            payment_sub_merchant_data,
        ) => {
            if payment_sub_merchant_data.for_user_id
                != Some(xendit_split_refund_request.for_user_id.clone())
            {
                return Err(errors::ApiErrorResponse::InvalidDataValue {
                    field_name: "xendit_split_refund.for_user_id does not match xendit_split_payment.for_user_id",
                }.into());
            }
            Ok(Some(xendit_split_refund_request.for_user_id.clone()))
        }
        common_types::payments::XenditChargeResponseData::SingleSplit(
            payment_sub_merchant_data,
        ) => {
            if payment_sub_merchant_data.for_user_id != xendit_split_refund_request.for_user_id {
                return Err(errors::ApiErrorResponse::InvalidDataValue {
                    field_name: "xendit_split_refund.for_user_id does not match xendit_split_payment.for_user_id",
                }.into());
            }
            Ok(Some(xendit_split_refund_request.for_user_id.clone()))
        }
    }
}


// Struct: CustomerRequest
// File: crates/api_models/src/customers.rs
// Module: api_models
// Implementations: 2

pub struct CustomerRequest


// Struct: RefundResponse
// File: connector-template/transformers.rs
// Module: connector-template
// Implementations: 0

pub struct RefundResponse


// Function: trigger_payouts_webhook
// File: crates/router/src/utils.rs
// Module: router

pub fn trigger_payouts_webhook(
    state: &SessionState,
    merchant_context: &domain::MerchantContext,
    payout_response: &api_models::payouts::PayoutCreateResponse,
) -> RouterResult<()>


// Implementation: impl ConnectorCommon for for Adyenplatform
// File: crates/hyperswitch_connectors/src/connectors/adyenplatform.rs
// Module: hyperswitch_connectors
// Methods: 4 total (0 public)

impl ConnectorCommon for for Adyenplatform


// Function: get_id
// File: crates/hyperswitch_domain_models/src/router_flow_types/webhooks.rs
// Module: hyperswitch_domain_models

pub fn get_id(&self) -> &masking::Secret<String>


// Implementation: impl api::PaymentToken for for Tsys
// File: crates/hyperswitch_connectors/src/connectors/tsys.rs
// Module: hyperswitch_connectors
// Methods: 0 total (0 public)

impl api::PaymentToken for for Tsys


// Function: validate_payout_link_request
// File: crates/router/src/core/payouts/validator.rs
// Module: router

pub fn validate_payout_link_request(
    req: &payouts::PayoutCreateRequest,
) -> Result<(), errors::ApiErrorResponse>


// Function: get_auth_event_dimensions
// File: crates/analytics/src/utils.rs
// Module: analytics

pub fn get_auth_event_dimensions() -> Vec<NameDescription>


// Implementation: impl api::PaymentToken for for Bankofamerica
// File: crates/hyperswitch_connectors/src/connectors/bankofamerica.rs
// Module: hyperswitch_connectors
// Methods: 0 total (0 public)

impl api::PaymentToken for for Bankofamerica


// File: crates/router/src/services/api/generic_link_response/context.rs
// Module: router
// Public functions: 2

use common_utils::consts::DEFAULT_LOCALE;
use rust_i18n::t;
use tera::Context;

fn get_language(locale_str: &str) -> String {
    let lowercase_str = locale_str.to_lowercase();
    let primary_locale = lowercase_str.split(',').next().unwrap_or("").trim();

    if primary_locale.is_empty() {
        return DEFAULT_LOCALE.to_string();
    }

    let parts = primary_locale.split('-').collect::<Vec<&str>>();
    let language = *parts.first().unwrap_or(&DEFAULT_LOCALE);
    let country = parts.get(1).copied();

    match (language, country) {
        ("en", Some("gb")) => "en_gb".to_string(),
        ("fr", Some("be")) => "fr_be".to_string(),
        (lang, _) => lang.to_string(),
    }
}

pub fn insert_locales_in_context_for_payout_link(context: &mut Context, locale: &str) {
    let language = get_language(locale);
    let locale = language.as_str();
    let i18n_payout_link_title = t!("payout_link.initiate.title", locale = locale);
    let i18n_january = t!("months.january", locale = locale);
    let i18n_february = t!("months.february", locale = locale);
    let i18n_march = t!("months.march", locale = locale);
    let i18n_april = t!("months.april", locale = locale);
    let i18n_may = t!("months.may", locale = locale);
    let i18n_june = t!("months.june", locale = locale);
    let i18n_july = t!("months.july", locale = locale);
    let i18n_august = t!("months.august", locale = locale);
    let i18n_september = t!("months.september", locale = locale);
    let i18n_october = t!("months.october", locale = locale);
    let i18n_november = t!("months.november", locale = locale);
    let i18n_december = t!("months.december", locale = locale);
    let i18n_not_allowed = t!("payout_link.initiate.not_allowed", locale = locale);
    let i18n_am = t!("time.am", locale = locale);
    let i18n_pm = t!("time.pm", locale = locale);

    context.insert("i18n_payout_link_title", &i18n_payout_link_title);
    context.insert("i18n_january", &i18n_january);
    context.insert("i18n_february", &i18n_february);
    context.insert("i18n_march", &i18n_march);
    context.insert("i18n_april", &i18n_april);
    context.insert("i18n_may", &i18n_may);
    context.insert("i18n_june", &i18n_june);
    context.insert("i18n_july", &i18n_july);
    context.insert("i18n_august", &i18n_august);
    context.insert("i18n_september", &i18n_september);
    context.insert("i18n_october", &i18n_october);
    context.insert("i18n_november", &i18n_november);
    context.insert("i18n_december", &i18n_december);
    context.insert("i18n_not_allowed", &i18n_not_allowed);
    context.insert("i18n_am", &i18n_am);
    context.insert("i18n_pm", &i18n_pm);
}

pub fn insert_locales_in_context_for_payout_link_status(context: &mut Context, locale: &str) {
    let language = get_language(locale);
    let locale = language.as_str();
    let i18n_payout_link_status_title = t!("payout_link.status.title", locale = locale);
    let i18n_success_text = t!("payout_link.status.text.success", locale = locale);
    let i18n_success_message = t!("payout_link.status.message.success", locale = locale);
    let i18n_pending_text = t!("payout_link.status.text.processing", locale = locale);
    let i18n_pending_message = t!("payout_link.status.message.processing", locale = locale);
    let i18n_failed_text = t!("payout_link.status.text.failed", locale = locale);
    let i18n_failed_message = t!("payout_link.status.message.failed", locale = locale);
    let i18n_ref_id_text = t!("payout_link.status.info.ref_id", locale = locale);
    let i18n_error_code_text = t!("payout_link.status.info.error_code", locale = locale);
    let i18n_error_message = t!("payout_link.status.info.error_message", locale = locale);
    let i18n_redirecting_text = t!(
        "payout_link.status.redirection_text.redirecting",
        locale = locale
    );
    let i18n_redirecting_in_text = t!(
        "payout_link.status.redirection_text.redirecting_in",
        locale = locale
    );
    let i18n_seconds_text = t!(
        "payout_link.status.redirection_text.seconds",
        locale = locale
    );

    context.insert(
        "i18n_payout_link_status_title",
        &i18n_payout_link_status_title,
    );
    context.insert("i18n_success_text", &i18n_success_text);
    context.insert("i18n_success_message", &i18n_success_message);
    context.insert("i18n_pending_text", &i18n_pending_text);
    context.insert("i18n_pending_message", &i18n_pending_message);
    context.insert("i18n_failed_text", &i18n_failed_text);
    context.insert("i18n_failed_message", &i18n_failed_message);
    context.insert("i18n_ref_id_text", &i18n_ref_id_text);
    context.insert("i18n_error_code_text", &i18n_error_code_text);
    context.insert("i18n_error_message", &i18n_error_message);
    context.insert("i18n_redirecting_text", &i18n_redirecting_text);
    context.insert("i18n_redirecting_in_text", &i18n_redirecting_in_text);
    context.insert("i18n_seconds_text", &i18n_seconds_text);
}


// Struct: AcquirerDetails
// File: crates/router/src/types/api/authentication.rs
// Module: router
// Implementations: 0

pub struct AcquirerDetails


// Function: new
// File: crates/hyperswitch_connectors/src/connectors/fiservemea.rs
// Module: hyperswitch_connectors

pub fn new() -> &'static Self


// Struct: Payone
// File: crates/hyperswitch_connectors/src/connectors/payone.rs
// Module: hyperswitch_connectors
// Implementations: 20
// Traits: Payment, PaymentSession, ConnectorAccessToken, MandateSetup, PaymentAuthorize, PaymentSync, PaymentCapture, PaymentVoid, Refund, RefundExecute, RefundSync, PaymentToken, ConnectorCommon, ConnectorValidation, Payouts, PayoutFulfill, IncomingWebhook, ConnectorErrorTypeMapping, ConnectorSpecifications

pub struct Payone

Payment PaymentSession ConnectorAccessToken MandateSetup PaymentAuthorize PaymentSync PaymentCapture PaymentVoid Refund RefundExecute RefundSync PaymentToken ConnectorCommon ConnectorValidation Payouts PayoutFulfill IncomingWebhook ConnectorErrorTypeMapping ConnectorSpecifications


// Implementation: impl api::RefundSync for for Globalpay
// File: crates/hyperswitch_connectors/src/connectors/globalpay.rs
// Module: hyperswitch_connectors
// Methods: 0 total (0 public)

impl api::RefundSync for for Globalpay


// Function: set_card_details
// File: crates/router/src/core/payment_methods/tokenize/card_executor.rs
// Module: router

pub fn set_card_details(
        self,
        card_req: &'a domain::TokenizeCardRequest,
        optional_card_info: Option<diesel_models::CardInfo>,
    ) -> NetworkTokenizationBuilder<'a, CardDetailsAssigned>


// Struct: Error
// File: crates/hyperswitch_connectors/src/connectors/gigadat/transformers.rs
// Module: hyperswitch_connectors
// Implementations: 0

pub struct Error


// Implementation: impl PaymentSession for for Payone
// File: crates/hyperswitch_connectors/src/connectors/payone.rs
// Module: hyperswitch_connectors
// Methods: 0 total (0 public)

impl PaymentSession for for Payone


// Struct: ErrorParameters
// File: crates/hyperswitch_connectors/src/connectors/aci/transformers.rs
// Module: hyperswitch_connectors
// Implementations: 0

pub struct ErrorParameters


// Function: get_amount_to_capture
// File: crates/hyperswitch_domain_models/src/payments/payment_attempt.rs
// Module: hyperswitch_domain_models

pub fn get_amount_to_capture(&self) -> Option<MinorUnit>


// Implementation: impl OutgoingWebhookEvent
// File: crates/router/src/events/outgoing_webhook_logs.rs
// Module: router
// Methods: 1 total (1 public)

impl OutgoingWebhookEvent


// Implementation: impl ConnectorValidation for for Bluecode
// File: crates/hyperswitch_connectors/src/connectors/bluecode.rs
// Module: hyperswitch_connectors
// Methods: 2 total (0 public)

impl ConnectorValidation for for Bluecode


// Module Structure
// File: crates/router/src/analytics.rs
// Module: router

// Public exports:
pub use analytics::*;


// Implementation: impl api::ConnectorAccessToken for for Braintree
// File: crates/hyperswitch_connectors/src/connectors/braintree.rs
// Module: hyperswitch_connectors
// Methods: 0 total (0 public)

impl api::ConnectorAccessToken for for Braintree


// Function: get_id
// File: crates/hyperswitch_domain_models/src/customer.rs
// Module: hyperswitch_domain_models

pub fn get_id(&self) -> &id_type::CustomerId


// Function: get_merchant_dispute_filters
// File: crates/router/src/analytics.rs
// Module: router

pub fn get_merchant_dispute_filters(
        state: web::Data<AppState>,
        req: actix_web::HttpRequest,
        json_payload: web::Json<api_models::analytics::GetDisputeFilterRequest>,
    ) -> impl Responder


// Function: find_by_internal_reference_id_merchant_id
// File: crates/diesel_models/src/query/refund.rs
// Module: diesel_models

pub fn find_by_internal_reference_id_merchant_id(
        conn: &PgPooledConn,
        internal_reference_id: &str,
        merchant_id: &common_utils::id_type::MerchantId,
    ) -> StorageResult<Self>


// Struct: VerifyTotpRequest
// File: crates/api_models/src/user.rs
// Module: api_models
// Implementations: 0

pub struct VerifyTotpRequest


// Implementation: impl ConnectorValidation for for Nexinets
// File: crates/hyperswitch_connectors/src/connectors/nexinets.rs
// Module: hyperswitch_connectors
// Methods: 1 total (0 public)

impl ConnectorValidation for for Nexinets


// File: crates/router/src/utils/currency.rs
// Module: router
// Public functions: 3
// Public structs: 1

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

use api_models::enums;
use common_utils::{date_time, errors::CustomResult, events::ApiEventMetric, ext_traits::AsyncExt};
use currency_conversion::types::{CurrencyFactors, ExchangeRates};
use error_stack::ResultExt;
use masking::PeekInterface;
use redis_interface::DelReply;
use router_env::{instrument, tracing};
use rust_decimal::Decimal;
use strum::IntoEnumIterator;
use tokio::sync::RwLock;
use tracing_futures::Instrument;

use crate::{
    logger,
    routes::app::settings::{Conversion, DefaultExchangeRates},
    services, SessionState,
};
const REDIX_FOREX_CACHE_KEY: &str = "{forex_cache}_lock";
const REDIX_FOREX_CACHE_DATA: &str = "{forex_cache}_data";
const FOREX_API_TIMEOUT: u64 = 5;
const FOREX_BASE_URL: &str = "https://openexchangerates.org/api/latest.json?app_id=";
const FOREX_BASE_CURRENCY: &str = "&base=USD";
const FALLBACK_FOREX_BASE_URL: &str = "http://apilayer.net/api/live?access_key=";
const FALLBACK_FOREX_API_CURRENCY_PREFIX: &str = "USD";

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct FxExchangeRatesCacheEntry {
    pub data: Arc<ExchangeRates>,
    timestamp: i64,
}

static FX_EXCHANGE_RATES_CACHE: LazyLock<RwLock<Option<FxExchangeRatesCacheEntry>>> =
    LazyLock::new(|| RwLock::new(None));

impl ApiEventMetric for FxExchangeRatesCacheEntry {}

#[derive(Debug, Clone, thiserror::Error)]
pub enum ForexError {
    #[error("API error")]
    ApiError,
    #[error("API timeout")]
    ApiTimeout,
    #[error("API unresponsive")]
    ApiUnresponsive,
    #[error("Conversion error")]
    ConversionError,
    #[error("Could not acquire the lock for cache entry")]
    CouldNotAcquireLock,
    #[error("Provided currency not acceptable")]
    CurrencyNotAcceptable,
    #[error("Forex configuration error: {0}")]
    ConfigurationError(String),
    #[error("Incorrect entries in default Currency response")]
    DefaultCurrencyParsingError,
    #[error("Entry not found in cache")]
    EntryNotFound,
    #[error("Forex data unavailable")]
    ForexDataUnavailable,
    #[error("Expiration time invalid")]
    InvalidLogExpiry,
    #[error("Error reading local")]
    LocalReadError,
    #[error("Error writing to local cache")]
    LocalWriteError,
    #[error("Json Parsing error")]
    ParsingError,
    #[error("Aws Kms decryption error")]
    AwsKmsDecryptionFailed,
    #[error("Error connecting to redis")]
    RedisConnectionError,
    #[error("Not able to release write lock")]
    RedisLockReleaseFailed,
    #[error("Error writing to redis")]
    RedisWriteError,
    #[error("Not able to acquire write lock")]
    WriteLockNotAcquired,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct ForexResponse {
    pub rates: HashMap<String, FloatDecimal>,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct FallbackForexResponse {
    pub quotes: HashMap<String, FloatDecimal>,
}

#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
struct FloatDecimal(#[serde(with = "rust_decimal::serde::float")] Decimal);

impl Deref for FloatDecimal {
    type Target = Decimal;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl FxExchangeRatesCacheEntry {
    fn new(exchange_rate: ExchangeRates) -> Self {
        Self {
            data: Arc::new(exchange_rate),
            timestamp: date_time::now_unix_timestamp(),
        }
    }
    fn is_expired(&self, data_expiration_delay: u32) -> bool {
        self.timestamp + i64::from(data_expiration_delay) < date_time::now_unix_timestamp()
    }
}

async fn retrieve_forex_from_local_cache() -> Option<FxExchangeRatesCacheEntry> {
    FX_EXCHANGE_RATES_CACHE.read().await.clone()
}

async fn save_forex_data_to_local_cache(
    exchange_rates_cache_entry: FxExchangeRatesCacheEntry,
) -> CustomResult<(), ForexError> {
    let mut local = FX_EXCHANGE_RATES_CACHE.write().await;
    *local = Some(exchange_rates_cache_entry);
    logger::debug!("forex_log: forex saved in cache");
    Ok(())
}

impl TryFrom<DefaultExchangeRates> for ExchangeRates {
    type Error = error_stack::Report<ForexError>;
    fn try_from(value: DefaultExchangeRates) -> Result<Self, Self::Error> {
        let mut conversion_usable: HashMap<enums::Currency, CurrencyFactors> = HashMap::new();
        for (curr, conversion) in value.conversion {
            let enum_curr = enums::Currency::from_str(curr.as_str())
                .change_context(ForexError::ConversionError)
                .attach_printable("Unable to Convert currency received")?;
            conversion_usable.insert(enum_curr, CurrencyFactors::from(conversion));
        }
        let base_curr = enums::Currency::from_str(value.base_currency.as_str())
            .change_context(ForexError::ConversionError)
            .attach_printable("Unable to convert base currency")?;
        Ok(Self {
            base_currency: base_curr,
            conversion: conversion_usable,
        })
    }
}

impl From<Conversion> for CurrencyFactors {
    fn from(value: Conversion) -> Self {
        Self {
            to_factor: value.to_factor,
            from_factor: value.from_factor,
        }
    }
}

#[instrument(skip_all)]
pub async fn get_forex_rates(
    state: &SessionState,
    data_expiration_delay: u32,
) -> CustomResult<FxExchangeRatesCacheEntry, ForexError> {
    if let Some(local_rates) = retrieve_forex_from_local_cache().await {
        if local_rates.is_expired(data_expiration_delay) {
            // expired local data
            logger::debug!("forex_log: Forex stored in cache is expired");
            call_forex_api_and_save_data_to_cache_and_redis(state, Some(local_rates)).await
        } else {
            // Valid data present in local
            logger::debug!("forex_log: forex found in cache");
            Ok(local_rates)
        }
    } else {
        // No data in local
        call_api_if_redis_forex_data_expired(state, data_expiration_delay).await
    }
}

async fn call_api_if_redis_forex_data_expired(
    state: &SessionState,
    data_expiration_delay: u32,
) -> CustomResult<FxExchangeRatesCacheEntry, ForexError> {
    match retrieve_forex_data_from_redis(state).await {
        Ok(Some(data)) => {
            call_forex_api_if_redis_data_expired(state, data, data_expiration_delay).await
        }
        Ok(None) => {
            // No data in local as well as redis
            call_forex_api_and_save_data_to_cache_and_redis(state, None).await?;
            Err(ForexError::ForexDataUnavailable.into())
        }
        Err(error) => {
            // Error in deriving forex rates from redis
            logger::error!("forex_error: {:?}", error);
            call_forex_api_and_save_data_to_cache_and_redis(state, None).await?;
            Err(ForexError::ForexDataUnavailable.into())
        }
    }
}

async fn call_forex_api_and_save_data_to_cache_and_redis(
    state: &SessionState,
    stale_redis_data: Option<FxExchangeRatesCacheEntry>,
) -> CustomResult<FxExchangeRatesCacheEntry, ForexError> {
    // spawn a new thread and do the api fetch and write operations on redis.
    let forex_api_key = state.conf.forex_api.get_inner().api_key.peek();
    if forex_api_key.is_empty() {
        Err(ForexError::ConfigurationError("api_keys not provided".into()).into())
    } else {
        let state = state.clone();
        tokio::spawn(
            async move {
                acquire_redis_lock_and_call_forex_api(&state)
                    .await
                    .map_err(|err| {
                        logger::error!(forex_error=?err);
                    })
                    .ok();
            }
            .in_current_span(),
        );
        stale_redis_data.ok_or(ForexError::EntryNotFound.into())
    }
}

async fn acquire_redis_lock_and_call_forex_api(
    state: &SessionState,
) -> CustomResult<(), ForexError> {
    let lock_acquired = acquire_redis_lock(state).await?;
    if !lock_acquired {
        Err(ForexError::CouldNotAcquireLock.into())
    } else {
        logger::debug!("forex_log: redis lock acquired");
        let api_rates = fetch_forex_rates_from_primary_api(state).await;
        match api_rates {
            Ok(rates) => save_forex_data_to_cache_and_redis(state, rates).await,
            Err(error) => {
                logger::error!(forex_error=?error,"primary_forex_error");
                // API not able to fetch data call secondary service
                let secondary_api_rates = fetch_forex_rates_from_fallback_api(state).await;
                match secondary_api_rates {
                    Ok(rates) => save_forex_data_to_cache_and_redis(state, rates).await,
                    Err(error) => {
                        release_redis_lock(state).await?;
                        Err(error)
                    }
                }
            }
        }
    }
}

async fn save_forex_data_to_cache_and_redis(
    state: &SessionState,
    forex: FxExchangeRatesCacheEntry,
) -> CustomResult<(), ForexError> {
    save_forex_data_to_redis(state, &forex)
        .await
        .async_and_then(|_rates| release_redis_lock(state))
        .await
        .async_and_then(|_val| save_forex_data_to_local_cache(forex.clone()))
        .await
}

async fn call_forex_api_if_redis_data_expired(
    state: &SessionState,
    redis_data: FxExchangeRatesCacheEntry,
    data_expiration_delay: u32,
) -> CustomResult<FxExchangeRatesCacheEntry, ForexError> {
    match is_redis_expired(Some(redis_data.clone()).as_ref(), data_expiration_delay).await {
        Some(redis_forex) => {
            // Valid data present in redis
            let exchange_rates = FxExchangeRatesCacheEntry::new(redis_forex.as_ref().clone());
            logger::debug!("forex_log: forex response found in redis");
            save_forex_data_to_local_cache(exchange_rates.clone()).await?;
            Ok(exchange_rates)
        }
        None => {
            // redis expired
            call_forex_api_and_save_data_to_cache_and_redis(state, Some(redis_data)).await
        }
    }
}

async fn fetch_forex_rates_from_primary_api(
    state: &SessionState,
) -> Result<FxExchangeRatesCacheEntry, error_stack::Report<ForexError>> {
    let forex_api_key = state.conf.forex_api.get_inner().api_key.peek();

    logger::debug!("forex_log: Primary api call for forex fetch");
    let forex_url: String = format!("{FOREX_BASE_URL}{forex_api_key}{FOREX_BASE_CURRENCY}");
    let forex_request = services::RequestBuilder::new()
        .method(services::Method::Get)
        .url(&forex_url)
        .build();

    logger::info!(primary_forex_request=?forex_request,"forex_log: Primary api call for forex fetch");
    let response = state
        .api_client
        .send_request(
            &state.clone(),
            forex_request,
            Some(FOREX_API_TIMEOUT),
            false,
        )
        .await
        .change_context(ForexError::ApiUnresponsive)
        .attach_printable("Primary forex fetch api unresponsive")?;
    let forex_response = response
        .json::<ForexResponse>()
        .await
        .change_context(ForexError::ParsingError)
        .attach_printable(
            "Unable to parse response received from primary api into ForexResponse",
        )?;

    logger::info!(primary_forex_response=?forex_response,"forex_log");

    let mut conversions: HashMap<enums::Currency, CurrencyFactors> = HashMap::new();
    for enum_curr in enums::Currency::iter() {
        match forex_response.rates.get(&enum_curr.to_string()) {
            Some(rate) => {
                let from_factor = match Decimal::new(1, 0).checked_div(**rate) {
                    Some(rate) => rate,
                    None => {
                        logger::error!(
                            "forex_error: Rates for {} not received from API",
                            &enum_curr
                        );
                        continue;
                    }
                };
                let currency_factors = CurrencyFactors::new(**rate, from_factor);
                conversions.insert(enum_curr, currency_factors);
            }
            None => {
                logger::error!(
                    "forex_error: Rates for {} not received from API",
                    &enum_curr
                );
            }
        };
    }

    Ok(FxExchangeRatesCacheEntry::new(ExchangeRates::new(
        enums::Currency::USD,
        conversions,
    )))
}

pub async fn fetch_forex_rates_from_fallback_api(
    state: &SessionState,
) -> CustomResult<FxExchangeRatesCacheEntry, ForexError> {
    let fallback_forex_api_key = state.conf.forex_api.get_inner().fallback_api_key.peek();

    let fallback_forex_url: String = format!("{FALLBACK_FOREX_BASE_URL}{fallback_forex_api_key}");
    let fallback_forex_request = services::RequestBuilder::new()
        .method(services::Method::Get)
        .url(&fallback_forex_url)
        .build();

    logger::info!(fallback_forex_request=?fallback_forex_request,"forex_log: Fallback api call for forex fetch");
    let response = state
        .api_client
        .send_request(
            &state.clone(),
            fallback_forex_request,
            Some(FOREX_API_TIMEOUT),
            false,
        )
        .await
        .change_context(ForexError::ApiUnresponsive)
        .attach_printable("Fallback forex fetch api unresponsive")?;

    let fallback_forex_response = response
        .json::<FallbackForexResponse>()
        .await
        .change_context(ForexError::ParsingError)
        .attach_printable(
            "Unable to parse response received from fallback api into ForexResponse",
        )?;

    logger::info!(fallback_forex_response=?fallback_forex_response,"forex_log");

    let mut conversions: HashMap<enums::Currency, CurrencyFactors> = HashMap::new();
    for enum_curr in enums::Currency::iter() {
        match fallback_forex_response.quotes.get(
            format!(
                "{}{}",
                FALLBACK_FOREX_API_CURRENCY_PREFIX,
                &enum_curr.to_string()
            )
            .as_str(),
        ) {
            Some(rate) => {
                let from_factor = match Decimal::new(1, 0).checked_div(**rate) {
                    Some(rate) => rate,
                    None => {
                        logger::error!(
                            "forex_error: Rates for {} not received from API",
                            &enum_curr
                        );
                        continue;
                    }
                };
                let currency_factors = CurrencyFactors::new(**rate, from_factor);
                conversions.insert(enum_curr, currency_factors);
            }
            None => {
                if enum_curr == enums::Currency::USD {
                    let currency_factors =
                        CurrencyFactors::new(Decimal::new(1, 0), Decimal::new(1, 0));
                    conversions.insert(enum_curr, currency_factors);
                } else {
                    logger::error!(
                        "forex_error: Rates for {} not received from API",
                        &enum_curr
                    );
                }
            }
        };
    }

    let rates =
        FxExchangeRatesCacheEntry::new(ExchangeRates::new(enums::Currency::USD, conversions));
    match acquire_redis_lock(state).await {
        Ok(_) => {
            save_forex_data_to_cache_and_redis(state, rates.clone()).await?;
            Ok(rates)
        }
        Err(e) => Err(e),
    }
}

async fn release_redis_lock(
    state: &SessionState,
) -> Result<DelReply, error_stack::Report<ForexError>> {
    logger::debug!("forex_log: Releasing redis lock");
    state
        .store
        .get_redis_conn()
        .change_context(ForexError::RedisConnectionError)?
        .delete_key(&REDIX_FOREX_CACHE_KEY.into())
        .await
        .change_context(ForexError::RedisLockReleaseFailed)
        .attach_printable("Unable to release redis lock")
}

async fn acquire_redis_lock(state: &SessionState) -> CustomResult<bool, ForexError> {
    let forex_api = state.conf.forex_api.get_inner();
    logger::debug!("forex_log: Acquiring redis lock");
    state
        .store
        .get_redis_conn()
        .change_context(ForexError::RedisConnectionError)?
        .set_key_if_not_exists_with_expiry(
            &REDIX_FOREX_CACHE_KEY.into(),
            "",
            Some(i64::from(forex_api.redis_lock_timeout_in_seconds)),
        )
        .await
        .map(|val| matches!(val, redis_interface::SetnxReply::KeySet))
        .change_context(ForexError::CouldNotAcquireLock)
        .attach_printable("Unable to acquire redis lock")
}

async fn save_forex_data_to_redis(
    app_state: &SessionState,
    forex_exchange_cache_entry: &FxExchangeRatesCacheEntry,
) -> CustomResult<(), ForexError> {
    let forex_api = app_state.conf.forex_api.get_inner();
    logger::debug!("forex_log: Saving forex to redis");
    app_state
        .store
        .get_redis_conn()
        .change_context(ForexError::RedisConnectionError)?
        .serialize_and_set_key_with_expiry(
            &REDIX_FOREX_CACHE_DATA.into(),
            forex_exchange_cache_entry,
            i64::from(forex_api.redis_ttl_in_seconds),
        )
        .await
        .change_context(ForexError::RedisWriteError)
        .attach_printable("Unable to save forex data to redis")
}

async fn retrieve_forex_data_from_redis(
    app_state: &SessionState,
) -> CustomResult<Option<FxExchangeRatesCacheEntry>, ForexError> {
    logger::debug!("forex_log: Retrieving forex from redis");
    app_state
        .store
        .get_redis_conn()
        .change_context(ForexError::RedisConnectionError)?
        .get_and_deserialize_key(&REDIX_FOREX_CACHE_DATA.into(), "FxExchangeRatesCache")
        .await
        .change_context(ForexError::EntryNotFound)
        .attach_printable("Forex entry not found in redis")
}

async fn is_redis_expired(
    redis_cache: Option<&FxExchangeRatesCacheEntry>,
    data_expiration_delay: u32,
) -> Option<Arc<ExchangeRates>> {
    redis_cache.and_then(|cache| {
        if cache.timestamp + i64::from(data_expiration_delay) > date_time::now_unix_timestamp() {
            Some(cache.data.clone())
        } else {
            logger::debug!("forex_log: Forex stored in redis is expired");
            None
        }
    })
}

#[instrument(skip_all)]
pub async fn convert_currency(
    state: SessionState,
    amount: i64,
    to_currency: String,
    from_currency: String,
) -> CustomResult<api_models::currency::CurrencyConversionResponse, ForexError> {
    let forex_api = state.conf.forex_api.get_inner();
    let rates = get_forex_rates(&state, forex_api.data_expiration_delay_in_seconds)
        .await
        .change_context(ForexError::ApiError)?;

    let to_currency = enums::Currency::from_str(to_currency.as_str())
        .change_context(ForexError::CurrencyNotAcceptable)
        .attach_printable("The provided currency is not acceptable")?;

    let from_currency = enums::Currency::from_str(from_currency.as_str())
        .change_context(ForexError::CurrencyNotAcceptable)
        .attach_printable("The provided currency is not acceptable")?;

    let converted_amount =
        currency_conversion::conversion::convert(&rates.data, from_currency, to_currency, amount)
            .change_context(ForexError::ConversionError)
            .attach_printable("Unable to perform currency conversion")?;

    Ok(api_models::currency::CurrencyConversionResponse {
        converted_amount: converted_amount.to_string(),
        currency: to_currency.to_string(),
    })
}


// Struct: SessionObject
// File: crates/hyperswitch_connectors/src/connectors/zen/transformers.rs
// Module: hyperswitch_connectors
// Implementations: 0

pub struct SessionObject


// Implementation: impl api::PaymentCapture for for Nexixpay
// File: crates/hyperswitch_connectors/src/connectors/nexixpay.rs
// Module: hyperswitch_connectors
// Methods: 0 total (0 public)

impl api::PaymentCapture for for Nexixpay


// Struct: PaysafeApplePayDecryptedPaymentData
// File: crates/hyperswitch_connectors/src/connectors/paysafe/transformers.rs
// Module: hyperswitch_connectors
// Implementations: 0

pub struct PaysafeApplePayDecryptedPaymentData


// Function: update_organization
// File: crates/router/src/core/admin.rs
// Module: router

pub fn update_organization(
    state: SessionState,
    org_id: api::OrganizationId,
    req: api::OrganizationUpdateRequest,
) -> RouterResponse<api::OrganizationResponse>


// Implementation: impl ConnectorValidation for for Elavon
// File: crates/hyperswitch_connectors/src/connectors/elavon.rs
// Module: hyperswitch_connectors
// Methods: 1 total (0 public)

impl ConnectorValidation for for Elavon


// Struct: TDS2ApiError
// File: crates/hyperswitch_connectors/src/connectors/gpayments/gpayments_types.rs
// Module: hyperswitch_connectors
// Implementations: 0

pub struct TDS2ApiError


// Struct: KafkaRefund
// File: crates/router/src/services/kafka/refund.rs
// Module: router
// Implementations: 3
// Traits: super::KafkaMessage

pub struct KafkaRefund<'a>


// Implementation: impl api::RefundSync for for Paysafe
// File: crates/hyperswitch_connectors/src/connectors/paysafe.rs
// Module: hyperswitch_connectors
// Methods: 0 total (0 public)

impl api::RefundSync for for Paysafe


// Implementation: impl api::UasAuthentication for for UnifiedAuthenticationService
// File: crates/hyperswitch_connectors/src/connectors/unified_authentication_service.rs
// Module: hyperswitch_connectors
// Methods: 0 total (0 public)

impl api::UasAuthentication for for UnifiedAuthenticationService


// Function: resource
// File: crates/router_derive/src/macros/generate_permissions.rs
// Module: router_derive

pub fn resource(&self) -> Resource


// File: crates/router/src/core/webhooks/outgoing.rs
// Module: router

use std::collections::HashMap;

use api_models::{
    webhook_events::{OutgoingWebhookRequestContent, OutgoingWebhookResponseContent},
    webhooks,
};
use common_utils::{
    ext_traits::{Encode, StringExt},
    request::RequestContent,
    type_name,
    types::keymanager::{Identifier, KeyManagerState},
};
use diesel_models::process_tracker::business_status;
use error_stack::{report, ResultExt};
use hyperswitch_domain_models::type_encryption::{crypto_operation, CryptoOperation};
use hyperswitch_interfaces::consts;
use masking::{ExposeInterface, Mask, PeekInterface, Secret};
use router_env::{
    instrument,
    tracing::{self, Instrument},
};

use super::{types, utils, MERCHANT_ID};
#[cfg(feature = "stripe")]
use crate::compatibility::stripe::webhooks as stripe_webhooks;
use crate::{
    core::{
        errors::{self, CustomResult},
        metrics,
    },
    db::StorageInterface,
    events::outgoing_webhook_logs::{
        OutgoingWebhookEvent, OutgoingWebhookEventContent, OutgoingWebhookEventMetric,
    },
    logger,
    routes::{app::SessionStateInfo, SessionState},
    services,
    types::{
        api,
        domain::{self},
        storage::{self, enums},
        transformers::ForeignFrom,
    },
    utils::{OptionExt, ValueExt},
    workflows::outgoing_webhook_retry,
};

#[allow(clippy::too_many_arguments)]
#[instrument(skip_all)]
pub(crate) async fn create_event_and_trigger_outgoing_webhook(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    business_profile: domain::Profile,
    event_type: enums::EventType,
    event_class: enums::EventClass,
    primary_object_id: String,
    primary_object_type: enums::EventObjectType,
    content: api::OutgoingWebhookContent,
    primary_object_created_at: Option<time::PrimitiveDateTime>,
) -> CustomResult<(), errors::ApiErrorResponse> {
    let delivery_attempt = enums::WebhookDeliveryAttempt::InitialAttempt;
    let idempotent_event_id =
        utils::get_idempotent_event_id(&primary_object_id, event_type, delivery_attempt)
            .change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
            .attach_printable("Failed to generate idempotent event ID")?;
    let webhook_url_result = get_webhook_url_from_business_profile(&business_profile);

    if !state.conf.webhooks.outgoing_enabled
        || webhook_url_result.is_err()
        || webhook_url_result.as_ref().is_ok_and(String::is_empty)
    {
        logger::debug!(
            business_profile_id=?business_profile.get_id(),
            %idempotent_event_id,
            "Outgoing webhooks are disabled in application configuration, or merchant webhook URL \
             could not be obtained; skipping outgoing webhooks for event"
        );
        return Ok(());
    }

    let event_id = utils::generate_event_id();
    let merchant_id = business_profile.merchant_id.clone();
    let now = common_utils::date_time::now();

    let outgoing_webhook = api::OutgoingWebhook {
        merchant_id: merchant_id.clone(),
        event_id: event_id.clone(),
        event_type,
        content: content.clone(),
        timestamp: now,
    };

    let request_content =
        get_outgoing_webhook_request(&merchant_context, outgoing_webhook, &business_profile)
            .change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
            .attach_printable("Failed to construct outgoing webhook request content")?;

    let event_metadata = storage::EventMetadata::foreign_from(&content);
    let key_manager_state = &(&state).into();
    let new_event = domain::Event {
        event_id: event_id.clone(),
        event_type,
        event_class,
        is_webhook_notified: false,
        primary_object_id,
        primary_object_type,
        created_at: now,
        merchant_id: Some(business_profile.merchant_id.clone()),
        business_profile_id: Some(business_profile.get_id().to_owned()),
        primary_object_created_at,
        idempotent_event_id: Some(idempotent_event_id.clone()),
        initial_attempt_id: Some(event_id.clone()),
        request: Some(
            crypto_operation(
                key_manager_state,
                type_name!(domain::Event),
                CryptoOperation::Encrypt(
                    request_content
                        .encode_to_string_of_json()
                        .change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
                        .attach_printable("Failed to encode outgoing webhook request content")
                        .map(Secret::new)?,
                ),
                Identifier::Merchant(
                    merchant_context
                        .get_merchant_key_store()
                        .merchant_id
                        .clone(),
                ),
                merchant_context
                    .get_merchant_key_store()
                    .key
                    .get_inner()
                    .peek(),
            )
            .await
            .and_then(|val| val.try_into_operation())
            .change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
            .attach_printable("Failed to encrypt outgoing webhook request content")?,
        ),
        response: None,
        delivery_attempt: Some(delivery_attempt),
        metadata: Some(event_metadata),
        is_overall_delivery_successful: Some(false),
    };

    let lock_value = utils::perform_redis_lock(
        &state,
        &idempotent_event_id,
        merchant_context.get_merchant_account().get_id().to_owned(),
    )
    .await?;

    if lock_value.is_none() {
        return Ok(());
    }

    if (state
        .store
        .find_event_by_merchant_id_idempotent_event_id(
            key_manager_state,
            &merchant_id,
            &idempotent_event_id,
            merchant_context.get_merchant_key_store(),
        )
        .await)
        .is_ok()
    {
        logger::debug!(
            "Event with idempotent ID `{idempotent_event_id}` already exists in the database"
        );
        utils::free_redis_lock(
            &state,
            &idempotent_event_id,
            merchant_context.get_merchant_account().get_id().to_owned(),
            lock_value,
        )
        .await?;
        return Ok(());
    }

    let event_insert_result = state
        .store
        .insert_event(
            key_manager_state,
            new_event,
            merchant_context.get_merchant_key_store(),
        )
        .await;

    let event = match event_insert_result {
        Ok(event) => Ok(event),
        Err(error) => {
            logger::error!(event_insertion_failure=?error);
            Err(error
                .change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
                .attach_printable("Failed to insert event in events table"))
        }
    }?;

    utils::free_redis_lock(
        &state,
        &idempotent_event_id,
        merchant_context.get_merchant_account().get_id().to_owned(),
        lock_value,
    )
    .await?;

    let process_tracker = add_outgoing_webhook_retry_task_to_process_tracker(
        &*state.store,
        &business_profile,
        &event,
    )
    .await
    .inspect_err(|error| {
        logger::error!(
            ?error,
            "Failed to add outgoing webhook retry task to process tracker"
        );
    })
    .ok();

    let cloned_key_store = merchant_context.get_merchant_key_store().clone();
    // Using a tokio spawn here and not arbiter because not all caller of this function
    // may have an actix arbiter
    tokio::spawn(
        async move {
            Box::pin(trigger_webhook_and_raise_event(
                state,
                business_profile,
                &cloned_key_store,
                event,
                request_content,
                delivery_attempt,
                Some(content),
                process_tracker,
            ))
            .await;
        }
        .in_current_span(),
    );

    Ok(())
}

#[allow(clippy::too_many_arguments)]
#[instrument(skip_all)]
pub(crate) async fn trigger_webhook_and_raise_event(
    state: SessionState,
    business_profile: domain::Profile,
    merchant_key_store: &domain::MerchantKeyStore,
    event: domain::Event,
    request_content: OutgoingWebhookRequestContent,
    delivery_attempt: enums::WebhookDeliveryAttempt,
    content: Option<api::OutgoingWebhookContent>,
    process_tracker: Option<storage::ProcessTracker>,
) {
    logger::debug!(
        event_id=%event.event_id,
        idempotent_event_id=?event.idempotent_event_id,
        initial_attempt_id=?event.initial_attempt_id,
        "Attempting to send webhook"
    );

    let merchant_id = business_profile.merchant_id.clone();
    let trigger_webhook_result = trigger_webhook_to_merchant(
        state.clone(),
        business_profile,
        merchant_key_store,
        event.clone(),
        request_content,
        delivery_attempt,
        process_tracker,
    )
    .await;

    let _ = raise_webhooks_analytics_event(
        state,
        trigger_webhook_result,
        content,
        merchant_id,
        event,
        merchant_key_store,
    )
    .await;
}

async fn trigger_webhook_to_merchant(
    state: SessionState,
    business_profile: domain::Profile,
    merchant_key_store: &domain::MerchantKeyStore,
    event: domain::Event,
    request_content: OutgoingWebhookRequestContent,
    delivery_attempt: enums::WebhookDeliveryAttempt,
    process_tracker: Option<storage::ProcessTracker>,
) -> CustomResult<(), errors::WebhooksFlowError> {
    let webhook_url = match (
        get_webhook_url_from_business_profile(&business_profile),
        process_tracker.clone(),
    ) {
        (Ok(webhook_url), _) => Ok(webhook_url),
        (Err(error), Some(process_tracker)) => {
            if !error
                .current_context()
                .is_webhook_delivery_retryable_error()
            {
                logger::debug!("Failed to obtain merchant webhook URL, aborting retries");
                state
                    .store
                    .as_scheduler()
                    .finish_process_with_business_status(process_tracker, business_status::FAILURE)
                    .await
                    .change_context(
                        errors::WebhooksFlowError::OutgoingWebhookProcessTrackerTaskUpdateFailed,
                    )?;
            }
            Err(error)
        }
        (Err(error), None) => Err(error),
    }?;

    let event_id = event.event_id;

    let headers = request_content
        .headers
        .into_iter()
        .map(|(name, value)| (name, value.into_masked()))
        .collect();
    let request = services::RequestBuilder::new()
        .method(services::Method::Post)
        .url(&webhook_url)
        .attach_default_headers()
        .headers(headers)
        .set_body(RequestContent::RawBytes(
            request_content.body.expose().into_bytes(),
        ))
        .build();

    let response = state
        .api_client
        .send_request(&state, request, None, false)
        .await;

    metrics::WEBHOOK_OUTGOING_COUNT.add(
        1,
        router_env::metric_attributes!((MERCHANT_ID, business_profile.merchant_id.clone())),
    );
    logger::debug!(outgoing_webhook_response=?response);

    match delivery_attempt {
        enums::WebhookDeliveryAttempt::InitialAttempt => match response {
            Err(client_error) => {
                api_client_error_handler(
                    state.clone(),
                    merchant_key_store.clone(),
                    &business_profile.merchant_id,
                    &event_id,
                    client_error,
                    delivery_attempt,
                    ScheduleWebhookRetry::NoSchedule,
                )
                .await?
            }
            Ok(response) => {
                let status_code = response.status();
                let updated_event = update_event_in_storage(
                    state.clone(),
                    merchant_key_store.clone(),
                    &business_profile.merchant_id,
                    &event_id,
                    response,
                )
                .await?;

                if status_code.is_success() {
                    update_overall_delivery_status_in_storage(
                        state.clone(),
                        merchant_key_store.clone(),
                        &business_profile.merchant_id,
                        updated_event,
                    )
                    .await?;

                    success_response_handler(
                        state.clone(),
                        &business_profile.merchant_id,
                        process_tracker,
                        business_status::INITIAL_DELIVERY_ATTEMPT_SUCCESSFUL,
                    )
                    .await?;
                } else {
                    error_response_handler(
                        state.clone(),
                        &business_profile.merchant_id,
                        delivery_attempt,
                        status_code.as_u16(),
                        "Ignoring error when sending webhook to merchant",
                        ScheduleWebhookRetry::NoSchedule,
                    )
                    .await?;
                }
            }
        },
        enums::WebhookDeliveryAttempt::AutomaticRetry => {
            let process_tracker = process_tracker
                .get_required_value("process_tracker")
                .change_context(errors::WebhooksFlowError::OutgoingWebhookRetrySchedulingFailed)
                .attach_printable("`process_tracker` is unavailable in automatic retry flow")?;
            match response {
                Err(client_error) => {
                    api_client_error_handler(
                        state.clone(),
                        merchant_key_store.clone(),
                        &business_profile.merchant_id,
                        &event_id,
                        client_error,
                        delivery_attempt,
                        ScheduleWebhookRetry::WithProcessTracker(Box::new(process_tracker)),
                    )
                    .await?;
                }
                Ok(response) => {
                    let status_code = response.status();
                    let updated_event = update_event_in_storage(
                        state.clone(),
                        merchant_key_store.clone(),
                        &business_profile.merchant_id,
                        &event_id,
                        response,
                    )
                    .await?;

                    if status_code.is_success() {
                        update_overall_delivery_status_in_storage(
                            state.clone(),
                            merchant_key_store.clone(),
                            &business_profile.merchant_id,
                            updated_event,
                        )
                        .await?;

                        success_response_handler(
                            state.clone(),
                            &business_profile.merchant_id,
                            Some(process_tracker),
                            "COMPLETED_BY_PT",
                        )
                        .await?;
                    } else {
                        error_response_handler(
                            state.clone(),
                            &business_profile.merchant_id,
                            delivery_attempt,
                            status_code.as_u16(),
                            "An error occurred when sending webhook to merchant",
                            ScheduleWebhookRetry::WithProcessTracker(Box::new(process_tracker)),
                        )
                        .await?;
                    }
                }
            }
        }
        enums::WebhookDeliveryAttempt::ManualRetry => match response {
            Err(client_error) => {
                api_client_error_handler(
                    state.clone(),
                    merchant_key_store.clone(),
                    &business_profile.merchant_id,
                    &event_id,
                    client_error,
                    delivery_attempt,
                    ScheduleWebhookRetry::NoSchedule,
                )
                .await?
            }
            Ok(response) => {
                let status_code = response.status();
                let _updated_event = update_event_in_storage(
                    state.clone(),
                    merchant_key_store.clone(),
                    &business_profile.merchant_id,
                    &event_id,
                    response,
                )
                .await?;

                if status_code.is_success() {
                    increment_webhook_outgoing_received_count(&business_profile.merchant_id);
                } else {
                    error_response_handler(
                        state,
                        &business_profile.merchant_id,
                        delivery_attempt,
                        status_code.as_u16(),
                        "Ignoring error when sending webhook to merchant",
                        ScheduleWebhookRetry::NoSchedule,
                    )
                    .await?;
                }
            }
        },
    }

    Ok(())
}

async fn raise_webhooks_analytics_event(
    state: SessionState,
    trigger_webhook_result: CustomResult<(), errors::WebhooksFlowError>,
    content: Option<api::OutgoingWebhookContent>,
    merchant_id: common_utils::id_type::MerchantId,
    event: domain::Event,
    merchant_key_store: &domain::MerchantKeyStore,
) {
    let key_manager_state: &KeyManagerState = &(&state).into();
    let event_id = event.event_id;

    let error = if let Err(error) = trigger_webhook_result {
        logger::error!(?error, "Failed to send webhook to merchant");

        serde_json::to_value(error.current_context())
            .change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
            .inspect_err(|error| {
                logger::error!(?error, "Failed to serialize outgoing webhook error as JSON");
            })
            .ok()
    } else {
        None
    };

    let outgoing_webhook_event_content = content
        .as_ref()
        .and_then(api::OutgoingWebhookContent::get_outgoing_webhook_event_content)
        .or_else(|| get_outgoing_webhook_event_content_from_event_metadata(event.metadata));

    // Fetch updated_event from db
    let updated_event = state
        .store
        .find_event_by_merchant_id_event_id(
            key_manager_state,
            &merchant_id,
            &event_id,
            merchant_key_store,
        )
        .await
        .attach_printable_lazy(|| format!("event not found for id: {}", &event_id))
        .map_err(|error| {
            logger::error!(?error);
            error
        })
        .ok();

    // Get status_code from webhook response
    let status_code = updated_event.and_then(|updated_event| {
        let webhook_response: Option<OutgoingWebhookResponseContent> =
            updated_event.response.and_then(|res| {
                res.peek()
                    .parse_struct("OutgoingWebhookResponseContent")
                    .map_err(|error| {
                        logger::error!(?error, "Error deserializing webhook response");
                        error
                    })
                    .ok()
            });
        webhook_response.and_then(|res| res.status_code)
    });

    let webhook_event = OutgoingWebhookEvent::new(
        state.tenant.tenant_id.clone(),
        merchant_id,
        event_id,
        event.event_type,
        outgoing_webhook_event_content,
        error,
        event.initial_attempt_id,
        status_code,
        event.delivery_attempt,
    );
    state.event_handler().log_event(&webhook_event);
}

pub(crate) async fn add_outgoing_webhook_retry_task_to_process_tracker(
    db: &dyn StorageInterface,
    business_profile: &domain::Profile,
    event: &domain::Event,
) -> CustomResult<storage::ProcessTracker, errors::StorageError> {
    let schedule_time = outgoing_webhook_retry::get_webhook_delivery_retry_schedule_time(
        db,
        &business_profile.merchant_id,
        0,
    )
    .await
    .ok_or(errors::StorageError::ValueNotFound(
        "Process tracker schedule time".into(), // Can raise a better error here
    ))
    .attach_printable("Failed to obtain initial process tracker schedule time")?;

    let tracking_data = types::OutgoingWebhookTrackingData {
        merchant_id: business_profile.merchant_id.clone(),
        business_profile_id: business_profile.get_id().to_owned(),
        event_type: event.event_type,
        event_class: event.event_class,
        primary_object_id: event.primary_object_id.clone(),
        primary_object_type: event.primary_object_type,
        initial_attempt_id: event.initial_attempt_id.clone(),
    };

    let runner = storage::ProcessTrackerRunner::OutgoingWebhookRetryWorkflow;
    let task = "OUTGOING_WEBHOOK_RETRY";
    let tag = ["OUTGOING_WEBHOOKS"];
    let process_tracker_id = scheduler::utils::get_process_tracker_id(
        runner,
        task,
        &event.event_id,
        &business_profile.merchant_id,
    );
    let process_tracker_entry = storage::ProcessTrackerNew::new(
        process_tracker_id,
        task,
        runner,
        tag,
        tracking_data,
        None,
        schedule_time,
        common_types::consts::API_VERSION,
    )
    .map_err(errors::StorageError::from)?;

    let attributes = router_env::metric_attributes!(("flow", "OutgoingWebhookRetry"));
    match db.insert_process(process_tracker_entry).await {
        Ok(process_tracker) => {
            crate::routes::metrics::TASKS_ADDED_COUNT.add(1, attributes);
            Ok(process_tracker)
        }
        Err(error) => {
            crate::routes::metrics::TASK_ADDITION_FAILURES_COUNT.add(1, attributes);
            Err(error)
        }
    }
}

fn get_webhook_url_from_business_profile(
    business_profile: &domain::Profile,
) -> CustomResult<String, errors::WebhooksFlowError> {
    let webhook_details = business_profile
        .webhook_details
        .clone()
        .get_required_value("webhook_details")
        .change_context(errors::WebhooksFlowError::MerchantWebhookDetailsNotFound)?;

    webhook_details
        .webhook_url
        .get_required_value("webhook_url")
        .change_context(errors::WebhooksFlowError::MerchantWebhookUrlNotConfigured)
        .map(ExposeInterface::expose)
}

pub(crate) fn get_outgoing_webhook_request(
    merchant_context: &domain::MerchantContext,
    outgoing_webhook: api::OutgoingWebhook,
    business_profile: &domain::Profile,
) -> CustomResult<OutgoingWebhookRequestContent, errors::WebhooksFlowError> {
    #[inline]
    fn get_outgoing_webhook_request_inner<WebhookType: types::OutgoingWebhookType>(
        outgoing_webhook: api::OutgoingWebhook,
        business_profile: &domain::Profile,
    ) -> CustomResult<OutgoingWebhookRequestContent, errors::WebhooksFlowError> {
        let mut headers = vec![
            (
                reqwest::header::CONTENT_TYPE.to_string(),
                mime::APPLICATION_JSON.essence_str().into(),
            ),
            (
                reqwest::header::USER_AGENT.to_string(),
                consts::USER_AGENT.to_string().into(),
            ),
        ];

        let transformed_outgoing_webhook = WebhookType::from(outgoing_webhook);
        let payment_response_hash_key = business_profile.payment_response_hash_key.clone();
        let custom_headers = business_profile
            .outgoing_webhook_custom_http_headers
            .clone()
            .map(|headers| {
                headers
                    .into_inner()
                    .expose()
                    .parse_value::<HashMap<String, String>>("HashMap<String,String>")
                    .change_context(errors::WebhooksFlowError::OutgoingWebhookEncodingFailed)
                    .attach_printable("Failed to deserialize outgoing webhook custom HTTP headers")
            })
            .transpose()?;
        if let Some(ref map) = custom_headers {
            headers.extend(
                map.iter()
                    .map(|(key, value)| (key.clone(), value.clone().into_masked())),
            );
        };
        let outgoing_webhooks_signature = transformed_outgoing_webhook
            .get_outgoing_webhooks_signature(payment_response_hash_key)?;

        if let Some(signature) = outgoing_webhooks_signature.signature {
            WebhookType::add_webhook_header(&mut headers, signature)
        }

        Ok(OutgoingWebhookRequestContent {
            body: outgoing_webhooks_signature.payload,
            headers: headers
                .into_iter()
                .map(|(name, value)| (name, Secret::new(value.into_inner())))
                .collect(),
        })
    }

    match merchant_context
        .get_merchant_account()
        .get_compatible_connector()
    {
        #[cfg(feature = "stripe")]
        Some(api_models::enums::Connector::Stripe) => get_outgoing_webhook_request_inner::<
            stripe_webhooks::StripeOutgoingWebhook,
        >(outgoing_webhook, business_profile),
        _ => get_outgoing_webhook_request_inner::<webhooks::OutgoingWebhook>(
            outgoing_webhook,
            business_profile,
        ),
    }
}

#[derive(Debug)]
enum ScheduleWebhookRetry {
    WithProcessTracker(Box<storage::ProcessTracker>),
    NoSchedule,
}

async fn update_event_if_client_error(
    state: SessionState,
    merchant_key_store: domain::MerchantKeyStore,
    merchant_id: &common_utils::id_type::MerchantId,
    event_id: &str,
    error_message: String,
) -> CustomResult<domain::Event, errors::WebhooksFlowError> {
    let is_webhook_notified = false;
    let key_manager_state = &(&state).into();
    let response_to_store = OutgoingWebhookResponseContent {
        body: None,
        headers: None,
        status_code: None,
        error_message: Some(error_message),
    };

    let event_update = domain::EventUpdate::UpdateResponse {
        is_webhook_notified,
        response: Some(
            crypto_operation(
                key_manager_state,
                type_name!(domain::Event),
                CryptoOperation::Encrypt(
                    response_to_store
                        .encode_to_string_of_json()
                        .change_context(
                            errors::WebhooksFlowError::OutgoingWebhookResponseEncodingFailed,
                        )
                        .map(Secret::new)?,
                ),
                Identifier::Merchant(merchant_key_store.merchant_id.clone()),
                merchant_key_store.key.get_inner().peek(),
            )
            .await
            .and_then(|val| val.try_into_operation())
            .change_context(errors::WebhooksFlowError::WebhookEventUpdationFailed)
            .attach_printable("Failed to encrypt outgoing webhook response content")?,
        ),
    };

    state
        .store
        .update_event_by_merchant_id_event_id(
            key_manager_state,
            merchant_id,
            event_id,
            event_update,
            &merchant_key_store,
        )
        .await
        .change_context(errors::WebhooksFlowError::WebhookEventUpdationFailed)
}

async fn api_client_error_handler(
    state: SessionState,
    merchant_key_store: domain::MerchantKeyStore,
    merchant_id: &common_utils::id_type::MerchantId,
    event_id: &str,
    client_error: error_stack::Report<errors::ApiClientError>,
    delivery_attempt: enums::WebhookDeliveryAttempt,
    schedule_webhook_retry: ScheduleWebhookRetry,
) -> CustomResult<(), errors::WebhooksFlowError> {
    // Not including detailed error message in response information since it contains too
    // much of diagnostic information to be exposed to the merchant.
    update_event_if_client_error(
        state.clone(),
        merchant_key_store,
        merchant_id,
        event_id,
        "Unable to send request to merchant server".to_string(),
    )
    .await?;

    let error = client_error.change_context(errors::WebhooksFlowError::CallToMerchantFailed);
    logger::error!(
        ?error,
        ?delivery_attempt,
        "An error occurred when sending webhook to merchant"
    );

    if let ScheduleWebhookRetry::WithProcessTracker(process_tracker) = schedule_webhook_retry {
        // Schedule a retry attempt for webhook delivery
        outgoing_webhook_retry::retry_webhook_delivery_task(
            &*state.store,
            merchant_id,
            *process_tracker,
        )
        .await
        .change_context(errors::WebhooksFlowError::OutgoingWebhookRetrySchedulingFailed)?;
    }

    Err(error)
}

async fn update_event_in_storage(
    state: SessionState,
    merchant_key_store: domain::MerchantKeyStore,
    merchant_id: &common_utils::id_type::MerchantId,
    event_id: &str,
    response: reqwest::Response,
) -> CustomResult<domain::Event, errors::WebhooksFlowError> {
    let status_code = response.status();
    let is_webhook_notified = status_code.is_success();
    let key_manager_state = &(&state).into();
    let response_headers = response
        .headers()
        .iter()
        .map(|(name, value)| {
            (
                name.as_str().to_owned(),
                value
                    .to_str()
                    .map(|s| Secret::from(String::from(s)))
                    .unwrap_or_else(|error| {
                        logger::warn!(
                            "Response header {} contains non-UTF-8 characters: {error:?}",
                            name.as_str()
                        );
                        Secret::from(String::from("Non-UTF-8 header value"))
                    }),
            )
        })
        .collect::<Vec<_>>();
    let response_body = response
        .text()
        .await
        .map(Secret::from)
        .unwrap_or_else(|error| {
            logger::warn!("Response contains non-UTF-8 characters: {error:?}");
            Secret::from(String::from("Non-UTF-8 response body"))
        });
    let response_to_store = OutgoingWebhookResponseContent {
        body: Some(response_body),
        headers: Some(response_headers),
        status_code: Some(status_code.as_u16()),
        error_message: None,
    };

    let event_update = domain::EventUpdate::UpdateResponse {
        is_webhook_notified,
        response: Some(
            crypto_operation(
                key_manager_state,
                type_name!(domain::Event),
                CryptoOperation::Encrypt(
                    response_to_store
                        .encode_to_string_of_json()
                        .change_context(
                            errors::WebhooksFlowError::OutgoingWebhookResponseEncodingFailed,
                        )
                        .map(Secret::new)?,
                ),
                Identifier::Merchant(merchant_key_store.merchant_id.clone()),
                merchant_key_store.key.get_inner().peek(),
            )
            .await
            .and_then(|val| val.try_into_operation())
            .change_context(errors::WebhooksFlowError::WebhookEventUpdationFailed)
            .attach_printable("Failed to encrypt outgoing webhook response content")?,
        ),
    };
    state
        .store
        .update_event_by_merchant_id_event_id(
            key_manager_state,
            merchant_id,
            event_id,
            event_update,
            &merchant_key_store,
        )
        .await
        .change_context(errors::WebhooksFlowError::WebhookEventUpdationFailed)
}

async fn update_overall_delivery_status_in_storage(
    state: SessionState,
    merchant_key_store: domain::MerchantKeyStore,
    merchant_id: &common_utils::id_type::MerchantId,
    updated_event: domain::Event,
) -> CustomResult<(), errors::WebhooksFlowError> {
    let key_manager_state = &(&state).into();

    let update_overall_delivery_status = domain::EventUpdate::OverallDeliveryStatusUpdate {
        is_overall_delivery_successful: true,
    };

    let initial_attempt_id = updated_event.initial_attempt_id.as_ref();
    let delivery_attempt = updated_event.delivery_attempt;

    if let Some((
        initial_attempt_id,
        enums::WebhookDeliveryAttempt::InitialAttempt
        | enums::WebhookDeliveryAttempt::AutomaticRetry,
    )) = initial_attempt_id.zip(delivery_attempt)
    {
        state
            .store
            .update_event_by_merchant_id_event_id(
                key_manager_state,
                merchant_id,
                initial_attempt_id.as_str(),
                update_overall_delivery_status,
                &merchant_key_store,
            )
            .await
            .change_context(errors::WebhooksFlowError::WebhookEventUpdationFailed)
            .attach_printable("Failed to update initial delivery attempt")?;
    }

    Ok(())
}

fn increment_webhook_outgoing_received_count(merchant_id: &common_utils::id_type::MerchantId) {
    metrics::WEBHOOK_OUTGOING_RECEIVED_COUNT.add(
        1,
        router_env::metric_attributes!((MERCHANT_ID, merchant_id.clone())),
    )
}

async fn success_response_handler(
    state: SessionState,
    merchant_id: &common_utils::id_type::MerchantId,
    process_tracker: Option<storage::ProcessTracker>,
    business_status: &'static str,
) -> CustomResult<(), errors::WebhooksFlowError> {
    increment_webhook_outgoing_received_count(merchant_id);

    match process_tracker {
        Some(process_tracker) => state
            .store
            .as_scheduler()
            .finish_process_with_business_status(process_tracker, business_status)
            .await
            .change_context(
                errors::WebhooksFlowError::OutgoingWebhookProcessTrackerTaskUpdateFailed,
            ),
        None => Ok(()),
    }
}

async fn error_response_handler(
    state: SessionState,
    merchant_id: &common_utils::id_type::MerchantId,
    delivery_attempt: enums::WebhookDeliveryAttempt,
    status_code: u16,
    log_message: &'static str,
    schedule_webhook_retry: ScheduleWebhookRetry,
) -> CustomResult<(), errors::WebhooksFlowError> {
    metrics::WEBHOOK_OUTGOING_NOT_RECEIVED_COUNT.add(
        1,
        router_env::metric_attributes!((MERCHANT_ID, merchant_id.clone())),
    );

    let error = report!(errors::WebhooksFlowError::NotReceivedByMerchant);
    logger::warn!(?error, ?delivery_attempt, status_code, %log_message);

    if let ScheduleWebhookRetry::WithProcessTracker(process_tracker) = schedule_webhook_retry {
        // Schedule a retry attempt for webhook delivery
        outgoing_webhook_retry::retry_webhook_delivery_task(
            &*state.store,
            merchant_id,
            *process_tracker,
        )
        .await
        .change_context(errors::WebhooksFlowError::OutgoingWebhookRetrySchedulingFailed)?;
    }

    Err(error)
}

impl ForeignFrom<&api::OutgoingWebhookContent> for storage::EventMetadata {
    fn foreign_from(content: &api::OutgoingWebhookContent) -> Self {
        match content {
            webhooks::OutgoingWebhookContent::PaymentDetails(payments_response) => Self::Payment {
                payment_id: payments_response.payment_id.clone(),
            },
            webhooks::OutgoingWebhookContent::RefundDetails(refund_response) => Self::Refund {
                payment_id: refund_response.payment_id.clone(),
                refund_id: refund_response.refund_id.clone(),
            },
            webhooks::OutgoingWebhookContent::DisputeDetails(dispute_response) => Self::Dispute {
                payment_id: dispute_response.payment_id.clone(),
                attempt_id: dispute_response.attempt_id.clone(),
                dispute_id: dispute_response.dispute_id.clone(),
            },
            webhooks::OutgoingWebhookContent::MandateDetails(mandate_response) => Self::Mandate {
                payment_method_id: mandate_response.payment_method_id.clone(),
                mandate_id: mandate_response.mandate_id.clone(),
            },
            #[cfg(feature = "payouts")]
            webhooks::OutgoingWebhookContent::PayoutDetails(payout_response) => Self::Payout {
                payout_id: payout_response.payout_id.clone(),
            },
        }
    }
}

fn get_outgoing_webhook_event_content_from_event_metadata(
    event_metadata: Option<storage::EventMetadata>,
) -> Option<OutgoingWebhookEventContent> {
    event_metadata.map(|metadata| match metadata {
        diesel_models::EventMetadata::Payment { payment_id } => {
            OutgoingWebhookEventContent::Payment {
                payment_id,
                content: serde_json::Value::Null,
            }
        }
        diesel_models::EventMetadata::Payout { payout_id } => OutgoingWebhookEventContent::Payout {
            payout_id,
            content: serde_json::Value::Null,
        },
        diesel_models::EventMetadata::Refund {
            payment_id,
            refund_id,
        } => OutgoingWebhookEventContent::Refund {
            payment_id,
            refund_id,
            content: serde_json::Value::Null,
        },
        diesel_models::EventMetadata::Dispute {
            payment_id,
            attempt_id,
            dispute_id,
        } => OutgoingWebhookEventContent::Dispute {
            payment_id,
            attempt_id,
            dispute_id,
            content: serde_json::Value::Null,
        },
        diesel_models::EventMetadata::Mandate {
            payment_method_id,
            mandate_id,
        } => OutgoingWebhookEventContent::Mandate {
            payment_method_id,
            mandate_id,
            content: serde_json::Value::Null,
        },
    })
}


// Implementation: impl DecisionEngineEliminationData
// File: crates/api_models/src/open_router.rs
// Module: api_models
// Methods: 1 total (1 public)

impl DecisionEngineEliminationData


// Struct: BarclaycardConsumerAuthInformationResponse
// File: crates/hyperswitch_connectors/src/connectors/barclaycard/transformers.rs
// Module: hyperswitch_connectors
// Implementations: 0

pub struct BarclaycardConsumerAuthInformationResponse


// Function: get_payment_id
// File: crates/api_models/src/webhooks.rs
// Module: api_models

pub fn get_payment_id(&self) -> Option<common_utils::id_type::GlobalPaymentId>


// Struct: JpmorganRouterData
// File: crates/hyperswitch_connectors/src/connectors/jpmorgan/transformers.rs
// Module: hyperswitch_connectors
// Implementations: 0

pub struct JpmorganRouterData<T>


// File: crates/router/src/core/cards_info.rs
// Module: router
// Public functions: 6
// Public structs: 7

use actix_multipart::form::{bytes::Bytes, MultipartForm};
use api_models::cards_info as cards_info_api_types;
use common_utils::fp_utils::when;
use csv::Reader;
use diesel_models::cards_info as card_info_models;
use error_stack::{report, ResultExt};
use hyperswitch_domain_models::cards_info;
use rdkafka::message::ToBytes;
use router_env::{instrument, tracing};

use crate::{
    core::{
        errors::{self, RouterResponse, RouterResult, StorageErrorExt},
        payments::helpers,
    },
    routes,
    services::ApplicationResponse,
    types::{
        domain,
        transformers::{ForeignFrom, ForeignInto},
    },
};

fn verify_iin_length(card_iin: &str) -> Result<(), errors::ApiErrorResponse> {
    let is_bin_length_in_range = card_iin.len() == 6 || card_iin.len() == 8;
    when(!is_bin_length_in_range, || {
        Err(errors::ApiErrorResponse::InvalidCardIinLength)
    })
}

#[instrument(skip_all)]
pub async fn retrieve_card_info(
    state: routes::SessionState,
    merchant_context: domain::MerchantContext,
    request: cards_info_api_types::CardsInfoRequest,
) -> RouterResponse<cards_info_api_types::CardInfoResponse> {
    let db = state.store.as_ref();

    verify_iin_length(&request.card_iin)?;
    helpers::verify_payment_intent_time_and_client_secret(
        &state,
        &merchant_context,
        request.client_secret,
    )
    .await?;

    let card_info = db
        .get_card_info(&request.card_iin)
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Failed to retrieve card information")?
        .ok_or(report!(errors::ApiErrorResponse::InvalidCardIin))?;

    Ok(ApplicationResponse::Json(
        cards_info_api_types::CardInfoResponse::foreign_from(card_info),
    ))
}

#[instrument(skip_all)]
pub async fn create_card_info(
    state: routes::SessionState,
    card_info_request: cards_info_api_types::CardInfoCreateRequest,
) -> RouterResponse<cards_info_api_types::CardInfoResponse> {
    let db = state.store.as_ref();
    cards_info::CardsInfoInterface::add_card_info(db, card_info_request.foreign_into())
        .await
        .to_duplicate_response(errors::ApiErrorResponse::GenericDuplicateError {
            message: "CardInfo with given key already exists in our records".to_string(),
        })
        .map(|card_info| ApplicationResponse::Json(card_info.foreign_into()))
}

#[instrument(skip_all)]
pub async fn update_card_info(
    state: routes::SessionState,
    card_info_request: cards_info_api_types::CardInfoUpdateRequest,
) -> RouterResponse<cards_info_api_types::CardInfoResponse> {
    let db = state.store.as_ref();
    cards_info::CardsInfoInterface::update_card_info(
        db,
        card_info_request.card_iin,
        card_info_models::UpdateCardInfo {
            card_issuer: card_info_request.card_issuer,
            card_network: card_info_request.card_network,
            card_type: card_info_request.card_type,
            card_subtype: card_info_request.card_subtype,
            card_issuing_country: card_info_request.card_issuing_country,
            bank_code_id: card_info_request.bank_code_id,
            bank_code: card_info_request.bank_code,
            country_code: card_info_request.country_code,
            last_updated: Some(common_utils::date_time::now()),
            last_updated_provider: card_info_request.last_updated_provider,
        },
    )
    .await
    .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError {
        message: "Card info with given key does not exist in our records".to_string(),
    })
    .attach_printable("Failed while updating card info")
    .map(|card_info| ApplicationResponse::Json(card_info.foreign_into()))
}

#[derive(Debug, MultipartForm)]
pub struct CardsInfoUpdateForm {
    #[multipart(limit = "1MB")]
    pub file: Bytes,
}

fn parse_cards_bin_csv(
    data: &[u8],
) -> csv::Result<Vec<cards_info_api_types::CardInfoUpdateRequest>> {
    let mut csv_reader = Reader::from_reader(data);
    let mut records = Vec::new();
    let mut id_counter = 0;
    for result in csv_reader.deserialize() {
        let mut record: cards_info_api_types::CardInfoUpdateRequest = result?;
        id_counter += 1;
        record.line_number = Some(id_counter);
        records.push(record);
    }
    Ok(records)
}

pub fn get_cards_bin_records(
    form: CardsInfoUpdateForm,
) -> Result<Vec<cards_info_api_types::CardInfoUpdateRequest>, errors::ApiErrorResponse> {
    match parse_cards_bin_csv(form.file.data.to_bytes()) {
        Ok(records) => Ok(records),
        Err(e) => Err(errors::ApiErrorResponse::PreconditionFailed {
            message: e.to_string(),
        }),
    }
}

#[instrument(skip_all)]
pub async fn migrate_cards_info(
    state: routes::SessionState,
    card_info_records: Vec<cards_info_api_types::CardInfoUpdateRequest>,
) -> RouterResponse<Vec<cards_info_api_types::CardInfoMigrationResponse>> {
    let mut result = Vec::new();
    for record in card_info_records {
        let res = card_info_flow(record.clone(), state.clone()).await;
        result.push(cards_info_api_types::CardInfoMigrationResponse::from((
            match res {
                Ok(ApplicationResponse::Json(response)) => Ok(response),
                Err(e) => Err(e.to_string()),
                _ => Err("Failed to migrate card info".to_string()),
            },
            record,
        )));
    }
    Ok(ApplicationResponse::Json(result))
}

pub trait State {}
pub trait TransitionTo<S: State> {}
// Available states for card info migration
pub struct CardInfoFetch;
pub struct CardInfoAdd;
pub struct CardInfoUpdate;
pub struct CardInfoResponse;

impl State for CardInfoFetch {}
impl State for CardInfoAdd {}
impl State for CardInfoUpdate {}
impl State for CardInfoResponse {}

// State transitions for card info migration
impl TransitionTo<CardInfoAdd> for CardInfoFetch {}
impl TransitionTo<CardInfoUpdate> for CardInfoFetch {}
impl TransitionTo<CardInfoResponse> for CardInfoAdd {}
impl TransitionTo<CardInfoResponse> for CardInfoUpdate {}

// Async executor
pub struct CardInfoMigrateExecutor<'a> {
    state: &'a routes::SessionState,
    record: &'a cards_info_api_types::CardInfoUpdateRequest,
}

impl<'a> CardInfoMigrateExecutor<'a> {
    fn new(
        state: &'a routes::SessionState,
        record: &'a cards_info_api_types::CardInfoUpdateRequest,
    ) -> Self {
        Self { state, record }
    }

    async fn fetch_card_info(&self) -> RouterResult<Option<card_info_models::CardInfo>> {
        let db = self.state.store.as_ref();
        let maybe_card_info = db
            .get_card_info(&self.record.card_iin)
            .await
            .change_context(errors::ApiErrorResponse::InvalidCardIin)?;
        Ok(maybe_card_info)
    }

    async fn add_card_info(&self) -> RouterResult<card_info_models::CardInfo> {
        let db = self.state.store.as_ref();
        let card_info =
            cards_info::CardsInfoInterface::add_card_info(db, self.record.clone().foreign_into())
                .await
                .to_duplicate_response(errors::ApiErrorResponse::GenericDuplicateError {
                    message: "CardInfo with given key already exists in our records".to_string(),
                })?;
        Ok(card_info)
    }

    async fn update_card_info(&self) -> RouterResult<card_info_models::CardInfo> {
        let db = self.state.store.as_ref();
        let card_info = cards_info::CardsInfoInterface::update_card_info(
            db,
            self.record.card_iin.clone(),
            card_info_models::UpdateCardInfo {
                card_issuer: self.record.card_issuer.clone(),
                card_network: self.record.card_network.clone(),
                card_type: self.record.card_type.clone(),
                card_subtype: self.record.card_subtype.clone(),
                card_issuing_country: self.record.card_issuing_country.clone(),
                bank_code_id: self.record.bank_code_id.clone(),
                bank_code: self.record.bank_code.clone(),
                country_code: self.record.country_code.clone(),
                last_updated: Some(common_utils::date_time::now()),
                last_updated_provider: self.record.last_updated_provider.clone(),
            },
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError {
            message: "Card info with given key does not exist in our records".to_string(),
        })
        .attach_printable("Failed while updating card info")?;
        Ok(card_info)
    }
}

// Builder
pub struct CardInfoBuilder<S: State> {
    state: std::marker::PhantomData<S>,
    pub card_info: Option<card_info_models::CardInfo>,
}

impl CardInfoBuilder<CardInfoFetch> {
    fn new() -> Self {
        Self {
            state: std::marker::PhantomData,
            card_info: None,
        }
    }
}

impl CardInfoBuilder<CardInfoFetch> {
    fn set_card_info(
        self,
        card_info: card_info_models::CardInfo,
    ) -> CardInfoBuilder<CardInfoUpdate> {
        CardInfoBuilder {
            state: std::marker::PhantomData,
            card_info: Some(card_info),
        }
    }

    fn transition(self) -> CardInfoBuilder<CardInfoAdd> {
        CardInfoBuilder {
            state: std::marker::PhantomData,
            card_info: None,
        }
    }
}

impl CardInfoBuilder<CardInfoUpdate> {
    fn set_updated_card_info(
        self,
        card_info: card_info_models::CardInfo,
    ) -> CardInfoBuilder<CardInfoResponse> {
        CardInfoBuilder {
            state: std::marker::PhantomData,
            card_info: Some(card_info),
        }
    }
}

impl CardInfoBuilder<CardInfoAdd> {
    fn set_added_card_info(
        self,
        card_info: card_info_models::CardInfo,
    ) -> CardInfoBuilder<CardInfoResponse> {
        CardInfoBuilder {
            state: std::marker::PhantomData,
            card_info: Some(card_info),
        }
    }
}

impl CardInfoBuilder<CardInfoResponse> {
    pub fn build(self) -> cards_info_api_types::CardInfoMigrateResponseRecord {
        match self.card_info {
            Some(card_info) => cards_info_api_types::CardInfoMigrateResponseRecord {
                card_iin: Some(card_info.card_iin),
                card_issuer: card_info.card_issuer,
                card_network: card_info.card_network.map(|cn| cn.to_string()),
                card_type: card_info.card_type,
                card_sub_type: card_info.card_subtype,
                card_issuing_country: card_info.card_issuing_country,
            },
            None => cards_info_api_types::CardInfoMigrateResponseRecord {
                card_iin: None,
                card_issuer: None,
                card_network: None,
                card_type: None,
                card_sub_type: None,
                card_issuing_country: None,
            },
        }
    }
}

async fn card_info_flow(
    record: cards_info_api_types::CardInfoUpdateRequest,
    state: routes::SessionState,
) -> RouterResponse<cards_info_api_types::CardInfoMigrateResponseRecord> {
    let builder = CardInfoBuilder::new();
    let executor = CardInfoMigrateExecutor::new(&state, &record);
    let fetched_card_info_details = executor.fetch_card_info().await?;

    let builder = match fetched_card_info_details {
        Some(card_info) => {
            let builder = builder.set_card_info(card_info);
            let updated_card_info = executor.update_card_info().await?;
            builder.set_updated_card_info(updated_card_info)
        }
        None => {
            let builder = builder.transition();
            let added_card_info = executor.add_card_info().await?;
            builder.set_added_card_info(added_card_info)
        }
    };

    Ok(ApplicationResponse::Json(builder.build()))
}


// Trait: ConvertRaw
// File: crates/common_utils/src/keymanager.rs
// Module: common_utils

// Documentation: Trait to convert the raw data to the required format for encryption service request

pub trait ConvertRaw


// Implementation: impl ApiEventMetricsBucketIdentifier
// File: crates/api_models/src/analytics/api_event.rs
// Module: api_models
// Methods: 1 total (1 public)

impl ApiEventMetricsBucketIdentifier


// Struct: ZslWebhookResponse
// File: crates/hyperswitch_connectors/src/connectors/zsl/transformers.rs
// Module: hyperswitch_connectors
// Implementations: 0

pub struct ZslWebhookResponse


// Struct: PlaidSyncResponse
// File: crates/hyperswitch_connectors/src/connectors/plaid/transformers.rs
// Module: hyperswitch_connectors
// Implementations: 0

pub struct PlaidSyncResponse


// Struct: PaypalFulfillRequest
// File: crates/hyperswitch_connectors/src/connectors/paypal/transformers.rs
// Module: hyperswitch_connectors
// Implementations: 0

pub struct PaypalFulfillRequest


// Struct: MerchantKeyStore
// File: crates/diesel_models/src/merchant_key_store.rs
// Module: diesel_models
// Implementations: 0

pub struct MerchantKeyStore


// Struct: User
// File: crates/hyperswitch_connectors/src/connectors/plaid/transformers.rs
// Module: hyperswitch_connectors
// Implementations: 0

pub struct User


// Function: which
// File: crates/router_env/src/env.rs
// Module: router_env

// Documentation: Name of current environment. Either "development", "sandbox" or "production".

pub fn which() -> Env


// Implementation: impl Drop for for RdKafkaProducer
// File: crates/router/src/services/kafka.rs
// Module: router
// Methods: 1 total (0 public)

impl Drop for for RdKafkaProducer


// Struct: RoutingStrategy
// File: crates/external_services/src/grpc_client/dynamic_routing.rs
// Module: external_services
// Implementations: 0

pub struct RoutingStrategy


// Struct: PaytmPaymentsResponse
// File: crates/hyperswitch_connectors/src/connectors/paytm/transformers.rs
// Module: hyperswitch_connectors
// Implementations: 0

pub struct PaytmPaymentsResponse


// Implementation: impl api::PaymentAuthorize for for Square
// File: crates/hyperswitch_connectors/src/connectors/square.rs
// Module: hyperswitch_connectors
// Methods: 0 total (0 public)

impl api::PaymentAuthorize for for Square


// Implementation: impl Bluecode
// File: crates/hyperswitch_connectors/src/connectors/bluecode.rs
// Module: hyperswitch_connectors
// Methods: 1 total (1 public)

impl Bluecode


// Function: get_user_theme_using_theme_id
// File: crates/router/src/routes/user/theme.rs
// Module: router

pub fn get_user_theme_using_theme_id(
    state: web::Data<AppState>,
    req: HttpRequest,
    path: web::Path<String>,
) -> HttpResponse


// Function: find_resources
// File: crates/storage_impl/src/lib.rs
// Module: storage_impl

pub fn find_resources<D, R, M>(
        &self,
        state: &KeyManagerState,
        key_store: &MerchantKeyStore,
        execute_query: R,
    ) -> error_stack::Result<Vec<D>, StorageError>
    where
        D: Debug + Sync + Conversion,
        R: futures::Future<
                Output = error_stack::Result<Vec<M>, diesel_models::errors::DatabaseError>,
            > + Send,
        M: ReverseConversion<D>,


// Struct: KlarnaSdkResponse
// File: crates/hyperswitch_domain_models/src/router_data.rs
// Module: hyperswitch_domain_models
// Implementations: 0

pub struct KlarnaSdkResponse