File: crates/router/src/core/payments/routing/utils.rs
 Public functions: 24
 Public structs: 42

use std::{
    collections::{HashMap, HashSet},
    str::FromStr,
};

use api_models::{
    open_router as or_types,
    routing::{
        self as api_routing, ComparisonType, ConnectorSelection, ConnectorVolumeSplit,
        DeRoutableConnectorChoice, MetadataValue, NumberComparison, RoutableConnectorChoice,
        RoutingEvaluateRequest, RoutingEvaluateResponse, ValueType,
    },
};
use async_trait::async_trait;
use common_enums::{RoutableConnectors, TransactionType};
use common_utils::{
    ext_traits::{BytesExt, StringExt},
    id_type,
};
use diesel_models::{enums, routing_algorithm};
use error_stack::ResultExt;
use euclid::{
    backend::BackendInput,
    frontend::{
        ast::{self},
        dir::{self, transformers::IntoDirValue},
    },
};
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
use external_services::grpc_client::dynamic_routing as ir_client;
use hyperswitch_domain_models::business_profile;
use hyperswitch_interfaces::events::routing_api_logs as routing_events;
use router_env::tracing_actix_web::RequestId;
use serde::{Deserialize, Serialize};

use super::RoutingResult;
use crate::{
    core::errors,
    db::domain,
    routes::{app::SessionStateInfo, SessionState},
    services::{self, logger},
    types::transformers::ForeignInto,
};

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

// Struct to implement the DecisionEngineApiHandler trait
pub struct EuclidApiClient;

pub struct ConfigApiClient;

pub struct SRApiClient;

pub async fn build_and_send_decision_engine_http_request<Req, Res, ErrRes>(
    state: &SessionState,
    http_method: services::Method,
    path: &str,
    request_body: Option<Req>,
    _timeout: Option<u64>,
    context_message: &str,
    events_wrapper: Option<RoutingEventsWrapper<Req>>,
) -> RoutingResult<RoutingEventsResponse<Res>>
where
    Req: Serialize + Send + Sync + 'static + Clone,
    Res: Serialize + serde::de::DeserializeOwned + std::fmt::Debug + Clone + 'static,
    ErrRes: serde::de::DeserializeOwned + std::fmt::Debug + Clone + DecisionEngineErrorsInterface,
{
    let decision_engine_base_url = &state.conf.open_router.url;
    let url = format!("{decision_engine_base_url}/{path}");
    logger::debug!(decision_engine_api_call_url = %url, decision_engine_request_path = %path, http_method = ?http_method, "decision_engine: Initiating decision_engine API call ({})", context_message);

    let mut request_builder = services::RequestBuilder::new()
        .method(http_method)
        .url(&url);

    if let Some(body_content) = request_body {
        let body = common_utils::request::RequestContent::Json(Box::new(body_content));
        request_builder = request_builder.set_body(body);
    }

    let http_request = request_builder.build();
    logger::info!(?http_request, decision_engine_request_path = %path, "decision_engine: Constructed Decision Engine API request details ({})", context_message);
    let should_parse_response = events_wrapper
        .as_ref()
        .map(|wrapper| wrapper.parse_response)
        .unwrap_or(true);

    let closure = || async {
        let response =
            services::call_connector_api(state, http_request, "Decision Engine API call")
                .await
                .change_context(errors::RoutingError::OpenRouterCallFailed)?;

        match response {
            Ok(resp) => {
                logger::debug!(
                    "decision_engine: Received response from Decision Engine API ({:?})",
                    String::from_utf8_lossy(&resp.response) // For logging
                );

                let resp = should_parse_response
                    .then(|| {
                        if std::any::TypeId::of::<Res>() == std::any::TypeId::of::<String>()
                            && resp.response.is_empty()
                        {
                            return serde_json::from_str::<Res>("\"\"").change_context(
                                errors::RoutingError::OpenRouterError(
                                    "Failed to parse empty response as String".into(),
                                ),
                            );
                        }
                        let response_type: Res = resp
                            .response
                            .parse_struct(std::any::type_name::<Res>())
                            .change_context(errors::RoutingError::OpenRouterError(
                                "Failed to parse the response from open_router".into(),
                            ))?;

                        Ok::<_, error_stack::Report<errors::RoutingError>>(response_type)
                    })
                    .transpose()?;

                logger::debug!("decision_engine_success_response: {:?}", resp);

                Ok(resp)
            }
            Err(err) => {
                logger::debug!(
                    "decision_engine: Received response from Decision Engine API ({:?})",
                    String::from_utf8_lossy(&err.response) // For logging
                );

                let err_resp: ErrRes = err
                    .response
                    .parse_struct(std::any::type_name::<ErrRes>())
                    .change_context(errors::RoutingError::OpenRouterError(
                    "Failed to parse the response from open_router".into(),
                ))?;

                logger::error!(
                    decision_engine_error_code = %err_resp.get_error_code(),
                    decision_engine_error_message = %err_resp.get_error_message(),
                    decision_engine_raw_response = ?err_resp.get_error_data(),
                );

                Err(error_stack::report!(
                    errors::RoutingError::RoutingEventsError {
                        message: err_resp.get_error_message(),
                        status_code: err.status_code,
                    }
                ))
            }
        }
    };

    let events_response = if let Some(wrapper) = events_wrapper {
        wrapper
            .construct_event_builder(
                url,
                routing_events::RoutingEngine::DecisionEngine,
                routing_events::ApiMethod::Rest(http_method),
            )?
            .trigger_event(state, closure)
            .await?
    } else {
        let resp = closure()
            .await
            .change_context(errors::RoutingError::OpenRouterCallFailed)?;

        RoutingEventsResponse::new(None, resp)
    };

    Ok(events_response)
}

#[async_trait]
impl DecisionEngineApiHandler for EuclidApiClient {
    async fn send_decision_engine_request<Req, Res>(
        state: &SessionState,
        http_method: services::Method,
        path: &str,
        request_body: Option<Req>, // Option to handle GET/DELETE requests without body
        timeout: Option<u64>,
        events_wrapper: Option<RoutingEventsWrapper<Req>>,
    ) -> RoutingResult<RoutingEventsResponse<Res>>
    where
        Req: Serialize + Send + Sync + 'static + Clone,
        Res: Serialize + serde::de::DeserializeOwned + Send + 'static + std::fmt::Debug + Clone,
    {
        let event_response = build_and_send_decision_engine_http_request::<_, _, DeErrorResponse>(
            state,
            http_method,
            path,
            request_body,
            timeout,
            "parsing response",
            events_wrapper,
        )
        .await?;

        let parsed_response =
            event_response
                .response
                .as_ref()
                .ok_or(errors::RoutingError::OpenRouterError(
                    "Response from decision engine API is empty".to_string(),
                ))?;

        logger::debug!(parsed_response = ?parsed_response, response_type = %std::any::type_name::<Res>(), euclid_request_path = %path, "decision_engine_euclid: Successfully parsed response from Euclid API");
        Ok(event_response)
    }
}

#[async_trait]
impl DecisionEngineApiHandler for ConfigApiClient {
    async fn send_decision_engine_request<Req, Res>(
        state: &SessionState,
        http_method: services::Method,
        path: &str,
        request_body: Option<Req>,
        timeout: Option<u64>,
        events_wrapper: Option<RoutingEventsWrapper<Req>>,
    ) -> RoutingResult<RoutingEventsResponse<Res>>
    where
        Req: Serialize + Send + Sync + 'static + Clone,
        Res: Serialize + serde::de::DeserializeOwned + Send + 'static + std::fmt::Debug + Clone,
    {
        let events_response = build_and_send_decision_engine_http_request::<_, _, DeErrorResponse>(
            state,
            http_method,
            path,
            request_body,
            timeout,
            "parsing response",
            events_wrapper,
        )
        .await?;

        let parsed_response =
            events_response
                .response
                .as_ref()
                .ok_or(errors::RoutingError::OpenRouterError(
                    "Response from decision engine API is empty".to_string(),
                ))?;
        logger::debug!(parsed_response = ?parsed_response, response_type = %std::any::type_name::<Res>(), decision_engine_request_path = %path, "decision_engine_config: Successfully parsed response from Decision Engine config API");
        Ok(events_response)
    }
}

#[async_trait]
impl DecisionEngineApiHandler for SRApiClient {
    async fn send_decision_engine_request<Req, Res>(
        state: &SessionState,
        http_method: services::Method,
        path: &str,
        request_body: Option<Req>,
        timeout: Option<u64>,
        events_wrapper: Option<RoutingEventsWrapper<Req>>,
    ) -> RoutingResult<RoutingEventsResponse<Res>>
    where
        Req: Serialize + Send + Sync + 'static + Clone,
        Res: Serialize + serde::de::DeserializeOwned + Send + 'static + std::fmt::Debug + Clone,
    {
        let events_response =
            build_and_send_decision_engine_http_request::<_, _, or_types::ErrorResponse>(
                state,
                http_method,
                path,
                request_body,
                timeout,
                "parsing response",
                events_wrapper,
            )
            .await?;

        let parsed_response =
            events_response
                .response
                .as_ref()
                .ok_or(errors::RoutingError::OpenRouterError(
                    "Response from decision engine API is empty".to_string(),
                ))?;
        logger::debug!(parsed_response = ?parsed_response, response_type = %std::any::type_name::<Res>(), decision_engine_request_path = %path, "decision_engine_config: Successfully parsed response from Decision Engine config API");
        Ok(events_response)
    }
}

const EUCLID_API_TIMEOUT: u64 = 5;

