File: crates/router/src/core/payout_link.rs
 Public functions: 4

use std::{
    cmp::Ordering,
    collections::{HashMap, HashSet},
};

use actix_web::http::header;
use api_models::payouts;
use common_utils::{
    ext_traits::{AsyncExt, Encode, OptionExt},
    link_utils,
    types::{AmountConvertor, StringMajorUnitForConnector},
};
use diesel_models::PayoutLinkUpdate;
use error_stack::ResultExt;
use hyperswitch_domain_models::api::{GenericLinks, GenericLinksData};

use super::errors::{RouterResponse, StorageErrorExt};
use crate::{
    configs::settings::{PaymentMethodFilterKey, PaymentMethodFilters},
    core::payouts::{helpers as payout_helpers, validator},
    errors,
    routes::{app::StorageInterface, SessionState},
    services,
    types::{api, domain, transformers::ForeignFrom},
    utils::get_payout_attempt_id,
};

#[cfg(feature = "v2")]
pub async fn initiate_payout_link(
    _state: SessionState,
    _merchant_context: domain::MerchantContext,
    _req: payouts::PayoutLinkInitiateRequest,
    _request_headers: &header::HeaderMap,
    _locale: String,
) -> RouterResponse<services::GenericLinkFormData> {
    todo!()
}

#[cfg(feature = "v1")]
pub async fn initiate_payout_link(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    req: payouts::PayoutLinkInitiateRequest,
    request_headers: &header::HeaderMap,
) -> RouterResponse<services::GenericLinkFormData> {
    let db: &dyn StorageInterface = &*state.store;
    let merchant_id = merchant_context.get_merchant_account().get_id();
    // Fetch payout
    let payout = db
        .find_payout_by_merchant_id_payout_id(
            merchant_id,
            &req.payout_id,
            merchant_context.get_merchant_account().storage_scheme,
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::PayoutNotFound)?;
    let payout_attempt = db
        .find_payout_attempt_by_merchant_id_payout_attempt_id(
            merchant_id,
            &get_payout_attempt_id(payout.payout_id.get_string_repr(), payout.attempt_count),
            merchant_context.get_merchant_account().storage_scheme,
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::PayoutNotFound)?;
    let payout_link_id = payout
        .payout_link_id
        .clone()
        .get_required_value("payout link id")
        .change_context(errors::ApiErrorResponse::GenericNotFoundError {
            message: "payout link not found".to_string(),
        })?;
    // Fetch payout link
    let payout_link = db
        .find_payout_link_by_link_id(&payout_link_id)
        .await
        .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError {
            message: "payout link not found".to_string(),
        })?;

    let allowed_domains = validator::validate_payout_link_render_request_and_get_allowed_domains(
        request_headers,
        &payout_link,
    )?;

    // Check status and return form data accordingly
    let has_expired = common_utils::date_time::now() > payout_link.expiry;
    let status = payout_link.link_status.clone();
    let link_data = payout_link.link_data.clone();
    let default_config = &state.conf.generic_link.payout_link.clone();
    let default_ui_config = default_config.ui_config.clone();
    let ui_config_data = link_utils::GenericLinkUiConfigFormData {
        merchant_name: link_data
            .ui_config
            .merchant_name
            .unwrap_or(default_ui_config.merchant_name),
        logo: link_data.ui_config.logo.unwrap_or(default_ui_config.logo),
        theme: link_data
            .ui_config
            .theme
            .clone()
            .unwrap_or(default_ui_config.theme.clone()),
    };
    match (has_expired, &status) {
        // Send back generic expired page
        (true, _) | (_, &link_utils::PayoutLinkStatus::Invalidated) => {
            let expired_link_data = services::GenericExpiredLinkData {
                title: "Payout Expired".to_string(),
                message: "This payout link has expired.".to_string(),
                theme: link_data.ui_config.theme.unwrap_or(default_ui_config.theme),
            };

            if status != link_utils::PayoutLinkStatus::Invalidated {
                let payout_link_update = PayoutLinkUpdate::StatusUpdate {
                    link_status: link_utils::PayoutLinkStatus::Invalidated,
                };
                db.update_payout_link(payout_link, payout_link_update)
                    .await
                    .change_context(errors::ApiErrorResponse::InternalServerError)
                    .attach_printable("Error updating payout links in db")?;
            }

            Ok(services::ApplicationResponse::GenericLinkForm(Box::new(
                GenericLinks {
                    allowed_domains,
                    data: GenericLinksData::ExpiredLink(expired_link_data),
                    locale: state.locale,
                },
            )))
        }

        // Initiate Payout link flow
        (_, link_utils::PayoutLinkStatus::Initiated) => {
            let customer_id = link_data.customer_id;
            let required_amount_type = StringMajorUnitForConnector;
            let amount = required_amount_type
                .convert(payout.amount, payout.destination_currency)
                .change_context(errors::ApiErrorResponse::CurrencyConversionFailed)?;
            // Fetch customer
            let customer = db
                .find_customer_by_customer_id_merchant_id(
                    &(&state).into(),
                    &customer_id,
                    &req.merchant_id,
                    merchant_context.get_merchant_key_store(),
                    merchant_context.get_merchant_account().storage_scheme,
                )
                .await
                .change_context(errors::ApiErrorResponse::InvalidRequestData {
                    message: format!(
                        "Customer [{:?}] not found for link_id - {}",
                        customer_id, payout_link.link_id
                    ),
                })
                .attach_printable_lazy(|| format!("customer [{customer_id:?}] not found"))?;
            let address = payout
                .address_id
                .as_ref()
                .async_map(|address_id| async {
                    db.find_address_by_address_id(
                        &(&state).into(),
                        address_id,
                        merchant_context.get_merchant_key_store(),
                    )
                    .await
                })
                .await
                .transpose()
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable_lazy(|| {
                    format!(
                        "Failed while fetching address [id - {:?}] for payout [id - {:?}]",
                        payout.address_id, payout.payout_id
                    )
                })?;

            let enabled_payout_methods =
                filter_payout_methods(&state, &merchant_context, &payout, address.as_ref()).await?;
            // Fetch default enabled_payout_methods
            let mut default_enabled_payout_methods: Vec<link_utils::EnabledPaymentMethod> = vec![];
            for (payment_method, payment_method_types) in
                default_config.enabled_payment_methods.clone().into_iter()
            {
                let enabled_payment_method = link_utils::EnabledPaymentMethod {
                    payment_method,
                    payment_method_types,
                };
                default_enabled_payout_methods.push(enabled_payment_method);
            }
            let fallback_enabled_payout_methods = if enabled_payout_methods.is_empty() {
                &default_enabled_payout_methods
            } else {
                &enabled_payout_methods
            };
            // Fetch enabled payout methods from the request. If not found, fetch the enabled payout methods from MCA,
            // If none are configured for merchant connector accounts, fetch them from the default enabled payout methods.
            let mut enabled_payment_methods = link_data
                .enabled_payment_methods
                .unwrap_or(fallback_enabled_payout_methods.to_vec());

            // Sort payment methods (cards first)
            enabled_payment_methods.sort_by(|a, b| match (a.payment_method, b.payment_method) {
                (_, common_enums::PaymentMethod::Card) => Ordering::Greater,
                (common_enums::PaymentMethod::Card, _) => Ordering::Less,
                _ => Ordering::Equal,
            });

            let required_field_override = api::RequiredFieldsOverrideRequest {
                billing: address
                    .as_ref()
                    .map(hyperswitch_domain_models::address::Address::from)
                    .map(From::from),
            };

            let enabled_payment_methods_with_required_fields = ForeignFrom::foreign_from((
                &state.conf.payouts.required_fields,
                enabled_payment_methods.clone(),
                required_field_override,
            ));

            let js_data = payouts::PayoutLinkDetails {
                publishable_key: masking::Secret::new(
                    merchant_context
                        .get_merchant_account()
                        .clone()
                        .publishable_key,
                ),
                client_secret: link_data.client_secret.clone(),
                payout_link_id: payout_link.link_id,
                payout_id: payout_link.primary_reference.clone(),
                customer_id: customer.customer_id,
                session_expiry: payout_link.expiry,
                return_url: payout_link
                    .return_url
                    .as_ref()
                    .map(|url| url::Url::parse(url))
                    .transpose()
                    .change_context(errors::ApiErrorResponse::InternalServerError)
                    .attach_printable("Failed to parse payout status link's return URL")?,
                ui_config: ui_config_data,
                enabled_payment_methods,
                enabled_payment_methods_with_required_fields,
                amount,
                currency: payout.destination_currency,
                locale: state.locale.clone(),
                form_layout: link_data.form_layout,
                test_mode: link_data.test_mode.unwrap_or(false),
            };

            let serialized_css_content = String::new();

            let serialized_js_content = format!(
                "window.__PAYOUT_DETAILS = {}",
                js_data
                    .encode_to_string_of_json()
                    .change_context(errors::ApiErrorResponse::InternalServerError)
                    .attach_printable("Failed to serialize PaymentMethodCollectLinkDetails")?
            );

            let generic_form_data = services::GenericLinkFormData {
                js_data: serialized_js_content,
                css_data: serialized_css_content,
                sdk_url: default_config.sdk_url.clone(),
                html_meta_tags: String::new(),
            };
            Ok(services::ApplicationResponse::GenericLinkForm(Box::new(
                GenericLinks {
                    allowed_domains,
                    data: GenericLinksData::PayoutLink(generic_form_data),
                    locale: state.locale.clone(),
                },
            )))
        }

        // Send back status page
        (_, link_utils::PayoutLinkStatus::Submitted) => {
            let translated_unified_message =
                payout_helpers::get_translated_unified_code_and_message(
                    &state,
                    payout_attempt.unified_code.as_ref(),
                    payout_attempt.unified_message.as_ref(),
                    &state.locale.clone(),
                )
                .await?;
            let js_data = payouts::PayoutLinkStatusDetails {
                payout_link_id: payout_link.link_id,
                payout_id: payout_link.primary_reference.clone(),
                customer_id: link_data.customer_id,
                session_expiry: payout_link.expiry,
                return_url: payout_link
                    .return_url
                    .as_ref()
                    .map(|url| url::Url::parse(url))
                    .transpose()
                    .change_context(errors::ApiErrorResponse::InternalServerError)
                    .attach_printable("Failed to parse payout status link's return URL")?,
                status: payout.status,
                error_code: payout_attempt.unified_code,
                error_message: translated_unified_message,
                ui_config: ui_config_data,
                test_mode: link_data.test_mode.unwrap_or(false),
            };

            let serialized_css_content = String::new();

            let serialized_js_content = format!(
                "window.__PAYOUT_DETAILS = {}",
                js_data
                    .encode_to_string_of_json()
                    .change_context(errors::ApiErrorResponse::InternalServerError)
                    .attach_printable("Failed to serialize PaymentMethodCollectLinkDetails")?
            );

            let generic_status_data = services::GenericLinkStatusData {
                js_data: serialized_js_content,
                css_data: serialized_css_content,
            };
            Ok(services::ApplicationResponse::GenericLinkForm(Box::new(
                GenericLinks {
                    allowed_domains,
                    data: GenericLinksData::PayoutLinkStatus(generic_status_data),
                    locale: state.locale.clone(),
                },
            )))
        }
    }
}

