// File: crates/router/src/core/surcharge_decision_config.rs

use api_models::surcharge_decision_configs::{
    SurchargeDecisionConfigReq, SurchargeDecisionManagerRecord, SurchargeDecisionManagerResponse,
};
use common_utils::ext_traits::StringExt;
use error_stack::ResultExt;

use crate::{
    core::errors::{self, RouterResponse},
    routes::SessionState,
    services::api as service_api,
    types::domain,
};

#[cfg(feature = "v1")]
pub async fn upsert_surcharge_decision_config(
    state: SessionState,
    platform: domain::Platform,
    request: SurchargeDecisionConfigReq,
) -> RouterResponse<SurchargeDecisionManagerRecord> {
    use common_utils::ext_traits::{Encode, OptionExt, ValueExt};
    use diesel_models::configs;
    use storage_impl::redis::cache;

    use super::routing::helpers::update_merchant_active_algorithm_ref;

    let db = state.store.as_ref();
    let name = request.name;

    let program = request
        .algorithm
        .get_required_value("algorithm")
        .change_context(errors::ApiErrorResponse::MissingRequiredField {
            field_name: "algorithm",
        })
        .attach_printable("Program for config not given")?;
    let merchant_surcharge_configs = request.merchant_surcharge_configs;

    let timestamp = common_utils::date_time::now_unix_timestamp();
    let mut algo_id: api_models::routing::RoutingAlgorithmRef = platform
        .get_processor()
        .get_account()
        .routing_algorithm
        .clone()
        .map(|val| val.parse_value("routing algorithm"))
        .transpose()
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Could not decode the routing algorithm")?
        .unwrap_or_default();

    let key = platform
        .get_processor()
        .get_account()
        .get_id()
        .get_payment_method_surcharge_routing_id();
    let read_config_key = db.find_config_by_key(&key).await;

    euclid::frontend::ast::lowering::lower_program(program.clone())
        .change_context(errors::ApiErrorResponse::InvalidRequestData {
            message: "Invalid Request Data".to_string(),
        })
        .attach_printable("The Request has an Invalid Comparison")?;
    let surcharge_cache_key = platform
        .get_processor()
        .get_account()
        .get_id()
        .get_surcharge_dsk_key();
    match read_config_key {
        Ok(config) => {
            let previous_record: SurchargeDecisionManagerRecord = config
                .config
                .parse_struct("SurchargeDecisionManagerRecord")
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("The Payment Config Key Not Found")?;

            let new_algo = SurchargeDecisionManagerRecord {
                name: name.unwrap_or(previous_record.name),
                algorithm: program,
                modified_at: timestamp,
                created_at: previous_record.created_at,
                merchant_surcharge_configs,
            };

            let serialize_updated_str = new_algo
                .encode_to_string_of_json()
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Unable to serialize config to string")?;

            let updated_config = configs::ConfigUpdate::Update {
                config: Some(serialize_updated_str),
            };

            db.update_config_by_key(&key, updated_config)
                .await
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Error serializing the config")?;

            algo_id.update_surcharge_config_id(key.clone());
            let config_key = cache::CacheKind::Surcharge(surcharge_cache_key.into());
            update_merchant_active_algorithm_ref(
                &state,
                platform.get_processor().get_key_store(),
                config_key,
                algo_id,
            )
            .await
            .change_context(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("Failed to update routing algorithm ref")?;

            Ok(service_api::ApplicationResponse::Json(new_algo))
        }
        Err(e) if e.current_context().is_db_not_found() => {
            let new_rec = SurchargeDecisionManagerRecord {
                name: name
                    .get_required_value("name")
                    .change_context(errors::ApiErrorResponse::MissingRequiredField {
                        field_name: "name",
                    })
                    .attach_printable("name of the config not found")?,
                algorithm: program,
                merchant_surcharge_configs,
                modified_at: timestamp,
                created_at: timestamp,
            };

            let serialized_str = new_rec
                .encode_to_string_of_json()
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Error serializing the config")?;
            let new_config = configs::ConfigNew {
                key: key.clone(),
                config: serialized_str,
            };

            db.insert_config(new_config)
                .await
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Error fetching the config")?;

            algo_id.update_surcharge_config_id(key.clone());
            let config_key = cache::CacheKind::Surcharge(surcharge_cache_key.into());
            update_merchant_active_algorithm_ref(
                &state,
                platform.get_processor().get_key_store(),
                config_key,
                algo_id,
            )
            .await
            .change_context(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("Failed to update routing algorithm ref")?;

            Ok(service_api::ApplicationResponse::Json(new_rec))
        }
        Err(e) => Err(e)
            .change_context(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("Error fetching payment config"),
    }
}

#[cfg(feature = "v2")]
pub async fn upsert_surcharge_decision_config(
    _state: SessionState,
    _platform: domain::Platform,
    _request: SurchargeDecisionConfigReq,
) -> RouterResponse<SurchargeDecisionManagerRecord> {
    todo!();
}

#[cfg(feature = "v1")]
pub async fn delete_surcharge_decision_config(
    state: SessionState,
    platform: domain::Platform,
) -> RouterResponse<()> {
    use common_utils::ext_traits::ValueExt;
    use storage_impl::redis::cache;

    use super::routing::helpers::update_merchant_active_algorithm_ref;

    let db = state.store.as_ref();
    let key = platform
        .get_processor()
        .get_account()
        .get_id()
        .get_payment_method_surcharge_routing_id();
    let mut algo_id: api_models::routing::RoutingAlgorithmRef = platform
        .get_processor()
        .get_account()
        .routing_algorithm
        .clone()
        .map(|value| value.parse_value("routing algorithm"))
        .transpose()
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Could not decode the surcharge conditional_config algorithm")?
        .unwrap_or_default();
    algo_id.surcharge_config_algo_id = None;
    let surcharge_cache_key = platform
        .get_processor()
        .get_account()
        .get_id()
        .get_surcharge_dsk_key();
    let config_key = cache::CacheKind::Surcharge(surcharge_cache_key.into());
    update_merchant_active_algorithm_ref(
        &state,
        platform.get_processor().get_key_store(),
        config_key,
        algo_id,
    )
    .await
    .change_context(errors::ApiErrorResponse::InternalServerError)
    .attach_printable("Failed to update deleted algorithm ref")?;

    db.delete_config_by_key(&key)
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Failed to delete routing config from DB")?;
    Ok(service_api::ApplicationResponse::StatusOk)
}

#[cfg(feature = "v2")]
pub async fn delete_surcharge_decision_config(
    _state: SessionState,
    _platform: domain::Platform,
) -> RouterResponse<()> {
    todo!()
}

pub async fn retrieve_surcharge_decision_config(
    state: SessionState,
    platform: domain::Platform,
) -> RouterResponse<SurchargeDecisionManagerResponse> {
    let db = state.store.as_ref();
    let algorithm_id = platform
        .get_processor()
        .get_account()
        .get_id()
        .get_payment_method_surcharge_routing_id();
    let algo_config = db
        .find_config_by_key(&algorithm_id)
        .await
        .change_context(errors::ApiErrorResponse::ResourceIdNotFound)
        .attach_printable("The surcharge conditional config was not found in the DB")?;
    let record: SurchargeDecisionManagerRecord = algo_config
        .config
        .parse_struct("SurchargeDecisionConfigsRecord")
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("The Surcharge Decision Config Record was not found")?;
    Ok(service_api::ApplicationResponse::Json(record))
}


// File: crates/router/src/core/health_check.rs

#[cfg(feature = "olap")]
use analytics::health_check::HealthCheck;
#[cfg(feature = "dynamic_routing")]
use api_models::health_check::HealthCheckMap;
use api_models::health_check::HealthState;
use error_stack::ResultExt;
use router_env::logger;

use crate::{
    consts,
    core::errors::{self, CustomResult},
    routes::app,
    services::api as services,
};

#[async_trait::async_trait]
pub trait HealthCheckInterface {
    async fn health_check_db(&self) -> CustomResult<HealthState, errors::HealthCheckDBError>;
    async fn health_check_redis(&self) -> CustomResult<HealthState, errors::HealthCheckRedisError>;
    async fn health_check_locker(
        &self,
    ) -> CustomResult<HealthState, errors::HealthCheckLockerError>;
    async fn health_check_outgoing(&self)
        -> CustomResult<HealthState, errors::HealthCheckOutGoing>;
    #[cfg(feature = "olap")]
    async fn health_check_analytics(&self)
        -> CustomResult<HealthState, errors::HealthCheckDBError>;

    #[cfg(feature = "olap")]
    async fn health_check_opensearch(
        &self,
    ) -> CustomResult<HealthState, errors::HealthCheckDBError>;

    #[cfg(feature = "dynamic_routing")]
    async fn health_check_grpc(
        &self,
    ) -> CustomResult<HealthCheckMap, errors::HealthCheckGRPCServiceError>;

    #[cfg(feature = "dynamic_routing")]
    async fn health_check_decision_engine(
        &self,
    ) -> CustomResult<HealthState, errors::HealthCheckDecisionEngineError>;

    async fn health_check_unified_connector_service(
        &self,
    ) -> CustomResult<HealthState, errors::HealthCheckUnifiedConnectorServiceError>;
}

#[async_trait::async_trait]
impl HealthCheckInterface for app::SessionState {
    async fn health_check_db(&self) -> CustomResult<HealthState, errors::HealthCheckDBError> {
        let db = &*self.store;
        db.health_check_db().await?;
        Ok(HealthState::Running)
    }

    async fn health_check_redis(&self) -> CustomResult<HealthState, errors::HealthCheckRedisError> {
        let db = &*self.store;
        let redis_conn = db
            .get_redis_conn()
            .change_context(errors::HealthCheckRedisError::RedisConnectionError)?;

        redis_conn
            .serialize_and_set_key_with_expiry(&"test_key".into(), "test_value", 30)
            .await
            .change_context(errors::HealthCheckRedisError::SetFailed)?;

        logger::debug!("Redis set_key was successful");

        redis_conn
            .get_key::<()>(&"test_key".into())
            .await
            .change_context(errors::HealthCheckRedisError::GetFailed)?;

        logger::debug!("Redis get_key was successful");

        redis_conn
            .delete_key(&"test_key".into())
            .await
            .change_context(errors::HealthCheckRedisError::DeleteFailed)?;

        logger::debug!("Redis delete_key was successful");

        Ok(HealthState::Running)
    }

    async fn health_check_locker(
        &self,
    ) -> CustomResult<HealthState, errors::HealthCheckLockerError> {
        let locker = &self.conf.locker;
        if !locker.mock_locker {
            let mut url = locker.host_rs.to_owned();
            url.push_str(consts::LOCKER_HEALTH_CALL_PATH);
            let request = services::Request::new(services::Method::Get, &url);
            services::call_connector_api(self, request, "health_check_for_locker")
                .await
                .change_context(errors::HealthCheckLockerError::FailedToCallLocker)?
                .map_err(|_| {
                    error_stack::report!(errors::HealthCheckLockerError::FailedToCallLocker)
                })?;
            Ok(HealthState::Running)
        } else {
            Ok(HealthState::NotApplicable)
        }
    }

    #[cfg(feature = "olap")]
    async fn health_check_analytics(
        &self,
    ) -> CustomResult<HealthState, errors::HealthCheckDBError> {
        let analytics = &self.pool;
        match analytics {
            analytics::AnalyticsProvider::Sqlx(client) => client
                .deep_health_check()
                .await
                .change_context(errors::HealthCheckDBError::SqlxAnalyticsError),
            analytics::AnalyticsProvider::Clickhouse(client) => client
                .deep_health_check()
                .await
                .change_context(errors::HealthCheckDBError::ClickhouseAnalyticsError),
            analytics::AnalyticsProvider::CombinedCkh(sqlx_client, ckh_client) => {
                sqlx_client
                    .deep_health_check()
                    .await
                    .change_context(errors::HealthCheckDBError::SqlxAnalyticsError)?;
                ckh_client
                    .deep_health_check()
                    .await
                    .change_context(errors::HealthCheckDBError::ClickhouseAnalyticsError)
            }
            analytics::AnalyticsProvider::CombinedSqlx(sqlx_client, ckh_client) => {
                sqlx_client
                    .deep_health_check()
                    .await
                    .change_context(errors::HealthCheckDBError::SqlxAnalyticsError)?;
                ckh_client
                    .deep_health_check()
                    .await
                    .change_context(errors::HealthCheckDBError::ClickhouseAnalyticsError)
            }
        }?;

        Ok(HealthState::Running)
    }

    #[cfg(feature = "olap")]
    async fn health_check_opensearch(
        &self,
    ) -> CustomResult<HealthState, errors::HealthCheckDBError> {
        if let Some(client) = self.opensearch_client.as_ref() {
            client
                .deep_health_check()
                .await
                .change_context(errors::HealthCheckDBError::OpensearchError)?;
            Ok(HealthState::Running)
        } else {
            Ok(HealthState::NotApplicable)
        }
    }

    async fn health_check_outgoing(
        &self,
    ) -> CustomResult<HealthState, errors::HealthCheckOutGoing> {
        let request = services::Request::new(services::Method::Get, consts::OUTGOING_CALL_URL);
        services::call_connector_api(self, request, "outgoing_health_check")
            .await
            .map_err(|err| errors::HealthCheckOutGoing::OutGoingFailed {
                message: err.to_string(),
            })?
            .map_err(|err| errors::HealthCheckOutGoing::OutGoingFailed {
                message: format!(
                    "Got a non 200 status while making outgoing request. Error {:?}",
                    err.response
                ),
            })?;

        logger::debug!("Outgoing request successful");
        Ok(HealthState::Running)
    }

    #[cfg(feature = "dynamic_routing")]
    async fn health_check_grpc(
        &self,
    ) -> CustomResult<HealthCheckMap, errors::HealthCheckGRPCServiceError> {
        let health_client = &self.grpc_client.health_client;
        let grpc_config = &self.conf.grpc_client;

        let health_check_map = health_client
            .perform_health_check(grpc_config)
            .await
            .change_context(errors::HealthCheckGRPCServiceError::FailedToCallService)?;

        logger::debug!("Health check successful");
        Ok(health_check_map)
    }

    #[cfg(feature = "dynamic_routing")]
    async fn health_check_decision_engine(
        &self,
    ) -> CustomResult<HealthState, errors::HealthCheckDecisionEngineError> {
        if self.conf.open_router.dynamic_routing_enabled {
            let url = format!("{}/{}", &self.conf.open_router.url, "health");
            let request = services::Request::new(services::Method::Get, &url);
            let _ = services::call_connector_api(self, request, "health_check_for_decision_engine")
                .await
                .change_context(
                    errors::HealthCheckDecisionEngineError::FailedToCallDecisionEngineService,
                )?;

            logger::debug!("Decision engine health check successful");
            Ok(HealthState::Running)
        } else {
            logger::debug!("Decision engine health check not applicable");
            Ok(HealthState::NotApplicable)
        }
    }

    async fn health_check_unified_connector_service(
        &self,
    ) -> CustomResult<HealthState, errors::HealthCheckUnifiedConnectorServiceError> {
        if let Some(_ucs_client) = &self.grpc_client.unified_connector_service_client {
            // For now, we'll just check if the client exists and is configured
            // In the future, this could be enhanced to make an actual health check call
            // to the unified connector service if it supports health check endpoints
            logger::debug!("Unified Connector Service client is configured and available");
            Ok(HealthState::Running)
        } else {
            logger::debug!("Unified Connector Service client not configured");
            Ok(HealthState::NotApplicable)
        }
    }
}


// File: crates/router/src/core/payment_method_balance.rs

use std::{collections::HashMap, marker::PhantomData};

use api_models::payments::{
    ApplyPaymentMethodDataRequest, CheckAndApplyPaymentMethodDataResponse, GetPaymentMethodType,
    PMBalanceCheckFailureResponse, PMBalanceCheckSuccessResponse,
};
use common_enums::CallConnectorAction;
use common_utils::{
    ext_traits::{Encode, StringExt},
    id_type,
    types::MinorUnit,
};
use error_stack::ResultExt;
use hyperswitch_domain_models::{
    payments::HeaderPayload,
    router_data_v2::{flow_common_types::GiftCardBalanceCheckFlowData, RouterDataV2},
    router_flow_types::GiftCardBalanceCheck,
    router_request_types::GiftCardBalanceCheckRequestData,
    router_response_types::GiftCardBalanceCheckResponseData,
};
use hyperswitch_interfaces::connector_integration_interface::RouterDataConversion;
use masking::ExposeInterface;
use router_env::{instrument, tracing};

use crate::{
    consts,
    core::{
        errors::{self, RouterResponse},
        payments::helpers,
    },
    db::errors::StorageErrorExt,
    routes::{app::ReqState, SessionState},
    services::{self, logger},
    types::{api, domain},
};

#[allow(clippy::too_many_arguments)]
pub async fn payments_check_gift_card_balance_core(
    state: &SessionState,
    platform: &domain::Platform,
    profile: &domain::Profile,
    _req_state: &ReqState,
    payment_method_data: api_models::payments::BalanceCheckPaymentMethodData,
    payment_id: &id_type::GlobalPaymentId,
) -> errors::RouterResult<(MinorUnit, common_enums::Currency)> {
    let db = state.store.as_ref();

    let storage_scheme = platform.get_processor().get_account().storage_scheme;
    let payment_intent = db
        .find_payment_intent_by_id(
            payment_id,
            platform.get_processor().get_key_store(),
            storage_scheme,
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;

    let redis_conn = db
        .get_redis_conn()
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Could not get redis connection")?;

    let gift_card_connector_id: String = redis_conn
        .get_key(&payment_id.get_gift_card_connector_key().as_str().into())
        .await
        .attach_printable("Failed to fetch gift card connector from redis")
        .change_context(errors::ApiErrorResponse::GenericNotFoundError {
            message: "No connector found with Gift Card Support".to_string(),
        })?;

    let gift_card_connector_id = id_type::MerchantConnectorAccountId::wrap(gift_card_connector_id)
        .attach_printable("Failed to deserialize MCA")
        .change_context(errors::ApiErrorResponse::GenericNotFoundError {
            message: "No connector found with Gift Card Support".to_string(),
        })?;

    let merchant_connector_account =
        domain::MerchantConnectorAccountTypeDetails::MerchantConnectorAccount(Box::new(
            helpers::get_merchant_connector_account_v2(
                state,
                platform.get_processor().get_key_store(),
                Some(&gift_card_connector_id),
            )
            .await
            .attach_printable(
                "failed to fetch merchant connector account for gift card balance check",
            )?,
        ));

    let connector_name = merchant_connector_account
        .get_connector_name()
        .ok_or(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Connector name not present for gift card balance check")?; // always get the connector name from this call

    let connector_data = api::ConnectorData::get_connector_by_name(
        &state.conf.connectors,
        &connector_name.to_string(),
        api::GetToken::Connector,
        merchant_connector_account.get_mca_id(),
    )
    .change_context(errors::ApiErrorResponse::InternalServerError)
    .attach_printable("Failed to get the connector data")?;

    let connector_auth_type = merchant_connector_account
        .get_connector_account_details()
        .change_context(errors::ApiErrorResponse::InternalServerError)?;

    let resource_common_data = GiftCardBalanceCheckFlowData;

    let api_models::payments::BalanceCheckPaymentMethodData::GiftCard(gift_card_data) =
        payment_method_data;

    let router_data: RouterDataV2<
        GiftCardBalanceCheck,
        GiftCardBalanceCheckFlowData,
        GiftCardBalanceCheckRequestData,
        GiftCardBalanceCheckResponseData,
    > = RouterDataV2 {
        flow: PhantomData,
        resource_common_data,
        tenant_id: state.tenant.tenant_id.clone(),
        connector_auth_type,
        request: GiftCardBalanceCheckRequestData {
            payment_method_data: domain::PaymentMethodData::GiftCard(Box::new(
                gift_card_data.clone().into(),
            )),
            currency: Some(payment_intent.amount_details.currency),
            minor_amount: Some(payment_intent.amount_details.order_amount),
        },
        response: Err(hyperswitch_domain_models::router_data::ErrorResponse::default()),
    };

    let old_router_data = GiftCardBalanceCheckFlowData::to_old_router_data(router_data)
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable(
            "Cannot construct router data for making the gift card balance check api call",
        )?;
    let connector_integration: services::BoxedGiftCardBalanceCheckIntegrationInterface<
        GiftCardBalanceCheck,
        GiftCardBalanceCheckRequestData,
        GiftCardBalanceCheckResponseData,
    > = connector_data.connector.get_connector_integration();

    let connector_response = services::execute_connector_processing_step(
        state,
        connector_integration,
        &old_router_data,
        CallConnectorAction::Trigger,
        None,
        None,
    )
    .await
    .change_context(errors::ApiErrorResponse::InternalServerError)
    .attach_printable("Failed while calling gift card balance check connector api")?;

    let gift_card_balance = connector_response
        .response
        .map_err(|_| errors::ApiErrorResponse::UnprocessableEntity {
            message: "Error while fetching gift card balance".to_string(),
        })
        .attach_printable("Connector returned invalid response")?;

    let balance = gift_card_balance.balance;
    let currency = gift_card_balance.currency;

    let payment_method_key = domain::GiftCardData::from(gift_card_data.clone())
        .get_payment_method_key()
        .change_context(errors::ApiErrorResponse::InvalidRequestData {
            message: "Unable to get unique key for payment method".to_string(),
        })?
        .expose();

    let balance_data = domain::PaymentMethodBalanceData {
        payment_intent_id: &payment_intent.id,
        pm_balance_data: vec![(
            domain::PaymentMethodBalanceKey {
                payment_method_type: common_enums::PaymentMethod::GiftCard,
                payment_method_subtype: gift_card_data.get_payment_method_type(),
                payment_method_key,
            },
            domain::PaymentMethodBalance { balance, currency },
        )]
        .into_iter()
        .collect(),
    };

    persist_individual_pm_balance_details_in_redis(state, profile, &balance_data)
        .await
        .attach_printable("Failed to persist gift card balance details in redis")?;

    let resp = (balance, currency);

    Ok(resp)
}

#[allow(clippy::too_many_arguments)]
pub async fn payments_check_and_apply_pm_data_core(
    state: SessionState,
    platform: domain::Platform,
    profile: domain::Profile,
    _req_state: ReqState,
    req: ApplyPaymentMethodDataRequest,
    payment_id: id_type::GlobalPaymentId,
) -> RouterResponse<CheckAndApplyPaymentMethodDataResponse> {
    let db = state.store.as_ref();
    let storage_scheme = platform.get_processor().get_account().storage_scheme;
    let payment_intent = db
        .find_payment_intent_by_id(
            &payment_id,
            platform.get_processor().get_key_store(),
            storage_scheme,
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;

    let payment_method_balances = fetch_payment_methods_balances_from_redis_fallible(
        &state,
        &payment_intent.id,
        &req.payment_methods,
    )
    .await
    .attach_printable("Failed to retrieve payment method balances from redis")?;

    let mut balance_values: Vec<api_models::payments::EligibilityBalanceCheckResponseItem> =
        futures::future::join_all(req.payment_methods.into_iter().map(|pm| async {
            let api_models::payments::BalanceCheckPaymentMethodData::GiftCard(gift_card_data) =
                pm.clone();
            let pm_balance_key = domain::PaymentMethodBalanceKey {
                payment_method_type: common_enums::PaymentMethod::GiftCard,
                payment_method_subtype: gift_card_data.get_payment_method_type(),
                payment_method_key: domain::GiftCardData::from(gift_card_data.clone())
                    .get_payment_method_key()
                    .change_context(errors::ApiErrorResponse::InvalidRequestData {
                        message: "Unable to get unique key for payment method".to_string(),
                    })?
                    .expose(),
            };

            let eligibility = match payment_method_balances
                .get(&pm_balance_key)
                .and_then(|inner| inner.as_ref())
            {
                Some(balance) => {
                    api_models::payments::PMBalanceCheckEligibilityResponse::Success(
                        PMBalanceCheckSuccessResponse {
                            balance: balance.balance,
                            applicable_amount: MinorUnit::zero(), // Will be calculated after sorting
                            currency: balance.currency,
                        },
                    )
                }
                None => {
                    match payments_check_gift_card_balance_core(
                        &state,
                        &platform,
                        &profile,
                        &_req_state,
                        pm.clone(),
                        &payment_id,
                    )
                    .await
                    {
                        Ok((balance, currency)) => {
                            api_models::payments::PMBalanceCheckEligibilityResponse::Success(
                                PMBalanceCheckSuccessResponse {
                                    balance,
                                    applicable_amount: MinorUnit::zero(), // Will be calculated after sorting
                                    currency,
                                },
                            )
                        }
                        Err(err) => {
                            api_models::payments::PMBalanceCheckEligibilityResponse::Failure(
                                PMBalanceCheckFailureResponse {
                                    error: err.to_string(),
                                },
                            )
                        }
                    }
                }
            };

            Ok::<_, error_stack::Report<errors::ApiErrorResponse>>(
                api_models::payments::EligibilityBalanceCheckResponseItem {
                    payment_method_data: pm,
                    eligibility,
                },
            )
        }))
        .await
        .into_iter()
        .collect::<Result<Vec<_>, _>>()?;

    // Sort balance_values by balance in ascending order (smallest first)
    // This ensures smaller gift cards are fully utilized before larger ones
    balance_values.sort_by(|a, b| {
        a.eligibility
            .get_balance()
            .cmp(&b.eligibility.get_balance())
    });

    // Calculate applicable amounts with running total
    let mut running_total = MinorUnit::zero();
    let order_amount = payment_intent.amount_details.order_amount;
    for balance_item in balance_values.iter_mut() {
        if let api_models::payments::PMBalanceCheckEligibilityResponse::Success(balance_api) =
            &mut balance_item.eligibility
        {
            let remaining_amount = (order_amount - running_total).max(MinorUnit::zero());
            balance_api.applicable_amount = std::cmp::min(balance_api.balance, remaining_amount);
            running_total = running_total + balance_api.applicable_amount;
        }
    }

    let total_applicable_balance = running_total;

    // remaining_amount cannot be negative, hence using max with 0. This situation can arise when
    // the gift card balance exceeds the order amount
    let remaining_amount = (payment_intent.amount_details.order_amount - total_applicable_balance)
        .max(MinorUnit::zero());

    let resp = CheckAndApplyPaymentMethodDataResponse {
        balances: balance_values,
        remaining_amount,
        currency: payment_intent.amount_details.currency,
        requires_additional_pm_data: remaining_amount.is_greater_than(0),
        surcharge_details: None, // TODO: Implement surcharge recalculation logic
    };

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

#[instrument(skip_all)]
pub async fn persist_individual_pm_balance_details_in_redis<'a>(
    state: &SessionState,
    business_profile: &domain::Profile,
    pm_balance_data: &domain::PaymentMethodBalanceData<'_>,
) -> errors::RouterResult<()> {
    if !pm_balance_data.is_empty() {
        let redis_conn = state
            .store
            .get_redis_conn()
            .change_context(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("Failed to get redis connection")?;
        let redis_key = pm_balance_data.get_pm_balance_redis_key();

        let value_list = pm_balance_data
            .get_individual_pm_balance_key_value_pairs()
            .into_iter()
            .map(|(key, value)| {
                value
                    .encode_to_string_of_json()
                    .change_context(errors::ApiErrorResponse::InternalServerError)
                    .attach_printable("Failed to encode to string of json")
                    .map(|encoded_value| (key, encoded_value))
            })
            .collect::<Result<Vec<_>, _>>()?;

        let intent_fulfillment_time = business_profile
            .get_order_fulfillment_time()
            .unwrap_or(consts::DEFAULT_FULFILLMENT_TIME);

        redis_conn
            .set_hash_fields(
                &redis_key.as_str().into(),
                value_list,
                Some(intent_fulfillment_time),
            )
            .await
            .change_context(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("Failed to write to redis")?;

        logger::debug!("Surcharge results stored in redis with key = {}", redis_key);
    }
    Ok(())
}

pub async fn fetch_payment_methods_balances_from_redis(
    state: &SessionState,
    payment_intent_id: &id_type::GlobalPaymentId,
    payment_methods: &[api_models::payments::BalanceCheckPaymentMethodData],
) -> errors::RouterResult<HashMap<domain::PaymentMethodBalanceKey, domain::PaymentMethodBalance>> {
    let redis_conn = state
        .store
        .get_redis_conn()
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Failed to get redis connection")?;

    let balance_data = domain::PaymentMethodBalanceData::new(payment_intent_id);

    let balance_values: HashMap<String, domain::PaymentMethodBalance> = redis_conn
        .get_hash_fields::<Vec<(String, String)>>(&balance_data.get_pm_balance_redis_key().into())
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Failed to read payment method balance data from redis")?
        .into_iter()
        .map(|(key, value)| {
            value
                .parse_struct("PaymentMethodBalance")
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Failed to parse PaymentMethodBalance")
                .map(|parsed| (key, parsed))
        })
        .collect::<errors::RouterResult<HashMap<_, _>>>()?;

    payment_methods
        .iter()
        .map(|pm| {
            let api_models::payments::BalanceCheckPaymentMethodData::GiftCard(gift_card_data) = pm;
            let pm_balance_key = domain::PaymentMethodBalanceKey {
                payment_method_type: common_enums::PaymentMethod::GiftCard,
                payment_method_subtype: gift_card_data.get_payment_method_type(),
                payment_method_key: domain::GiftCardData::from(gift_card_data.clone())
                    .get_payment_method_key()
                    .change_context(errors::ApiErrorResponse::InvalidRequestData {
                        message: "Unable to get unique key for payment method".to_string(),
                    })?
                    .expose(),
            };
            let redis_key = pm_balance_key.get_redis_key();
            let balance_value = balance_values.get(&redis_key).cloned().ok_or(
                errors::ApiErrorResponse::GenericNotFoundError {
                    message: "Balance not found for one or more payment methods".to_string(),
                },
            )?;
            Ok((pm_balance_key, balance_value))
        })
        .collect::<errors::RouterResult<HashMap<_, _>>>()
}

/// This function does not return an error if balance for a payment method is not found, it just sets
/// the corresponding value in the HashMap to None
pub async fn fetch_payment_methods_balances_from_redis_fallible(
    state: &SessionState,
    payment_intent_id: &id_type::GlobalPaymentId,
    payment_methods: &[api_models::payments::BalanceCheckPaymentMethodData],
) -> errors::RouterResult<
    HashMap<domain::PaymentMethodBalanceKey, Option<domain::PaymentMethodBalance>>,
> {
    let redis_conn = state
        .store
        .get_redis_conn()
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Failed to get redis connection")?;

    let balance_data = domain::PaymentMethodBalanceData::new(payment_intent_id);

    let balance_values: HashMap<String, domain::PaymentMethodBalance> = redis_conn
        .get_hash_fields::<Vec<(String, String)>>(&balance_data.get_pm_balance_redis_key().into())
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Failed to read payment method balance data from redis")?
        .into_iter()
        .map(|(key, value)| {
            value
                .parse_struct("PaymentMethodBalance")
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Failed to parse PaymentMethodBalance")
                .map(|parsed| (key, parsed))
        })
        .collect::<errors::RouterResult<HashMap<_, _>>>()?;

    payment_methods
        .iter()
        .map(|pm| {
            let api_models::payments::BalanceCheckPaymentMethodData::GiftCard(gift_card_data) = pm;
            let pm_balance_key = domain::PaymentMethodBalanceKey {
                payment_method_type: common_enums::PaymentMethod::GiftCard,
                payment_method_subtype: gift_card_data.get_payment_method_type(),
                payment_method_key: domain::GiftCardData::from(gift_card_data.clone())
                    .get_payment_method_key()
                    .change_context(errors::ApiErrorResponse::InvalidRequestData {
                        message: "Unable to get unique key for payment method".to_string(),
                    })?
                    .expose(),
            };
            let redis_key = pm_balance_key.get_redis_key();
            let balance_value = balance_values.get(&redis_key).cloned();
            Ok((pm_balance_key, balance_value))
        })
        .collect::<errors::RouterResult<HashMap<_, _>>>()
}


// File: crates/router/src/core/split_payments.rs

use api_models::{
    enums,
    payments::{
        self as payments_api, GetPaymentMethodType, PaymentMethodData,
        SplitPaymentMethodDataRequest,
    },
};
use common_enums::CallConnectorAction;
use common_utils::{
    ext_traits::{OptionExt, ValueExt},
    id_type,
    types::MinorUnit,
};
use error_stack::{report, Report, ResultExt};
use hyperswitch_domain_models::payments::{
    split_payments, HeaderPayload, PaymentConfirmData, PaymentIntent,
};
use masking::ExposeInterface;

use super::errors::StorageErrorExt;
use crate::{
    core::{
        errors::{self, RouterResponse},
        payment_method_balance,
        payments::{
            operations::{self, Operation, PaymentIntentConfirm},
            payments_operation_core,
            transformers::GenerateResponse,
        },
    },
    db::errors::RouterResult,
    routes::{app::ReqState, SessionState},
    types::{api, domain},
};

pub(crate) struct SplitPaymentResponseData {
    pub primary_payment_response_data: PaymentConfirmData<api::Authorize>,
    pub secondary_payment_response_data: Vec<PaymentConfirmData<api::Authorize>>,
}

impl SplitPaymentResponseData {
    fn get_intent_status(&self) -> common_enums::IntentStatus {
        let primary_status = common_enums::IntentStatus::from(
            self.primary_payment_response_data.payment_attempt.status,
        );

        let secondary_status_vec: Vec<common_enums::IntentStatus> = self
            .secondary_payment_response_data
            .iter()
            .map(|elem| common_enums::IntentStatus::from(elem.payment_attempt.status))
            .collect();

        // If all statuses are the same, return that status
        let all_same = secondary_status_vec
            .iter()
            .all(|status| *status == primary_status);

        if all_same {
            primary_status
        } else {
            // Return the last secondary status if array is not empty, otherwise primary status
            secondary_status_vec
                .last()
                .copied()
                .unwrap_or(primary_status)
        }
    }
}

/// This function has been written to support multiple gift cards + at most one non-gift card
/// payment method.
async fn get_payment_method_amount_split(
    state: &SessionState,
    payment_id: &id_type::GlobalPaymentId,
    request: &payments_api::PaymentsConfirmIntentRequest,
    payment_intent: &PaymentIntent,
) -> RouterResult<split_payments::PaymentMethodAmountSplit> {
    // This function is called inside split payments flow, so its mandatory to have split_payment_method_data
    let split_payment_method_data = request
        .split_payment_method_data
        .clone()
        .ok_or(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("split_payment_method_data not found in split payments flow")?;

    // The primary/secondary payment method distinction is decided on the backend. Its irrelevant whether a payment_method
    // is received in the top level `payment_method_data` field or inside `split_payment_method_data`
    // Add the outer payment_method_data to the PMs inside split_payment_method_data to create a combined Vec and then segregate.
    let payment_method_data = SplitPaymentMethodDataRequest {
        payment_method_data: request
            .payment_method_data
            .payment_method_data
            .clone()
            .ok_or(errors::ApiErrorResponse::MissingRequiredField {
                field_name: "payment_method_data",
            })?,
        payment_method_type: request.payment_method_type,
        payment_method_subtype: request.payment_method_subtype,
    };

    let combined_pm_data: Vec<_> = split_payment_method_data
        .into_iter()
        .chain(std::iter::once(payment_method_data))
        .collect();

    let (gift_card_pm_data, non_gift_card_pm_data): (Vec<_>, Vec<_>) = combined_pm_data
        .into_iter()
        .partition(|pm_data| pm_data.payment_method_type.is_gift_card());

    // Validate at most one non-gift-card payment method
    if non_gift_card_pm_data.len() > 1 {
        Err(errors::ApiErrorResponse::InvalidRequestData {
            message: "At most one non-gift card payment method is allowed".to_string(),
        })?
    }

    // It is possible that non-gift card payment method is not present, e.g. only 2 gift cards were provided
    let non_gift_card_pm_data = non_gift_card_pm_data.into_iter().next();

    let gift_card_data_vec = gift_card_pm_data
        .iter()
        .map(|elem| {
            if let PaymentMethodData::GiftCard(gift_card_data) = elem.payment_method_data.clone() {
                Ok(gift_card_data.as_ref().clone())
            } else {
                Err(report!(errors::ApiErrorResponse::InvalidRequestData {
                    message: "Only Gift Card supported for Split Payments".to_string(),
                }))
            }
        })
        .collect::<RouterResult<Vec<_>>>()?;

    let balance_check_data_vec = gift_card_data_vec
        .iter()
        .cloned()
        .map(api_models::payments::BalanceCheckPaymentMethodData::GiftCard)
        .collect::<Vec<_>>();

    let balances = payment_method_balance::fetch_payment_methods_balances_from_redis(
        state,
        payment_id,
        &balance_check_data_vec,
    )
    .await?;

    // TODO: Add surcharge calculation when it is added in v2
    let total_amount = payment_intent.amount_details.calculate_net_amount();

    let mut remaining_to_allocate = total_amount;
    let pm_split_amt_tuple: Vec<(PaymentMethodData, MinorUnit)> = gift_card_data_vec
        .iter()
        .filter_map(|gift_card_card| {
            if remaining_to_allocate == MinorUnit::zero() {
                return None; // Payment already fully covered
            }

            let pm_balance_key = domain::PaymentMethodBalanceKey {
                payment_method_type: common_enums::PaymentMethod::GiftCard,
                payment_method_subtype: gift_card_card.get_payment_method_type(),
                payment_method_key: domain::GiftCardData::from(gift_card_card.clone())
                    .get_payment_method_key()
                    .change_context(errors::ApiErrorResponse::InvalidRequestData {
                        message: "Unable to get unique key for payment method".to_string(),
                    })
                    .ok()?
                    .expose(),
            };

            let pm_balance = balances
                .get(&pm_balance_key)
                .ok_or(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Payment Method Balance not present in Redis")
                .ok()?;

            // Use minimum of available balance and remaining amount
            let amount_to_use = pm_balance.balance.min(remaining_to_allocate);
            remaining_to_allocate = remaining_to_allocate - amount_to_use;

            Some(Ok((
                PaymentMethodData::GiftCard(Box::new(gift_card_card.to_owned())),
                amount_to_use,
            )))
        })
        .collect::<RouterResult<Vec<_>>>()?;

    // If the gift card balances are not sufficient for payment, use the non-gift card payment method
    // for the remaining amount
    if remaining_to_allocate > MinorUnit::zero() {
        let non_gift_card_pm_data = non_gift_card_pm_data
            .ok_or(errors::ApiErrorResponse::InvalidRequestData {
                message: "Requires additional payment method data".to_string(),
            })?
            .payment_method_data;

        Ok(split_payments::PaymentMethodAmountSplit {
            balance_pm_split: pm_split_amt_tuple,
            non_balance_pm_split: Some((non_gift_card_pm_data, remaining_to_allocate)),
        })
    } else {
        Ok(split_payments::PaymentMethodAmountSplit {
            balance_pm_split: pm_split_amt_tuple,
            non_balance_pm_split: None,
        })
    }
}

pub(crate) async fn split_payments_execute_core(
    state: SessionState,
    req_state: ReqState,
    platform: domain::Platform,
    profile: domain::Profile,
    request: payments_api::PaymentsConfirmIntentRequest,
    header_payload: HeaderPayload,
    payment_id: id_type::GlobalPaymentId,
) -> RouterResponse<payments_api::PaymentsResponse> {
    let db = &*state.store;

    let payment_intent = db
        .find_payment_intent_by_id(
            &payment_id,
            platform.get_processor().get_key_store(),
            platform.get_processor().get_account().storage_scheme,
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;

    if !payment_intent.supports_split_payments() {
        Err(errors::ApiErrorResponse::InvalidRequestData {
            message: "Split Payments not enabled in Payment Intent".to_string(),
        })?
    }

    let cell_id = state.conf.cell_information.id.clone();

    let attempts_group_id = id_type::GlobalAttemptGroupId::generate(&cell_id);

    // Change the active_attempt_id_type of PaymentIntent to `GroupID`. This indicates that the customer
    // has attempted a split payment for this intent
    let payment_intent_update =
        hyperswitch_domain_models::payments::payment_intent::PaymentIntentUpdate::AttemptGroupUpdate {
            updated_by: platform
                .get_processor()
                .get_account()
                .storage_scheme
                .to_string(),
            active_attempt_id_type: enums::ActiveAttemptIDType::GroupID,
            active_attempts_group_id: attempts_group_id.clone(),
        };

    let payment_intent = db
        .update_payment_intent(
            payment_intent,
            payment_intent_update,
            platform.get_processor().get_key_store(),
            platform.get_processor().get_account().storage_scheme,
        )
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Unable to update payment intent")?;

    let pm_amount_split =
        get_payment_method_amount_split(&state, &payment_id, &request, &payment_intent).await?;

    let (
        primary_pm_response,
        connector_http_status_code,
        external_latency,
        connector_response_data,
    ) = {
        // If a non-balance Payment Method is present, we will execute that first, otherwise we will execute
        // a balance Payment Method
        let (payment_method_data, amount) = pm_amount_split
            .non_balance_pm_split
            .clone()
            .or_else(|| pm_amount_split.balance_pm_split.first().cloned())
            .get_required_value("payment method amount split")
            .change_context(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("At least one payment method is required")?;

        let operation = PaymentIntentConfirm;

        let get_tracker_response: operations::GetTrackerResponse<
            PaymentConfirmData<api::Authorize>,
        > = operation
            .to_get_tracker()?
            .get_trackers_for_split_payments(
                &state,
                &payment_id,
                &request,
                &platform,
                &profile,
                &header_payload,
                (payment_method_data, amount),
                &attempts_group_id,
            )
            .await?;

        let (
            payment_data,
            _req,
            _customer,
            connector_http_status_code,
            external_latency,
            connector_response_data,
        ) = Box::pin(payments_operation_core(
            &state,
            req_state.clone(),
            platform.clone(),
            &profile,
            operation,
            request.clone(),
            get_tracker_response,
            CallConnectorAction::Trigger,
            header_payload.clone(),
        ))
        .await?;

        // payments_operation_core marks the intent as succeeded when the attempt is succesful
        // However, for split case, we can't mark the intent as succesful until all the attempts
        // have succeeded, so reverting the state of Payment Intent
        if payment_data.payment_intent.is_succeeded() {
            let payment_intent_update =
            hyperswitch_domain_models::payments::payment_intent::PaymentIntentUpdate::SplitPaymentStatusUpdate {
                status: common_enums::IntentStatus::RequiresPaymentMethod,
                updated_by: platform
                    .get_processor()
                    .get_account()
                    .storage_scheme
                    .to_string(),
            };

            let updated_payment_intent = db
                .update_payment_intent(
                    payment_data.payment_intent.clone(),
                    payment_intent_update,
                    platform.get_processor().get_key_store(),
                    platform.get_processor().get_account().storage_scheme,
                )
                .await
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Unable to update payment intent")?;
        }

        (
            payment_data,
            connector_http_status_code,
            external_latency,
            connector_response_data,
        )
    };

    let mut split_pm_response_data = SplitPaymentResponseData {
        primary_payment_response_data: primary_pm_response,
        secondary_payment_response_data: vec![],
    };

    // We have executed the primary payment method, now get a vector of the secondary payment methods
    // If we had a non-balance payment method, it was executed first, so the remaining ones are the balance PMs
    // otherwise the first balance payment method was executed, so remove it from the remaining PMs
    let remaining_pm_amount_split = if pm_amount_split.non_balance_pm_split.is_some() {
        pm_amount_split.balance_pm_split
    } else {
        pm_amount_split
            .balance_pm_split
            .iter()
            .skip(1)
            .cloned()
            .collect()
    };

    for (payment_method_data, amount) in remaining_pm_amount_split {
        let operation = PaymentIntentConfirm;

        let get_tracker_response: operations::GetTrackerResponse<
            PaymentConfirmData<api::Authorize>,
        > = operation
            .to_get_tracker()?
            .get_trackers_for_split_payments(
                &state,
                &payment_id,
                &request,
                &platform,
                &profile,
                &header_payload,
                (payment_method_data.to_owned(), amount.to_owned()),
                &attempts_group_id,
            )
            .await?;

        let (
            payment_data,
            _req,
            _customer,
            _connector_http_status_code,
            _external_latency,
            _connector_response_data,
        ) = Box::pin(payments_operation_core(
            &state,
            req_state.clone(),
            platform.clone(),
            &profile,
            operation,
            request.clone(),
            get_tracker_response,
            CallConnectorAction::Trigger,
            header_payload.clone(),
        ))
        .await?;

        // payments_operation_core marks the intent as succeeded when the attempt is succesful
        // However, for split case, we can't mark the intent as succesful until all the attempts
        // have succeeded, so reverting the state of Payment Intent
        if payment_data.payment_intent.is_succeeded() {
            let payment_intent_update =
            hyperswitch_domain_models::payments::payment_intent::PaymentIntentUpdate::SplitPaymentStatusUpdate {
                status: common_enums::IntentStatus::RequiresPaymentMethod,
                updated_by: platform
                    .get_processor()
                    .get_account()
                    .storage_scheme
                    .to_string(),
            };

            let _updated_payment_intent = db
                .update_payment_intent(
                    payment_data.payment_intent.clone(),
                    payment_intent_update,
                    platform.get_processor().get_key_store(),
                    platform.get_processor().get_account().storage_scheme,
                )
                .await
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Unable to update payment intent")?;
        } else {
            split_pm_response_data
                .secondary_payment_response_data
                .push(payment_data);

            // Exit the loop if a payment failed
            break;
        }

        split_pm_response_data
            .secondary_payment_response_data
            .push(payment_data);
    }

    let payment_intent_update =
        hyperswitch_domain_models::payments::payment_intent::PaymentIntentUpdate::SplitPaymentStatusUpdate {
            status: split_pm_response_data.get_intent_status(),
            updated_by: platform
                .get_processor()
                .get_account()
                .storage_scheme
                .to_string(),
        };

    let _updated_payment_intent = db
        .update_payment_intent(
            payment_intent,
            payment_intent_update,
            platform.get_processor().get_key_store(),
            platform.get_processor().get_account().storage_scheme,
        )
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Unable to update payment intent")?;

    split_pm_response_data.generate_response(
        &state,
        connector_http_status_code,
        external_latency,
        header_payload.x_hs_latency,
        &platform,
        &profile,
        Some(connector_response_data),
    )
}

/// Construct the domain model from the ConfirmIntentRequest and PaymentIntent
#[cfg(feature = "v2")]
pub async fn create_domain_model_for_split_payment(
    payment_intent: &PaymentIntent,
    cell_id: id_type::CellId,
    storage_scheme: enums::MerchantStorageScheme,
    request: &api_models::payments::PaymentsConfirmIntentRequest,
    encrypted_data: hyperswitch_domain_models::payments::payment_attempt::DecryptedPaymentAttempt,
    split_amount: MinorUnit,
    attempts_group_id: &id_type::GlobalAttemptGroupId,
) -> common_utils::errors::CustomResult<domain::PaymentAttempt, errors::ApiErrorResponse> {
    let id = id_type::GlobalAttemptId::generate(&cell_id);
    let intent_amount_details = payment_intent.amount_details.clone();

    let attempt_amount_details =
        intent_amount_details.create_split_attempt_amount_details(request, split_amount);

    let now = common_utils::date_time::now();

    let payment_method_billing_address = encrypted_data
        .payment_method_billing_address
        .as_ref()
        .map(|data| {
            data.clone()
                .deserialize_inner_value(|value| value.parse_value("Address"))
        })
        .transpose()
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Unable to decode billing address")?;

    let connector_token = Some(diesel_models::ConnectorTokenDetails {
        connector_mandate_id: None,
        connector_token_request_reference_id: Some(common_utils::generate_id_with_len(
            hyperswitch_domain_models::consts::CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH,
        )),
    });

    let authentication_type = payment_intent.authentication_type.unwrap_or_default();

    Ok(domain::PaymentAttempt {
        payment_id: payment_intent.id.clone(),
        merchant_id: payment_intent.merchant_id.clone(),
        attempts_group_id: Some(attempts_group_id.to_owned()),
        amount_details: attempt_amount_details,
        status: common_enums::AttemptStatus::Started,
        // This will be decided by the routing algorithm and updated in update trackers
        // right before calling the connector
        connector: None,
        authentication_type,
        created_at: now,
        modified_at: now,
        last_synced: None,
        cancellation_reason: None,
        browser_info: request.browser_info.clone(),
        payment_token: request.payment_token.clone(),
        connector_metadata: None,
        payment_experience: None,
        payment_method_data: None,
        routing_result: None,
        preprocessing_step_id: None,
        multiple_capture_count: None,
        connector_response_reference_id: None,
        updated_by: storage_scheme.to_string(),
        redirection_data: None,
        encoded_data: None,
        merchant_connector_id: None,
        external_three_ds_authentication_attempted: None,
        authentication_connector: None,
        authentication_id: None,
        fingerprint_id: None,
        charges: None,
        client_source: None,
        client_version: None,
        customer_acceptance: request
            .customer_acceptance
            .clone()
            .map(masking::Secret::new),
        profile_id: payment_intent.profile_id.clone(),
        organization_id: payment_intent.organization_id.clone(),
        payment_method_type: request.payment_method_type,
        payment_method_id: request.payment_method_id.clone(),
        connector_payment_id: None,
        payment_method_subtype: request.payment_method_subtype,
        authentication_applied: None,
        external_reference_id: None,
        payment_method_billing_address,
        error: None,
        connector_token_details: connector_token,
        id,
        card_discovery: None,
        feature_metadata: None,
        processor_merchant_id: payment_intent.merchant_id.clone(),
        created_by: None,
        connector_request_reference_id: None,
        network_transaction_id: None,
        authorized_amount: None,
    })
}


// File: crates/router/src/core/apple_pay_certificates_migration.rs

use api_models::apple_pay_certificates_migration;
use common_utils::{errors::CustomResult, type_name, types::keymanager::Identifier};
use error_stack::ResultExt;
use masking::{PeekInterface, Secret};

use super::{
    errors::{self, StorageErrorExt},
    payments::helpers,
};
use crate::{
    routes::SessionState,
    services::{self, logger},
    types::{domain::types as domain_types, storage},
};

#[cfg(feature = "v1")]
pub async fn apple_pay_certificates_migration(
    state: SessionState,
    req: &apple_pay_certificates_migration::ApplePayCertificatesMigrationRequest,
) -> CustomResult<
    services::ApplicationResponse<
        apple_pay_certificates_migration::ApplePayCertificatesMigrationResponse,
    >,
    errors::ApiErrorResponse,
> {
    let db = state.store.as_ref();

    let merchant_id_list = &req.merchant_ids;

    let mut migration_successful_merchant_ids = vec![];
    let mut migration_failed_merchant_ids = vec![];
    for merchant_id in merchant_id_list {
        let key_store = state
            .store
            .get_merchant_key_store_by_merchant_id(
                merchant_id,
                &state.store.get_master_key().to_vec().into(),
            )
            .await
            .change_context(errors::ApiErrorResponse::InternalServerError)?;

        let merchant_connector_accounts = db
            .find_merchant_connector_account_by_merchant_id_and_disabled_list(
                merchant_id,
                true,
                &key_store,
            )
            .await
            .to_not_found_response(errors::ApiErrorResponse::InternalServerError)?;

        let mut mca_to_update = vec![];

        for connector_account in merchant_connector_accounts {
            let connector_apple_pay_metadata =
                helpers::get_applepay_metadata(connector_account.clone().metadata)
                    .map_err(|error| {
                        logger::error!(
                "Apple pay metadata parsing failed for {:?} in certificates migrations api {:?}",
                connector_account.clone().connector_name,
                error
            )
                    })
                    .ok();
            if let Some(apple_pay_metadata) = connector_apple_pay_metadata {
                let encrypted_apple_pay_metadata = domain_types::crypto_operation(
                    &(&state).into(),
                    type_name!(storage::MerchantConnectorAccount),
                    domain_types::CryptoOperation::Encrypt(Secret::new(
                        serde_json::to_value(apple_pay_metadata)
                            .change_context(errors::ApiErrorResponse::InternalServerError)
                            .attach_printable("Failed to serialize apple pay metadata as JSON")?,
                    )),
                    Identifier::Merchant(merchant_id.clone()),
                    key_store.key.get_inner().peek(),
                )
                .await
                .and_then(|val| val.try_into_operation())
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Unable to encrypt connector apple pay metadata")?;

                let updated_mca =
                    storage::MerchantConnectorAccountUpdate::ConnectorWalletDetailsUpdate {
                        connector_wallets_details: encrypted_apple_pay_metadata,
                    };

                mca_to_update.push((connector_account, updated_mca.into()));
            }
        }

        let merchant_connector_accounts_update = db
            .update_multiple_merchant_connector_accounts(mca_to_update)
            .await;

        match merchant_connector_accounts_update {
            Ok(_) => {
                logger::debug!(
                    "Merchant connector accounts updated for merchant id {merchant_id:?}"
                );
                migration_successful_merchant_ids.push(merchant_id.clone());
            }
            Err(error) => {
                logger::debug!(
                    "Merchant connector accounts update failed with error {error} for merchant id {merchant_id:?}");
                migration_failed_merchant_ids.push(merchant_id.clone());
            }
        };
    }

    Ok(services::api::ApplicationResponse::Json(
        apple_pay_certificates_migration::ApplePayCertificatesMigrationResponse {
            migration_successful: migration_successful_merchant_ids,
            migration_failed: migration_failed_merchant_ids,
        },
    ))
}


// File: crates/router/src/core/three_ds_decision_rule.rs

pub mod utils;

use common_types::three_ds_decision_rule_engine::ThreeDSDecisionRule;
use common_utils::ext_traits::ValueExt;
use error_stack::ResultExt;
use euclid::{
    backend::{self, inputs as dsl_inputs, EuclidBackend},
    frontend::ast,
};
use hyperswitch_domain_models::platform::Platform;
use router_env::{instrument, tracing};

use crate::{
    core::{
        errors,
        errors::{RouterResponse, StorageErrorExt},
    },
    services,
    types::transformers::ForeignFrom,
    SessionState,
};

#[instrument(skip_all)]
pub async fn execute_three_ds_decision_rule(
    state: SessionState,
    platform: Platform,
    request: api_models::three_ds_decision_rule::ThreeDsDecisionRuleExecuteRequest,
) -> RouterResponse<api_models::three_ds_decision_rule::ThreeDsDecisionRuleExecuteResponse> {
    let decision = get_three_ds_decision_rule_output(
        &state,
        platform.get_processor().get_account().get_id(),
        request.clone(),
    )
    .await?;
    // Construct response
    let response =
        api_models::three_ds_decision_rule::ThreeDsDecisionRuleExecuteResponse { decision };
    Ok(services::ApplicationResponse::Json(response))
}

pub async fn get_three_ds_decision_rule_output(
    state: &SessionState,
    merchant_id: &common_utils::id_type::MerchantId,
    request: api_models::three_ds_decision_rule::ThreeDsDecisionRuleExecuteRequest,
) -> errors::RouterResult<common_types::three_ds_decision_rule_engine::ThreeDSDecision> {
    let db = state.store.as_ref();
    // Retrieve the rule from database
    let routing_algorithm = db
        .find_routing_algorithm_by_algorithm_id_merchant_id(&request.routing_id, merchant_id)
        .await
        .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?;
    let algorithm: Algorithm = routing_algorithm
        .algorithm_data
        .parse_value("Algorithm")
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Error parsing program from three_ds_decision rule algorithm")?;
    let program: ast::Program<ThreeDSDecisionRule> = algorithm
        .data
        .parse_value("Program<ThreeDSDecisionRule>")
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Error parsing program from three_ds_decision rule algorithm")?;
    // Construct backend input from request
    let backend_input = dsl_inputs::BackendInput::foreign_from(request.clone());
    // Initialize interpreter with the rule program
    let interpreter = backend::VirInterpreterBackend::with_program(program)
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Error initializing DSL interpreter backend")?;
    // Execute the rule
    let result = interpreter
        .execute(backend_input)
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Error executing 3DS decision rule")?;
    // Apply PSD2 validations to the decision
    let final_decision =
        utils::apply_psd2_validations_during_execute(result.get_output().get_decision(), &request);
    Ok(final_decision)
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Algorithm {
    data: serde_json::Value,
}


// File: crates/router/src/core/relay.rs

use std::marker::PhantomData;

use api_models::relay as relay_api_models;
use async_trait::async_trait;
use common_enums::RelayStatus;
use common_utils::{
    self, fp_utils,
    id_type::{self, GenerateId},
};
use error_stack::ResultExt;
use hyperswitch_domain_models::relay;

use super::errors::{self, ConnectorErrorExt, RouterResponse, RouterResult, StorageErrorExt};
use crate::{
    core::payments,
    routes::SessionState,
    services,
    types::{
        api::{self},
        domain,
    },
    utils::OptionExt,
};

pub mod utils;

pub trait Validate {
    type Error: error_stack::Context;
    fn validate(&self) -> Result<(), Self::Error>;
}

impl Validate for relay_api_models::RelayRefundRequestData {
    type Error = errors::ApiErrorResponse;
    fn validate(&self) -> Result<(), Self::Error> {
        fp_utils::when(self.amount.get_amount_as_i64() <= 0, || {
            Err(errors::ApiErrorResponse::PreconditionFailed {
                message: "Amount should be greater than 0".to_string(),
            })
        })?;
        Ok(())
    }
}

#[async_trait]
pub trait RelayInterface {
    type Request: Validate;
    fn validate_relay_request(req: &Self::Request) -> RouterResult<()> {
        req.validate()
            .change_context(errors::ApiErrorResponse::PreconditionFailed {
                message: "Invalid relay request".to_string(),
            })
    }

    fn get_domain_models(
        relay_request: RelayRequestInner<Self>,
        merchant_id: &id_type::MerchantId,
        profile_id: &id_type::ProfileId,
    ) -> relay::Relay;

    async fn process_relay(
        state: &SessionState,
        platform: domain::Platform,
        connector_account: domain::MerchantConnectorAccount,
        relay_record: &relay::Relay,
    ) -> RouterResult<relay::RelayUpdate>;

    fn generate_response(value: relay::Relay) -> RouterResult<api_models::relay::RelayResponse>;
}

pub struct RelayRequestInner<T: RelayInterface + ?Sized> {
    pub connector_resource_id: String,
    pub connector_id: id_type::MerchantConnectorAccountId,
    pub relay_type: PhantomData<T>,
    pub data: T::Request,
}

impl RelayRequestInner<RelayRefund> {
    pub fn from_relay_request(relay_request: relay_api_models::RelayRequest) -> RouterResult<Self> {
        match relay_request.data {
            Some(relay_api_models::RelayData::Refund(ref_data)) => Ok(Self {
                connector_resource_id: relay_request.connector_resource_id,
                connector_id: relay_request.connector_id,
                relay_type: PhantomData,
                data: ref_data,
            }),
            None => Err(errors::ApiErrorResponse::InvalidRequestData {
                message: "Relay data is required for relay type refund".to_string(),
            })?,
        }
    }
}

pub struct RelayRefund;

#[async_trait]
impl RelayInterface for RelayRefund {
    type Request = relay_api_models::RelayRefundRequestData;

    fn get_domain_models(
        relay_request: RelayRequestInner<Self>,
        merchant_id: &id_type::MerchantId,
        profile_id: &id_type::ProfileId,
    ) -> relay::Relay {
        let relay_id = id_type::RelayId::generate();
        let relay_refund: relay::RelayRefundData = relay_request.data.into();
        relay::Relay {
            id: relay_id.clone(),
            connector_resource_id: relay_request.connector_resource_id.clone(),
            connector_id: relay_request.connector_id.clone(),
            profile_id: profile_id.clone(),
            merchant_id: merchant_id.clone(),
            relay_type: common_enums::RelayType::Refund,
            request_data: Some(relay::RelayData::Refund(relay_refund)),
            status: RelayStatus::Created,
            connector_reference_id: None,
            error_code: None,
            error_message: None,
            created_at: common_utils::date_time::now(),
            modified_at: common_utils::date_time::now(),
            response_data: None,
        }
    }

    async fn process_relay(
        state: &SessionState,
        platform: domain::Platform,
        connector_account: domain::MerchantConnectorAccount,
        relay_record: &relay::Relay,
    ) -> RouterResult<relay::RelayUpdate> {
        let connector_id = &relay_record.connector_id;

        let merchant_id = platform.get_processor().get_account().get_id();

        let connector_name = &connector_account.get_connector_name_as_string();

        let connector_data = api::ConnectorData::get_connector_by_name(
            &state.conf.connectors,
            connector_name,
            api::GetToken::Connector,
            Some(connector_id.clone()),
        )?;

        let connector_integration: services::BoxedRefundConnectorIntegrationInterface<
            api::Execute,
            hyperswitch_domain_models::router_request_types::RefundsData,
            hyperswitch_domain_models::router_response_types::RefundsResponseData,
        > = connector_data.connector.get_connector_integration();

        let router_data = utils::construct_relay_refund_router_data(
            state,
            merchant_id,
            &connector_account,
            relay_record,
        )
        .await?;

        let router_data_res = services::execute_connector_processing_step(
            state,
            connector_integration,
            &router_data,
            payments::CallConnectorAction::Trigger,
            None,
            None,
        )
        .await
        .to_refund_failed_response()?;

        let relay_update = relay::RelayUpdate::from(router_data_res.response);

        Ok(relay_update)
    }

    fn generate_response(value: relay::Relay) -> RouterResult<api_models::relay::RelayResponse> {
        let error = value
            .error_code
            .zip(value.error_message)
            .map(
                |(error_code, error_message)| api_models::relay::RelayError {
                    code: error_code,
                    message: error_message,
                },
            );

        let data =
            api_models::relay::RelayData::from(value.request_data.get_required_value("RelayData")?);

        Ok(api_models::relay::RelayResponse {
            id: value.id,
            status: value.status,
            error,
            connector_resource_id: value.connector_resource_id,
            connector_id: value.connector_id,
            profile_id: value.profile_id,
            relay_type: value.relay_type,
            data: Some(data),
            connector_reference_id: value.connector_reference_id,
        })
    }
}

pub async fn relay_flow_decider(
    state: SessionState,
    platform: domain::Platform,
    profile_id_optional: Option<id_type::ProfileId>,
    request: relay_api_models::RelayRequest,
) -> RouterResponse<relay_api_models::RelayResponse> {
    let relay_flow_request = match request.relay_type {
        common_enums::RelayType::Refund => {
            RelayRequestInner::<RelayRefund>::from_relay_request(request)?
        }
    };
    relay(state, platform, profile_id_optional, relay_flow_request).await
}

pub async fn relay<T: RelayInterface>(
    state: SessionState,
    platform: domain::Platform,
    profile_id_optional: Option<id_type::ProfileId>,
    req: RelayRequestInner<T>,
) -> RouterResponse<relay_api_models::RelayResponse> {
    let db = state.store.as_ref();
    let merchant_id = platform.get_processor().get_account().get_id();
    let connector_id = &req.connector_id;

    let profile_id_from_auth_layer = profile_id_optional.get_required_value("ProfileId")?;

    let profile = db
        .find_business_profile_by_merchant_id_profile_id(
            platform.get_processor().get_key_store(),
            merchant_id,
            &profile_id_from_auth_layer,
        )
        .await
        .change_context(errors::ApiErrorResponse::ProfileNotFound {
            id: profile_id_from_auth_layer.get_string_repr().to_owned(),
        })?;

    #[cfg(feature = "v1")]
    let connector_account = db
        .find_by_merchant_connector_account_merchant_id_merchant_connector_id(
            merchant_id,
            connector_id,
            platform.get_processor().get_key_store(),
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
            id: connector_id.get_string_repr().to_string(),
        })?;

    #[cfg(feature = "v2")]
    let connector_account = db
        .find_merchant_connector_account_by_id(
            connector_id,
            platform.get_processor().get_key_store(),
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
            id: connector_id.get_string_repr().to_string(),
        })?;

    T::validate_relay_request(&req.data)?;

    let relay_domain = T::get_domain_models(req, merchant_id, profile.get_id());

    let relay_record = db
        .insert_relay(platform.get_processor().get_key_store(), relay_domain)
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Failed to insert a relay record in db")?;

    let relay_response =
        T::process_relay(&state, platform.clone(), connector_account, &relay_record)
            .await
            .attach_printable("Failed to process relay")?;

    let relay_update_record = db
        .update_relay(
            platform.get_processor().get_key_store(),
            relay_record,
            relay_response,
        )
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)?;

    let response = T::generate_response(relay_update_record)
        .attach_printable("Failed to generate relay response")?;

    Ok(hyperswitch_domain_models::api::ApplicationResponse::Json(
        response,
    ))
}

pub async fn relay_retrieve(
    state: SessionState,
    platform: domain::Platform,
    profile_id_optional: Option<id_type::ProfileId>,
    req: relay_api_models::RelayRetrieveRequest,
) -> RouterResponse<relay_api_models::RelayResponse> {
    let db = state.store.as_ref();
    let merchant_id = platform.get_processor().get_account().get_id();
    let relay_id = &req.id;

    let profile_id_from_auth_layer = profile_id_optional.get_required_value("ProfileId")?;

    db.find_business_profile_by_merchant_id_profile_id(
        platform.get_processor().get_key_store(),
        merchant_id,
        &profile_id_from_auth_layer,
    )
    .await
    .change_context(errors::ApiErrorResponse::ProfileNotFound {
        id: profile_id_from_auth_layer.get_string_repr().to_owned(),
    })?;

    let relay_record_result = db
        .find_relay_by_id(platform.get_processor().get_key_store(), relay_id)
        .await;

    let relay_record = match relay_record_result {
        Err(error) => {
            if error.current_context().is_db_not_found() {
                Err(error).change_context(errors::ApiErrorResponse::GenericNotFoundError {
                    message: "relay not found".to_string(),
                })?
            } else {
                Err(error)
                    .change_context(errors::ApiErrorResponse::InternalServerError)
                    .attach_printable("error while fetch relay record")?
            }
        }
        Ok(relay) => relay,
    };

    #[cfg(feature = "v1")]
    let connector_account = db
        .find_by_merchant_connector_account_merchant_id_merchant_connector_id(
            merchant_id,
            &relay_record.connector_id,
            platform.get_processor().get_key_store(),
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
            id: relay_record.connector_id.get_string_repr().to_string(),
        })?;

    #[cfg(feature = "v2")]
    let connector_account = db
        .find_merchant_connector_account_by_id(
            &relay_record.connector_id,
            platform.get_processor().get_key_store(),
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
            id: relay_record.connector_id.get_string_repr().to_string(),
        })?;

    let relay_response = match relay_record.relay_type {
        common_enums::RelayType::Refund => {
            if should_call_connector_for_relay_refund_status(&relay_record, req.force_sync) {
                let relay_response = sync_relay_refund_with_gateway(
                    &state,
                    &platform,
                    &relay_record,
                    connector_account,
                )
                .await?;

                db.update_relay(
                    platform.get_processor().get_key_store(),
                    relay_record,
                    relay_response,
                )
                .await
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Failed to update the relay record")?
            } else {
                relay_record
            }
        }
    };

    let response = relay_api_models::RelayResponse::from(relay_response);

    Ok(hyperswitch_domain_models::api::ApplicationResponse::Json(
        response,
    ))
}

fn should_call_connector_for_relay_refund_status(relay: &relay::Relay, force_sync: bool) -> bool {
    // This allows refund sync at connector level if force_sync is enabled, or
    // check if the refund is in terminal state
    !matches!(relay.status, RelayStatus::Failure | RelayStatus::Success) && force_sync
}

pub async fn sync_relay_refund_with_gateway(
    state: &SessionState,
    platform: &domain::Platform,
    relay_record: &relay::Relay,
    connector_account: domain::MerchantConnectorAccount,
) -> RouterResult<relay::RelayUpdate> {
    let connector_id = &relay_record.connector_id;
    let merchant_id = platform.get_processor().get_account().get_id();

    #[cfg(feature = "v1")]
    let connector_name = &connector_account.connector_name;

    #[cfg(feature = "v2")]
    let connector_name = &connector_account.connector_name.to_string();

    let connector_data: api::ConnectorData = api::ConnectorData::get_connector_by_name(
        &state.conf.connectors,
        connector_name,
        api::GetToken::Connector,
        Some(connector_id.clone()),
    )
    .change_context(errors::ApiErrorResponse::InternalServerError)
    .attach_printable("Failed to get the connector")?;

    let router_data = utils::construct_relay_refund_router_data(
        state,
        merchant_id,
        &connector_account,
        relay_record,
    )
    .await?;

    let connector_integration: services::BoxedRefundConnectorIntegrationInterface<
        api::RSync,
        hyperswitch_domain_models::router_request_types::RefundsData,
        hyperswitch_domain_models::router_response_types::RefundsResponseData,
    > = connector_data.connector.get_connector_integration();

    let router_data_res = services::execute_connector_processing_step(
        state,
        connector_integration,
        &router_data,
        payments::CallConnectorAction::Trigger,
        None,
        None,
    )
    .await
    .to_refund_failed_response()?;

    let relay_response = relay::RelayUpdate::from(router_data_res.response);

    Ok(relay_response)
}