pub async fn perform_decision_euclid_routing(
    state: &SessionState,
    input: BackendInput,
    created_by: String,
    events_wrapper: RoutingEventsWrapper<RoutingEvaluateRequest>,
    fallback_output: Vec<RoutableConnectorChoice>,
) -> RoutingResult<RoutingEvaluateResponse> {
    logger::debug!("decision_engine_euclid: evaluate api call for euclid routing evaluation");

    let mut events_wrapper = events_wrapper;
    let fallback_output = fallback_output
        .into_iter()
        .map(|c| DeRoutableConnectorChoice {
            gateway_name: c.connector,
            gateway_id: c.merchant_connector_id,
        })
        .collect::<Vec<_>>();

    let routing_request =
        convert_backend_input_to_routing_eval(created_by, input, fallback_output)?;
    events_wrapper.set_request_body(routing_request.clone());

    let event_response = EuclidApiClient::send_decision_engine_request(
        state,
        services::Method::Post,
        "routing/evaluate",
        Some(routing_request),
        Some(EUCLID_API_TIMEOUT),
        Some(events_wrapper),
    )
    .await?;

    let euclid_response: RoutingEvaluateResponse =
        event_response
            .response
            .ok_or(errors::RoutingError::OpenRouterError(
                "Response from decision engine API is empty".to_string(),
            ))?;

    let mut routing_event =
        event_response
            .event
            .ok_or(errors::RoutingError::RoutingEventsError {
                message: "Routing event not found in EventsResponse".to_string(),
                status_code: 500,
            })?;

    routing_event.set_routing_approach(RoutingApproach::StaticRouting.to_string());
    routing_event.set_routable_connectors(euclid_response.evaluated_output.clone());
    state.event_handler.log_event(&routing_event);

    logger::debug!(decision_engine_euclid_response=?euclid_response,"decision_engine_euclid");
    logger::debug!(decision_engine_euclid_selected_connector=?euclid_response.evaluated_output,"decision_engine_euclid");
    Ok(euclid_response)
}

/// This function transforms the decision_engine response in a way that's usable for further flows:
/// It places evaluated_output connectors first, followed by remaining output connectors (no duplicates).
pub fn transform_de_output_for_router(
    de_output: Vec<ConnectorInfo>,
    de_evaluated_output: Vec<RoutableConnectorChoice>,
) -> RoutingResult<Vec<RoutableConnectorChoice>> {
    let mut seen = HashSet::new();

    // evaluated connectors on top, to ensure the fallback is based on other connectors.
    let mut ordered = Vec::with_capacity(de_output.len() + de_evaluated_output.len());
    for eval_conn in de_evaluated_output {
        if seen.insert(eval_conn.connector) {
            ordered.push(eval_conn);
        }
    }

    // Add remaining connectors from de_output (only if not already seen), for fallback
    for conn in de_output {
        let key = RoutableConnectors::from_str(&conn.gateway_name).map_err(|_| {
            errors::RoutingError::GenericConversionError {
                from: "String".to_string(),
                to: "RoutableConnectors".to_string(),
            }
        })?;
        if seen.insert(key) {
            let de_choice = DeRoutableConnectorChoice::try_from(conn)?;
            ordered.push(RoutableConnectorChoice::from(de_choice));
        }
    }
    Ok(ordered)
}

pub async fn decision_engine_routing(
    state: &SessionState,
    backend_input: BackendInput,
    business_profile: &domain::Profile,
    payment_id: String,
    merchant_fallback_config: Vec<RoutableConnectorChoice>,
) -> RoutingResult<Vec<RoutableConnectorChoice>> {
    let routing_events_wrapper = RoutingEventsWrapper::new(
        state.tenant.tenant_id.clone(),
        state.request_id,
        payment_id,
        business_profile.get_id().to_owned(),
        business_profile.merchant_id.to_owned(),
        "DecisionEngine: Euclid Static Routing".to_string(),
        None,
        true,
        false,
    );

    let de_euclid_evaluate_response = perform_decision_euclid_routing(
        state,
        backend_input.clone(),
        business_profile.get_id().get_string_repr().to_string(),
        routing_events_wrapper,
        merchant_fallback_config,
    )
    .await;

    let Ok(de_euclid_response) = de_euclid_evaluate_response else {
        logger::error!("decision_engine_euclid_evaluation_error: error in evaluation of rule");
        return Ok(Vec::default());
    };

    let de_output_connector = extract_de_output_connectors(de_euclid_response.output)
            .map_err(|e| {
                logger::error!(error=?e, "decision_engine_euclid_evaluation_error: Failed to extract connector from Output");
                e
            })?;

    transform_de_output_for_router(
            de_output_connector.clone(),
            de_euclid_response.evaluated_output.clone(),
        )
        .map_err(|e| {
            logger::error!(error=?e, "decision_engine_euclid_evaluation_error: failed to transform connector from de-output");
            e
        })
}

/// Custom deserializer for output from decision_engine, this is required as untagged enum is
/// stored but the enum requires tagged deserialization, hence deserializing it into specific
/// variants
pub fn extract_de_output_connectors(
    output_value: serde_json::Value,
) -> RoutingResult<Vec<ConnectorInfo>> {
    const SINGLE: &str = "straight_through";
    const PRIORITY: &str = "priority";
    const VOLUME_SPLIT: &str = "volume_split";
    const VOLUME_SPLIT_PRIORITY: &str = "volume_split_priority";

    let obj = output_value.as_object().ok_or_else(|| {
        logger::error!("decision_engine_euclid_error: output is not a JSON object");
        errors::RoutingError::OpenRouterError("Expected output to be a JSON object".into())
    })?;

    let type_str = obj.get("type").and_then(|v| v.as_str()).ok_or_else(|| {
        logger::error!("decision_engine_euclid_error: missing or invalid 'type' in output");
        errors::RoutingError::OpenRouterError("Missing or invalid 'type' field in output".into())
    })?;

    match type_str {
        SINGLE => {
            let connector_value = obj.get("connector").ok_or_else(|| {
                logger::error!(
                    "decision_engine_euclid_error: missing 'connector' field for type=single"
                );
                errors::RoutingError::OpenRouterError(
                    "Missing 'connector' field for single output".into(),
                )
            })?;
            let connector: ConnectorInfo = serde_json::from_value(connector_value.clone())
                .map_err(|e| {
                    logger::error!(
                        ?e,
                        "decision_engine_euclid_error: Failed to parse single connector"
                    );
                    errors::RoutingError::OpenRouterError(
                        "Failed to deserialize single connector".into(),
                    )
                })?;
            Ok(vec![connector])
        }

        PRIORITY => {
            let connectors_value = obj.get("connectors").ok_or_else(|| {
                logger::error!(
                    "decision_engine_euclid_error: missing 'connectors' field for type=priority"
                );
                errors::RoutingError::OpenRouterError(
                    "Missing 'connectors' field for priority output".into(),
                )
            })?;
            let connectors: Vec<ConnectorInfo> = serde_json::from_value(connectors_value.clone())
                .map_err(|e| {
                logger::error!(
                    ?e,
                    "decision_engine_euclid_error: Failed to parse connectors for priority"
                );
                errors::RoutingError::OpenRouterError(
                    "Failed to deserialize priority connectors".into(),
                )
            })?;
            Ok(connectors)
        }

        VOLUME_SPLIT => {
            let splits_value = obj.get("splits").ok_or_else(|| {
                logger::error!(
                    "decision_engine_euclid_error: missing 'splits' field for type=volume_split"
                );
                errors::RoutingError::OpenRouterError(
                    "Missing 'splits' field for volume_split output".into(),
                )
            })?;

            // Transform each {connector, split} into {output, split}
            let fixed_splits: Vec<_> = splits_value
                .as_array()
                .ok_or_else(|| {
                    logger::error!("decision_engine_euclid_error: 'splits' is not an array");
                    errors::RoutingError::OpenRouterError("'splits' field must be an array".into())
                })?
                .iter()
                .map(|entry| {
                    let mut entry_map = entry.as_object().cloned().ok_or_else(|| {
                        logger::error!(
                            "decision_engine_euclid_error: invalid split entry in volume_split"
                        );
                        errors::RoutingError::OpenRouterError(
                            "Invalid entry in splits array".into(),
                        )
                    })?;
                    if let Some(connector) = entry_map.remove("connector") {
                        entry_map.insert("output".to_string(), connector);
                    }
                    Ok::<_, error_stack::Report<errors::RoutingError>>(serde_json::Value::Object(
                        entry_map,
                    ))
                })
                .collect::<Result<Vec<_>, _>>()?;

            let splits: Vec<VolumeSplit<ConnectorInfo>> =
                serde_json::from_value(serde_json::Value::Array(fixed_splits)).map_err(|e| {
                    logger::error!(
                        ?e,
                        "decision_engine_euclid_error: Failed to parse volume_split"
                    );
                    errors::RoutingError::OpenRouterError(
                        "Failed to deserialize volume_split connectors".into(),
                    )
                })?;

            Ok(splits.into_iter().map(|s| s.output).collect())
        }

        VOLUME_SPLIT_PRIORITY => {
            let splits_value = obj.get("splits").ok_or_else(|| {
                logger::error!("decision_engine_euclid_error: missing 'splits' field for type=volume_split_priority");
                errors::RoutingError::OpenRouterError("Missing 'splits' field for volume_split_priority output".into())
            })?;

            // Transform each {connector: [...], split} into {output: [...], split}
            let fixed_splits: Vec<_> = splits_value
                .as_array()
                .ok_or_else(|| {
                    logger::error!("decision_engine_euclid_error: 'splits' is not an array");
                    errors::RoutingError::OpenRouterError("'splits' field must be an array".into())
                })?
                .iter()
                .map(|entry| {
                    let mut entry_map = entry.as_object().cloned().ok_or_else(|| {
                        logger::error!("decision_engine_euclid_error: invalid split entry in volume_split_priority");
                        errors::RoutingError::OpenRouterError("Invalid entry in splits array".into())
                    })?;
                    if let Some(connector) = entry_map.remove("connector") {
                        entry_map.insert("output".to_string(), connector);
                    }
                    Ok::<_, error_stack::Report<errors::RoutingError>>(serde_json::Value::Object(entry_map))
                })
                .collect::<Result<Vec<_>, _>>()?;

            let splits: Vec<VolumeSplit<Vec<ConnectorInfo>>> =
                serde_json::from_value(serde_json::Value::Array(fixed_splits)).map_err(|e| {
                    logger::error!(
                        ?e,
                        "decision_engine_euclid_error: Failed to parse volume_split_priority"
                    );
                    errors::RoutingError::OpenRouterError(
                        "Failed to deserialize volume_split_priority connectors".into(),
                    )
                })?;

            Ok(splits.into_iter().flat_map(|s| s.output).collect())
        }

        other => {
            logger::error!(type_str=%other, "decision_engine_euclid_error: unknown output type");
            Err(
                errors::RoutingError::OpenRouterError(format!("Unknown output type: {other}"))
                    .into(),
            )
        }
    }
}