#[cfg(all(feature = "payouts", feature = "v1"))]
pub async fn filter_payout_methods(
    state: &SessionState,
    merchant_context: &domain::MerchantContext,
    payout: &hyperswitch_domain_models::payouts::payouts::Payouts,
    address: Option<&domain::Address>,
) -> errors::RouterResult<Vec<link_utils::EnabledPaymentMethod>> {
    use masking::ExposeInterface;

    let db = &*state.store;
    let key_manager_state = &state.into();
    //Fetch all merchant connector accounts
    let all_mcas = db
        .find_merchant_connector_account_by_merchant_id_and_disabled_list(
            key_manager_state,
            merchant_context.get_merchant_account().get_id(),
            false,
            merchant_context.get_merchant_key_store(),
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
    // Filter MCAs based on profile_id and connector_type
    let filtered_mcas = all_mcas.filter_based_on_profile_and_connector_type(
        &payout.profile_id,
        common_enums::ConnectorType::PayoutProcessor,
    );

    let mut response: Vec<link_utils::EnabledPaymentMethod> = vec![];
    let mut payment_method_list_hm: HashMap<
        common_enums::PaymentMethod,
        HashSet<common_enums::PaymentMethodType>,
    > = HashMap::new();
    let mut bank_transfer_hash_set: HashSet<common_enums::PaymentMethodType> = HashSet::new();
    let mut card_hash_set: HashSet<common_enums::PaymentMethodType> = HashSet::new();
    let mut wallet_hash_set: HashSet<common_enums::PaymentMethodType> = HashSet::new();
    let payout_filter_config = &state.conf.payout_method_filters.clone();
    for mca in &filtered_mcas {
        let payout_methods = match &mca.payment_methods_enabled {
            Some(pm) => pm,
            None => continue,
        };
        for payout_method in payout_methods.iter() {
            let parse_result = serde_json::from_value::<api_models::admin::PaymentMethodsEnabled>(
                payout_method.clone().expose(),
            );
            if let Ok(payment_methods_enabled) = parse_result {
                let payment_method = payment_methods_enabled.payment_method;
                let payment_method_types = match payment_methods_enabled.payment_method_types {
                    Some(payment_method_types) => payment_method_types,
                    None => continue,
                };
                let connector = mca.connector_name.clone();
                let payout_filter = payout_filter_config.0.get(&connector);
                for request_payout_method_type in &payment_method_types {
                    let currency_country_filter = check_currency_country_filters(
                        payout_filter,
                        request_payout_method_type,
                        payout.destination_currency,
                        address
                            .as_ref()
                            .and_then(|address| address.country)
                            .as_ref(),
                    )?;
                    if currency_country_filter.unwrap_or(true) {
                        match payment_method {
                            common_enums::PaymentMethod::Card => {
                                card_hash_set
                                    .insert(request_payout_method_type.payment_method_type);
                                payment_method_list_hm
                                    .insert(payment_method, card_hash_set.clone());
                            }
                            common_enums::PaymentMethod::Wallet => {
                                wallet_hash_set
                                    .insert(request_payout_method_type.payment_method_type);
                                payment_method_list_hm
                                    .insert(payment_method, wallet_hash_set.clone());
                            }
                            common_enums::PaymentMethod::BankTransfer => {
                                bank_transfer_hash_set
                                    .insert(request_payout_method_type.payment_method_type);
                                payment_method_list_hm
                                    .insert(payment_method, bank_transfer_hash_set.clone());
                            }
                            common_enums::PaymentMethod::CardRedirect
                            | common_enums::PaymentMethod::PayLater
                            | common_enums::PaymentMethod::BankRedirect
                            | common_enums::PaymentMethod::Crypto
                            | common_enums::PaymentMethod::BankDebit
                            | common_enums::PaymentMethod::Reward
                            | common_enums::PaymentMethod::RealTimePayment
                            | common_enums::PaymentMethod::MobilePayment
                            | common_enums::PaymentMethod::Upi
                            | common_enums::PaymentMethod::Voucher
                            | common_enums::PaymentMethod::OpenBanking
                            | common_enums::PaymentMethod::GiftCard => continue,
                        }
                    }
                }
            }
        }
    }
    for (payment_method, payment_method_types) in payment_method_list_hm {
        if !payment_method_types.is_empty() {
            let enabled_payment_method = link_utils::EnabledPaymentMethod {
                payment_method,
                payment_method_types,
            };
            response.push(enabled_payment_method);
        }
    }
    Ok(response)
}

pub fn check_currency_country_filters(
    payout_method_filter: Option<&PaymentMethodFilters>,
    request_payout_method_type: &api_models::payment_methods::RequestPaymentMethodTypes,
    currency: common_enums::Currency,
    country: Option<&common_enums::CountryAlpha2>,
) -> errors::RouterResult<Option<bool>> {
    if matches!(
        request_payout_method_type.payment_method_type,
        common_enums::PaymentMethodType::Credit | common_enums::PaymentMethodType::Debit
    ) {
        Ok(Some(true))
    } else {
        let payout_method_type_filter =
            payout_method_filter.and_then(|payout_method_filter: &PaymentMethodFilters| {
                payout_method_filter
                    .0
                    .get(&PaymentMethodFilterKey::PaymentMethodType(
                        request_payout_method_type.payment_method_type,
                    ))
            });
        let country_filter = country.as_ref().and_then(|country| {
            payout_method_type_filter.and_then(|currency_country_filter| {
                currency_country_filter
                    .country
                    .as_ref()
                    .map(|country_hash_set| country_hash_set.contains(country))
            })
        });
        let currency_filter = payout_method_type_filter.and_then(|currency_country_filter| {
            currency_country_filter
                .currency
                .as_ref()
                .map(|currency_hash_set| currency_hash_set.contains(&currency))
        });
        Ok(currency_filter.or(country_filter))
    }
}


pub struct GooglePayRedirectData {}


pub struct DatatransRefundRequest {
    pub amount: MinorUnit,
    pub currency: enums::Currency,
    pub refno: String,
}


pub async fn payments_intent_core<F, Res, Req, Op, D>(
    state: SessionState,
    req_state: ReqState,
    merchant_context: domain::MerchantContext,
    profile: domain::Profile,
    operation: Op,
    req: Req,
    payment_id: id_type::GlobalPaymentId,
    header_payload: HeaderPayload,
) -> RouterResponse<Res>
where
    F: Send + Clone + Sync,
    Op: Operation<F, Req, Data = D> + Send + Sync + Clone,
    Req: Debug + Clone,
    D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
    Res: transformers::ToResponse<F, D, Op>,
{
    let (payment_data, _req, customer) = payments_intent_operation_core::<_, _, _, _>(
        &state,
        req_state,
        merchant_context.clone(),
        profile,
        operation.clone(),
        req,
        payment_id,
        header_payload.clone(),
    )
    .await?;

    Res::generate_response(
        payment_data,
        customer,
        &state.base_url,
        operation,
        &state.conf.connector_request_reference_id_config,
        None,
        None,
        header_payload.x_hs_latency,
        &merchant_context,
    )
}


File: crates/router/tests/connectors/blackhawknetwork.rs

use hyperswitch_domain_models::payment_method_data::{Card, PaymentMethodData};
use masking::Secret;
use router::types::{self, api, storage::enums};
use test_utils::connector_auth;

use crate::utils::{self, ConnectorActions};

#[derive(Clone, Copy)]
struct BlackhawknetworkTest;
impl ConnectorActions for BlackhawknetworkTest {}
impl utils::Connector for BlackhawknetworkTest {
    fn get_data(&self) -> api::ConnectorData {
        use router::connector::Blackhawknetwork;
        utils::construct_connector_data_old(
            Box::new(Blackhawknetwork::new()),
            types::Connector::Plaid,
            api::GetToken::Connector,
            None,
        )
    }

    fn get_auth_token(&self) -> types::ConnectorAuthType {
        utils::to_connector_auth_type(
            connector_auth::ConnectorAuthentication::new()
                .blackhawknetwork
                .expect("Missing connector authentication configuration")
                .into(),
        )
    }

    fn get_name(&self) -> String {
        "blackhawknetwork".to_string()
    }
}

static CONNECTOR: BlackhawknetworkTest = BlackhawknetworkTest {};

fn get_default_payment_info() -> Option<utils::PaymentInfo> {
    None
}

fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
    None
}

// Cards Positive Tests
// Creates a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_only_authorize_payment() {
    let response = CONNECTOR
        .authorize_payment(payment_method_details(), get_default_payment_info())
        .await
        .expect("Authorize payment response");
    assert_eq!(response.status, enums::AttemptStatus::Authorized);
}

// Captures a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_capture_authorized_payment() {
    let response = CONNECTOR
        .authorize_and_capture_payment(payment_method_details(), None, get_default_payment_info())
        .await
        .expect("Capture payment response");
    assert_eq!(response.status, enums::AttemptStatus::Charged);
}

// Partially captures a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_partially_capture_authorized_payment() {
    let response = CONNECTOR
        .authorize_and_capture_payment(
            payment_method_details(),
            Some(types::PaymentsCaptureData {
                amount_to_capture: 50,
                ..utils::PaymentCaptureType::default().0
            }),
            get_default_payment_info(),
        )
        .await
        .expect("Capture payment response");
    assert_eq!(response.status, enums::AttemptStatus::Charged);
}

