// File: crates/storage_impl/src/payouts/payout_attempt.rs

use std::str::FromStr;

use api_models::enums::PayoutConnectors;
use common_utils::{errors::CustomResult, ext_traits::Encode, fallback_reverse_lookup_not_found};
use diesel_models::{
    enums::MerchantStorageScheme,
    kv,
    payout_attempt::{
        PayoutAttempt as DieselPayoutAttempt, PayoutAttemptNew as DieselPayoutAttemptNew,
        PayoutAttemptUpdate as DieselPayoutAttemptUpdate,
    },
    reverse_lookup::ReverseLookup,
    ReverseLookupNew,
};
use error_stack::ResultExt;
use hyperswitch_domain_models::payouts::{
    payout_attempt::{
        PayoutAttempt, PayoutAttemptInterface, PayoutAttemptNew, PayoutAttemptUpdate,
        PayoutListFilters,
    },
    payouts::Payouts,
};
use redis_interface::HsetnxReply;
use router_env::{instrument, logger, tracing};

use crate::{
    diesel_error_to_data_error, errors,
    errors::RedisErrorExt,
    kv_router_store::KVRouterStore,
    lookup::ReverseLookupInterface,
    redis::kv_store::{decide_storage_scheme, kv_wrapper, KvOperation, Op, PartitionKey},
    utils::{self, pg_connection_read, pg_connection_write},
    DataModelExt, DatabaseStore,
};

#[async_trait::async_trait]
impl<T: DatabaseStore> PayoutAttemptInterface for KVRouterStore<T> {
    type Error = errors::StorageError;
    #[instrument(skip_all)]
    async fn insert_payout_attempt(
        &self,
        new_payout_attempt: PayoutAttemptNew,
        payouts: &Payouts,
        storage_scheme: MerchantStorageScheme,
    ) -> error_stack::Result<PayoutAttempt, errors::StorageError> {
        let storage_scheme = Box::pin(decide_storage_scheme::<_, DieselPayoutAttempt>(
            self,
            storage_scheme,
            Op::Insert,
        ))
        .await;
        match storage_scheme {
            MerchantStorageScheme::PostgresOnly => {
                self.router_store
                    .insert_payout_attempt(new_payout_attempt, payouts, storage_scheme)
                    .await
            }
            MerchantStorageScheme::RedisKv => {
                let merchant_id = new_payout_attempt.merchant_id.clone();
                let payout_attempt_id = new_payout_attempt.payout_id.clone();
                let key = PartitionKey::MerchantIdPayoutAttemptId {
                    merchant_id: &merchant_id,
                    payout_attempt_id: payout_attempt_id.get_string_repr(),
                };
                let key_str = key.to_string();
                let created_attempt = PayoutAttempt {
                    payout_attempt_id: new_payout_attempt.payout_attempt_id.clone(),
                    payout_id: new_payout_attempt.payout_id.clone(),
                    additional_payout_method_data: new_payout_attempt
                        .additional_payout_method_data
                        .clone(),
                    customer_id: new_payout_attempt.customer_id.clone(),
                    merchant_id: new_payout_attempt.merchant_id.clone(),
                    address_id: new_payout_attempt.address_id.clone(),
                    connector: new_payout_attempt.connector.clone(),
                    connector_payout_id: new_payout_attempt.connector_payout_id.clone(),
                    payout_token: new_payout_attempt.payout_token.clone(),
                    status: new_payout_attempt.status,
                    is_eligible: new_payout_attempt.is_eligible,
                    error_message: new_payout_attempt.error_message.clone(),
                    error_code: new_payout_attempt.error_code.clone(),
                    business_country: new_payout_attempt.business_country,
                    business_label: new_payout_attempt.business_label.clone(),
                    created_at: new_payout_attempt.created_at,
                    last_modified_at: new_payout_attempt.last_modified_at,
                    profile_id: new_payout_attempt.profile_id.clone(),
                    merchant_connector_id: new_payout_attempt.merchant_connector_id.clone(),
                    routing_info: new_payout_attempt.routing_info.clone(),
                    unified_code: new_payout_attempt.unified_code.clone(),
                    unified_message: new_payout_attempt.unified_message.clone(),
                    merchant_order_reference_id: new_payout_attempt
                        .merchant_order_reference_id
                        .clone(),
                    payout_connector_metadata: new_payout_attempt.payout_connector_metadata.clone(),
                };

                let redis_entry = kv::TypedSql {
                    op: kv::DBOperation::Insert {
                        insertable: Box::new(kv::Insertable::PayoutAttempt(
                            new_payout_attempt.to_storage_model(),
                        )),
                    },
                };

                // Reverse lookup for payout_attempt_id
                let field = format!("poa_{}", created_attempt.payout_attempt_id);
                let reverse_lookup = ReverseLookupNew {
                    lookup_id: format!(
                        "poa_{}_{}",
                        &created_attempt.merchant_id.get_string_repr(),
                        &created_attempt.payout_attempt_id,
                    ),
                    pk_id: key_str.clone(),
                    sk_id: field.clone(),
                    source: "payout_attempt".to_string(),
                    updated_by: storage_scheme.to_string(),
                };
                self.insert_reverse_lookup(reverse_lookup, storage_scheme)
                    .await?;

                match Box::pin(kv_wrapper::<DieselPayoutAttempt, _, _>(
                    self,
                    KvOperation::<DieselPayoutAttempt>::HSetNx(
                        &field,
                        &created_attempt.clone().to_storage_model(),
                        redis_entry,
                    ),
                    key,
                ))
                .await
                .map_err(|err| err.to_redis_failed_response(&key_str))?
                .try_into_hsetnx()
                {
                    Ok(HsetnxReply::KeyNotSet) => Err(errors::StorageError::DuplicateValue {
                        entity: "payout attempt",
                        key: Some(key_str),
                    }
                    .into()),
                    Ok(HsetnxReply::KeySet) => Ok(created_attempt),
                    Err(error) => Err(error.change_context(errors::StorageError::KVError)),
                }
            }
        }
    }

    #[instrument(skip_all)]
    async fn update_payout_attempt(
        &self,
        this: &PayoutAttempt,
        payout_update: PayoutAttemptUpdate,
        payouts: &Payouts,
        storage_scheme: MerchantStorageScheme,
    ) -> error_stack::Result<PayoutAttempt, errors::StorageError> {
        let key = PartitionKey::MerchantIdPayoutAttemptId {
            merchant_id: &this.merchant_id,
            payout_attempt_id: this.payout_id.get_string_repr(),
        };
        let field = format!("poa_{}", this.payout_attempt_id);
        let storage_scheme = Box::pin(decide_storage_scheme::<_, DieselPayoutAttempt>(
            self,
            storage_scheme,
            Op::Update(key.clone(), &field, None),
        ))
        .await;
        match storage_scheme {
            MerchantStorageScheme::PostgresOnly => {
                self.router_store
                    .update_payout_attempt(this, payout_update, payouts, storage_scheme)
                    .await
            }
            MerchantStorageScheme::RedisKv => {
                let key_str = key.to_string();

                let diesel_payout_update = payout_update.clone().to_storage_model();
                let origin_diesel_payout = this.clone().to_storage_model();

                let diesel_payout = diesel_payout_update
                    .clone()
                    .apply_changeset(origin_diesel_payout.clone());
                // Check for database presence as well Maybe use a read replica here ?

                let redis_value = diesel_payout
                    .encode_to_string_of_json()
                    .change_context(errors::StorageError::SerializationFailed)?;

                let redis_entry = kv::TypedSql {
                    op: kv::DBOperation::Update {
                        updatable: Box::new(kv::Updateable::PayoutAttemptUpdate(
                            kv::PayoutAttemptUpdateMems {
                                orig: origin_diesel_payout,
                                update_data: diesel_payout_update,
                            },
                        )),
                    },
                };

                let updated_attempt = PayoutAttempt::from_storage_model(
                    payout_update
                        .to_storage_model()
                        .apply_changeset(this.clone().to_storage_model()),
                );
                let old_connector_payout_id = this.connector_payout_id.clone();

                match (
                    old_connector_payout_id,
                    updated_attempt.connector_payout_id.clone(),
                ) {
                    (Some(old), Some(new)) if old != new => {
                        add_connector_payout_id_to_reverse_lookup(
                            self,
                            key_str.as_str(),
                            &this.merchant_id,
                            updated_attempt.payout_attempt_id.as_str(),
                            new.as_str(),
                            storage_scheme,
                        )
                        .await?;
                    }
                    (None, Some(new)) => {
                        add_connector_payout_id_to_reverse_lookup(
                            self,
                            key_str.as_str(),
                            &this.merchant_id,
                            updated_attempt.payout_attempt_id.as_str(),
                            new.as_str(),
                            storage_scheme,
                        )
                        .await?;
                    }
                    _ => {}
                }

                Box::pin(kv_wrapper::<(), _, _>(
                    self,
                    KvOperation::<DieselPayoutAttempt>::Hset((&field, redis_value), redis_entry),
                    key,
                ))
                .await
                .map_err(|err| err.to_redis_failed_response(&key_str))?
                .try_into_hset()
                .change_context(errors::StorageError::KVError)?;

                Ok(PayoutAttempt::from_storage_model(diesel_payout))
            }
        }
    }