pub async fn create_de_euclid_routing_algo(
    state: &SessionState,
    routing_request: &RoutingRule,
) -> RoutingResult<String> {
    logger::debug!("decision_engine_euclid: create api call for euclid routing rule creation");

    logger::debug!(decision_engine_euclid_request=?routing_request,"decision_engine_euclid");
    let events_response = EuclidApiClient::send_decision_engine_request(
        state,
        services::Method::Post,
        "routing/create",
        Some(routing_request.clone()),
        Some(EUCLID_API_TIMEOUT),
        None,
    )
    .await?;

    let euclid_response: RoutingDictionaryRecord =
        events_response
            .response
            .ok_or(errors::RoutingError::OpenRouterError(
                "Response from decision engine API is empty".to_string(),
            ))?;

    logger::debug!(decision_engine_euclid_parsed_response=?euclid_response,"decision_engine_euclid");
    Ok(euclid_response.rule_id)
}

pub async fn link_de_euclid_routing_algorithm(
    state: &SessionState,
    routing_request: ActivateRoutingConfigRequest,
) -> RoutingResult<()> {
    logger::debug!("decision_engine_euclid: link api call for euclid routing algorithm");

    EuclidApiClient::send_decision_engine_request::<_, String>(
        state,
        services::Method::Post,
        "routing/activate",
        Some(routing_request.clone()),
        Some(EUCLID_API_TIMEOUT),
        None,
    )
    .await?;

    logger::debug!(decision_engine_euclid_activated=?routing_request, "decision_engine_euclid: link_de_euclid_routing_algorithm completed");
    Ok(())
}

pub async fn list_de_euclid_routing_algorithms(
    state: &SessionState,
    routing_list_request: ListRountingAlgorithmsRequest,
) -> RoutingResult<Vec<api_routing::RoutingDictionaryRecord>> {
    logger::debug!("decision_engine_euclid: list api call for euclid routing algorithms");
    let created_by = routing_list_request.created_by;
    let events_response = EuclidApiClient::send_decision_engine_request(
        state,
        services::Method::Post,
        format!("routing/list/{created_by}").as_str(),
        None::<()>,
        Some(EUCLID_API_TIMEOUT),
        None,
    )
    .await?;

    let euclid_response: Vec<RoutingAlgorithmRecord> =
        events_response
            .response
            .ok_or(errors::RoutingError::OpenRouterError(
                "Response from decision engine API is empty".to_string(),
            ))?;

    Ok(euclid_response
        .into_iter()
        .map(routing_algorithm::RoutingProfileMetadata::from)
        .map(ForeignInto::foreign_into)
        .collect::<Vec<_>>())
}

pub async fn list_de_euclid_active_routing_algorithm(
    state: &SessionState,
    created_by: String,
) -> RoutingResult<Vec<api_routing::RoutingDictionaryRecord>> {
    logger::debug!("decision_engine_euclid: list api call for euclid active routing algorithm");
    let response: Vec<RoutingAlgorithmRecord> = EuclidApiClient::send_decision_engine_request(
        state,
        services::Method::Post,
        format!("routing/list/active/{created_by}").as_str(),
        None::<()>,
        Some(EUCLID_API_TIMEOUT),
        None,
    )
    .await?
    .response
    .ok_or(errors::RoutingError::OpenRouterError(
        "Response from decision engine API is empty".to_string(),
    ))?;

    Ok(response
        .into_iter()
        .map(|record| routing_algorithm::RoutingProfileMetadata::from(record).foreign_into())
        .collect())
}

pub fn compare_and_log_result<T: RoutingEq<T> + Serialize>(
    de_result: Vec<T>,
    result: Vec<T>,
    flow: String,
) {
    let is_equal = de_result
        .iter()
        .zip(result.iter())
        .all(|(a, b)| T::is_equal(a, b));

    let is_equal_in_length = de_result.len() == result.len();
    router_env::logger::debug!(routing_flow=?flow, is_equal=?is_equal, is_equal_length=?is_equal_in_length, de_response=?to_json_string(&de_result), hs_response=?to_json_string(&result), "decision_engine_euclid");
}

pub trait RoutingEq<T> {
    fn is_equal(a: &T, b: &T) -> bool;
}

impl RoutingEq<Self> for api_routing::RoutingDictionaryRecord {
    fn is_equal(a: &Self, b: &Self) -> bool {
        a.id == b.id
            && a.name == b.name
            && a.profile_id == b.profile_id
            && a.description == b.description
            && a.kind == b.kind
            && a.algorithm_for == b.algorithm_for
    }
}

impl RoutingEq<Self> for String {
    fn is_equal(a: &Self, b: &Self) -> bool {
        a.to_lowercase() == b.to_lowercase()
    }
}

impl RoutingEq<Self> for RoutableConnectorChoice {
    fn is_equal(a: &Self, b: &Self) -> bool {
        a.connector.eq(&b.connector)
            && a.choice_kind.eq(&b.choice_kind)
            && a.merchant_connector_id.eq(&b.merchant_connector_id)
    }
}

pub fn to_json_string<T: Serialize>(value: &T) -> String {
    serde_json::to_string(value)
        .map_err(|_| errors::RoutingError::GenericConversionError {
            from: "T".to_string(),
            to: "JsonValue".to_string(),
        })
        .unwrap_or_default()
}

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct ActivateRoutingConfigRequest {
    pub created_by: String,
    pub routing_algorithm_id: String,
}

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct ListRountingAlgorithmsRequest {
    pub created_by: String,
}

// Maps Hyperswitch `BackendInput` to a `RoutingEvaluateRequest` compatible with Decision Engine
pub fn convert_backend_input_to_routing_eval(
    created_by: String,
    input: BackendInput,
    fallback_output: Vec<DeRoutableConnectorChoice>,
) -> RoutingResult<RoutingEvaluateRequest> {
    let mut params: HashMap<String, Option<ValueType>> = HashMap::new();

    // Payment
    params.insert(
        "amount".to_string(),
        Some(ValueType::Number(
            input
                .payment
                .amount
                .get_amount_as_i64()
                .try_into()
                .unwrap_or_default(),
        )),
    );
    params.insert(
        "currency".to_string(),
        Some(ValueType::EnumVariant(input.payment.currency.to_string())),
    );

    if let Some(auth_type) = input.payment.authentication_type {
        params.insert(
            "authentication_type".to_string(),
            Some(ValueType::EnumVariant(auth_type.to_string())),
        );
    }
    if let Some(bin) = input.payment.card_bin {
        params.insert("card_bin".to_string(), Some(ValueType::StrValue(bin)));
    }
    if let Some(capture_method) = input.payment.capture_method {
        params.insert(
            "capture_method".to_string(),
            Some(ValueType::EnumVariant(capture_method.to_string())),
        );
    }
    if let Some(country) = input.payment.business_country {
        params.insert(
            "business_country".to_string(),
            Some(ValueType::EnumVariant(country.to_string())),
        );
    }
    if let Some(country) = input.payment.billing_country {
        params.insert(
            "billing_country".to_string(),
            Some(ValueType::EnumVariant(country.to_string())),
        );
    }
    if let Some(label) = input.payment.business_label {
        params.insert(
            "business_label".to_string(),
            Some(ValueType::StrValue(label)),
        );
    }
    if let Some(sfu) = input.payment.setup_future_usage {
        params.insert(
            "setup_future_usage".to_string(),
            Some(ValueType::EnumVariant(sfu.to_string())),
        );
    }

    // PaymentMethod
    if let Some(pm) = input.payment_method.payment_method {
        params.insert(
            "payment_method".to_string(),
            Some(ValueType::EnumVariant(pm.to_string())),
        );
        if let Some(pmt) = input.payment_method.payment_method_type {
            match (pmt, pm).into_dir_value() {
                Ok(dv) => insert_dirvalue_param(&mut params, dv),
                Err(e) => logger::debug!(
                    ?e,
                    ?pmt,
                    ?pm,
                    "decision_engine_euclid: into_dir_value failed; skipping subset param"
                ),
            }
        }
    }
    if let Some(pmt) = input.payment_method.payment_method_type {
        params.insert(
            "payment_method_type".to_string(),
            Some(ValueType::EnumVariant(pmt.to_string())),
        );
    }
    if let Some(network) = input.payment_method.card_network {
        params.insert(
            "card_network".to_string(),
            Some(ValueType::EnumVariant(network.to_string())),
        );
    }

    // Mandate
    if let Some(pt) = input.mandate.payment_type {
        params.insert(
            "payment_type".to_string(),
            Some(ValueType::EnumVariant(pt.to_string())),
        );
    }
    if let Some(mt) = input.mandate.mandate_type {
        params.insert(
            "mandate_type".to_string(),
            Some(ValueType::EnumVariant(mt.to_string())),
        );
    }
    if let Some(mat) = input.mandate.mandate_acceptance_type {
        params.insert(
            "mandate_acceptance_type".to_string(),
            Some(ValueType::EnumVariant(mat.to_string())),
        );
    }

    // Metadata
    if let Some(meta) = input.metadata {
        for (k, v) in meta.into_iter() {
            params.insert(
                k.clone(),
                Some(ValueType::MetadataVariant(MetadataValue {
                    key: k,
                    value: v,
                })),
            );
        }
    }

    Ok(RoutingEvaluateRequest {
        created_by,
        parameters: params,
        fallback_output,
    })
}