// Synchronizes a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_sync_authorized_payment() {
    let authorize_response = CONNECTOR
        .authorize_payment(payment_method_details(), get_default_payment_info())
        .await
        .expect("Authorize payment response");
    let txn_id = utils::get_connector_transaction_id(authorize_response.response);
    let response = CONNECTOR
        .psync_retry_till_status_matches(
            enums::AttemptStatus::Authorized,
            Some(types::PaymentsSyncData {
                connector_transaction_id: types::ResponseId::ConnectorTransactionId(
                    txn_id.unwrap(),
                ),
                ..Default::default()
            }),
            get_default_payment_info(),
        )
        .await
        .expect("PSync response");
    assert_eq!(response.status, enums::AttemptStatus::Authorized,);
}

// Voids a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_void_authorized_payment() {
    let response = CONNECTOR
        .authorize_and_void_payment(
            payment_method_details(),
            Some(types::PaymentsCancelData {
                connector_transaction_id: String::from(""),
                cancellation_reason: Some("requested_by_customer".to_string()),
                ..Default::default()
            }),
            get_default_payment_info(),
        )
        .await
        .expect("Void payment response");
    assert_eq!(response.status, enums::AttemptStatus::Voided);
}

// Refunds a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_refund_manually_captured_payment() {
    let response = CONNECTOR
        .capture_payment_and_refund(
            payment_method_details(),
            None,
            None,
            get_default_payment_info(),
        )
        .await
        .unwrap();
    assert_eq!(
        response.response.unwrap().refund_status,
        enums::RefundStatus::Success,
    );
}

// Partially refunds a payment using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_partially_refund_manually_captured_payment() {
    let response = CONNECTOR
        .capture_payment_and_refund(
            payment_method_details(),
            None,
            Some(types::RefundsData {
                refund_amount: 50,
                ..utils::PaymentRefundType::default().0
            }),
            get_default_payment_info(),
        )
        .await
        .unwrap();
    assert_eq!(
        response.response.unwrap().refund_status,
        enums::RefundStatus::Success,
    );
}

// Synchronizes a refund using the manual capture flow (Non 3DS).
#[actix_web::test]
async fn should_sync_manually_captured_refund() {
    let refund_response = CONNECTOR
        .capture_payment_and_refund(
            payment_method_details(),
            None,
            None,
            get_default_payment_info(),
        )
        .await
        .unwrap();
    let response = CONNECTOR
        .rsync_retry_till_status_matches(
            enums::RefundStatus::Success,
            refund_response.response.unwrap().connector_refund_id,
            None,
            get_default_payment_info(),
        )
        .await
        .unwrap();
    assert_eq!(
        response.response.unwrap().refund_status,
        enums::RefundStatus::Success,
    );
}

// Creates a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_make_payment() {
    let authorize_response = CONNECTOR
        .make_payment(payment_method_details(), get_default_payment_info())
        .await
        .unwrap();
    assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
}

// Synchronizes a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_sync_auto_captured_payment() {
    let authorize_response = CONNECTOR
        .make_payment(payment_method_details(), get_default_payment_info())
        .await
        .unwrap();
    assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
    let txn_id = utils::get_connector_transaction_id(authorize_response.response);
    assert_ne!(txn_id, None, "Empty connector transaction id");
    let response = CONNECTOR
        .psync_retry_till_status_matches(
            enums::AttemptStatus::Charged,
            Some(types::PaymentsSyncData {
                connector_transaction_id: types::ResponseId::ConnectorTransactionId(
                    txn_id.unwrap(),
                ),
                capture_method: Some(enums::CaptureMethod::Automatic),
                ..Default::default()
            }),
            get_default_payment_info(),
        )
        .await
        .unwrap();
    assert_eq!(response.status, enums::AttemptStatus::Charged,);
}

// Refunds a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_refund_auto_captured_payment() {
    let response = CONNECTOR
        .make_payment_and_refund(payment_method_details(), None, get_default_payment_info())
        .await
        .unwrap();
    assert_eq!(
        response.response.unwrap().refund_status,
        enums::RefundStatus::Success,
    );
}

// Partially refunds a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_partially_refund_succeeded_payment() {
    let refund_response = CONNECTOR
        .make_payment_and_refund(
            payment_method_details(),
            Some(types::RefundsData {
                refund_amount: 50,
                ..utils::PaymentRefundType::default().0
            }),
            get_default_payment_info(),
        )
        .await
        .unwrap();
    assert_eq!(
        refund_response.response.unwrap().refund_status,
        enums::RefundStatus::Success,
    );
}

// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_refund_succeeded_payment_multiple_times() {
    CONNECTOR
        .make_payment_and_multiple_refund(
            payment_method_details(),
            Some(types::RefundsData {
                refund_amount: 50,
                ..utils::PaymentRefundType::default().0
            }),
            get_default_payment_info(),
        )
        .await;
}

// Synchronizes a refund using the automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_sync_refund() {
    let refund_response = CONNECTOR
        .make_payment_and_refund(payment_method_details(), None, get_default_payment_info())
        .await
        .unwrap();
    let response = CONNECTOR
        .rsync_retry_till_status_matches(
            enums::RefundStatus::Success,
            refund_response.response.unwrap().connector_refund_id,
            None,
            get_default_payment_info(),
        )
        .await
        .unwrap();
    assert_eq!(
        response.response.unwrap().refund_status,
        enums::RefundStatus::Success,
    );
}

// Cards Negative scenarios
// Creates a payment with incorrect CVC.
#[actix_web::test]
async fn should_fail_payment_for_incorrect_cvc() {
    let response = CONNECTOR
        .make_payment(
            Some(types::PaymentsAuthorizeData {
                payment_method_data: PaymentMethodData::Card(Card {
                    card_cvc: Secret::new("12345".to_string()),
                    ..utils::CCardType::default().0
                }),
                ..utils::PaymentAuthorizeType::default().0
            }),
            get_default_payment_info(),
        )
        .await
        .unwrap();
    assert_eq!(
        response.response.unwrap_err().message,
        "Your card's security code is invalid.".to_string(),
    );
}

// Creates a payment with incorrect expiry month.
#[actix_web::test]
async fn should_fail_payment_for_invalid_exp_month() {
    let response = CONNECTOR
        .make_payment(
            Some(types::PaymentsAuthorizeData {
                payment_method_data: PaymentMethodData::Card(Card {
                    card_exp_month: Secret::new("20".to_string()),
                    ..utils::CCardType::default().0
                }),
                ..utils::PaymentAuthorizeType::default().0
            }),
            get_default_payment_info(),
        )
        .await
        .unwrap();
    assert_eq!(
        response.response.unwrap_err().message,
        "Your card's expiration month is invalid.".to_string(),
    );
}

// Creates a payment with incorrect expiry year.
#[actix_web::test]
async fn should_fail_payment_for_incorrect_expiry_year() {
    let response = CONNECTOR
        .make_payment(
            Some(types::PaymentsAuthorizeData {
                payment_method_data: PaymentMethodData::Card(Card {
                    card_exp_year: Secret::new("2000".to_string()),
                    ..utils::CCardType::default().0
                }),
                ..utils::PaymentAuthorizeType::default().0
            }),
            get_default_payment_info(),
        )
        .await
        .unwrap();
    assert_eq!(
        response.response.unwrap_err().message,
        "Your card's expiration year is invalid.".to_string(),
    );
}

// Voids a payment using automatic capture flow (Non 3DS).
#[actix_web::test]
async fn should_fail_void_payment_for_auto_capture() {
    let authorize_response = CONNECTOR
        .make_payment(payment_method_details(), get_default_payment_info())
        .await
        .unwrap();
    assert_eq!(authorize_response.status, enums::AttemptStatus::Charged);
    let txn_id = utils::get_connector_transaction_id(authorize_response.response);
    assert_ne!(txn_id, None, "Empty connector transaction id");
    let void_response = CONNECTOR
        .void_payment(txn_id.unwrap(), None, get_default_payment_info())
        .await
        .unwrap();
    assert_eq!(
        void_response.response.unwrap_err().message,
        "You cannot cancel this PaymentIntent because it has a status of succeeded."
    );
}

// Captures a payment using invalid connector payment id.
#[actix_web::test]
async fn should_fail_capture_for_invalid_payment() {
    let capture_response = CONNECTOR
        .capture_payment("123456789".to_string(), None, get_default_payment_info())
        .await
        .unwrap();
    assert_eq!(
        capture_response.response.unwrap_err().message,
        String::from("No such payment_intent: '123456789'")
    );
}

// Refunds a payment with refund amount higher than payment amount.
#[actix_web::test]
async fn should_fail_for_refund_amount_higher_than_payment_amount() {
    let response = CONNECTOR
        .make_payment_and_refund(
            payment_method_details(),
            Some(types::RefundsData {
                refund_amount: 150,
                ..utils::PaymentRefundType::default().0
            }),
            get_default_payment_info(),
        )
        .await
        .unwrap();
    assert_eq!(
        response.response.unwrap_err().message,
        "Refund amount (₹1.50) is greater than charge amount (₹1.00)",
    );
}

// Connector dependent test cases goes here

// [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests


impl super::KafkaMessage for KafkaPaymentIntent<'_> {
    fn key(&self) -> String {
        format!(
            "{}_{}",
            self.merchant_id.get_string_repr(),
            self.get_id().get_string_repr(),
        )
    }

    fn event_type(&self) -> crate::events::EventType {
        crate::events::EventType::PaymentIntent
    }
}

impl super::KafkaMessage for for KafkaPaymentIntent


pub async fn payouts_fulfill(
    state: web::Data<AppState>,
    req: HttpRequest,
    path: web::Path<id_type::PayoutId>,
) -> HttpResponse {
    let flow = Flow::PayoutsFulfill;
    let payload = payout_types::PayoutActionRequest {
        payout_id: path.into_inner(),
    };

    Box::pin(api::server_wrap(
        flow,
        state,
        &req,
        payload,
        |state, auth: auth::AuthenticationData, req, _| {
            let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
                domain::Context(auth.merchant_account, auth.key_store),
            ));
            payouts_fulfill_core(state, merchant_context, req)
        },
        &auth::HeaderAuth(auth::ApiKeyAuth {
            is_connected_allowed: false,
            is_platform_allowed: false,
        }),
        api_locking::LockAction::NotApplicable,
    ))
    .await
}