    #[instrument(skip_all)]
    async fn find_payout_attempt_by_merchant_id_payout_attempt_id(
        &self,
        merchant_id: &common_utils::id_type::MerchantId,
        payout_attempt_id: &str,
        storage_scheme: MerchantStorageScheme,
    ) -> error_stack::Result<PayoutAttempt, errors::StorageError> {
        let storage_scheme = Box::pin(decide_storage_scheme::<_, DieselPayoutAttempt>(
            self,
            storage_scheme,
            Op::Find,
        ))
        .await;
        match storage_scheme {
            MerchantStorageScheme::PostgresOnly => {
                self.router_store
                    .find_payout_attempt_by_merchant_id_payout_attempt_id(
                        merchant_id,
                        payout_attempt_id,
                        storage_scheme,
                    )
                    .await
            }
            MerchantStorageScheme::RedisKv => {
                let lookup_id =
                    format!("poa_{}_{payout_attempt_id}", merchant_id.get_string_repr());
                let lookup = fallback_reverse_lookup_not_found!(
                    self.get_lookup_by_lookup_id(&lookup_id, storage_scheme)
                        .await,
                    self.router_store
                        .find_payout_attempt_by_merchant_id_payout_attempt_id(
                            merchant_id,
                            payout_attempt_id,
                            storage_scheme
                        )
                        .await
                );
                let key = PartitionKey::CombinationKey {
                    combination: &lookup.pk_id,
                };
                Box::pin(utils::try_redis_get_else_try_database_get(
                    async {
                        Box::pin(kv_wrapper(
                            self,
                            KvOperation::<DieselPayoutAttempt>::HGet(&lookup.sk_id),
                            key,
                        ))
                        .await?
                        .try_into_hget()
                    },
                    || async {
                        self.router_store
                            .find_payout_attempt_by_merchant_id_payout_attempt_id(
                                merchant_id,
                                payout_attempt_id,
                                storage_scheme,
                            )
                            .await
                    },
                ))
                .await
            }
        }
    }

    #[instrument(skip_all)]
    async fn find_payout_attempt_by_merchant_id_connector_payout_id(
        &self,
        merchant_id: &common_utils::id_type::MerchantId,
        connector_payout_id: &str,
        storage_scheme: MerchantStorageScheme,
    ) -> error_stack::Result<PayoutAttempt, errors::StorageError> {
        match storage_scheme {
            MerchantStorageScheme::PostgresOnly => {
                self.router_store
                    .find_payout_attempt_by_merchant_id_connector_payout_id(
                        merchant_id,
                        connector_payout_id,
                        storage_scheme,
                    )
                    .await
            }
            MerchantStorageScheme::RedisKv => {
                let lookup_id = format!(
                    "po_conn_payout_{}_{connector_payout_id}",
                    merchant_id.get_string_repr()
                );
                let lookup = fallback_reverse_lookup_not_found!(
                    self.get_lookup_by_lookup_id(&lookup_id, storage_scheme)
                        .await,
                    self.router_store
                        .find_payout_attempt_by_merchant_id_connector_payout_id(
                            merchant_id,
                            connector_payout_id,
                            storage_scheme,
                        )
                        .await
                );
                let key = PartitionKey::CombinationKey {
                    combination: &lookup.pk_id,
                };
                Box::pin(utils::try_redis_get_else_try_database_get(
                    async {
                        Box::pin(kv_wrapper(
                            self,
                            KvOperation::<DieselPayoutAttempt>::HGet(&lookup.sk_id),
                            key,
                        ))
                        .await?
                        .try_into_hget()
                    },
                    || async {
                        self.router_store
                            .find_payout_attempt_by_merchant_id_connector_payout_id(
                                merchant_id,
                                connector_payout_id,
                                storage_scheme,
                            )
                            .await
                    },
                ))
                .await
            }
        }
    }

    #[instrument(skip_all)]
    async fn get_filters_for_payouts(
        &self,
        payouts: &[Payouts],
        merchant_id: &common_utils::id_type::MerchantId,
        storage_scheme: MerchantStorageScheme,
    ) -> error_stack::Result<PayoutListFilters, errors::StorageError> {
        self.router_store
            .get_filters_for_payouts(payouts, merchant_id, storage_scheme)
            .await
    }

    #[instrument(skip_all)]
    async fn find_payout_attempt_by_merchant_id_merchant_order_reference_id(
        &self,
        merchant_id: &common_utils::id_type::MerchantId,
        merchant_order_reference_id: &str,
        storage_scheme: MerchantStorageScheme,
    ) -> error_stack::Result<PayoutAttempt, errors::StorageError> {
        self.router_store
            .find_payout_attempt_by_merchant_id_merchant_order_reference_id(
                merchant_id,
                merchant_order_reference_id,
                storage_scheme,
            )
            .await
    }
}

#[async_trait::async_trait]
impl<T: DatabaseStore> PayoutAttemptInterface for crate::RouterStore<T> {
    type Error = errors::StorageError;
    #[instrument(skip_all)]
    async fn insert_payout_attempt(
        &self,
        new: PayoutAttemptNew,
        _payouts: &Payouts,
        _storage_scheme: MerchantStorageScheme,
    ) -> error_stack::Result<PayoutAttempt, errors::StorageError> {
        let conn = pg_connection_write(self).await?;
        new.to_storage_model()
            .insert(&conn)
            .await
            .map_err(|er| {
                let new_err = diesel_error_to_data_error(*er.current_context());
                er.change_context(new_err)
            })
            .map(PayoutAttempt::from_storage_model)
    }

    #[instrument(skip_all)]
    async fn update_payout_attempt(
        &self,
        this: &PayoutAttempt,
        payout: PayoutAttemptUpdate,
        _payouts: &Payouts,
        _storage_scheme: MerchantStorageScheme,
    ) -> error_stack::Result<PayoutAttempt, errors::StorageError> {
        let conn = pg_connection_write(self).await?;
        this.clone()
            .to_storage_model()
            .update_with_attempt_id(&conn, payout.to_storage_model())
            .await
            .map_err(|er| {
                let new_err = diesel_error_to_data_error(*er.current_context());
                er.change_context(new_err)
            })
            .map(PayoutAttempt::from_storage_model)
    }

    #[instrument(skip_all)]
    async fn find_payout_attempt_by_merchant_id_payout_attempt_id(
        &self,
        merchant_id: &common_utils::id_type::MerchantId,
        payout_attempt_id: &str,
        _storage_scheme: MerchantStorageScheme,
    ) -> error_stack::Result<PayoutAttempt, errors::StorageError> {
        let conn = pg_connection_read(self).await?;
        DieselPayoutAttempt::find_by_merchant_id_payout_attempt_id(
            &conn,
            merchant_id,
            payout_attempt_id,
        )
        .await
        .map(PayoutAttempt::from_storage_model)
        .map_err(|er| {
            let new_err = diesel_error_to_data_error(*er.current_context());
            er.change_context(new_err)
        })
    }

    #[instrument(skip_all)]
    async fn find_payout_attempt_by_merchant_id_connector_payout_id(
        &self,
        merchant_id: &common_utils::id_type::MerchantId,
        connector_payout_id: &str,
        _storage_scheme: MerchantStorageScheme,
    ) -> error_stack::Result<PayoutAttempt, errors::StorageError> {
        let conn = pg_connection_read(self).await?;
        DieselPayoutAttempt::find_by_merchant_id_connector_payout_id(
            &conn,
            merchant_id,
            connector_payout_id,
        )
        .await
        .map(PayoutAttempt::from_storage_model)
        .map_err(|er| {
            let new_err = diesel_error_to_data_error(*er.current_context());
            er.change_context(new_err)
        })
    }

    #[instrument(skip_all)]
    async fn get_filters_for_payouts(
        &self,
        payouts: &[Payouts],
        merchant_id: &common_utils::id_type::MerchantId,
        _storage_scheme: MerchantStorageScheme,
    ) -> CustomResult<PayoutListFilters, errors::StorageError> {
        let conn = pg_connection_read(self).await?;
        let payouts = payouts
            .iter()
            .cloned()
            .map(|payouts| payouts.to_storage_model())
            .collect::<Vec<diesel_models::Payouts>>();
        DieselPayoutAttempt::get_filters_for_payouts(&conn, payouts.as_slice(), merchant_id)
            .await
            .map_err(|er| {
                let new_err = diesel_error_to_data_error(*er.current_context());
                er.change_context(new_err)
            })
            .map(
                |(connector, currency, status, payout_method)| PayoutListFilters {
                    connector: connector
                        .iter()
                        .filter_map(|c| {
                            PayoutConnectors::from_str(c)
                                .map_err(|e| {
                                    logger::error!(
                                        "Failed to parse payout connector '{}' - {}",
                                        c,
                                        e
                                    );
                                })
                                .ok()
                        })
                        .collect(),
                    currency,
                    status,
                    payout_method,
                },
            )
    }