// All the independent variants of payment method types, configured via dashboard
fn insert_dirvalue_param(params: &mut HashMap<String, Option<ValueType>>, dv: dir::DirValue) {
    match dv {
        dir::DirValue::RewardType(v) => {
            params.insert(
                "reward".to_string(),
                Some(ValueType::EnumVariant(v.to_string())),
            );
        }
        dir::DirValue::CardType(v) => {
            params.insert(
                "card".to_string(),
                Some(ValueType::EnumVariant(v.to_string())),
            );
        }
        dir::DirValue::PayLaterType(v) => {
            params.insert(
                "pay_later".to_string(),
                Some(ValueType::EnumVariant(v.to_string())),
            );
        }
        dir::DirValue::WalletType(v) => {
            params.insert(
                "wallet".to_string(),
                Some(ValueType::EnumVariant(v.to_string())),
            );
        }
        dir::DirValue::VoucherType(v) => {
            params.insert(
                "voucher".to_string(),
                Some(ValueType::EnumVariant(v.to_string())),
            );
        }
        dir::DirValue::BankRedirectType(v) => {
            params.insert(
                "bank_redirect".to_string(),
                Some(ValueType::EnumVariant(v.to_string())),
            );
        }
        dir::DirValue::BankDebitType(v) => {
            params.insert(
                "bank_debit".to_string(),
                Some(ValueType::EnumVariant(v.to_string())),
            );
        }
        dir::DirValue::BankTransferType(v) => {
            params.insert(
                "bank_transfer".to_string(),
                Some(ValueType::EnumVariant(v.to_string())),
            );
        }
        dir::DirValue::RealTimePaymentType(v) => {
            params.insert(
                "real_time_payment".to_string(),
                Some(ValueType::EnumVariant(v.to_string())),
            );
        }
        dir::DirValue::UpiType(v) => {
            params.insert(
                "upi".to_string(),
                Some(ValueType::EnumVariant(v.to_string())),
            );
        }
        dir::DirValue::GiftCardType(v) => {
            params.insert(
                "gift_card".to_string(),
                Some(ValueType::EnumVariant(v.to_string())),
            );
        }
        dir::DirValue::CardRedirectType(v) => {
            params.insert(
                "card_redirect".to_string(),
                Some(ValueType::EnumVariant(v.to_string())),
            );
        }
        dir::DirValue::OpenBankingType(v) => {
            params.insert(
                "open_banking".to_string(),
                Some(ValueType::EnumVariant(v.to_string())),
            );
        }
        dir::DirValue::MobilePaymentType(v) => {
            params.insert(
                "mobile_payment".to_string(),
                Some(ValueType::EnumVariant(v.to_string())),
            );
        }
        dir::DirValue::CryptoType(v) => {
            params.insert(
                "crypto".to_string(),
                Some(ValueType::EnumVariant(v.to_string())),
            );
        }
        other => {
            // all other values can be ignored for now as they don't converge with
            // payment method type
            logger::warn!(
                ?other,
                "decision_engine_euclid: unmapped dir::DirValue; add a mapping here"
            );
        }
    }
}

#[derive(Debug, Clone, serde::Deserialize)]
struct DeErrorResponse {
    code: String,
    message: String,
    data: Option<serde_json::Value>,
}

impl DecisionEngineErrorsInterface for DeErrorResponse {
    fn get_error_message(&self) -> String {
        self.message.clone()
    }

    fn get_error_code(&self) -> String {
        self.code.clone()
    }

    fn get_error_data(&self) -> Option<String> {
        self.data.as_ref().map(|data| data.to_string())
    }
}

impl DecisionEngineErrorsInterface for or_types::ErrorResponse {
    fn get_error_message(&self) -> String {
        self.error_message.clone()
    }

    fn get_error_code(&self) -> String {
        self.error_code.clone()
    }

    fn get_error_data(&self) -> Option<String> {
        Some(format!(
            "decision_engine Error: {}",
            self.error_message.clone()
        ))
    }
}

pub type Metadata = HashMap<String, serde_json::Value>;

/// Represents a single comparison condition.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Comparison {
    /// The left hand side which will always be a domain input identifier like "payment.method.cardtype"
    pub lhs: String,
    /// The comparison operator
    pub comparison: ComparisonType,
    /// The value to compare against
    pub value: ValueType,
    /// Additional metadata that the Static Analyzer and Backend does not touch.
    /// This can be used to store useful information for the frontend and is required for communication
    /// between the static analyzer and the frontend.
    // #[schema(value_type=HashMap<String, serde_json::Value>)]
    pub metadata: Metadata,
}

/// Represents all the conditions of an IF statement
/// eg:
///
/// ```text
/// payment.method = card & payment.method.cardtype = debit & payment.method.network = diners
/// ```


pub struct NetworkTokenizedCard {
    number: NetworkTokenNumber,
    expiration_month: Secret<String>,
    expiration_year: Secret<String>,
    cryptogram: Option<Secret<String>>,
    transaction_type: TransactionType,
}


impl SerializableSecret for Value {}


impl State for CardInfoResponse {}


pub struct OrganizationResponse {
    /// The unique identifier for the Organization
    #[schema(value_type = String, max_length = 64, min_length = 1, example = "org_q98uSGAYbjEwqs0mJwnz")]
    pub id: id_type::OrganizationId,

    /// Name of the Organization
    pub organization_name: Option<String>,

    /// Details about the organization
    #[schema(value_type = Option<Object>)]
    pub organization_details: Option<pii::SecretSerdeValue>,

    /// Metadata is useful for storing additional, unstructured information on an object.
    #[schema(value_type = Option<Object>)]
    pub metadata: Option<pii::SecretSerdeValue>,
    pub modified_at: time::PrimitiveDateTime,
    pub created_at: time::PrimitiveDateTime,

    /// Organization Type of the organization
    #[schema(value_type = Option<OrganizationType>, example = "standard")]
    pub organization_type: Option<OrganizationType>,
}


pub struct AciCaptureResult {
    code: String,
    description: String,
}


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

use std::collections::HashSet;

use api_models::analytics::{
    payment_intents::{
        PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetrics,
        PaymentIntentMetricsBucketIdentifier,
    },
    Granularity, TimeRange,
};
use diesel_models::enums as storage_enums;
use time::PrimitiveDateTime;

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

mod payment_intent_count;
mod payment_processed_amount;
mod payments_success_rate;
mod sessionized_metrics;
mod smart_retried_amount;
mod successful_smart_retries;
mod total_smart_retries;

use payment_intent_count::PaymentIntentCount;
use payment_processed_amount::PaymentProcessedAmount;
use payments_success_rate::PaymentsSuccessRate;
use smart_retried_amount::SmartRetriedAmount;
use successful_smart_retries::SuccessfulSmartRetries;
use total_smart_retries::TotalSmartRetries;

#[derive(Debug, PartialEq, Eq, serde::Deserialize, Hash)]
pub struct PaymentIntentMetricRow {
    pub status: Option<DBEnumWrapper<storage_enums::IntentStatus>>,
    pub currency: Option<DBEnumWrapper<storage_enums::Currency>>,
    pub profile_id: Option<String>,
    pub connector: Option<String>,
    pub authentication_type: Option<DBEnumWrapper<storage_enums::AuthenticationType>>,
    pub payment_method: Option<String>,
    pub payment_method_type: Option<String>,
    pub card_network: Option<String>,
    pub merchant_id: Option<String>,
    pub card_last_4: Option<String>,
    pub card_issuer: Option<String>,
    pub error_reason: Option<String>,
    pub first_attempt: Option<i64>,
    pub total: Option<bigdecimal::BigDecimal>,
    pub count: Option<i64>,
    #[serde(with = "common_utils::custom_serde::iso8601::option")]
    pub start_bucket: Option<PrimitiveDateTime>,
    #[serde(with = "common_utils::custom_serde::iso8601::option")]
    pub end_bucket: Option<PrimitiveDateTime>,
}

pub trait PaymentIntentMetricAnalytics: LoadRow<PaymentIntentMetricRow> {}

#[async_trait::async_trait]
pub trait PaymentIntentMetric<T>
where
    T: AnalyticsDataSource + PaymentIntentMetricAnalytics,
{
    async fn load_metrics(
        &self,
        dimensions: &[PaymentIntentDimensions],
        auth: &AuthInfo,
        filters: &PaymentIntentFilters,
        granularity: Option<Granularity>,
        time_range: &TimeRange,
        pool: &T,
    ) -> MetricsResult<HashSet<(PaymentIntentMetricsBucketIdentifier, PaymentIntentMetricRow)>>;
}