impl<V: ValueNode> Default for Memoization<V> {
    #[inline]
    fn default() -> Self {
        Self::new()
    }
}


impl api::Refund for Flexiti {}


impl Default for EventsHandler {
    fn default() -> Self {
        Self::Logs(event_logger::EventLogger {})
    }
}


pub fn get_optional_card_and_cvc(
        &self,
    ) -> (Option<domain::CardDetail>, Option<masking::Secret<String>>) {
        (self.card.clone(), self.card_cvc.clone())
    }


pub struct ProphetpayCardTokenData {
    card_token: Secret<String>,
}


impl api::PaymentAuthorize for Bankofamerica {}

impl api::PaymentAuthorize for for Bankofamerica


pub struct PayloadErrorResponse {
    pub error_type: String,
    pub error_description: String,
    pub object: String,
    /// Payload returns arbitrary details in JSON format
    pub details: Option<serde_json::Value>,
}


File: crates/analytics/src/payments/metrics/sessionized_metrics/connector_success_rate.rs

use std::collections::HashSet;

use api_models::analytics::{
    payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier},
    Granularity, TimeRange,
};
use common_utils::errors::ReportSwitchExt;
use error_stack::ResultExt;
use time::PrimitiveDateTime;

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

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

#[async_trait::async_trait]
impl<T> super::PaymentMetric<T> for ConnectorSuccessRate
where
    T: AnalyticsDataSource + super::PaymentMetricAnalytics,
    PrimitiveDateTime: ToSql<T>,
    AnalyticsCollection: ToSql<T>,
    Granularity: GroupByClause<T>,
    Aggregate<&'static str>: ToSql<T>,
    Window<&'static str>: ToSql<T>,
{
    async fn load_metrics(
        &self,
        dimensions: &[PaymentDimensions],
        auth: &AuthInfo,
        filters: &PaymentFilters,
        granularity: Option<Granularity>,
        time_range: &TimeRange,
        pool: &T,
    ) -> MetricsResult<HashSet<(PaymentMetricsBucketIdentifier, PaymentMetricRow)>> {
        let mut query_builder: QueryBuilder<T> =
            QueryBuilder::new(AnalyticsCollection::PaymentSessionized);
        let mut dimensions = dimensions.to_vec();

        dimensions.push(PaymentDimensions::PaymentStatus);

        for dim in dimensions.iter() {
            query_builder.add_select_column(dim).switch()?;
        }

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

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

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

        query_builder
            .add_custom_filter_clause(PaymentDimensions::Connector, "NULL", FilterTypes::IsNotNull)
            .switch()?;
        time_range
            .set_filter_clause(&mut query_builder)
            .attach_printable("Error filtering time range")
            .switch()?;

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

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

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


OpenAPI Block Path: servers
[
  {
    "url": "https://sandbox.hyperswitch.io",
    "description": "Sandbox Environment"
  }
]


pub struct PostAuthenticationDetails {
    pub eci: Option<String>,
    pub token_details: Option<TokenDetails>,
    pub dynamic_data_details: Option<DynamicData>,
    pub trans_status: Option<common_enums::TransactionStatus>,
    pub challenge_cancel: Option<String>,
    pub challenge_code_reason: Option<String>,
}


impl api::MandateSetup for Mifinity {}


pub struct ThemeUpdateInternal {
    pub email_primary_color: Option<String>,
    pub email_foreground_color: Option<String>,
    pub email_background_color: Option<String>,
    pub email_entity_name: Option<String>,
    pub email_entity_logo_url: Option<String>,
}


pub struct PaypalRedirectionStruct {
    experience_context: ContextStruct,
    attributes: Option<Attributes>,
}


impl<'a> NetworkTokenizationBuilder<'a, PmTokenStored> {
    pub fn set_payment_method(
        self,
        payment_method: &'a domain::PaymentMethod,
    ) -> NetworkTokenizationBuilder<'a, PmTokenUpdated> {
        let payment_method_response = api::PaymentMethodResponse {
            merchant_id: payment_method.merchant_id.clone(),
            customer_id: Some(payment_method.customer_id.clone()),
            payment_method_id: payment_method.payment_method_id.clone(),
            payment_method: payment_method.payment_method,
            payment_method_type: payment_method.payment_method_type,
            recurring_enabled: Some(true),
            installment_payment_enabled: Some(false),
            metadata: payment_method.metadata.clone(),
            created: Some(payment_method.created_at),
            last_used_at: Some(payment_method.last_used_at),
            client_secret: payment_method.client_secret.clone(),
            card: None,
            bank_transfer: None,
            payment_experience: None,
        };
        NetworkTokenizationBuilder {
            state: std::marker::PhantomData,
            payment_method_response: Some(payment_method_response),
            customer: self.customer,
            card: self.card,
            card_cvc: self.card_cvc,
            stored_token: self.stored_token,
            network_token: self.network_token,
            stored_card: self.stored_card,
            card_tokenized: self.card_tokenized,
            error_code: self.error_code,
            error_message: self.error_message,
        }
    }
}


pub struct CustomerDetails {
    pub name: Secret<String>,
    pub email: Option<Secret<String>>,
    pub phone_number: Option<Secret<String>>,
    pub customer_id: String,
    #[serde(rename = "type")]
    pub customer_type: Option<String>,
    pub billing_address: Address,
    pub shipping_address: Address,
    pub wallet_account_id: Secret<String>,
    pub email_hash: Secret<String>,
    pub country_code: String,
    pub national_identifier: String,
}


pub struct CalContractScoreEventResponse {
    pub labels_with_score: Vec<ScoreDataEventResponse>,
}


File: crates/diesel_models/src/query/merchant_account.rs
 Public functions: 19

#[cfg(feature = "v1")]
use common_types::consts::API_VERSION;
#[cfg(feature = "v1")]
use diesel::BoolExpressionMethods;
use diesel::{associations::HasTable, ExpressionMethods, Table};

use super::generics;
#[cfg(feature = "v1")]
use crate::schema::merchant_account::dsl;
#[cfg(feature = "v2")]
use crate::schema_v2::merchant_account::dsl;
use crate::{
    errors,
    merchant_account::{MerchantAccount, MerchantAccountNew, MerchantAccountUpdateInternal},
    PgPooledConn, StorageResult,
};

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

#[cfg(feature = "v1")]
impl MerchantAccount {
    pub async fn update(
        self,
        conn: &PgPooledConn,
        merchant_account: MerchantAccountUpdateInternal,
    ) -> StorageResult<Self> {
        match generics::generic_update_by_id::<<Self as HasTable>::Table, _, _, _>(
            conn,
            self.get_id().to_owned(),
            merchant_account,
        )
        .await
        {
            Err(error) => match error.current_context() {
                errors::DatabaseError::NoFieldsToUpdate => Ok(self),
                _ => Err(error),
            },
            result => result,
        }
    }

    pub async fn update_with_specific_fields(
        conn: &PgPooledConn,
        identifier: &common_utils::id_type::MerchantId,
        merchant_account: MerchantAccountUpdateInternal,
    ) -> StorageResult<Self> {
        generics::generic_update_with_unique_predicate_get_result::<
            <Self as HasTable>::Table,
            _,
            _,
            _,
        >(
            conn,
            dsl::merchant_id.eq(identifier.to_owned()),
            merchant_account,
        )
        .await
    }

    pub async fn delete_by_merchant_id(
        conn: &PgPooledConn,
        identifier: &common_utils::id_type::MerchantId,
    ) -> StorageResult<bool> {
        generics::generic_delete::<<Self as HasTable>::Table, _>(
            conn,
            dsl::merchant_id.eq(identifier.to_owned()),
        )
        .await
    }

    pub async fn find_by_merchant_id(
        conn: &PgPooledConn,
        identifier: &common_utils::id_type::MerchantId,
    ) -> StorageResult<Self> {
        generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
            conn,
            dsl::merchant_id.eq(identifier.to_owned()),
        )
        .await
    }

    pub async fn find_by_publishable_key(
        conn: &PgPooledConn,
        publishable_key: &str,
    ) -> StorageResult<Self> {
        generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
            conn,
            dsl::publishable_key.eq(publishable_key.to_owned()),
        )
        .await
    }

    pub async fn list_by_organization_id(
        conn: &PgPooledConn,
        organization_id: &common_utils::id_type::OrganizationId,
    ) -> StorageResult<Vec<Self>> {
        generics::generic_filter::<
            <Self as HasTable>::Table,
            _,
            <<Self as HasTable>::Table as Table>::PrimaryKey,
            _,
        >(
            conn,
            dsl::organization_id
                .eq(organization_id.to_owned())
                .and(dsl::version.eq(API_VERSION)),
            None,
            None,
            None,
        )
        .await
    }

    pub async fn list_multiple_merchant_accounts(
        conn: &PgPooledConn,
        merchant_ids: Vec<common_utils::id_type::MerchantId>,
    ) -> StorageResult<Vec<Self>> {
        generics::generic_filter::<
            <Self as HasTable>::Table,
            _,
            <<Self as HasTable>::Table as Table>::PrimaryKey,
            _,
        >(
            conn,
            dsl::merchant_id.eq_any(merchant_ids.clone()),
            None,
            None,
            None,
        )
        .await
    }

    pub async fn list_all_merchant_accounts(
        conn: &PgPooledConn,
        limit: u32,
        offset: Option<u32>,
    ) -> StorageResult<Vec<Self>> {
        generics::generic_filter::<
            <Self as HasTable>::Table,
            _,
            <<Self as HasTable>::Table as Table>::PrimaryKey,
            _,
        >(
            conn,
            dsl::merchant_id.ne_all(vec![""]),
            Some(i64::from(limit)),
            offset.map(i64::from),
            None,
        )
        .await
    }

    pub async fn update_all_merchant_accounts(
        conn: &PgPooledConn,
        merchant_account: MerchantAccountUpdateInternal,
    ) -> StorageResult<Vec<Self>> {
        generics::generic_update_with_results::<<Self as HasTable>::Table, _, _, _>(
            conn,
            dsl::merchant_id.ne_all(vec![""]),
            merchant_account,
        )
        .await
    }
}