    #[instrument(skip_all)]
    async fn find_payout_attempt_by_merchant_id_merchant_order_reference_id(
        &self,
        merchant_id: &common_utils::id_type::MerchantId,
        merchant_order_reference_id: &str,
        _storage_scheme: MerchantStorageScheme,
    ) -> error_stack::Result<PayoutAttempt, errors::StorageError> {
        let conn = pg_connection_read(self).await?;
        DieselPayoutAttempt::find_by_merchant_id_merchant_order_reference_id(
            &conn,
            merchant_id,
            merchant_order_reference_id,
        )
        .await
        .map(PayoutAttempt::from_storage_model)
        .map_err(|er| {
            let new_err = diesel_error_to_data_error(*er.current_context());
            er.change_context(new_err)
        })
    }
}

impl DataModelExt for PayoutAttempt {
    type StorageModel = DieselPayoutAttempt;

    fn to_storage_model(self) -> Self::StorageModel {
        DieselPayoutAttempt {
            payout_attempt_id: self.payout_attempt_id,
            payout_id: self.payout_id,
            customer_id: self.customer_id,
            merchant_id: self.merchant_id,
            address_id: self.address_id,
            connector: self.connector,
            connector_payout_id: self.connector_payout_id,
            payout_token: self.payout_token,
            status: self.status,
            is_eligible: self.is_eligible,
            error_message: self.error_message,
            error_code: self.error_code,
            business_country: self.business_country,
            business_label: self.business_label,
            created_at: self.created_at,
            last_modified_at: self.last_modified_at,
            profile_id: self.profile_id,
            merchant_connector_id: self.merchant_connector_id,
            routing_info: self.routing_info,
            unified_code: self.unified_code,
            unified_message: self.unified_message,
            additional_payout_method_data: self.additional_payout_method_data,
            merchant_order_reference_id: self.merchant_order_reference_id,
            payout_connector_metadata: self.payout_connector_metadata,
        }
    }

    fn from_storage_model(storage_model: Self::StorageModel) -> Self {
        Self {
            payout_attempt_id: storage_model.payout_attempt_id,
            payout_id: storage_model.payout_id,
            customer_id: storage_model.customer_id,
            merchant_id: storage_model.merchant_id,
            address_id: storage_model.address_id,
            connector: storage_model.connector,
            connector_payout_id: storage_model.connector_payout_id,
            payout_token: storage_model.payout_token,
            status: storage_model.status,
            is_eligible: storage_model.is_eligible,
            error_message: storage_model.error_message,
            error_code: storage_model.error_code,
            business_country: storage_model.business_country,
            business_label: storage_model.business_label,
            created_at: storage_model.created_at,
            last_modified_at: storage_model.last_modified_at,
            profile_id: storage_model.profile_id,
            merchant_connector_id: storage_model.merchant_connector_id,
            routing_info: storage_model.routing_info,
            unified_code: storage_model.unified_code,
            unified_message: storage_model.unified_message,
            additional_payout_method_data: storage_model.additional_payout_method_data,
            merchant_order_reference_id: storage_model.merchant_order_reference_id,
            payout_connector_metadata: storage_model.payout_connector_metadata,
        }
    }
}
impl DataModelExt for PayoutAttemptNew {
    type StorageModel = DieselPayoutAttemptNew;

    fn to_storage_model(self) -> Self::StorageModel {
        DieselPayoutAttemptNew {
            payout_attempt_id: self.payout_attempt_id,
            payout_id: self.payout_id,
            customer_id: self.customer_id,
            merchant_id: self.merchant_id,
            address_id: self.address_id,
            connector: self.connector,
            connector_payout_id: self.connector_payout_id,
            payout_token: self.payout_token,
            status: self.status,
            is_eligible: self.is_eligible,
            error_message: self.error_message,
            error_code: self.error_code,
            business_country: self.business_country,
            business_label: self.business_label,
            created_at: self.created_at,
            last_modified_at: self.last_modified_at,
            profile_id: self.profile_id,
            merchant_connector_id: self.merchant_connector_id,
            routing_info: self.routing_info,
            unified_code: self.unified_code,
            unified_message: self.unified_message,
            additional_payout_method_data: self.additional_payout_method_data,
            merchant_order_reference_id: self.merchant_order_reference_id,
            payout_connector_metadata: self.payout_connector_metadata,
        }
    }

    fn from_storage_model(storage_model: Self::StorageModel) -> Self {
        Self {
            payout_attempt_id: storage_model.payout_attempt_id,
            payout_id: storage_model.payout_id,
            customer_id: storage_model.customer_id,
            merchant_id: storage_model.merchant_id,
            address_id: storage_model.address_id,
            connector: storage_model.connector,
            connector_payout_id: storage_model.connector_payout_id,
            payout_token: storage_model.payout_token,
            status: storage_model.status,
            is_eligible: storage_model.is_eligible,
            error_message: storage_model.error_message,
            error_code: storage_model.error_code,
            business_country: storage_model.business_country,
            business_label: storage_model.business_label,
            created_at: storage_model.created_at,
            last_modified_at: storage_model.last_modified_at,
            profile_id: storage_model.profile_id,
            merchant_connector_id: storage_model.merchant_connector_id,
            routing_info: storage_model.routing_info,
            unified_code: storage_model.unified_code,
            unified_message: storage_model.unified_message,
            additional_payout_method_data: storage_model.additional_payout_method_data,
            merchant_order_reference_id: storage_model.merchant_order_reference_id,
            payout_connector_metadata: storage_model.payout_connector_metadata,
        }
    }
}
impl DataModelExt for PayoutAttemptUpdate {
    type StorageModel = DieselPayoutAttemptUpdate;
    fn to_storage_model(self) -> Self::StorageModel {
        match self {
            Self::StatusUpdate {
                connector_payout_id,
                status,
                error_message,
                error_code,
                is_eligible,
                unified_code,
                unified_message,
                payout_connector_metadata,
            } => DieselPayoutAttemptUpdate::StatusUpdate {
                connector_payout_id,
                status,
                error_message,
                error_code,
                is_eligible,
                unified_code,
                unified_message,
                payout_connector_metadata,
            },
            Self::PayoutTokenUpdate { payout_token } => {
                DieselPayoutAttemptUpdate::PayoutTokenUpdate { payout_token }
            }
            Self::BusinessUpdate {
                business_country,
                business_label,
                address_id,
                customer_id,
            } => DieselPayoutAttemptUpdate::BusinessUpdate {
                business_country,
                business_label,
                address_id,
                customer_id,
            },
            Self::UpdateRouting {
                connector,
                routing_info,
                merchant_connector_id,
            } => DieselPayoutAttemptUpdate::UpdateRouting {
                connector,
                routing_info,
                merchant_connector_id,
            },
            Self::AdditionalPayoutMethodDataUpdate {
                additional_payout_method_data,
            } => DieselPayoutAttemptUpdate::AdditionalPayoutMethodDataUpdate {
                additional_payout_method_data,
            },
        }
    }

    #[allow(clippy::todo)]
    fn from_storage_model(_storage_model: Self::StorageModel) -> Self {
        todo!("Reverse map should no longer be needed")
    }
}

#[inline]
#[instrument(skip_all)]
async fn add_connector_payout_id_to_reverse_lookup<T: DatabaseStore>(
    store: &KVRouterStore<T>,
    key: &str,
    merchant_id: &common_utils::id_type::MerchantId,
    updated_attempt_attempt_id: &str,
    connector_payout_id: &str,
    storage_scheme: MerchantStorageScheme,
) -> CustomResult<ReverseLookup, errors::StorageError> {
    let field = format!("poa_{updated_attempt_attempt_id}");
    let reverse_lookup_new = ReverseLookupNew {
        lookup_id: format!(
            "po_conn_payout_{}_{}",
            merchant_id.get_string_repr(),
            connector_payout_id
        ),
        pk_id: key.to_owned(),
        sk_id: field.clone(),
        source: "payout_attempt".to_string(),
        updated_by: storage_scheme.to_string(),
    };
    store
        .insert_reverse_lookup(reverse_lookup_new, storage_scheme)
        .await
}


// File: crates/common_utils/Cargo.toml

[package]
name = "common_utils"
description = "Utilities shared across `router` and other crates"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
readme = "README.md"
license.workspace = true

[features]
default = []
keymanager = ["dep:router_env"]
keymanager_mtls = ["reqwest/rustls-tls"]
encryption_service = ["dep:router_env"]
km_forward_x_request_id = ["dep:router_env", "router_env/actix_web"]
signals = ["dep:signal-hook-tokio", "dep:signal-hook", "dep:tokio", "dep:router_env", "dep:futures"]
async_ext = ["dep:async-trait", "dep:futures"]
logs = ["dep:router_env"]
metrics = ["dep:router_env", "dep:futures"]
payouts = ["common_enums/payouts"]
v1 = []
v2 = []
crypto_openssl = ["dep:openssl"]
tokenization_v2 = []