#[async_trait::async_trait]
impl<T> PaymentIntentMetric<T> for PaymentIntentMetrics
where
    T: AnalyticsDataSource + PaymentIntentMetricAnalytics,
    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: &[PaymentIntentDimensions],
        auth: &AuthInfo,
        filters: &PaymentIntentFilters,
        granularity: Option<Granularity>,
        time_range: &TimeRange,
        pool: &T,
    ) -> MetricsResult<HashSet<(PaymentIntentMetricsBucketIdentifier, PaymentIntentMetricRow)>>
    {
        match self {
            Self::SuccessfulSmartRetries => {
                SuccessfulSmartRetries
                    .load_metrics(dimensions, auth, filters, granularity, time_range, pool)
                    .await
            }
            Self::TotalSmartRetries => {
                TotalSmartRetries
                    .load_metrics(dimensions, auth, filters, granularity, time_range, pool)
                    .await
            }
            Self::SmartRetriedAmount => {
                SmartRetriedAmount
                    .load_metrics(dimensions, auth, filters, granularity, time_range, pool)
                    .await
            }
            Self::PaymentIntentCount => {
                PaymentIntentCount
                    .load_metrics(dimensions, auth, filters, granularity, time_range, pool)
                    .await
            }
            Self::PaymentsSuccessRate => {
                PaymentsSuccessRate
                    .load_metrics(dimensions, auth, filters, granularity, time_range, pool)
                    .await
            }
            Self::PaymentProcessedAmount => {
                PaymentProcessedAmount
                    .load_metrics(dimensions, auth, filters, granularity, time_range, pool)
                    .await
            }
            Self::SessionizedSuccessfulSmartRetries => {
                sessionized_metrics::SuccessfulSmartRetries
                    .load_metrics(dimensions, auth, filters, granularity, time_range, pool)
                    .await
            }
            Self::SessionizedTotalSmartRetries => {
                sessionized_metrics::TotalSmartRetries
                    .load_metrics(dimensions, auth, filters, granularity, time_range, pool)
                    .await
            }
            Self::SessionizedSmartRetriedAmount => {
                sessionized_metrics::SmartRetriedAmount
                    .load_metrics(dimensions, auth, filters, granularity, time_range, pool)
                    .await
            }
            Self::SessionizedPaymentIntentCount => {
                sessionized_metrics::PaymentIntentCount
                    .load_metrics(dimensions, auth, filters, granularity, time_range, pool)
                    .await
            }
            Self::SessionizedPaymentsSuccessRate => {
                sessionized_metrics::PaymentsSuccessRate
                    .load_metrics(dimensions, auth, filters, granularity, time_range, pool)
                    .await
            }
            Self::SessionizedPaymentProcessedAmount => {
                sessionized_metrics::PaymentProcessedAmount
                    .load_metrics(dimensions, auth, filters, granularity, time_range, pool)
                    .await
            }
            Self::SessionizedPaymentsDistribution => {
                sessionized_metrics::PaymentsDistribution
                    .load_metrics(dimensions, auth, filters, granularity, time_range, pool)
                    .await
            }
        }
    }
}


pub struct ThunesRefundRequest {
    pub amount: StringMinorUnit,
}


pub struct Payouts {
    pub payout_id: common_utils::id_type::PayoutId,
    pub merchant_id: common_utils::id_type::MerchantId,
    pub customer_id: Option<common_utils::id_type::CustomerId>,
    pub address_id: Option<String>,
    pub payout_type: Option<storage_enums::PayoutType>,
    pub payout_method_id: Option<String>,
    pub amount: MinorUnit,
    pub destination_currency: storage_enums::Currency,
    pub source_currency: storage_enums::Currency,
    pub description: Option<String>,
    pub recurring: bool,
    pub auto_fulfill: bool,
    pub return_url: Option<String>,
    pub entity_type: storage_enums::PayoutEntityType,
    pub metadata: Option<pii::SecretSerdeValue>,
    #[serde(with = "common_utils::custom_serde::iso8601")]
    pub created_at: PrimitiveDateTime,
    #[serde(with = "common_utils::custom_serde::iso8601")]
    pub last_modified_at: PrimitiveDateTime,
    pub attempt_count: i16,
    pub profile_id: common_utils::id_type::ProfileId,
    pub status: storage_enums::PayoutStatus,
    pub confirm: Option<bool>,
    pub payout_link_id: Option<String>,
    pub client_secret: Option<String>,
    pub priority: Option<storage_enums::PayoutSendPriority>,
}


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


pub struct PayeezyRouterData<T> {
    pub amount: String,
    pub router_data: T,
}


Documentation: api-reference/v2/customers/customers--update.mdx
# Type: Doc File

---
openapi: post /v2/customers/{id}
---