#[cfg(feature = "v2")]
impl MerchantAccount {
    pub async fn update(
        self,
        conn: &PgPooledConn,
        merchant_account: MerchantAccountUpdateInternal,
    ) -> StorageResult<Self> {
        match generics::generic_update_by_id::<<Self as HasTable>::Table, _, _, _>(
            conn,
            self.get_id().to_owned(),
            merchant_account,
        )
        .await
        {
            Err(error) => match error.current_context() {
                errors::DatabaseError::NoFieldsToUpdate => Ok(self),
                _ => Err(error),
            },
            result => result,
        }
    }

    pub async fn update_with_specific_fields(
        conn: &PgPooledConn,
        identifier: &common_utils::id_type::MerchantId,
        merchant_account: MerchantAccountUpdateInternal,
    ) -> StorageResult<Self> {
        generics::generic_update_with_unique_predicate_get_result::<
            <Self as HasTable>::Table,
            _,
            _,
            _,
        >(conn, dsl::id.eq(identifier.to_owned()), merchant_account)
        .await
    }

    pub async fn delete_by_merchant_id(
        conn: &PgPooledConn,
        identifier: &common_utils::id_type::MerchantId,
    ) -> StorageResult<bool> {
        generics::generic_delete::<<Self as HasTable>::Table, _>(
            conn,
            dsl::id.eq(identifier.to_owned()),
        )
        .await
    }

    pub async fn find_by_merchant_id(
        conn: &PgPooledConn,
        identifier: &common_utils::id_type::MerchantId,
    ) -> StorageResult<Self> {
        generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
            conn,
            dsl::id.eq(identifier.to_owned()),
        )
        .await
    }

    pub async fn find_by_publishable_key(
        conn: &PgPooledConn,
        publishable_key: &str,
    ) -> StorageResult<Self> {
        generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
            conn,
            dsl::publishable_key.eq(publishable_key.to_owned()),
        )
        .await
    }

    pub async fn list_by_organization_id(
        conn: &PgPooledConn,
        organization_id: &common_utils::id_type::OrganizationId,
    ) -> StorageResult<Vec<Self>> {
        generics::generic_filter::<
            <Self as HasTable>::Table,
            _,
            <<Self as HasTable>::Table as Table>::PrimaryKey,
            _,
        >(
            conn,
            dsl::organization_id.eq(organization_id.to_owned()),
            None,
            None,
            None,
        )
        .await
    }

    pub async fn list_multiple_merchant_accounts(
        conn: &PgPooledConn,
        merchant_ids: Vec<common_utils::id_type::MerchantId>,
    ) -> StorageResult<Vec<Self>> {
        generics::generic_filter::<
            <Self as HasTable>::Table,
            _,
            <<Self as HasTable>::Table as Table>::PrimaryKey,
            _,
        >(conn, dsl::id.eq_any(merchant_ids), None, None, None)
        .await
    }

    pub async fn list_all_merchant_accounts(
        conn: &PgPooledConn,
        limit: u32,
        offset: Option<u32>,
    ) -> StorageResult<Vec<Self>> {
        generics::generic_filter::<
            <Self as HasTable>::Table,
            _,
            <<Self as HasTable>::Table as Table>::PrimaryKey,
            _,
        >(
            conn,
            dsl::id.ne_all(vec![""]),
            Some(i64::from(limit)),
            offset.map(i64::from),
            None,
        )
        .await
    }

    pub async fn update_all_merchant_accounts(
        conn: &PgPooledConn,
        merchant_account: MerchantAccountUpdateInternal,
    ) -> StorageResult<Vec<Self>> {
        generics::generic_update_with_results::<<Self as HasTable>::Table, _, _, _>(
            conn,
            dsl::id.ne_all(vec![""]),
            merchant_account,
        )
        .await
    }
}


File: crates/router/src/core/payouts.rs
 Public functions: 41
 Public structs: 1

pub mod access_token;
pub mod helpers;
#[cfg(feature = "payout_retry")]
pub mod retry;
pub mod transformers;
pub mod validator;
use std::{
    collections::{HashMap, HashSet},
    vec::IntoIter,
};

#[cfg(feature = "olap")]
use api_models::payments as payment_enums;
use api_models::{self, enums as api_enums, payouts::PayoutLinkResponse};
#[cfg(feature = "payout_retry")]
use common_enums::PayoutRetryType;
use common_utils::{
    consts,
    ext_traits::{AsyncExt, ValueExt},
    id_type::{self, GenerateId},
    link_utils::{GenericLinkStatus, GenericLinkUiConfig, PayoutLinkData, PayoutLinkStatus},
    types::{MinorUnit, UnifiedCode, UnifiedMessage},
};
use diesel_models::{
    enums as storage_enums,
    generic_link::{GenericLinkNew, PayoutLink},
    CommonMandateReference, PayoutsMandateReference, PayoutsMandateReferenceRecord,
};
use error_stack::{report, ResultExt};
#[cfg(feature = "olap")]
use futures::future::join_all;
use hyperswitch_domain_models::{self as domain_models, payment_methods::PaymentMethod};
use masking::{PeekInterface, Secret};
#[cfg(feature = "payout_retry")]
use retry::GsmValidation;
use router_env::{instrument, logger, tracing, Env};
use scheduler::utils as pt_utils;
use serde_json;
use time::Duration;

#[cfg(feature = "olap")]
use crate::types::domain::behaviour::Conversion;
#[cfg(feature = "olap")]
use crate::types::PayoutActionData;
use crate::{
    core::{
        errors::{
            self, ConnectorErrorExt, CustomResult, RouterResponse, RouterResult, StorageErrorExt,
        },
        payments::{self, customers, helpers as payment_helpers},
        utils as core_utils,
    },
    db::StorageInterface,
    routes::SessionState,
    services,
    types::{
        self,
        api::{self, payments as payment_api_types, payouts},
        domain,
        storage::{self, PaymentRoutingInfo},
        transformers::{ForeignFrom, ForeignTryFrom},
    },
    utils::{self, OptionExt},
};

// ********************************************** TYPES **********************************************
#[derive(Clone)]
pub struct PayoutData {
    pub billing_address: Option<domain_models::address::Address>,
    pub business_profile: domain::Profile,
    pub customer_details: Option<domain::Customer>,
    pub merchant_connector_account: Option<payment_helpers::MerchantConnectorAccountType>,
    pub payouts: storage::Payouts,
    pub payout_attempt: storage::PayoutAttempt,
    pub payout_method_data: Option<payouts::PayoutMethodData>,
    pub profile_id: id_type::ProfileId,
    pub should_terminate: bool,
    pub payout_link: Option<PayoutLink>,
    pub current_locale: String,
    pub payment_method: Option<PaymentMethod>,
    pub connector_transfer_method_id: Option<String>,
}