[dependencies]
async-trait = { version = "0.1.88", optional = true }
base64 = "0.22.1"
base64-serde = "0.8.0"
blake3 = { version = "1.8.2", features = ["serde"] }
bytes = "1.10.1"
diesel = "2.2.10"
error-stack = "0.4.1"
futures = { version = "0.3.31", optional = true }
globset = "0.4.16"
hex = "0.4.3"
http = "0.2.12"
md5 = "0.7.0"
nanoid = "0.4.0"
nutype = { version = "0.4.3", features = ["serde"] }
once_cell = "1.21.3"
openssl = {version = "0.10.72", optional = true}
pem = "2.0.0"
phonenumber = "0.3.7"
quick-xml = { version = "0.31.0", features = ["serialize"] }
rand = "0.8.5"
regex = "1.11.1"
reqwest = { version = "0.11.27", features = ["json", "native-tls", "gzip", "multipart"] }
ring = { version = "0.17.14", features = ["std", "wasm32_unknown_unknown_js"] }
rsa = {version = "0.9.8", features = ["sha2"]}
rust_decimal = "1.37"
rustc-hash = "1.1.0"
semver = { version = "1.0.26", features = ["serde"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
serde_urlencoded = "0.7.1"
signal-hook = { version = "0.3.18", optional = true }
strum = { version = "0.26.3", features = ["derive"] }
thiserror = "1.0.69"
time = { version = "0.3.41", features = ["serde", "serde-well-known", "std"] }
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread"], optional = true }
url = { version = "2.5.4", features = ["serde"] }
urlencoding = "2.1.3"
utoipa = { version = "4.2.3", features = ["preserve_order", "preserve_path_order"] }
uuid = { version = "1.17.0", features = ["v7"] }
ammonia = "3.3"

# First party crates
common_enums = { version = "0.1.0", path = "../common_enums" }
masking = { version = "0.1.0", path = "../masking" }
router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"], optional = true, default-features = false }

[target.'cfg(target_arch = "wasm32")'.dependencies.uuid]
version = "1.17.0"
features = ["v7", "js"]

[target.'cfg(not(target_os = "windows"))'.dependencies]
signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"], optional = true }

[dev-dependencies]
fake = "2.10.0"
proptest = "1.6.0"
test-case = "3.3.1"

[lints]
workspace = true


// File: crates/common_utils/README.md

# Common Utils

Utilities shared across `router` and other crates.


// File: crates/common_utils/tests/percentage.rs

#![allow(clippy::panic_in_result_fn)]
use common_utils::{errors::PercentageError, types::Percentage};
const PRECISION_2: u8 = 2;
const PRECISION_0: u8 = 0;

#[test]
fn invalid_range_more_than_100() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let percentage = Percentage::<PRECISION_2>::from_string("100.01".to_string());
    assert!(percentage.is_err());
    if let Err(err) = percentage {
        assert_eq!(
            *err.current_context(),
            PercentageError::InvalidPercentageValue
        )
    }
    Ok(())
}
#[test]
fn invalid_range_less_than_0() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let percentage = Percentage::<PRECISION_2>::from_string("-0.01".to_string());
    assert!(percentage.is_err());
    if let Err(err) = percentage {
        assert_eq!(
            *err.current_context(),
            PercentageError::InvalidPercentageValue
        )
    }
    Ok(())
}

#[test]
fn invalid_string() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let percentage = Percentage::<PRECISION_2>::from_string("-0.01ed".to_string());
    assert!(percentage.is_err());
    if let Err(err) = percentage {
        assert_eq!(
            *err.current_context(),
            PercentageError::InvalidPercentageValue
        )
    }
    Ok(())
}

#[test]
fn valid_range() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let percentage = Percentage::<PRECISION_2>::from_string("2.22".to_string());
    assert!(percentage.is_ok());
    if let Ok(percentage) = percentage {
        assert_eq!(percentage.get_percentage(), 2.22)
    }

    let percentage = Percentage::<PRECISION_2>::from_string("0.05".to_string());
    assert!(percentage.is_ok());
    if let Ok(percentage) = percentage {
        assert_eq!(percentage.get_percentage(), 0.05)
    }

    let percentage = Percentage::<PRECISION_2>::from_string("100.0".to_string());
    assert!(percentage.is_ok());
    if let Ok(percentage) = percentage {
        assert_eq!(percentage.get_percentage(), 100.0)
    }
    Ok(())
}
#[test]
fn valid_precision() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let percentage = Percentage::<PRECISION_2>::from_string("2.2".to_string());
    assert!(percentage.is_ok());
    if let Ok(percentage) = percentage {
        assert_eq!(percentage.get_percentage(), 2.2)
    }

    let percentage = Percentage::<PRECISION_2>::from_string("2.20000".to_string());
    assert!(percentage.is_ok());
    if let Ok(percentage) = percentage {
        assert_eq!(percentage.get_percentage(), 2.2)
    }

    let percentage = Percentage::<PRECISION_0>::from_string("2.0".to_string());
    assert!(percentage.is_ok());
    if let Ok(percentage) = percentage {
        assert_eq!(percentage.get_percentage(), 2.0)
    }

    Ok(())
}

#[test]
fn invalid_precision() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let percentage = Percentage::<PRECISION_2>::from_string("2.221".to_string());
    assert!(percentage.is_err());
    if let Err(err) = percentage {
        assert_eq!(
            *err.current_context(),
            PercentageError::InvalidPercentageValue
        )
    }
    Ok(())
}

#[test]
fn deserialization_test_ok() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let mut decimal = 0;
    let mut integer = 0;
    // check for all percentage values from 0 to 100
    while integer <= 100 {
        let json_string = format!(
            r#"
            {{
                "percentage" : {integer}.{decimal}
            }}
        "#,
        );
        let percentage = serde_json::from_str::<Percentage<PRECISION_2>>(&json_string);
        assert!(percentage.is_ok());
        if let Ok(percentage) = percentage {
            assert_eq!(
                percentage.get_percentage(),
                format!("{integer}.{decimal}")
                    .parse::<f32>()
                    .unwrap_or_default()
            )
        }
        if integer == 100 {
            break;
        }
        decimal += 1;
        if decimal == 100 {
            decimal = 0;
            integer += 1;
        }
    }

    let json_string = r#"
        {
            "percentage" : 18.7
        }
    "#;
    let percentage = serde_json::from_str::<Percentage<PRECISION_2>>(json_string);
    assert!(percentage.is_ok());
    if let Ok(percentage) = percentage {
        assert_eq!(percentage.get_percentage(), 18.7)
    }

    let json_string = r#"
        {
            "percentage" : 12.0
        }
    "#;
    let percentage = serde_json::from_str::<Percentage<PRECISION_0>>(json_string);
    assert!(percentage.is_ok());
    if let Ok(percentage) = percentage {
        assert_eq!(percentage.get_percentage(), 12.0)
    }
    Ok(())
}

#[test]
fn deserialization_test_err() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    // invalid percentage precision
    let json_string = r#"
        {
            "percentage" : 12.4
        }
    "#;
    let percentage = serde_json::from_str::<Percentage<PRECISION_0>>(json_string);
    assert!(percentage.is_err());
    if let Err(err) = percentage {
        assert_eq!(err.to_string(), "invalid value: percentage value 12.4, expected value should be a float between 0 to 100 and precise to only upto 0 decimal digits at line 4 column 9".to_string())
    }

    // invalid percentage value
    let json_string = r#"
        {
            "percentage" : 123.42
        }
    "#;
    let percentage = serde_json::from_str::<Percentage<PRECISION_2>>(json_string);
    assert!(percentage.is_err());
    if let Err(err) = percentage {
        assert_eq!(err.to_string(), "invalid value: percentage value 123.42, expected value should be a float between 0 to 100 and precise to only upto 2 decimal digits at line 4 column 9".to_string())
    }

    // missing percentage field
    let json_string = r#"
        {
            "percent": 22.0
        }
    "#;
    let percentage = serde_json::from_str::<Percentage<PRECISION_2>>(json_string);
    assert!(percentage.is_err());
    if let Err(err) = percentage {
        assert_eq!(
            err.to_string(),
            "missing field `percentage` at line 4 column 9".to_string()
        )
    }
    Ok(())
}


// File: crates/common_utils/src/tokenization.rs

//! Module for tokenization-related functionality
//!
//! This module provides types and functions for handling tokenized payment data,
//! including response structures and token generation utilities.

use crate::consts::TOKEN_LENGTH;

#[cfg(all(feature = "v2", feature = "tokenization_v2"))]
/// Generates a new token string
///
/// # Returns
/// A randomly generated token string of length `TOKEN_LENGTH`
pub fn generate_token() -> String {
    use nanoid::nanoid;
    nanoid!(TOKEN_LENGTH)
}


// File: crates/common_utils/src/ext_traits.rs

//! This module holds traits for extending functionalities for existing datatypes
//! & inbuilt datatypes.

use error_stack::ResultExt;
use masking::{ExposeInterface, PeekInterface, Secret, Strategy};
use quick_xml::de;
#[cfg(all(feature = "logs", feature = "async_ext"))]
use router_env::logger;
use serde::{Deserialize, Serialize};