pub async fn get_filters(
    pool: &AnalyticsProvider,
    req: GetFrmFilterRequest,
    merchant_id: &common_utils::id_type::MerchantId,
) -> AnalyticsResult<FrmFiltersResponse> {
    let mut res = FrmFiltersResponse::default();
    for dim in req.group_by_names {
        let values = match pool {
            AnalyticsProvider::Sqlx(pool) => {
    get_frm_filter_for_dimension(dim, merchant_id, &req.time_range, pool)
        .await
}
            AnalyticsProvider::Clickhouse(pool) => {
    get_frm_filter_for_dimension(dim, merchant_id, &req.time_range, pool)
        .await
}
        AnalyticsProvider::CombinedCkh(sqlx_pool, ckh_pool) => {
    let ckh_result = get_frm_filter_for_dimension(
        dim,
        merchant_id,
        &req.time_range,
        ckh_pool,
    )
    .await;
    let sqlx_result = get_frm_filter_for_dimension(
        dim,
        merchant_id,
        &req.time_range,
        sqlx_pool,
    )
    .await;
    match (&sqlx_result, &ckh_result) {
        (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => {
            logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres frm analytics filters")
        },
        _ => {}
    };
    ckh_result
}
        AnalyticsProvider::CombinedSqlx(sqlx_pool, ckh_pool) => {
    let ckh_result = get_frm_filter_for_dimension(
        dim,
        merchant_id,
        &req.time_range,
        ckh_pool,
    )
    .await;
    let sqlx_result = get_frm_filter_for_dimension(
        dim,
        merchant_id,
        &req.time_range,
        sqlx_pool,
    )
    .await;
    match (&sqlx_result, &ckh_result) {
        (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => {
            logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres frm analytics filters")
        },
        _ => {}
    };
    sqlx_result
}
}
        .change_context(AnalyticsError::UnknownError)?
        .into_iter()
        .filter_map(|fil: FrmFilterRow| match dim {
            FrmDimensions::FrmStatus => fil.frm_status.map(|i| i.as_ref().to_string()),
            FrmDimensions::FrmName => fil.frm_name,
            FrmDimensions::FrmTransactionType => {
                fil.frm_transaction_type.map(|i| i.as_ref().to_string())
            }
        })
        .collect::<Vec<String>>();
        res.query_data.push(FrmFilterValue {
            dimension: dim,
            values,
        })
    }
    Ok(res)
}


pub struct RedsysEmv3DSData {
    protocol_version: String,
    three_d_s_server_trans_i_d: Option<String>,
    three_d_s_info: Option<RedsysThreeDsInfo>,
    three_d_s_method_u_r_l: Option<String>,
    acs_u_r_l: Option<String>,
    creq: Option<String>,
}


pub struct BillingConnectorPaymentsSyncRequest {
    /// unique id for making billing connector psync call
    pub billing_connector_psync_id: String,
    /// connector params of the connector
    pub connector_params: connector_endpoints::ConnectorParams,
}


impl Responder {
        let flow = AnalyticsFlow::GetPaymentFilters;
        Box::pin(api::server_wrap(
            flow,
            state,
            &req,
            json_payload.into_inner(),
            |state, auth: AuthenticationData, req, _| async move {
                let org_id = auth.merchant_account.get_org_id();
                let auth: AuthInfo = AuthInfo::OrgLevel {
                    org_id: org_id.clone(),
                };
                analytics::payments::get_filters(&state.pool, req, &auth)
                    .await
                    .map(ApplicationResponse::Json)
            },
            auth::auth_type(
                &auth::PlatformOrgAdminAuth {
                    is_admin_auth_allowed: false,
                    organization_id: None,
                },
                &auth::JWTAuth {
                    permission: Permission::OrganizationAnalyticsRead,
                },
                req.headers(),
            ),
            api_locking::LockAction::NotApplicable,
        ))
        .await
    }


pub async fn is_stream_available(&self, stream_index: u8) -> bool {
        let stream_key_flag = self.get_stream_key_flag(stream_index);

        match self
            .redis_conn
            .set_key_if_not_exists_with_expiry(&stream_key_flag.as_str().into(), true, None)
            .await
        {
            Ok(resp) => resp == redis::types::SetnxReply::KeySet,
            Err(error) => {
                logger::error!(operation="lock_stream",err=?error);
                false
            }
        }
    }


OpenAPI Block Path: components.schemas.UpdateApiKeyRequest
{
  "type": "object",
  "description": "The request body for updating an API Key.",
  "properties": {
    "name": {
      "type": "string",
      "description": "A unique name for the API Key to help you identify it.",
      "example": "Sandbox integration key",
      "nullable": true,
      "maxLength": 64
    },
    "description": {
      "type": "string",
      "description": "A description to provide more context about the API Key.",
      "example": "Key used by our developers to integrate with the sandbox environment",
      "nullable": true,
      "maxLength": 256
    },
    "expiration": {
      "allOf": [
        {
          "$ref": "#/components/schemas/ApiKeyExpiration"
        }
      ],
      "nullable": true
    }
  },
  "additionalProperties": false
}


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


pub struct PayoneAuthType {
    pub(super) api_key: Secret<String>,
    pub merchant_account: Secret<String>,
    pub api_secret: Secret<String>,
}


pub async fn refund_create_core(
    state: SessionState,
    merchant_context: domain::MerchantContext,
    req: refunds::RefundsCreateRequest,
    global_refund_id: id_type::GlobalRefundId,
) -> errors::RouterResponse<refunds::RefundResponse> {
    let db = &*state.store;
    let (payment_intent, payment_attempt, amount);

    payment_intent = db
        .find_payment_intent_by_id(
            &(&state).into(),
            &req.payment_id,
            merchant_context.get_merchant_key_store(),
            merchant_context.get_merchant_account().storage_scheme,
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;

    utils::when(
        !(payment_intent.status == enums::IntentStatus::Succeeded
            || payment_intent.status == enums::IntentStatus::PartiallyCaptured),
        || {
            Err(report!(errors::ApiErrorResponse::PaymentUnexpectedState {
                current_flow: "refund".into(),
                field_name: "status".into(),
                current_value: payment_intent.status.to_string(),
                states: "succeeded, partially_captured".to_string()
            })
            .attach_printable("unable to refund for a unsuccessful payment intent"))
        },
    )?;

    let captured_amount = payment_intent
        .amount_captured
        .ok_or(errors::ApiErrorResponse::InternalServerError)
        .attach_printable("amount captured is none in a successful payment")?;

    // Amount is not passed in request refer from payment intent.
    amount = req.amount.unwrap_or(captured_amount);

    utils::when(amount <= common_utils_types::MinorUnit::new(0), || {
        Err(report!(errors::ApiErrorResponse::InvalidDataFormat {
            field_name: "amount".to_string(),
            expected_format: "positive integer".to_string()
        })
        .attach_printable("amount less than or equal to zero"))
    })?;

    payment_attempt = db
        .find_payment_attempt_last_successful_or_partially_captured_attempt_by_payment_id(
            &(&state).into(),
            merchant_context.get_merchant_key_store(),
            &req.payment_id,
            merchant_context.get_merchant_account().storage_scheme,
        )
        .await
        .to_not_found_response(errors::ApiErrorResponse::SuccessfulPaymentNotFound)?;

    tracing::Span::current().record("global_refund_id", global_refund_id.get_string_repr());

    let merchant_connector_details = req.merchant_connector_details.clone();

    Box::pin(validate_and_create_refund(
        &state,
        &merchant_context,
        &payment_attempt,
        &payment_intent,
        amount,
        req,
        global_refund_id,
        merchant_connector_details,
    ))
    .await
    .map(services::ApplicationResponse::Json)
}


pub struct ArchipelOrderRequest {
    amount: MinorUnit,
    currency: String,
    certainty: ArchipelPaymentCertainty,
    initiator: ArchipelPaymentInitiator,
}


impl ConnectorValidation for Santander {
    //TODO: implement functions when support enabled
}

impl ConnectorValidation for for Santander


pub struct GatewayResponse {
    gateway_transaction_id: Option<String>,
    transaction_state: FiservPaymentStatus,
    transaction_processing_details: TransactionProcessingDetails,
}


pub struct GetTokenizePayloadRequest {
    pub lookup_key: String,
    pub service_name: String,
    pub get_value2: bool,
}


pub struct AuthipayCaptureRequest {
    request_type: &'static str,
    transaction_amount: Amount,
}


pub struct HipayAuthType {
    pub(super) api_key: Secret<String>,
    pub(super) key1: Secret<String>,
}


pub async fn proxy_for_payments_core<F, Res, Req, Op, FData, D>(
    state: SessionState,
    req_state: ReqState,
    merchant_context: domain::MerchantContext,
    profile_id: Option<id_type::ProfileId>,
    operation: Op,
    req: Req,
    auth_flow: services::AuthFlow,
    call_connector_action: CallConnectorAction,
    header_payload: HeaderPayload,
    return_raw_connector_response: Option<bool>,
) -> RouterResponse<Res>
where
    F: Send + Clone + Sync,
    FData: Send + Sync + Clone,
    Op: Operation<F, Req, Data = D> + Send + Sync + Clone,
    Req: Debug + Authenticate + Clone,
    D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
    Res: transformers::ToResponse<F, D, Op>,
    // To create connector flow specific interface data
    D: ConstructFlowSpecificData<F, FData, router_types::PaymentsResponseData>,
    RouterData<F, FData, router_types::PaymentsResponseData>: Feature<F, FData>,

    // To construct connector flow specific api
    dyn api::Connector:
        services::api::ConnectorIntegration<F, FData, router_types::PaymentsResponseData>,

    // To perform router related operation for PaymentResponse
    PaymentResponse: Operation<F, FData, Data = D>,
{
    let (payment_data, _req, customer, connector_http_status_code, external_latency) =
        proxy_for_payments_operation_core::<_, _, _, _, _>(
            &state,
            req_state,
            merchant_context,
            profile_id,
            operation.clone(),
            req,
            call_connector_action,
            auth_flow,
            header_payload.clone(),
            return_raw_connector_response,
        )
        .await?;

    Res::generate_response(
        payment_data,
        customer,
        auth_flow,
        &state.base_url,
        operation,
        &state.conf.connector_request_reference_id_config,
        connector_http_status_code,
        external_latency,
        header_payload.x_hs_latency,
    )
}


Web Documentation: Get Started | Hyperswitch
# Type: Web Doc

At Juspay, we believe payment infrastructure should be transparent, adaptable, and under merchants' control—not confined by vendor restrictions. That's why we've made the bold move to open-source our Payment Orchestrator.
This modular, composable platform allows you to deploy on-premise for full access or select only the components you need.
To run Hyperswitch locally, follow our Docker setup guide. Alternatively, explore our cloud deployment guides for additional options.
Local Setup
Unified Local Setup using Docker (Recommended)
Simplified local setup of all components, using a single command with the help of docker.
Development Environment Setup
Local setup of development environment to build with Hyperswitch
Cloud Setup
Deploy Hyperswitch on AWS
Deploy Hyperswitch on AWS either as an independent stack or as individual components.
Deploy Hyperswitch on Kubernetes
Install Hyperswitch on GCP or Azure using our Helm charts.
Want to try without deploying? Use our dashboard on
sandbox
.
Learning Resources
In case you are completely new to payments - You can comeback to setup after reading the
Payment 101 Blog
, For more learning resources visit
Hyperswitch Blogs.
Have Questions?
Join our
Slack Community
Prefer direct support? Use our
Contact Us
page to reach out.
6 months ago
Was this helpful?

Get Started | Hyperswitch


impl<T> EncryptedJsonType<T>
    where
        T: std::fmt::Debug + Clone + serde::Serialize + serde::de::DeserializeOwned,
    {
        fn serialize_json_bytes(&self) -> CustomResult<Secret<Vec<u8>>, errors::CryptoError> {
            common_utils::ext_traits::Encode::encode_to_vec(self.inner())
                .change_context(errors::CryptoError::EncodingFailed)
                .attach_printable("Failed to JSON serialize data before encryption")
                .map(Secret::new)
        }

        fn deserialize_json_bytes<S>(
            bytes: Secret<Vec<u8>>,
        ) -> CustomResult<Secret<Self, S>, errors::ParsingError>
        where
            S: masking::Strategy<Self>,
        {
            bytes
                .peek()
                .as_slice()
                .parse_struct::<T>(std::any::type_name::<T>())
                .map(|result| Secret::new(Self::from(result)))
                .attach_printable("Failed to JSON deserialize data after decryption")
        }
    }


/// Represents a hashed string using blake3's hashing strategy.
pub struct HashedString<T: Strategy<String>


pub struct MerchantInitiatedTransaction {
    reason: Option<String>,
    previous_transaction_id: Option<Secret<String>>,
    //Required for recurring mandates payment
    original_authorized_amount: Option<String>,
}


OpenAPI Block Path: components.schemas.OpenBankingResponse
{
  "allOf": [
    {
      "allOf": [
        {
          "$ref": "#/components/schemas/OpenBankingData"
        }
      ],
      "nullable": true
    },
    {
      "type": "object"
    }
  ]
}


impl api::RefundExecute for Barclaycard {}

impl api::RefundExecute for for Barclaycard


pub struct GatewayStatusMap {
    pub connector: String,
    pub flow: String,
    pub sub_flow: String,
    pub code: String,
    pub message: String,
    pub status: String,
    pub router_error: Option<String>,
    pub decision: String,
    #[serde(with = "custom_serde::iso8601")]
    pub created_at: PrimitiveDateTime,
    #[serde(with = "custom_serde::iso8601")]
    pub last_modified: PrimitiveDateTime,
    pub step_up_possible: bool,
    pub unified_code: Option<String>,
    pub unified_message: Option<String>,
    pub error_category: Option<ErrorCategory>,
    pub clear_pan_possible: bool,
    pub feature_data: Option<common_types::domain::GsmFeatureData>,
    pub feature: Option<common_enums::GsmFeature>,
}


pub async fn connector_delete() {}


pub async fn start_web_server(
    conf: Settings,
    stores: HashMap<id_type::TenantId, Arc<Store>>,
) -> Result<Server, errors::DrainerError> {
    let server = conf.server.clone();
    let web_server = actix_web::HttpServer::new(move || {
        actix_web::App::new().service(health_check::Health::server(conf.clone(), stores.clone()))
    })
    .bind((server.host.as_str(), server.port))?
    .run();
    let _ = web_server.handle();

    Ok(web_server)
}


pub struct FrmConfigs {
    ///this is the connector that can be used for the payment
    #[schema(value_type = ConnectorType, example = "payment_processor")]
    pub gateway: Option<api_enums::Connector>,
    ///payment methods that can be used in the payment
    pub payment_methods: Vec<FrmPaymentMethod>,
}


File: crates/test_utils/src/lib.rs

#![allow(clippy::print_stdout, clippy::print_stderr)]

pub mod connector_auth;
pub mod newman_runner;


pub async fn compatibility_api_wrap<'a, 'b, U, T, Q, F, Fut, S, E, E2>(
    flow: impl router_env::types::FlowMetric,
    state: Arc<AppState>,
    request: &'a HttpRequest,
    payload: T,
    func: F,
    api_authentication: &dyn auth::AuthenticateAndFetch<U, SessionState>,
    lock_action: api_locking::LockAction,
) -> HttpResponse
where
    F: Fn(SessionState, U, T, ReqState) -> Fut,
    Fut: Future<Output = CustomResult<api::ApplicationResponse<Q>, E2>>,
    E2: ErrorSwitch<E> + std::error::Error + Send + Sync + 'static,
    Q: Serialize + std::fmt::Debug + 'a + ApiEventMetric,
    S: TryFrom<Q> + Serialize,
    E: Serialize + error_stack::Context + actix_web::ResponseError + Clone,
    error_stack::Report<E>: services::EmbedError,
    errors::ApiErrorResponse: ErrorSwitch<E>,
    T: std::fmt::Debug + Serialize + ApiEventMetric,
{
    let request_method = request.method().as_str();
    let url_path = request.path();
    tracing::Span::current().record("request_method", request_method);
    tracing::Span::current().record("request_url_path", url_path);

    let start_instant = Instant::now();
    logger::info!(tag = ?Tag::BeginRequest, payload = ?payload);

    let server_wrap_util_res = api::server_wrap_util(
        &flow,
        state.clone().into(),
        request.headers(),
        request,
        payload,
        func,
        api_authentication,
        lock_action,
    )
    .await
    .map(|response| {
        logger::info!(api_response =? response);
        response
    });

    let res = match server_wrap_util_res {
        Ok(api::ApplicationResponse::Json(response)) => {
            let response = S::try_from(response);
            match response {
                Ok(response) => match serde_json::to_string(&response) {
                    Ok(res) => api::http_response_json(res),
                    Err(_) => api::http_response_err(
                        r#"{
                                "error": {
                                    "message": "Error serializing response from connector"
                                }
                            }"#,
                    ),
                },
                Err(_) => api::http_response_err(
                    r#"{
                        "error": {
                            "message": "Error converting juspay response to stripe response"
                        }
                    }"#,
                ),
            }
        }
        Ok(api::ApplicationResponse::JsonWithHeaders((response, headers))) => {
            let response = S::try_from(response);
            match response {
                Ok(response) => match serde_json::to_string(&response) {
                    Ok(res) => api::http_response_json_with_headers(res, headers, None, None),
                    Err(_) => api::http_response_err(
                        r#"{
                                "error": {
                                    "message": "Error serializing response from connector"
                                }
                            }"#,
                    ),
                },
                Err(_) => api::http_response_err(
                    r#"{
                        "error": {
                            "message": "Error converting juspay response to stripe response"
                        }
                    }"#,
                ),
            }
        }
        Ok(api::ApplicationResponse::StatusOk) => api::http_response_ok(),
        Ok(api::ApplicationResponse::TextPlain(text)) => api::http_response_plaintext(text),
        Ok(api::ApplicationResponse::FileData((file_data, content_type))) => {
            api::http_response_file_data(file_data, content_type)
        }
        Ok(api::ApplicationResponse::JsonForRedirection(response)) => {
            match serde_json::to_string(&response) {
                Ok(res) => api::http_redirect_response(res, response),
                Err(_) => api::http_response_err(
                    r#"{
                    "error": {
                        "message": "Error serializing response from connector"
                    }
                }"#,
                ),
            }
        }
        Ok(api::ApplicationResponse::Form(redirection_data)) => {
            let config = state.conf();
            api::build_redirection_form(
                &redirection_data.redirect_form,
                redirection_data.payment_method_data,
                redirection_data.amount,
                redirection_data.currency,
                config,
            )
            .respond_to(request)
            .map_into_boxed_body()
        }

        Ok(api::ApplicationResponse::GenericLinkForm(boxed_generic_link_data)) => {
            let link_type = (boxed_generic_link_data).data.to_string();
            match services::generic_link_response::build_generic_link_html(
                boxed_generic_link_data.data,
                boxed_generic_link_data.locale,
            ) {
                Ok(rendered_html) => api::http_response_html_data(rendered_html, None),
                Err(_) => {
                    api::http_response_err(format!("Error while rendering {link_type} HTML page"))
                }
            }
        }

        Ok(api::ApplicationResponse::PaymentLinkForm(boxed_payment_link_data)) => {
            match *boxed_payment_link_data {
                api::PaymentLinkAction::PaymentLinkFormData(payment_link_data) => {
                    match api::build_payment_link_html(payment_link_data) {
                        Ok(rendered_html) => api::http_response_html_data(rendered_html, None),
                        Err(_) => api::http_response_err(
                            r#"{
                                "error": {
                                    "message": "Error while rendering payment link html page"
                                }
                            }"#,
                        ),
                    }
                }
                api::PaymentLinkAction::PaymentLinkStatus(payment_link_data) => {
                    match api::get_payment_link_status(payment_link_data) {
                        Ok(rendered_html) => api::http_response_html_data(rendered_html, None),
                        Err(_) => api::http_response_err(
                            r#"{
                                "error": {
                                    "message": "Error while rendering payment link status page"
                                }
                            }"#,
                        ),
                    }
                }
            }
        }
        Err(error) => api::log_and_return_error_response(error),
    };

    let response_code = res.status().as_u16();
    let end_instant = Instant::now();
    let request_duration = end_instant.saturating_duration_since(start_instant);
    logger::info!(
        tag = ?Tag::EndRequest,
        status_code = response_code,
        time_taken_ms = request_duration.as_millis(),
    );

    res
}