// ********************************************** CORE FLOWS **********************************************
pub fn get_next_connector(
    connectors: &mut IntoIter<api::ConnectorRoutingData>,
) -> RouterResult<api::ConnectorRoutingData> {
    connectors
        .next()
        .ok_or(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Connector not found in connectors iterator")
}

#[cfg(all(feature = "payouts", feature = "v1"))]
pub async fn get_connector_choice(
    state: &SessionState,
    merchant_context: &domain::MerchantContext,
    connector: Option<String>,
    routing_algorithm: Option<serde_json::Value>,
    payout_data: &mut PayoutData,
    eligible_connectors: Option<Vec<api_enums::PayoutConnectors>>,
) -> RouterResult<api::ConnectorCallType> {
    let eligible_routable_connectors = eligible_connectors.map(|connectors| {
        connectors
            .into_iter()
            .map(api::enums::RoutableConnectors::from)
            .collect()
    });
    let connector_choice = helpers::get_default_payout_connector(state, routing_algorithm).await?;
    match connector_choice {
        api::ConnectorChoice::SessionMultiple(_) => {
            Err(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Invalid connector choice - SessionMultiple")?
        }

        api::ConnectorChoice::StraightThrough(straight_through) => {
            let request_straight_through: api::routing::StraightThroughAlgorithm = straight_through
                .clone()
                .parse_value("StraightThroughAlgorithm")
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Invalid straight through routing rules format")?;
            payout_data.payout_attempt.routing_info = Some(straight_through);
            let mut routing_data = storage::RoutingData {
                routed_through: connector,
                merchant_connector_id: None,
                algorithm: Some(request_straight_through.clone()),
                routing_info: PaymentRoutingInfo {
                    algorithm: None,
                    pre_routing_results: None,
                },
            };
            helpers::decide_payout_connector(
                state,
                merchant_context,
                Some(request_straight_through),
                &mut routing_data,
                payout_data,
                eligible_routable_connectors,
            )
            .await
        }

        api::ConnectorChoice::Decide => {
            let mut routing_data = storage::RoutingData {
                routed_through: connector,
                merchant_connector_id: None,
                algorithm: None,
                routing_info: PaymentRoutingInfo {
                    algorithm: None,
                    pre_routing_results: None,
                },
            };
            helpers::decide_payout_connector(
                state,
                merchant_context,
                None,
                &mut routing_data,
                payout_data,
                eligible_routable_connectors,
            )
            .await
        }
    }
}

#[instrument(skip_all)]
pub async fn make_connector_decision(
    state: &SessionState,
    merchant_context: &domain::MerchantContext,
    connector_call_type: api::ConnectorCallType,
    payout_data: &mut PayoutData,
) -> RouterResult<()> {
    match connector_call_type {
        api::ConnectorCallType::PreDetermined(routing_data) => {
            Box::pin(call_connector_payout(
                state,
                merchant_context,
                &routing_data.connector_data,
                payout_data,
            ))
            .await?;

            #[cfg(feature = "payout_retry")]
            {
                let config_bool = retry::config_should_call_gsm_payout(
                    &*state.store,
                    merchant_context.get_merchant_account().get_id(),
                    PayoutRetryType::SingleConnector,
                )
                .await;

                if config_bool && payout_data.should_call_gsm() {
                    Box::pin(retry::do_gsm_single_connector_actions(
                        state,
                        routing_data.connector_data,
                        payout_data,
                        merchant_context,
                    ))
                    .await?;
                }
            }

            Ok(())
        }
        api::ConnectorCallType::Retryable(routing_data) => {
            let mut routing_data = routing_data.into_iter();

            let connector_data = get_next_connector(&mut routing_data)?.connector_data;

            Box::pin(call_connector_payout(
                state,
                merchant_context,
                &connector_data,
                payout_data,
            ))
            .await?;

            #[cfg(feature = "payout_retry")]
            {
                let config_multiple_connector_bool = retry::config_should_call_gsm_payout(
                    &*state.store,
                    merchant_context.get_merchant_account().get_id(),
                    PayoutRetryType::MultiConnector,
                )
                .await;

                if config_multiple_connector_bool && payout_data.should_call_gsm() {
                    Box::pin(retry::do_gsm_multiple_connector_actions(
                        state,
                        routing_data,
                        connector_data.clone(),
                        payout_data,
                        merchant_context,
                    ))
                    .await?;
                }

                let config_single_connector_bool = retry::config_should_call_gsm_payout(
                    &*state.store,
                    merchant_context.get_merchant_account().get_id(),
                    PayoutRetryType::SingleConnector,
                )
                .await;

                if config_single_connector_bool && payout_data.should_call_gsm() {
                    Box::pin(retry::do_gsm_single_connector_actions(
                        state,
                        connector_data,
                        payout_data,
                        merchant_context,
                    ))
                    .await?;
                }
            }

            Ok(())
        }
        _ => Err(errors::ApiErrorResponse::InternalServerError).attach_printable({
            "only PreDetermined and Retryable ConnectorCallTypes are supported".to_string()
        })?,
    }
}

#[cfg(feature = "v1")]
#[instrument(skip_all)]
pub async fn payouts_core(
    state: &SessionState,
    merchant_context: &domain::MerchantContext,
    payout_data: &mut PayoutData,
    routing_algorithm: Option<serde_json::Value>,
    eligible_connectors: Option<Vec<api_enums::PayoutConnectors>>,
) -> RouterResult<()> {
    let payout_attempt = &payout_data.payout_attempt;

    // Form connector data
    let connector_call_type = get_connector_choice(
        state,
        merchant_context,
        payout_attempt.connector.clone(),
        routing_algorithm,
        payout_data,
        eligible_connectors,
    )
    .await?;

    // Call connector steps
    Box::pin(make_connector_decision(
        state,
        merchant_context,
        connector_call_type,
        payout_data,
    ))
    .await
}

#[cfg(feature = "v2")]
#[instrument(skip_all)]
pub async fn payouts_core(
    state: &SessionState,
    merchant_context: &domain::MerchantContext,
    payout_data: &mut PayoutData,
    routing_algorithm: Option<serde_json::Value>,
    eligible_connectors: Option<Vec<api_enums::PayoutConnectors>>,
) -> RouterResult<()> {
    todo!()
}

#[instrument(skip_all)]
pub async fn payouts_create_core(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    req: payouts::PayoutCreateRequest,
) -> RouterResponse<payouts::PayoutCreateResponse> {
    // Validate create request
    let (payout_id, payout_method_data, profile_id, customer, payment_method) =
        validator::validate_create_request(&state, &merchant_context, &req).await?;

    // Create DB entries
    let mut payout_data = payout_create_db_entries(
        &state,
        &merchant_context,
        &req,
        &payout_id,
        &profile_id,
        payout_method_data.as_ref(),
        &state.locale,
        customer.as_ref(),
        payment_method.clone(),
    )
    .await?;

    let payout_attempt = payout_data.payout_attempt.to_owned();
    let payout_type = payout_data.payouts.payout_type.to_owned();

    // Persist payout method data in temp locker
    if req.payout_method_data.is_some() {
        let customer_id = payout_data
            .payouts
            .customer_id
            .clone()
            .get_required_value("customer_id when payout_method_data is provided")?;
        payout_data.payout_method_data = helpers::make_payout_method_data(
            &state,
            req.payout_method_data.as_ref(),
            payout_attempt.payout_token.as_deref(),
            &customer_id,
            &payout_attempt.merchant_id,
            payout_type,
            merchant_context.get_merchant_key_store(),
            Some(&mut payout_data),
            merchant_context.get_merchant_account().storage_scheme,
        )
        .await?;
    }

    if let Some(true) = payout_data.payouts.confirm {
        payouts_core(
            &state,
            &merchant_context,
            &mut payout_data,
            req.routing.clone(),
            req.connector.clone(),
        )
        .await?
    };

    trigger_webhook_and_handle_response(&state, &merchant_context, &payout_data).await
}

#[instrument(skip_all)]
pub async fn payouts_confirm_core(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    req: payouts::PayoutCreateRequest,
) -> RouterResponse<payouts::PayoutCreateResponse> {
    let mut payout_data = Box::pin(make_payout_data(
        &state,
        &merchant_context,
        None,
        &payouts::PayoutRequest::PayoutCreateRequest(Box::new(req.to_owned())),
        &state.locale,
    ))
    .await?;
    let payout_attempt = payout_data.payout_attempt.to_owned();
    let status = payout_attempt.status;

    helpers::validate_payout_status_against_not_allowed_statuses(
        status,
        &[
            storage_enums::PayoutStatus::Cancelled,
            storage_enums::PayoutStatus::Success,
            storage_enums::PayoutStatus::Failed,
            storage_enums::PayoutStatus::Pending,
            storage_enums::PayoutStatus::Ineligible,
            storage_enums::PayoutStatus::RequiresFulfillment,
            storage_enums::PayoutStatus::RequiresVendorAccountCreation,
        ],
        "confirm",
    )?;

    helpers::update_payouts_and_payout_attempt(&mut payout_data, &merchant_context, &req, &state)
        .await?;

    let db = &*state.store;

    payout_data.payout_link = payout_data
        .payout_link
        .clone()
        .async_map(|pl| async move {
            let payout_link_update = storage::PayoutLinkUpdate::StatusUpdate {
                link_status: PayoutLinkStatus::Submitted,
            };
            db.update_payout_link(pl, payout_link_update)
                .await
                .change_context(errors::ApiErrorResponse::InternalServerError)
                .attach_printable("Error updating payout links in db")
        })
        .await
        .transpose()?;

    payouts_core(
        &state,
        &merchant_context,
        &mut payout_data,
        req.routing.clone(),
        req.connector.clone(),
    )
    .await?;

    trigger_webhook_and_handle_response(&state, &merchant_context, &payout_data).await
}

pub async fn payouts_update_core(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    req: payouts::PayoutCreateRequest,
) -> RouterResponse<payouts::PayoutCreateResponse> {
    let payout_id = req.payout_id.clone().get_required_value("payout_id")?;
    let mut payout_data = Box::pin(make_payout_data(
        &state,
        &merchant_context,
        None,
        &payouts::PayoutRequest::PayoutCreateRequest(Box::new(req.to_owned())),
        &state.locale,
    ))
    .await?;

    let payout_attempt = payout_data.payout_attempt.to_owned();
    let status = payout_attempt.status;

    // Verify update feasibility
    if helpers::is_payout_terminal_state(status) || helpers::is_payout_initiated(status) {
        return Err(report!(errors::ApiErrorResponse::InvalidRequestData {
            message: format!(
                "Payout {} cannot be updated for status {status}",
                payout_id.get_string_repr()
            ),
        }));
    }
    helpers::update_payouts_and_payout_attempt(&mut payout_data, &merchant_context, &req, &state)
        .await?;
    let payout_attempt = payout_data.payout_attempt.to_owned();

    if (req.connector.is_none(), payout_attempt.connector.is_some()) != (true, true) {
        // if the connector is not updated but was provided during payout create
        payout_data.payout_attempt.connector = None;
        payout_data.payout_attempt.routing_info = None;
    };

    // Update payout method data in temp locker
    if req.payout_method_data.is_some() {
        let customer_id = payout_data
            .payouts
            .customer_id
            .clone()
            .get_required_value("customer_id when payout_method_data is provided")?;
        payout_data.payout_method_data = helpers::make_payout_method_data(
            &state,
            req.payout_method_data.as_ref(),
            payout_attempt.payout_token.as_deref(),
            &customer_id,
            &payout_attempt.merchant_id,
            payout_data.payouts.payout_type,
            merchant_context.get_merchant_key_store(),
            Some(&mut payout_data),
            merchant_context.get_merchant_account().storage_scheme,
        )
        .await?;
    }

    if let Some(true) = payout_data.payouts.confirm {
        payouts_core(
            &state,
            &merchant_context,
            &mut payout_data,
            req.routing.clone(),
            req.connector.clone(),
        )
        .await?;
    }

    trigger_webhook_and_handle_response(&state, &merchant_context, &payout_data).await
}

#[cfg(all(feature = "payouts", feature = "v1"))]
#[instrument(skip_all)]
pub async fn payouts_retrieve_core(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    profile_id: Option<id_type::ProfileId>,
    req: payouts::PayoutRetrieveRequest,
) -> RouterResponse<payouts::PayoutCreateResponse> {
    let mut payout_data = Box::pin(make_payout_data(
        &state,
        &merchant_context,
        profile_id,
        &payouts::PayoutRequest::PayoutRetrieveRequest(req.to_owned()),
        &state.locale,
    ))
    .await?;
    let payout_attempt = payout_data.payout_attempt.to_owned();
    let status = payout_attempt.status;

    if matches!(req.force_sync, Some(true)) && helpers::should_call_retrieve(status) {
        // Form connector data
        let connector_call_type = get_connector_choice(
            &state,
            &merchant_context,
            payout_attempt.connector.clone(),
            None,
            &mut payout_data,
            None,
        )
        .await?;

        complete_payout_retrieve(
            &state,
            &merchant_context,
            connector_call_type,
            &mut payout_data,
        )
        .await?;
    }

    Ok(services::ApplicationResponse::Json(
        response_handler(&state, &merchant_context, &payout_data).await?,
    ))
}

#[instrument(skip_all)]
pub async fn payouts_cancel_core(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    req: payouts::PayoutActionRequest,
) -> RouterResponse<payouts::PayoutCreateResponse> {
    let mut payout_data = Box::pin(make_payout_data(
        &state,
        &merchant_context,
        None,
        &payouts::PayoutRequest::PayoutActionRequest(req.to_owned()),
        &state.locale,
    ))
    .await?;

    let payout_attempt = payout_data.payout_attempt.to_owned();
    let status = payout_attempt.status;

    // Verify if cancellation can be triggered
    if helpers::is_payout_terminal_state(status) {
        return Err(report!(errors::ApiErrorResponse::InvalidRequestData {
            message: format!(
                "Payout {:?} cannot be cancelled for status {}",
                payout_attempt.payout_id, status
            ),
        }));

    // Make local cancellation
    } else if helpers::is_eligible_for_local_payout_cancellation(status) {
        let status = storage_enums::PayoutStatus::Cancelled;
        let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate {
            connector_payout_id: payout_attempt.connector_payout_id.to_owned(),
            status,
            error_message: Some("Cancelled by user".to_string()),
            error_code: None,
            is_eligible: None,
            unified_code: None,
            unified_message: None,
        };
        payout_data.payout_attempt = state
            .store
            .update_payout_attempt(
                &payout_attempt,
                updated_payout_attempt,
                &payout_data.payouts,
                merchant_context.get_merchant_account().storage_scheme,
            )
            .await
            .change_context(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("Error updating payout_attempt in db")?;
        payout_data.payouts = state
            .store
            .update_payout(
                &payout_data.payouts,
                storage::PayoutsUpdate::StatusUpdate { status },
                &payout_data.payout_attempt,
                merchant_context.get_merchant_account().storage_scheme,
            )
            .await
            .change_context(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("Error updating payouts in db")?;

    // Trigger connector's cancellation
    } else {
        // Form connector data
        let connector_data = match &payout_attempt.connector {
            Some(connector) => api::ConnectorData::get_payout_connector_by_name(
                &state.conf.connectors,
                connector,
                api::GetToken::Connector,
                payout_attempt.merchant_connector_id.clone(),
            )
            .change_context(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("Failed to get the connector data")?,
            _ => Err(errors::ApplicationError::InvalidConfigurationValueError(
                "Connector not found in payout_attempt - should not reach here".to_string(),
            ))
            .change_context(errors::ApiErrorResponse::MissingRequiredField {
                field_name: "connector",
            })
            .attach_printable("Connector not found for payout cancellation")?,
        };

        Box::pin(cancel_payout(
            &state,
            &merchant_context,
            &connector_data,
            &mut payout_data,
        ))
        .await
        .attach_printable("Payout cancellation failed for given Payout request")?;
    }

    Ok(services::ApplicationResponse::Json(
        response_handler(&state, &merchant_context, &payout_data).await?,
    ))
}

#[instrument(skip_all)]
pub async fn payouts_fulfill_core(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    req: payouts::PayoutActionRequest,
) -> RouterResponse<payouts::PayoutCreateResponse> {
    let mut payout_data = Box::pin(make_payout_data(
        &state,
        &merchant_context,
        None,
        &payouts::PayoutRequest::PayoutActionRequest(req.to_owned()),
        &state.locale,
    ))
    .await?;

    let payout_attempt = payout_data.payout_attempt.to_owned();
    let status = payout_attempt.status;

    // Verify if fulfillment can be triggered
    if helpers::is_payout_terminal_state(status)
        || status != api_enums::PayoutStatus::RequiresFulfillment
    {
        return Err(report!(errors::ApiErrorResponse::InvalidRequestData {
            message: format!(
                "Payout {:?} cannot be fulfilled for status {}",
                payout_attempt.payout_id, status
            ),
        }));
    }

    // Form connector data
    let connector_data = match &payout_attempt.connector {
        Some(connector) => api::ConnectorData::get_payout_connector_by_name(
            &state.conf.connectors,
            connector,
            api::GetToken::Connector,
            payout_attempt.merchant_connector_id.clone(),
        )
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Failed to get the connector data")?,
        _ => Err(errors::ApplicationError::InvalidConfigurationValueError(
            "Connector not found in payout_attempt - should not reach here.".to_string(),
        ))
        .change_context(errors::ApiErrorResponse::MissingRequiredField {
            field_name: "connector",
        })
        .attach_printable("Connector not found for payout fulfillment")?,
    };

    helpers::fetch_payout_method_data(&state, &mut payout_data, &connector_data, &merchant_context)
        .await?;
    Box::pin(fulfill_payout(
        &state,
        &merchant_context,
        &connector_data,
        &mut payout_data,
    ))
    .await
    .attach_printable("Payout fulfillment failed for given Payout request")?;

    if helpers::is_payout_err_state(status) {
        return Err(report!(errors::ApiErrorResponse::PayoutFailed {
            data: Some(
                serde_json::json!({"payout_status": status.to_string(), "error_message": payout_attempt.error_message, "error_code": payout_attempt.error_code})
            ),
        }));
    }

    trigger_webhook_and_handle_response(&state, &merchant_context, &payout_data).await
}

#[cfg(all(feature = "olap", feature = "v2"))]
pub async fn payouts_list_core(
    _state: SessionState,
    _merchant_context: domain::MerchantContext,
    _profile_id_list: Option<Vec<id_type::ProfileId>>,
    _constraints: payouts::PayoutListConstraints,
) -> RouterResponse<payouts::PayoutListResponse> {
    todo!()
}

#[cfg(all(feature = "olap", feature = "v1"))]
pub async fn payouts_list_core(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    profile_id_list: Option<Vec<id_type::ProfileId>>,
    constraints: payouts::PayoutListConstraints,
) -> RouterResponse<payouts::PayoutListResponse> {
    validator::validate_payout_list_request(&constraints)?;
    let merchant_id = merchant_context.get_merchant_account().get_id();
    let db = state.store.as_ref();
    let payouts = helpers::filter_by_constraints(
        db,
        &constraints,
        merchant_id,
        merchant_context.get_merchant_account().storage_scheme,
    )
    .await
    .to_not_found_response(errors::ApiErrorResponse::PayoutNotFound)?;
    let payouts = core_utils::filter_objects_based_on_profile_id_list(profile_id_list, payouts);

    let mut pi_pa_tuple_vec = PayoutActionData::new();

    for payout in payouts {
        match db
            .find_payout_attempt_by_merchant_id_payout_attempt_id(
                merchant_id,
                &utils::get_payout_attempt_id(
                    payout.payout_id.get_string_repr(),
                    payout.attempt_count,
                ),
                storage_enums::MerchantStorageScheme::PostgresOnly,
            )
            .await
            .change_context(errors::ApiErrorResponse::InternalServerError)
        {
            Ok(payout_attempt) => {
                let domain_customer = match payout.customer_id.clone() {
                    #[cfg(feature = "v1")]
                    Some(customer_id) => db
                        .find_customer_by_customer_id_merchant_id(
                            &(&state).into(),
                            &customer_id,
                            merchant_id,
                            merchant_context.get_merchant_key_store(),
                            merchant_context.get_merchant_account().storage_scheme,
                        )
                        .await
                        .map_err(|err| {
                            let err_msg = format!(
                                "failed while fetching customer for customer_id - {customer_id:?}",
                            );
                            logger::warn!(?err, err_msg);
                        })
                        .ok(),
                    _ => None,
                };

                let payout_id_as_payment_id_type =
                    id_type::PaymentId::wrap(payout.payout_id.get_string_repr().to_string())
                        .change_context(errors::ApiErrorResponse::InvalidRequestData {
                            message: "payout_id contains invalid data".to_string(),
                        })
                        .attach_printable("Error converting payout_id to PaymentId type")?;

                let payment_addr = payment_helpers::create_or_find_address_for_payment_by_request(
                    &state,
                    None,
                    payout.address_id.as_deref(),
                    merchant_id,
                    payout.customer_id.as_ref(),
                    merchant_context.get_merchant_key_store(),
                    &payout_id_as_payment_id_type,
                    merchant_context.get_merchant_account().storage_scheme,
                )
                .await
                .transpose()
                .and_then(|addr| {
                    addr.map_err(|err| {
                        let err_msg = format!(
                            "billing_address missing for address_id : {:?}",
                            payout.address_id
                        );
                        logger::warn!(?err, err_msg);
                    })
                    .ok()
                    .as_ref()
                    .map(domain_models::address::Address::from)
                    .map(payment_enums::Address::from)
                });

                pi_pa_tuple_vec.push((
                    payout.to_owned(),
                    payout_attempt.to_owned(),
                    domain_customer,
                    payment_addr,
                ));
            }
            Err(err) => {
                let err_msg = format!(
                    "failed while fetching payout_attempt for payout_id - {:?}",
                    payout.payout_id
                );
                logger::warn!(?err, err_msg);
            }
        }
    }

    let data: Vec<api::PayoutCreateResponse> = pi_pa_tuple_vec
        .into_iter()
        .map(ForeignFrom::foreign_from)
        .collect();

    Ok(services::ApplicationResponse::Json(
        api::PayoutListResponse {
            size: data.len(),
            data,
            total_count: None,
        },
    ))
}

#[cfg(feature = "olap")]
pub async fn payouts_filtered_list_core(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    profile_id_list: Option<Vec<id_type::ProfileId>>,
    filters: payouts::PayoutListFilterConstraints,
) -> RouterResponse<payouts::PayoutListResponse> {
    let limit = &filters.limit;
    validator::validate_payout_list_request_for_joins(*limit)?;
    let db = state.store.as_ref();
    let constraints = filters.clone().into();
    let list: Vec<(
        storage::Payouts,
        storage::PayoutAttempt,
        Option<diesel_models::Customer>,
        Option<diesel_models::Address>,
    )> = db
        .filter_payouts_and_attempts(
            merchant_context.get_merchant_account().get_id(),
            &constraints,
            merchant_context.get_merchant_account().storage_scheme,
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::PayoutNotFound)?;
    let list = core_utils::filter_objects_based_on_profile_id_list(profile_id_list, list);
    let data: Vec<api::PayoutCreateResponse> =
        join_all(list.into_iter().map(|(p, pa, customer, address)| async {
            let customer: Option<domain::Customer> = customer
                .async_and_then(|cust| async {
                    domain::Customer::convert_back(
                        &(&state).into(),
                        cust,
                        &(merchant_context.get_merchant_key_store().clone()).key,
                        merchant_context
                            .get_merchant_key_store()
                            .merchant_id
                            .clone()
                            .into(),
                    )
                    .await
                    .map_err(|err| {
                        let msg = format!("failed to convert customer for id: {:?}", p.customer_id);
                        logger::warn!(?err, msg);
                    })
                    .ok()
                })
                .await;

            let payout_addr: Option<payment_enums::Address> = address
                .async_and_then(|addr| async {
                    domain::Address::convert_back(
                        &(&state).into(),
                        addr,
                        &(merchant_context.get_merchant_key_store().clone()).key,
                        merchant_context
                            .get_merchant_key_store()
                            .merchant_id
                            .clone()
                            .into(),
                    )
                    .await
                    .map(ForeignFrom::foreign_from)
                    .map_err(|err| {
                        let msg = format!("failed to convert address for id: {:?}", p.address_id);
                        logger::warn!(?err, msg);
                    })
                    .ok()
                })
                .await;

            Some((p, pa, customer, payout_addr))
        }))
        .await
        .into_iter()
        .flatten()
        .map(ForeignFrom::foreign_from)
        .collect();

    let active_payout_ids = db
        .filter_active_payout_ids_by_constraints(
            merchant_context.get_merchant_account().get_id(),
            &constraints,
        )
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("Failed to filter active payout ids based on the constraints")?;

    let total_count = db
        .get_total_count_of_filtered_payouts(
            merchant_context.get_merchant_account().get_id(),
            &active_payout_ids,
            filters.connector.clone(),
            filters.currency.clone(),
            filters.status.clone(),
            filters.payout_method.clone(),
        )
        .await
        .change_context(errors::ApiErrorResponse::InternalServerError)
        .attach_printable_lazy(|| {
            format!(
                "Failed to fetch total count of filtered payouts for the given constraints - {filters:?}",

            )
        })?;

    Ok(services::ApplicationResponse::Json(
        api::PayoutListResponse {
            size: data.len(),
            data,
            total_count: Some(total_count),
        },
    ))
}

#[cfg(feature = "olap")]
pub async fn payouts_list_available_filters_core(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    profile_id_list: Option<Vec<id_type::ProfileId>>,
    time_range: common_utils::types::TimeRange,
) -> RouterResponse<api::PayoutListFilters> {
    let db = state.store.as_ref();
    let payouts = db
        .filter_payouts_by_time_range_constraints(
            merchant_context.get_merchant_account().get_id(),
            &time_range,
            merchant_context.get_merchant_account().storage_scheme,
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;

    let payouts = core_utils::filter_objects_based_on_profile_id_list(profile_id_list, payouts);

    let filters = db
        .get_filters_for_payouts(
            payouts.as_slice(),
            merchant_context.get_merchant_account().get_id(),
            storage_enums::MerchantStorageScheme::PostgresOnly,
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;

    Ok(services::ApplicationResponse::Json(
        api::PayoutListFilters {
            connector: filters.connector,
            currency: filters.currency,
            status: filters.status,
            payout_method: filters.payout_method,
        },
    ))
}

// ********************************************** HELPERS **********************************************
pub async fn call_connector_payout(
    state: &SessionState,
    merchant_context: &domain::MerchantContext,
    connector_data: &api::ConnectorData,
    payout_data: &mut PayoutData,
) -> RouterResult<()> {
    let payout_attempt = &payout_data.payout_attempt.to_owned();
    let payouts = &payout_data.payouts.to_owned();

    // fetch merchant connector account if not present
    if payout_data.merchant_connector_account.is_none()
        || payout_data.payout_attempt.merchant_connector_id.is_none()
    {
        let merchant_connector_account = get_mca_from_profile_id(
            state,
            merchant_context,
            &payout_data.profile_id,
            &connector_data.connector_name.to_string(),
            payout_attempt
                .merchant_connector_id
                .clone()
                .or(connector_data.merchant_connector_id.clone())
                .as_ref(),
        )
        .await?;
        payout_data.payout_attempt.merchant_connector_id = merchant_connector_account.get_mca_id();
        payout_data.merchant_connector_account = Some(merchant_connector_account);
    }

    // update connector_name
    if payout_data.payout_attempt.connector.is_none()
        || payout_data.payout_attempt.connector != Some(connector_data.connector_name.to_string())
    {
        payout_data.payout_attempt.connector = Some(connector_data.connector_name.to_string());
        let updated_payout_attempt = storage::PayoutAttemptUpdate::UpdateRouting {
            connector: connector_data.connector_name.to_string(),
            routing_info: payout_data.payout_attempt.routing_info.clone(),
            merchant_connector_id: payout_data.payout_attempt.merchant_connector_id.clone(),
        };
        let db = &*state.store;
        payout_data.payout_attempt = db
            .update_payout_attempt(
                &payout_data.payout_attempt,
                updated_payout_attempt,
                payouts,
                merchant_context.get_merchant_account().storage_scheme,
            )
            .await
            .change_context(errors::ApiErrorResponse::InternalServerError)
            .attach_printable("Error updating routing info in payout_attempt")?;
    };

    // Fetch / store payout_method_data
    if payout_data.payout_method_data.is_none() || payout_attempt.payout_token.is_none() {
        helpers::fetch_payout_method_data(state, payout_data, connector_data, merchant_context)
            .await?;
    }
    // Eligibility flow
    Box::pin(complete_payout_eligibility(
        state,
        merchant_context,
        connector_data,
        payout_data,
    ))
    .await?;
    // Create customer flow
    Box::pin(complete_create_recipient(
        state,
        merchant_context,
        connector_data,
        payout_data,
    ))
    .await?;
    // Create customer's disbursement account flow
    Box::pin(complete_create_recipient_disburse_account(
        state,
        merchant_context,
        connector_data,
        payout_data,
    ))
    .await?;
    // Payout creation flow
    Box::pin(complete_create_payout(
        state,
        merchant_context,
        connector_data,
        payout_data,
    ))
    .await?;

    // Auto fulfillment flow
    let status = payout_data.payout_attempt.status;
    if payouts.auto_fulfill && status == storage_enums::PayoutStatus::RequiresFulfillment {
        Box::pin(fulfill_payout(
            state,
            merchant_context,
            connector_data,
            payout_data,
        ))
        .await
        .attach_printable("Payout fulfillment failed for given Payout request")?;
    }

    Ok(())
}

pub async fn complete_create_recipient(
    state: &SessionState,
    merchant_context: &domain::MerchantContext,
    connector_data: &api::ConnectorData,
    payout_data: &mut PayoutData,
) -> RouterResult<()> {
    if !payout_data.should_terminate
        && matches!(
            payout_data.payout_attempt.status,
            common_enums::PayoutStatus::RequiresCreation
                | common_enums::PayoutStatus::RequiresConfirmation
                | common_enums::PayoutStatus::RequiresPayoutMethodData
        )
        && connector_data
            .connector_name
            .supports_create_recipient(payout_data.payouts.payout_type)
    {
        Box::pin(create_recipient(
            state,
            merchant_context,
            connector_data,
            payout_data,
        ))
        .await
        .attach_printable("Creation of customer failed")?;
    }

    Ok(())
}

#[cfg(feature = "v1")]
pub async fn create_recipient(
    state: &SessionState,
    merchant_context: &domain::MerchantContext,
    connector_data: &api::ConnectorData,
    payout_data: &mut PayoutData,
) -> RouterResult<()> {
    let customer_details = payout_data.customer_details.to_owned();
    let connector_name = connector_data.connector_name.to_string();

    // Create the connector label using {profile_id}_{connector_name}
    let connector_label = format!(
        "{}_{}",


pub fn filter_network_tokenization_supported_connectors(
    connectors: Vec<api::ConnectorRoutingData>,
    network_tokenization_supported_connectors: &HashSet<enums::Connector>,
) -> Vec<api::ConnectorRoutingData> {
    connectors
        .into_iter()
        .filter(|data| {
            network_tokenization_supported_connectors.contains(&data.connector_data.connector_name)
        })
        .collect()
}


pub struct DatatransPaymentsRequest {
    pub amount: Option<MinorUnit>,
    pub currency: enums::Currency,
    pub card: DataTransPaymentDetails,
    pub refno: String,
    pub auto_settle: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub redirect: Option<RedirectUrls>,
    pub option: Option<DataTransCreateAlias>,
}


/// Returns the payment ID of the event.
    pub fn get_payment_id(&self) -> &str {
        &self.payment_id
    }


pub trait DynamicRoutingCache {
    async fn get_cached_dynamic_routing_config_for_profile(
        state: &SessionState,
        key: &str,
    ) -> Option<Arc<Self>>;

    async fn refresh_dynamic_routing_cache<T, F, Fut>(
        state: &SessionState,
        key: &str,
        func: F,
    ) -> RouterResult<T>
    where
        F: FnOnce() -> Fut + Send,
        T: Cacheable + serde::Serialize + serde::de::DeserializeOwned + Debug + Clone,
        Fut: futures::Future<Output = errors::CustomResult<T, errors::StorageError>> + Send;
}


impl api::PaymentAuthorize for Payu {}


pub struct FeatureMatrixRequest {
    // List of connectors for which the feature matrix is requested
    #[schema(value_type = Option<Vec<Connector>>)]
    pub connectors: Option<Vec<common_enums::connector_enums::Connector>>,
}


impl SingleUseTokenKey {
    pub fn store_key(payment_method_id: &id_type::GlobalPaymentMethodId) -> Self {
        let new_token = format!("single_use_token_{}", payment_method_id.get_string_repr());
        Self(new_token)
    }

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


pub struct CustomerUpdateRequest {
    pub description: Option<Description>,
    pub email: Option<Email>,
    pub phone: Option<masking::Secret<String, masking::WithType>>,
    pub name: Option<masking::Secret<String>>,
    pub address: Option<StripeAddressDetails>,
    pub metadata: Option<pii::SecretSerdeValue>,
    pub shipping: Option<Shipping>,
    pub payment_method: Option<String>,              // not used
    pub balance: Option<i64>,                        // not used
    pub cash_balance: Option<pii::SecretSerdeValue>, // not used
    pub coupon: Option<String>,                      // not used
    pub default_source: Option<String>,              // not used
    pub invoice_settings: Option<pii::SecretSerdeValue>, // not used
    pub next_invoice_sequence: Option<String>,       // not used
    pub preferred_locales: Option<String>,           // not used
    pub promotion_code: Option<String>,              // not used
    pub source: Option<String>,                      // not used
    pub tax: Option<pii::SecretSerdeValue>,          // not used
    pub tax_exempt: Option<String>,                  // not used
}


pub async fn toggle_success_based_routing() {}


pub struct BokuRsyncResponse {
    refunds: RefundResponseData,
}


pub fn new() -> &'static Self {
        &Self {
            amount_converter: &StringMinorUnitForConnector,
        }
    }


pub struct RefundErrorMessageDistributionAccumulator {
    pub refund_error_message_vec: Vec<RefundErrorMessageDistributionRow>,
}