use crate::{
    crypto,
    errors::{self, CustomResult},
    fp_utils::when,
};

/// Encode interface
/// An interface for performing type conversions and serialization
pub trait Encode<'e>
where
    Self: 'e + std::fmt::Debug,
{
    // If needed get type information/custom error implementation.
    ///
    /// Converting `Self` into an intermediate representation `<P>`
    /// and then performing encoding operation using the `Serialize` trait from `serde`
    /// Specifically to convert into json, by using `serde_json`
    fn convert_and_encode
(&'e self) -> CustomResult<String, errors::ParsingError>
    where
        P: TryFrom<&'e Self> + Serialize,
        Result<P, <P as TryFrom<&'e Self>>::Error>: ResultExt,
        <Result<P, <P as TryFrom<&'e Self>>::Error> as ResultExt>::Ok: Serialize;

    /// Converting `Self` into an intermediate representation `<P>`
    /// and then performing encoding operation using the `Serialize` trait from `serde`
    /// Specifically, to convert into urlencoded, by using `serde_urlencoded`
    fn convert_and_url_encode
(&'e self) -> CustomResult<String, errors::ParsingError>
    where
        P: TryFrom<&'e Self> + Serialize,
        Result<P, <P as TryFrom<&'e Self>>::Error>: ResultExt,
        <Result<P, <P as TryFrom<&'e Self>>::Error> as ResultExt>::Ok: Serialize;

    /// Functionality, for specifically encoding `Self` into `String`
    /// after serialization by using `serde::Serialize`
    fn url_encode(&'e self) -> CustomResult<String, errors::ParsingError>
    where
        Self: Serialize;

    /// Functionality, for specifically encoding `Self` into `String`
    /// after serialization by using `serde::Serialize`
    /// specifically, to convert into JSON `String`.
    fn encode_to_string_of_json(&'e self) -> CustomResult<String, errors::ParsingError>
    where
        Self: Serialize;

    /// Functionality, for specifically encoding `Self` into `String`
    /// after serialization by using `serde::Serialize`
    /// specifically, to convert into XML `String`.
    fn encode_to_string_of_xml(&'e self) -> CustomResult<String, errors::ParsingError>
    where
        Self: Serialize;

    /// Functionality, for specifically encoding `Self` into `serde_json::Value`
    /// after serialization by using `serde::Serialize`
    fn encode_to_value(&'e self) -> CustomResult<serde_json::Value, errors::ParsingError>
    where
        Self: Serialize;

    /// Functionality, for specifically encoding `Self` into `Vec<u8>`
    /// after serialization by using `serde::Serialize`
    fn encode_to_vec(&'e self) -> CustomResult<Vec<u8>, errors::ParsingError>
    where
        Self: Serialize;
}

impl<'e, A> Encode<'e> for A
where
    Self: 'e + std::fmt::Debug,
{
    fn convert_and_encode
(&'e self) -> CustomResult<String, errors::ParsingError>
    where
        P: TryFrom<&'e Self> + Serialize,
        Result<P, <P as TryFrom<&'e Self>>::Error>: ResultExt,
        <Result<P, <P as TryFrom<&'e Self>>::Error> as ResultExt>::Ok: Serialize,
    {
        serde_json::to_string(
            &P::try_from(self).change_context(errors::ParsingError::UnknownError)?,
        )
        .change_context(errors::ParsingError::EncodeError("string"))
        .attach_printable_lazy(|| format!("Unable to convert {self:?} to a request"))
    }

    fn convert_and_url_encode
(&'e self) -> CustomResult<String, errors::ParsingError>
    where
        P: TryFrom<&'e Self> + Serialize,
        Result<P, <P as TryFrom<&'e Self>>::Error>: ResultExt,
        <Result<P, <P as TryFrom<&'e Self>>::Error> as ResultExt>::Ok: Serialize,
    {
        serde_urlencoded::to_string(
            &P::try_from(self).change_context(errors::ParsingError::UnknownError)?,
        )
        .change_context(errors::ParsingError::EncodeError("url-encoded"))
        .attach_printable_lazy(|| format!("Unable to convert {self:?} to a request"))
    }

    // Check without two functions can we combine this
    fn url_encode(&'e self) -> CustomResult<String, errors::ParsingError>
    where
        Self: Serialize,
    {
        serde_urlencoded::to_string(self)
            .change_context(errors::ParsingError::EncodeError("url-encoded"))
            .attach_printable_lazy(|| format!("Unable to convert {self:?} to a request"))
    }

    fn encode_to_string_of_json(&'e self) -> CustomResult<String, errors::ParsingError>
    where
        Self: Serialize,
    {
        serde_json::to_string(self)
            .change_context(errors::ParsingError::EncodeError("json"))
            .attach_printable_lazy(|| format!("Unable to convert {self:?} to a request"))
    }

    fn encode_to_string_of_xml(&'e self) -> CustomResult<String, errors::ParsingError>
    where
        Self: Serialize,
    {
        quick_xml::se::to_string(self)
            .change_context(errors::ParsingError::EncodeError("xml"))
            .attach_printable_lazy(|| format!("Unable to convert {self:?} to a request"))
    }

    fn encode_to_value(&'e self) -> CustomResult<serde_json::Value, errors::ParsingError>
    where
        Self: Serialize,
    {
        serde_json::to_value(self)
            .change_context(errors::ParsingError::EncodeError("json-value"))
            .attach_printable_lazy(|| format!("Unable to convert {self:?} to a value"))
    }

    fn encode_to_vec(&'e self) -> CustomResult<Vec<u8>, errors::ParsingError>
    where
        Self: Serialize,
    {
        serde_json::to_vec(self)
            .change_context(errors::ParsingError::EncodeError("byte-vec"))
            .attach_printable_lazy(|| format!("Unable to convert {self:?} to a value"))
    }
}

/// Extending functionalities of `bytes::Bytes`
pub trait BytesExt {
    /// Convert `bytes::Bytes` into type `<T>` using `serde::Deserialize`
    fn parse_struct<'de, T>(
        &'de self,
        type_name: &'static str,
    ) -> CustomResult<T, errors::ParsingError>
    where
        T: Deserialize<'de>;
}

impl BytesExt for bytes::Bytes {
    fn parse_struct<'de, T>(
        &'de self,
        type_name: &'static str,
    ) -> CustomResult<T, errors::ParsingError>
    where
        T: Deserialize<'de>,
    {
        use bytes::Buf;

        serde_json::from_slice::<T>(self.chunk())
            .change_context(errors::ParsingError::StructParseFailure(type_name))
            .attach_printable_lazy(|| {
                let variable_type = std::any::type_name::<T>();
                let value = serde_json::from_slice::<serde_json::Value>(self)
                    .unwrap_or_else(|_| serde_json::Value::String(String::new()));

                format!(
                    "Unable to parse {variable_type} from bytes {:?}",
                    Secret::<_, masking::JsonMaskStrategy>::new(value)
                )
            })
    }
}

/// Extending functionalities of `[u8]` for performing parsing
pub trait ByteSliceExt {
    /// Convert `[u8]` into type `<T>` by using `serde::Deserialize`
    fn parse_struct<'de, T>(
        &'de self,
        type_name: &'static str,
    ) -> CustomResult<T, errors::ParsingError>
    where
        T: Deserialize<'de>;
}

impl ByteSliceExt for [u8] {
    #[track_caller]
    fn parse_struct<'de, T>(
        &'de self,
        type_name: &'static str,
    ) -> CustomResult<T, errors::ParsingError>
    where
        T: Deserialize<'de>,
    {
        serde_json::from_slice(self)
            .change_context(errors::ParsingError::StructParseFailure(type_name))
            .attach_printable_lazy(|| {
                let value = serde_json::from_slice::<serde_json::Value>(self)
                    .unwrap_or_else(|_| serde_json::Value::String(String::new()));

                format!(
                    "Unable to parse {type_name} from &[u8] {:?}",
                    Secret::<_, masking::JsonMaskStrategy>::new(value)
                )
            })
    }
}

/// Extending functionalities of `serde_json::Value` for performing parsing
pub trait ValueExt {
    /// Convert `serde_json::Value` into type `<T>` by using `serde::Deserialize`
    fn parse_value<T>(self, type_name: &'static str) -> CustomResult<T, errors::ParsingError>
    where
        T: serde::de::DeserializeOwned;
}

impl ValueExt for serde_json::Value {
    fn parse_value<T>(self, type_name: &'static str) -> CustomResult<T, errors::ParsingError>
    where
        T: serde::de::DeserializeOwned,
    {
        serde_json::from_value::<T>(self.clone())
            .change_context(errors::ParsingError::StructParseFailure(type_name))
            .attach_printable_lazy(|| {
                format!(
                    "Unable to parse {type_name} from serde_json::Value: {:?}",
                    // Required to prevent logging sensitive data in case of deserialization failure
                    Secret::<_, masking::JsonMaskStrategy>::new(self)
                )
            })
    }
}

impl<MaskingStrategy> ValueExt for Secret<serde_json::Value, MaskingStrategy>
where
    MaskingStrategy: Strategy<serde_json::Value>,
{
    fn parse_value<T>(self, type_name: &'static str) -> CustomResult<T, errors::ParsingError>
    where
        T: serde::de::DeserializeOwned,
    {
        self.expose().parse_value(type_name)
    }
}

impl<E: ValueExt + Clone> ValueExt for crypto::Encryptable<E> {
    fn parse_value<T>(self, type_name: &'static str) -> CustomResult<T, errors::ParsingError>
    where
        T: serde::de::DeserializeOwned,
    {
        self.into_inner().parse_value(type_name)
    }
}

/// Extending functionalities of `String` for performing parsing
pub trait StringExt<T> {
    /// Convert `String` into type `<T>` (which being an `enum`)
    fn parse_enum(self, enum_name: &'static str) -> CustomResult<T, errors::ParsingError>
    where
        T: std::str::FromStr,
        // Requirement for converting the `Err` variant of `FromStr` to `Report<Err>`
        <T as std::str::FromStr>::Err: std::error::Error + Send + Sync + 'static;

    /// Convert `serde_json::Value` into type `<T>` by using `serde::Deserialize`
    fn parse_struct<'de>(
        &'de self,
        type_name: &'static str,
    ) -> CustomResult<T, errors::ParsingError>
    where
        T: Deserialize<'de>;
}

impl<T> StringExt<T> for String {
    fn parse_enum(self, enum_name: &'static str) -> CustomResult<T, errors::ParsingError>
    where
        T: std::str::FromStr,
        <T as std::str::FromStr>::Err: std::error::Error + Send + Sync + 'static,
    {
        T::from_str(&self)
            .change_context(errors::ParsingError::EnumParseFailure(enum_name))
            .attach_printable_lazy(|| format!("Invalid enum variant {self:?} for enum {enum_name}"))
    }

    fn parse_struct<'de>(
        &'de self,
        type_name: &'static str,
    ) -> CustomResult<T, errors::ParsingError>
    where
        T: Deserialize<'de>,
    {
        serde_json::from_str::<T>(self)
            .change_context(errors::ParsingError::StructParseFailure(type_name))
            .attach_printable_lazy(|| {
                format!(
                    "Unable to parse {type_name} from string {:?}",
                    Secret::<_, masking::JsonMaskStrategy>::new(serde_json::Value::String(
                        self.clone()
                    ))
                )
            })
    }
}

/// Extending functionalities of Wrapper types for idiomatic
#[cfg(feature = "async_ext")]
#[cfg_attr(feature = "async_ext", async_trait::async_trait)]
pub trait AsyncExt {
    /// Output type of the map function
    type WrappedSelf<T>;

    /// Extending map by allowing functions which are async
    async fn async_map<F, B, Fut>(self, func: F) -> Self::WrappedSelf
    where
        F: FnOnce(A) -> Fut + Send,
        Fut: futures::Future<Output = B> + Send;

    /// Extending the `and_then` by allowing functions which are async
    async fn async_and_then<F, B, Fut>(self, func: F) -> Self::WrappedSelf
    where
        F: FnOnce(A) -> Fut + Send,
        Fut: futures::Future<Output = Self::WrappedSelf> + Send;

    /// Extending `unwrap_or_else` to allow async fallback
    async fn async_unwrap_or_else<F, Fut>(self, func: F) -> A
    where
        F: FnOnce() -> Fut + Send,
        Fut: futures::Future<Output = A> + Send;

    /// Extending `or_else` to allow async fallback that returns Self::WrappedSelf
    async fn async_or_else<F, Fut>(self, func: F) -> Self::WrappedSelf
    where
        F: FnOnce() -> Fut + Send,
        Fut: futures::Future<Output = Self::WrappedSelf> + Send;
}

#[cfg(feature = "async_ext")]
#[cfg_attr(feature = "async_ext", async_trait::async_trait)]
impl AsyncExt for Result {
    type WrappedSelf<T> = Result<T, E>;
    async fn async_and_then<F, B, Fut>(self, func: F) -> Self::WrappedSelf
    where
        F: FnOnce(A) -> Fut + Send,
        Fut: futures::Future<Output = Self::WrappedSelf> + Send,
    {
        match self {
            Ok(a) => func(a).await,
            Err(err) => Err(err),
        }
    }

    async fn async_map<F, B, Fut>(self, func: F) -> Self::WrappedSelf
    where
        F: FnOnce(A) -> Fut + Send,
        Fut: futures::Future<Output = B> + Send,
    {
        match self {
            Ok(a) => Ok(func(a).await),
            Err(err) => Err(err),
        }
    }

    async fn async_unwrap_or_else<F, Fut>(self, func: F) -> A
    where
        F: FnOnce() -> Fut + Send,
        Fut: futures::Future<Output = A> + Send,
    {
        match self {
            Ok(a) => a,
            Err(_err) => {
                #[cfg(feature = "logs")]
                logger::error!("Error: {:?}", _err);
                func().await
            }
        }
    }

    async fn async_or_else<F, Fut>(self, func: F) -> Self::WrappedSelf
    where
        F: FnOnce() -> Fut + Send,
        Fut: futures::Future<Output = Self::WrappedSelf> + Send,
    {
        match self {
            Ok(a) => Ok(a),
            Err(_err) => {
                #[cfg(feature = "logs")]
                logger::error!("Error: {:?}", _err);
                func().await
            }
        }
    }
}

#[cfg(feature = "async_ext")]
#[cfg_attr(feature = "async_ext", async_trait::async_trait)]
impl AsyncExt for Option {
    type WrappedSelf<T> = Option<T>;
    async fn async_and_then<F, B, Fut>(self, func: F) -> Self::WrappedSelf
    where
        F: FnOnce(A) -> Fut + Send,
        Fut: futures::Future<Output = Self::WrappedSelf> + Send,
    {
        match self {
            Some(a) => func(a).await,
            None => None,
        }
    }

    async fn async_map<F, B, Fut>(self, func: F) -> Self::WrappedSelf
    where
        F: FnOnce(A) -> Fut + Send,
        Fut: futures::Future<Output = B> + Send,
    {
        match self {
            Some(a) => Some(func(a).await),
            None => None,
        }
    }

    async fn async_unwrap_or_else<F, Fut>(self, func: F) -> A
    where
        F: FnOnce() -> Fut + Send,
        Fut: futures::Future<Output = A> + Send,
    {
        match self {
            Some(a) => a,
            None => func().await,
        }
    }

    async fn async_or_else<F, Fut>(self, func: F) -> Self::WrappedSelf
    where
        F: FnOnce() -> Fut + Send,
        Fut: futures::Future<Output = Self::WrappedSelf> + Send,
    {
        match self {
            Some(a) => Some(a),
            None => func().await,
        }
    }
}

/// Extension trait for validating application configuration. This trait provides utilities to
/// check whether the value is either the default value or is empty.
pub trait ConfigExt {
    /// Returns whether the value of `self` is the default value for `Self`.
    fn is_default(&self) -> bool
    where
        Self: Default + PartialEq<Self>,
    {
        *self == Self::default()
    }

    /// Returns whether the value of `self` is empty after trimming whitespace on both left and
    /// right ends.
    fn is_empty_after_trim(&self) -> bool;

    /// Returns whether the value of `self` is the default value for `Self` or empty after trimming
    /// whitespace on both left and right ends.
    fn is_default_or_empty(&self) -> bool
    where
        Self: Default + PartialEq<Self>,
    {
        self.is_default() || self.is_empty_after_trim()
    }
}

impl ConfigExt for u32 {
    fn is_empty_after_trim(&self) -> bool {
        false
    }
}

impl ConfigExt for String {
    fn is_empty_after_trim(&self) -> bool {
        self.trim().is_empty()
    }
}

impl<T, U> ConfigExt for Secret<T, U>
where
    T: ConfigExt + Default + PartialEq<T>,
    U: Strategy<T>,
{
    fn is_default(&self) -> bool
    where
        T: Default + PartialEq<T>,
    {
        *self.peek() == T::default()
    }

    fn is_empty_after_trim(&self) -> bool {
        self.peek().is_empty_after_trim()
    }

    fn is_default_or_empty(&self) -> bool
    where
        T: Default + PartialEq<T>,
    {
        self.peek().is_default() || self.peek().is_empty_after_trim()
    }
}

/// Extension trait for deserializing XML strings using `quick-xml` crate
pub trait XmlExt {
    /// Deserialize an XML string into the specified type `<T>`.
    fn parse_xml<T>(self) -> Result<T, de::DeError>
    where
        T: serde::de::DeserializeOwned;
}

impl XmlExt for &str {
    fn parse_xml<T>(self) -> Result<T, de::DeError>
    where
        T: serde::de::DeserializeOwned,
    {
        de::from_str(self)
    }
}

/// Extension trait for Option to validate missing fields
pub trait OptionExt<T> {
    /// check if the current option is Some
    fn check_value_present(
        &self,
        field_name: &'static str,
    ) -> CustomResult<(), errors::ValidationError>;

    /// Throw missing required field error when the value is None
    fn get_required_value(
        self,
        field_name: &'static str,
    ) -> CustomResult<T, errors::ValidationError>;

    /// Try parsing the option as Enum
    fn parse_enum<E>(self, enum_name: &'static str) -> CustomResult<E, errors::ParsingError>
    where
        T: AsRef<str>,
        E: std::str::FromStr,
        // Requirement for converting the `Err` variant of `FromStr` to `Report<Err>`
        <E as std::str::FromStr>::Err: std::error::Error + Send + Sync + 'static;

    /// Try parsing the option as Type
    fn parse_value(self, type_name: &'static str) -> CustomResult
    where
        T: ValueExt,
        U: serde::de::DeserializeOwned;

    /// update option value
    fn update_value(&mut self, value: Option<T>);
}

impl<T> OptionExt<T> for Option<T>
where
    T: std::fmt::Debug,
{
    #[track_caller]
    fn check_value_present(
        &self,
        field_name: &'static str,
    ) -> CustomResult<(), errors::ValidationError> {
        when(self.is_none(), || {
            Err(errors::ValidationError::MissingRequiredField {
                field_name: field_name.to_string(),
            })
            .attach_printable(format!("Missing required field {field_name} in {self:?}"))
        })
    }

    // This will allow the error message that was generated in this function to point to the call site
    #[track_caller]
    fn get_required_value(
        self,
        field_name: &'static str,
    ) -> CustomResult<T, errors::ValidationError> {
        match self {
            Some(v) => Ok(v),
            None => Err(errors::ValidationError::MissingRequiredField {
                field_name: field_name.to_string(),
            })
            .attach_printable(format!("Missing required field {field_name} in {self:?}")),
        }
    }

    #[track_caller]
    fn parse_enum<E>(self, enum_name: &'static str) -> CustomResult<E, errors::ParsingError>
    where
        T: AsRef<str>,
        E: std::str::FromStr,
        <E as std::str::FromStr>::Err: std::error::Error + Send + Sync + 'static,
    {
        let value = self
            .get_required_value(enum_name)
            .change_context(errors::ParsingError::UnknownError)?;

        E::from_str(value.as_ref())
            .change_context(errors::ParsingError::UnknownError)
            .attach_printable_lazy(|| format!("Invalid {{ {enum_name}: {value:?} }} "))
    }

    #[track_caller]
    fn parse_value(self, type_name: &'static str) -> CustomResult
    where
        T: ValueExt,
        U: serde::de::DeserializeOwned,
    {
        let value = self
            .get_required_value(type_name)
            .change_context(errors::ParsingError::UnknownError)?;
        value.parse_value(type_name)
    }

    fn update_value(&mut self, value: Self) {
        if let Some(a) = value {
            *self = Some(a)
        }
    }
}


// File: crates/common_utils/src/request.rs

use masking::{Maskable, Secret};
use reqwest::multipart::Form;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

pub type Headers = std::collections::HashSet<(String, Maskable<String>)>;

#[derive(
    Clone,
    Copy,
    Debug,
    Eq,
    PartialEq,
    Deserialize,
    Serialize,
    strum::Display,
    strum::EnumString,
    ToSchema,
)]
#[serde(rename_all = "UPPERCASE")]
#[strum(serialize_all = "UPPERCASE")]
pub enum Method {
    Get,
    Post,
    Put,
    Delete,
    Patch,
}

#[derive(Deserialize, Serialize, Debug)]
pub enum ContentType {
    Json,
    FormUrlEncoded,
    FormData,
    Xml,
}

fn default_request_headers() -> [(String, Maskable<String>); 1] {
    use http::header;

    [(header::VIA.to_string(), "HyperSwitch".to_string().into())]
}

#[derive(Debug)]
pub struct Request {
    pub url: String,
    pub headers: Headers,
    pub method: Method,
    pub certificate: Option<Secret<String>>,
    pub certificate_key: Option<Secret<String>>,
    pub body: Option<RequestContent>,
    pub ca_certificate: Option<Secret<String>>,
}

impl std::fmt::Debug for RequestContent {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            Self::Json(_) => "JsonRequestBody",
            Self::FormUrlEncoded(_) => "FormUrlEncodedRequestBody",
            Self::FormData(_) => "FormDataRequestBody",
            Self::Xml(_) => "XmlRequestBody",
            Self::RawBytes(_) => "RawBytesRequestBody",
        })
    }
}

pub enum RequestContent {
    Json(Box<dyn masking::ErasedMaskSerialize + Send>),
    FormUrlEncoded(Box<dyn masking::ErasedMaskSerialize + Send>),
    FormData((Form, Box<dyn masking::ErasedMaskSerialize + Send>)),
    Xml(Box<dyn masking::ErasedMaskSerialize + Send>),
    RawBytes(Vec<u8>),
}

impl RequestContent {
    pub fn get_inner_value(&self) -> Secret<String> {
        match self {
            Self::Json(i) => serde_json::to_string(&i).unwrap_or_default().into(),
            Self::FormUrlEncoded(i) => serde_urlencoded::to_string(i).unwrap_or_default().into(),
            Self::Xml(i) => quick_xml::se::to_string(&i).unwrap_or_default().into(),
            Self::FormData((_, i)) => serde_json::to_string(i).unwrap_or_default().into(),
            Self::RawBytes(_) => String::new().into(),
        }
    }
}

impl Request {
    pub fn new(method: Method, url: &str) -> Self {
        Self {
            method,
            url: String::from(url),
            headers: std::collections::HashSet::new(),
            certificate: None,
            certificate_key: None,
            body: None,
            ca_certificate: None,
        }
    }

    pub fn set_body<T: Into<RequestContent>>(&mut self, body: T) {
        self.body.replace(body.into());
    }

    pub fn add_default_headers(&mut self) {
        self.headers.extend(default_request_headers());
    }

    pub fn add_header(&mut self, header: &str, value: Maskable<String>) {
        self.headers.insert((String::from(header), value));
    }

    pub fn add_certificate(&mut self, certificate: Option<Secret<String>>) {
        self.certificate = certificate;
    }

    pub fn add_certificate_key(&mut self, certificate_key: Option<Secret<String>>) {
        self.certificate = certificate_key;
    }
}

#[derive(Debug)]
pub struct RequestBuilder {
    pub url: String,
    pub headers: Headers,
    pub method: Method,
    pub certificate: Option<Secret<String>>,
    pub certificate_key: Option<Secret<String>>,
    pub body: Option<RequestContent>,
    pub ca_certificate: Option<Secret<String>>,
}

impl RequestBuilder {
    pub fn new() -> Self {
        Self {
            method: Method::Get,
            url: String::with_capacity(1024),
            headers: std::collections::HashSet::new(),
            certificate: None,
            certificate_key: None,
            body: None,
            ca_certificate: None,
        }
    }

    pub fn url(mut self, url: &str) -> Self {
        self.url = url.into();
        self
    }

    pub fn method(mut self, method: Method) -> Self {
        self.method = method;
        self
    }

    pub fn attach_default_headers(mut self) -> Self {
        self.headers.extend(default_request_headers());
        self
    }

    pub fn header(mut self, header: &str, value: &str) -> Self {
        self.headers.insert((header.into(), value.into()));
        self
    }

    pub fn headers(mut self, headers: Vec<(String, Maskable<String>)>) -> Self {
        self.headers.extend(headers);
        self
    }

    pub fn set_optional_body<T: Into<RequestContent>>(mut self, body: Option<T>) -> Self {
        body.map(|body| self.body.replace(body.into()));
        self
    }

    pub fn set_body<T: Into<RequestContent>>(mut self, body: T) -> Self {
        self.body.replace(body.into());
        self
    }

    pub fn add_certificate(mut self, certificate: Option<Secret<String>>) -> Self {
        self.certificate = certificate;
        self
    }

    pub fn add_certificate_key(mut self, certificate_key: Option<Secret<String>>) -> Self {
        self.certificate_key = certificate_key;
        self
    }

    pub fn add_ca_certificate_pem(mut self, ca_certificate: Option<Secret<String>>) -> Self {
        self.ca_certificate = ca_certificate;
        self
    }

    pub fn build(self) -> Request {
        Request {
            method: self.method,
            url: self.url,
            headers: self.headers,
            certificate: self.certificate,
            certificate_key: self.certificate_key,
            body: self.body,
            ca_certificate: self.ca_certificate,
        }
    }
}

impl Default for RequestBuilder {
    fn default() -> Self {
        Self::new()
    }
}


// File: crates/common_utils/src/consts.rs

//! Commonly used constants

/// Number of characters in a generated ID
pub const ID_LENGTH: usize = 20;

/// Characters to use for generating NanoID
pub(crate) const ALPHABETS: [char; 62] = [
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
    'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B',
    'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
    'V', 'W', 'X', 'Y', 'Z',
];

/// TTL for token
pub const TOKEN_TTL: i64 = 900;

///an example of the frm_configs json
pub static FRM_CONFIGS_EG: &str = r#"
[{"gateway":"stripe","payment_methods":[{"payment_method":"card","payment_method_types":[{"payment_method_type":"credit","card_networks":["Visa"],"flow":"pre","action":"cancel_txn"},{"payment_method_type":"debit","card_networks":["Visa"],"flow":"pre"}]}]}]
"#;

/// Maximum limit for payments list get api
pub const PAYMENTS_LIST_MAX_LIMIT_V1: u32 = 100;
/// Maximum limit for payments list post api with filters
pub const PAYMENTS_LIST_MAX_LIMIT_V2: u32 = 50;
/// Default limit for payments list API
pub fn default_payments_list_limit() -> u32 {
    10
}

/// Average delay (in seconds) between account onboarding's API response and the changes to actually reflect at Stripe's end
pub const STRIPE_ACCOUNT_ONBOARDING_DELAY_IN_SECONDS: i64 = 15;

/// Maximum limit for payment link list get api
pub const PAYMENTS_LINK_LIST_LIMIT: u32 = 100;

/// Maximum limit for payouts list get api
pub const PAYOUTS_LIST_MAX_LIMIT_GET: u32 = 100;
/// Maximum limit for payouts list post api
pub const PAYOUTS_LIST_MAX_LIMIT_POST: u32 = 20;
/// Default limit for payouts list API
pub fn default_payouts_list_limit() -> u32 {
    10
}

/// surcharge percentage maximum precision length
pub const SURCHARGE_PERCENTAGE_PRECISION_LENGTH: u8 = 2;

/// Header Key for application overhead of a request
pub const X_HS_LATENCY: &str = "x-hs-latency";

/// Redirect url for Prophetpay
pub const PROPHETPAY_REDIRECT_URL: &str = "https://ccm-thirdparty.cps.golf/hp/tokenize/";

/// Variable which store the card token for Prophetpay
pub const PROPHETPAY_TOKEN: &str = "cctoken";

/// Payment intent default client secret expiry (in seconds)
pub const DEFAULT_SESSION_EXPIRY: i64 = 15 * 60;

/// Payment intent fulfillment time (in seconds)
pub const DEFAULT_INTENT_FULFILLMENT_TIME: i64 = 15 * 60;

/// Payment order fulfillment time (in seconds)
pub const DEFAULT_ORDER_FULFILLMENT_TIME: i64 = 15 * 60;

/// Default ttl for Extended card info  in redis (in seconds)
pub const DEFAULT_TTL_FOR_EXTENDED_CARD_INFO: u16 = 15 * 60;

/// Max ttl for Extended card info in redis (in seconds)
pub const MAX_TTL_FOR_EXTENDED_CARD_INFO: u16 = 60 * 60 * 2;

/// Default tenant to be used when multitenancy is disabled
pub const DEFAULT_TENANT: &str = "public";

/// Default tenant to be used when multitenancy is disabled
pub const TENANT_HEADER: &str = "x-tenant-id";

/// Max Length for MerchantReferenceId
pub const MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH: u8 = 64;

/// Maximum length allowed for a global id
pub const MIN_GLOBAL_ID_LENGTH: u8 = 32;

/// Minimum length required for a global id
pub const MAX_GLOBAL_ID_LENGTH: u8 = 64;

/// Minimum allowed length for MerchantReferenceId
pub const MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH: u8 = 1;

/// Length of a cell identifier in a distributed system
pub const CELL_IDENTIFIER_LENGTH: u8 = 5;

/// General purpose base64 engine
pub const BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::general_purpose::STANDARD;

/// URL Safe base64 engine
pub const BASE64_ENGINE_URL_SAFE: base64::engine::GeneralPurpose =
    base64::engine::general_purpose::URL_SAFE;

/// URL Safe base64 engine without padding
pub const BASE64_ENGINE_URL_SAFE_NO_PAD: base64::engine::GeneralPurpose =
    base64::engine::general_purpose::URL_SAFE_NO_PAD;

/// Regex for matching a domain
/// Eg -
/// http://www.example.com
/// https://www.example.com
/// www.example.com
/// example.io
pub const STRICT_DOMAIN_REGEX: &str = r"^(https?://)?(([A-Za-z0-9][-A-Za-z0-9]\.)*[A-Za-z0-9][-A-Za-z0-9]*|(\d{1,3}\.){3}\d{1,3})+(:[0-9]{2,4})?$";

/// Regex for matching a wildcard domain
/// Eg -
/// *.example.com
/// *.subdomain.domain.com
/// *://example.com
/// *example.com
pub const WILDCARD_DOMAIN_REGEX: &str = r"^((\*|https?)?://)?((\*\.|[A-Za-z0-9][-A-Za-z0-9]*\.)*[A-Za-z0-9][-A-Za-z0-9]*|((\d{1,3}|\*)\.){3}(\d{1,3}|\*)|\*)(:\*|:[0-9]{2,4})?(/\*)?$";

/// Maximum allowed length for MerchantName
pub const MAX_ALLOWED_MERCHANT_NAME_LENGTH: usize = 64;

/// Default locale
pub const DEFAULT_LOCALE: &str = "en";

/// Role ID for Tenant Admin
pub const ROLE_ID_TENANT_ADMIN: &str = "tenant_admin";
/// Role ID for Org Admin
pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin";
/// Role ID for Internal View Only
pub const ROLE_ID_INTERNAL_VIEW_ONLY_USER: &str = "internal_view_only";
/// Role ID for Internal Admin
pub const ROLE_ID_INTERNAL_ADMIN: &str = "internal_admin";
/// Role ID for Internal Demo
pub const ROLE_ID_INTERNAL_DEMO: &str = "internal_demo";

/// Max length allowed for Description
pub const MAX_DESCRIPTION_LENGTH: u16 = 255;

/// Max length allowed for Statement Descriptor
pub const MAX_STATEMENT_DESCRIPTOR_LENGTH: u16 = 22;
/// Payout flow identifier used for performing GSM operations
pub const PAYOUT_FLOW_STR: &str = "payout_flow";

/// length of the publishable key
pub const PUBLISHABLE_KEY_LENGTH: u16 = 39;

/// The number of bytes allocated for the hashed connector transaction ID.
/// Total number of characters equals CONNECTOR_TRANSACTION_ID_HASH_BYTES times 2.
pub const CONNECTOR_TRANSACTION_ID_HASH_BYTES: usize = 25;

/// Apple Pay validation url
pub const APPLEPAY_VALIDATION_URL: &str =
    "https://apple-pay-gateway-cert.apple.com/paymentservices/startSession";

/// Request ID
pub const X_REQUEST_ID: &str = "x-request-id";

/// Flow name
pub const X_FLOW_NAME: &str = "x-flow";

/// Connector name
pub const X_CONNECTOR_NAME: &str = "x-connector";

/// Unified Connector Service Mode
pub const X_UNIFIED_CONNECTOR_SERVICE_MODE: &str = "x-shadow-mode";

/// Chat Session ID
pub const X_CHAT_SESSION_ID: &str = "x-chat-session-id";

/// Merchant ID Header
pub const X_MERCHANT_ID: &str = "x-merchant-id";

/// Default Tenant ID for the `Global` tenant
pub const DEFAULT_GLOBAL_TENANT_ID: &str = "global";

/// Default status of Card IP Blocking
pub const DEFAULT_CARD_IP_BLOCKING_STATUS: bool = false;

/// Default Threshold for Card IP Blocking
pub const DEFAULT_CARD_IP_BLOCKING_THRESHOLD: i32 = 3;

/// Default status of Guest User Card Blocking
pub const DEFAULT_GUEST_USER_CARD_BLOCKING_STATUS: bool = false;

/// Default Threshold for Card Blocking for Guest Users
pub const DEFAULT_GUEST_USER_CARD_BLOCKING_THRESHOLD: i32 = 10;

/// Default status of Customer ID Blocking
pub const DEFAULT_CUSTOMER_ID_BLOCKING_STATUS: bool = false;

/// Default Threshold for Customer ID Blocking
pub const DEFAULT_CUSTOMER_ID_BLOCKING_THRESHOLD: i32 = 5;

/// Default Card Testing Guard Redis Expiry in seconds
pub const DEFAULT_CARD_TESTING_GUARD_EXPIRY_IN_SECS: i32 = 3600;

/// SOAP 1.1 Envelope Namespace
pub const SOAP_ENV_NAMESPACE: &str = "http://schemas.xmlsoap.org/soap/envelope/";

#[cfg(all(feature = "v2", feature = "tokenization_v2"))]
/// Length of generated tokens
pub const TOKEN_LENGTH: usize = 32;

/// The tag name used for identifying the host in metrics.
pub const METRICS_HOST_TAG_NAME: &str = "host";

/// API client request timeout (in seconds)
pub const REQUEST_TIME_OUT: u64 = 30;

/// API client request timeout for ai service (in seconds)
pub const REQUEST_TIME_OUT_FOR_AI_SERVICE: u64 = 120;

/// Default limit for list operations (can be used across different entities)
pub const DEFAULT_LIST_LIMIT: i64 = 100;

/// Default offset for list operations (can be used across different entities)
pub const DEFAULT_LIST_OFFSET: i64 = 0;