pub struct WellsfargoPaymentsIncrementalAuthorizationRequest {
    processing_information: ProcessingInformation,
    order_information: OrderInformationIncrementalAuthorization,
}


pub struct MultisafepayAuthType {
    pub(super) api_key: Secret<String>,
}


impl DecisionEngineSuccessRateData {
    pub fn update(&mut self, new_config: Self) {
        if let Some(threshold) = new_config.default_latency_threshold {
            self.default_latency_threshold = Some(threshold);
        }
        if let Some(bucket_size) = new_config.default_bucket_size {
            self.default_bucket_size = Some(bucket_size);
        }
        if let Some(hedging_percent) = new_config.default_hedging_percent {
            self.default_hedging_percent = Some(hedging_percent);
        }
        if let Some(lower_reset_factor) = new_config.default_lower_reset_factor {
            self.default_lower_reset_factor = Some(lower_reset_factor);
        }
        if let Some(upper_reset_factor) = new_config.default_upper_reset_factor {
            self.default_upper_reset_factor = Some(upper_reset_factor);
        }
        if let Some(gateway_extra_score) = new_config.default_gateway_extra_score {
            self.default_gateway_extra_score
                .as_mut()
                .map(|score| score.extend(gateway_extra_score));
        }
        if let Some(sub_level_input_config) = new_config.sub_level_input_config {
            self.sub_level_input_config.as_mut().map(|config| {
                config.extend(sub_level_input_config);
            });
        }
    }
}


pub struct CardInfoMigrateResponseRecord {
    pub card_iin: Option<String>,
    pub card_issuer: Option<String>,
    pub card_network: Option<String>,
    pub card_type: Option<String>,
    pub card_sub_type: Option<String>,
    pub card_issuing_country: Option<String>,
}


/// Uses the [`Debug`][Debug] implementation of a type to derive its [`Display`][Display]
/// implementation.
///
/// Causes a compilation error if the type doesn't implement the [`Debug`][Debug] trait.
///
/// [Debug]: ::core::fmt::Debug
/// [Display]: ::core::fmt::Display
///
/// # Example
///
/// ```
/// use router_derive::DebugAsDisplay;
///
/// #[derive(Debug, DebugAsDisplay)]
/// struct Point {
///     x: f32,
///     y: f32,
/// }
///
/// #[derive(Debug, DebugAsDisplay)]
/// enum Color {
///     Red,
///     Green,
///     Blue,
/// }
/// ```
#[proc_macro_derive(DebugAsDisplay)]
pub fn debug_as_display_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let ast = syn::parse_macro_input!(input as syn::DeriveInput);

    let tokens =
        macros::debug_as_display_inner(&ast).unwrap_or_else(|error| error.to_compile_error());
    tokens.into()
}


File: crates/common_utils/src/custom_serde.rs
 Public functions: 14

//! Custom serialization/deserialization implementations.

/// Use the well-known ISO 8601 format when serializing and deserializing an
/// [`PrimitiveDateTime`][PrimitiveDateTime].
///
/// [PrimitiveDateTime]: ::time::PrimitiveDateTime
pub mod iso8601 {
    use std::num::NonZeroU8;

    use serde::{ser::Error as _, Deserializer, Serialize, Serializer};
    use time::{
        format_description::well_known::{
            iso8601::{Config, EncodedConfig, TimePrecision},
            Iso8601,
        },
        serde::iso8601,
        PrimitiveDateTime, UtcOffset,
    };

    const FORMAT_CONFIG: EncodedConfig = Config::DEFAULT
        .set_time_precision(TimePrecision::Second {
            decimal_digits: NonZeroU8::new(3),
        })
        .encode();

    /// Serialize a [`PrimitiveDateTime`] using the well-known ISO 8601 format.
    pub fn serialize<S>(date_time: &PrimitiveDateTime, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        date_time
            .assume_utc()
            .format(&Iso8601::<FORMAT_CONFIG>)
            .map_err(S::Error::custom)?
            .serialize(serializer)
    }

    /// Deserialize an [`PrimitiveDateTime`] from its ISO 8601 representation.
    pub fn deserialize<'a, D>(deserializer: D) -> Result<PrimitiveDateTime, D::Error>
    where
        D: Deserializer<'a>,
    {
        iso8601::deserialize(deserializer).map(|offset_date_time| {
            let utc_date_time = offset_date_time.to_offset(UtcOffset::UTC);
            PrimitiveDateTime::new(utc_date_time.date(), utc_date_time.time())
        })
    }

    /// Use the well-known ISO 8601 format when serializing and deserializing an
    /// [`Option<PrimitiveDateTime>`][PrimitiveDateTime].
    ///
    /// [PrimitiveDateTime]: ::time::PrimitiveDateTime
    pub mod option {
        use serde::Serialize;
        use time::format_description::well_known::Iso8601;

        use super::*;

        /// Serialize an [`Option<PrimitiveDateTime>`] using the well-known ISO 8601 format.
        pub fn serialize<S>(
            date_time: &Option<PrimitiveDateTime>,
            serializer: S,
        ) -> Result<S::Ok, S::Error>
        where
            S: Serializer,
        {
            date_time
                .map(|date_time| date_time.assume_utc().format(&Iso8601::<FORMAT_CONFIG>))
                .transpose()
                .map_err(S::Error::custom)?
                .serialize(serializer)
        }

        /// Deserialize an [`Option<PrimitiveDateTime>`] from its ISO 8601 representation.
        pub fn deserialize<'a, D>(deserializer: D) -> Result<Option<PrimitiveDateTime>, D::Error>
        where
            D: Deserializer<'a>,
        {
            iso8601::option::deserialize(deserializer).map(|option_offset_date_time| {
                option_offset_date_time.map(|offset_date_time| {
                    let utc_date_time = offset_date_time.to_offset(UtcOffset::UTC);
                    PrimitiveDateTime::new(utc_date_time.date(), utc_date_time.time())
                })
            })
        }
    }
    /// Use the well-known ISO 8601 format which is without timezone when serializing and deserializing an
    /// [`Option<PrimitiveDateTime>`][PrimitiveDateTime].
    ///
    /// [PrimitiveDateTime]: ::time::PrimitiveDateTime
    pub mod option_without_timezone {
        use serde::{de, Deserialize, Serialize};
        use time::macros::format_description;

        use super::*;

        /// Serialize an [`Option<PrimitiveDateTime>`] using the well-known ISO 8601 format which is without timezone.
        pub fn serialize<S>(
            date_time: &Option<PrimitiveDateTime>,
            serializer: S,
        ) -> Result<S::Ok, S::Error>
        where
            S: Serializer,
        {
            date_time
                .map(|date_time| {
                    let format =
                        format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]");
                    date_time.assume_utc().format(format)
                })
                .transpose()
                .map_err(S::Error::custom)?
                .serialize(serializer)
        }

        /// Deserialize an [`Option<PrimitiveDateTime>`] from its ISO 8601 representation.
        pub fn deserialize<'a, D>(deserializer: D) -> Result<Option<PrimitiveDateTime>, D::Error>
        where
            D: Deserializer<'a>,
        {
            Option::deserialize(deserializer)?
                .map(|time_string| {
                    let format =
                        format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]");
                    PrimitiveDateTime::parse(time_string, format).map_err(|_| {
                        de::Error::custom(format!(
                            "Failed to parse PrimitiveDateTime from {time_string}"
                        ))
                    })
                })
                .transpose()
        }
    }
}

/// Use the UNIX timestamp when serializing and deserializing an
/// [`PrimitiveDateTime`][PrimitiveDateTime].
///
/// [PrimitiveDateTime]: ::time::PrimitiveDateTime
pub mod timestamp {

    use serde::{Deserializer, Serialize, Serializer};
    use time::{serde::timestamp, PrimitiveDateTime, UtcOffset};

    /// Serialize a [`PrimitiveDateTime`] using UNIX timestamp.
    pub fn serialize<S>(date_time: &PrimitiveDateTime, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        date_time
            .assume_utc()
            .unix_timestamp()
            .serialize(serializer)
    }

    /// Deserialize an [`PrimitiveDateTime`] from UNIX timestamp.
    pub fn deserialize<'a, D>(deserializer: D) -> Result<PrimitiveDateTime, D::Error>
    where
        D: Deserializer<'a>,
    {
        timestamp::deserialize(deserializer).map(|offset_date_time| {
            let utc_date_time = offset_date_time.to_offset(UtcOffset::UTC);
            PrimitiveDateTime::new(utc_date_time.date(), utc_date_time.time())
        })
    }

    /// Use the UNIX timestamp when serializing and deserializing an
    /// [`Option<PrimitiveDateTime>`][PrimitiveDateTime].
    ///
    /// [PrimitiveDateTime]: ::time::PrimitiveDateTime
    pub mod option {
        use serde::Serialize;

        use super::*;

        /// Serialize an [`Option<PrimitiveDateTime>`] from UNIX timestamp.
        pub fn serialize<S>(
            date_time: &Option<PrimitiveDateTime>,
            serializer: S,
        ) -> Result<S::Ok, S::Error>
        where
            S: Serializer,
        {
            date_time
                .map(|date_time| date_time.assume_utc().unix_timestamp())
                .serialize(serializer)
        }

        /// Deserialize an [`Option<PrimitiveDateTime>`] from UNIX timestamp.
        pub fn deserialize<'a, D>(deserializer: D) -> Result<Option<PrimitiveDateTime>, D::Error>
        where
            D: Deserializer<'a>,
        {
            timestamp::option::deserialize(deserializer).map(|option_offset_date_time| {
                option_offset_date_time.map(|offset_date_time| {
                    let utc_date_time = offset_date_time.to_offset(UtcOffset::UTC);
                    PrimitiveDateTime::new(utc_date_time.date(), utc_date_time.time())
                })
            })
        }
    }
}

/// <https://github.com/serde-rs/serde/issues/994#issuecomment-316895860>
pub mod json_string {
    use serde::{
        de::{self, Deserialize, DeserializeOwned, Deserializer},
        ser::{self, Serialize, Serializer},
    };

    /// Serialize a type to json_string format
    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
    where
        T: Serialize,
        S: Serializer,
    {
        let j = serde_json::to_string(value).map_err(ser::Error::custom)?;
        j.serialize(serializer)
    }

    /// Deserialize a string which is in json format
    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
    where
        T: DeserializeOwned,
        D: Deserializer<'de>,
    {
        let j = String::deserialize(deserializer)?;
        serde_json::from_str(&j).map_err(de::Error::custom)
    }
}

/// Use a custom ISO 8601 format when serializing and deserializing
/// [`PrimitiveDateTime`][PrimitiveDateTime].
///
/// [PrimitiveDateTime]: ::time::PrimitiveDateTime
pub mod iso8601custom {

    use serde::{ser::Error as _, Deserializer, Serialize, Serializer};
    use time::{
        format_description::well_known::{
            iso8601::{Config, EncodedConfig, TimePrecision},
            Iso8601,
        },
        serde::iso8601,
        PrimitiveDateTime, UtcOffset,
    };

    const FORMAT_CONFIG: EncodedConfig = Config::DEFAULT
        .set_time_precision(TimePrecision::Second {
            decimal_digits: None,
        })
        .encode();

    /// Serialize a [`PrimitiveDateTime`] using the well-known ISO 8601 format.
    pub fn serialize<S>(date_time: &PrimitiveDateTime, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        date_time
            .assume_utc()
            .format(&Iso8601::<FORMAT_CONFIG>)
            .map_err(S::Error::custom)?
            .replace('T', " ")
            .replace('Z', "")
            .serialize(serializer)
    }

    /// Deserialize an [`PrimitiveDateTime`] from its ISO 8601 representation.
    pub fn deserialize<'a, D>(deserializer: D) -> Result<PrimitiveDateTime, D::Error>
    where
        D: Deserializer<'a>,
    {
        iso8601::deserialize(deserializer).map(|offset_date_time| {
            let utc_date_time = offset_date_time.to_offset(UtcOffset::UTC);
            PrimitiveDateTime::new(utc_date_time.date(), utc_date_time.time())
        })
    }
}

#[cfg(test)]
mod tests {
    use serde::{Deserialize, Serialize};
    use serde_json::json;

    #[test]
    fn test_leap_second_parse() {
        #[derive(Serialize, Deserialize)]
        struct Try {
            #[serde(with = "crate::custom_serde::iso8601")]
            f: time::PrimitiveDateTime,
        }
        let leap_second_date_time = json!({"f": "2023-12-31T23:59:60.000Z"});
        let deser = serde_json::from_value::<Try>(leap_second_date_time);

        assert!(deser.is_ok())
    }
}