Test-Implementation Pair

Implementation: crates/api_models/src/enums.rs
================================================================================
use std::str::FromStr;

pub use common_enums::*;
use smithy::SmithyModel;
use utoipa::ToSchema;

pub use super::connector_enums::Connector;

#[derive(
    Clone,
    Copy,
    Debug,
    Eq,
    PartialEq,
    serde::Deserialize,
    serde::Serialize,
    strum::Display,
    strum::EnumString,
)]

/// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom'
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum RoutingAlgorithm {
    RoundRobin,
    MaxConversion,
    MinCost,
    Custom,
}

#[cfg(feature = "payouts")]
#[derive(
    Clone,
    Copy,
    Debug,
    Eq,
    Hash,
    PartialEq,
    serde::Serialize,
    serde::Deserialize,
    strum::Display,
    strum::EnumString,
    ToSchema,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum PayoutConnectors {
    Adyen,
    Adyenplatform,
    Cybersource,
    Ebanx,
    Gigadat,
    Loonio,
    Nomupay,
    Nuvei,
    Payone,
    Paypal,
    Stripe,
    Wise,
    Worldpay,
    Worldpayxml,
}

#[cfg(feature = "v2")]
/// Whether active attempt is to be set/unset
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)]
pub enum UpdateActiveAttempt {
    /// Request to set the active attempt id
    #[schema(value_type = Option<String>)]
    Set(common_utils::id_type::GlobalAttemptId),
    /// To unset the active attempt id
    Unset,
}

#[cfg(feature = "payouts")]
impl From<PayoutConnectors> for RoutableConnectors {
    fn from(value: PayoutConnectors) -> Self {
        match value {
            PayoutConnectors::Adyen => Self::Adyen,
            PayoutConnectors::Adyenplatform => Self::Adyenplatform,
            PayoutConnectors::Cybersource => Self::Cybersource,
            PayoutConnectors::Ebanx => Self::Ebanx,
            PayoutConnectors::Gigadat => Self::Gigadat,
            PayoutConnectors::Loonio => Self::Loonio,
            PayoutConnectors::Nomupay => Self::Nomupay,
            PayoutConnectors::Nuvei => Self::Nuvei,
            PayoutConnectors::Payone => Self::Payone,
            PayoutConnectors::Paypal => Self::Paypal,
            PayoutConnectors::Stripe => Self::Stripe,
            PayoutConnectors::Wise => Self::Wise,
            PayoutConnectors::Worldpay => Self::Worldpay,
            PayoutConnectors::Worldpayxml => Self::Worldpayxml,
        }
    }
}

#[cfg(feature = "payouts")]
impl From<PayoutConnectors> for Connector {
    fn from(value: PayoutConnectors) -> Self {
        match value {
            PayoutConnectors::Adyen => Self::Adyen,
            PayoutConnectors::Adyenplatform => Self::Adyenplatform,
            PayoutConnectors::Cybersource => Self::Cybersource,
            PayoutConnectors::Ebanx => Self::Ebanx,
            PayoutConnectors::Gigadat => Self::Gigadat,
            PayoutConnectors::Loonio => Self::Loonio,
            PayoutConnectors::Nomupay => Self::Nomupay,
            PayoutConnectors::Nuvei => Self::Nuvei,
            PayoutConnectors::Payone => Self::Payone,
            PayoutConnectors::Paypal => Self::Paypal,
            PayoutConnectors::Stripe => Self::Stripe,
            PayoutConnectors::Wise => Self::Wise,
            PayoutConnectors::Worldpay => Self::Worldpay,
            PayoutConnectors::Worldpayxml => Self::Worldpayxml,
        }
    }
}

#[cfg(feature = "payouts")]
impl TryFrom<Connector> for PayoutConnectors {
    type Error = String;
    fn try_from(value: Connector) -> Result<Self, Self::Error> {
        match value {
            Connector::Adyen => Ok(Self::Adyen),
            Connector::Adyenplatform => Ok(Self::Adyenplatform),
            Connector::Cybersource => Ok(Self::Cybersource),
            Connector::Ebanx => Ok(Self::Ebanx),
            Connector::Gigadat => Ok(Self::Gigadat),
            Connector::Loonio => Ok(Self::Loonio),
            Connector::Nuvei => Ok(Self::Nuvei),
            Connector::Nomupay => Ok(Self::Nomupay),
            Connector::Payone => Ok(Self::Payone),
            Connector::Paypal => Ok(Self::Paypal),
            Connector::Stripe => Ok(Self::Stripe),
            Connector::Wise => Ok(Self::Wise),
            Connector::Worldpay => Ok(Self::Worldpay),
            Connector::Worldpayxml => Ok(Self::Worldpayxml),
            _ => Err(format!("Invalid payout connector {value}")),
        }
    }
}

#[cfg(feature = "frm")]
#[derive(
    Clone,
    Copy,
    Debug,
    Eq,
    Hash,
    PartialEq,
    serde::Serialize,
    serde::Deserialize,
    strum::Display,
    strum::EnumString,
    ToSchema,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum FrmConnectors {
    /// Signifyd Risk Manager. Official docs: https://docs.signifyd.com/
    Signifyd,
    Riskified,
}

#[derive(
    Clone,
    Copy,
    Debug,
    Eq,
    Hash,
    PartialEq,
    serde::Serialize,
    serde::Deserialize,
    strum::Display,
    strum::EnumString,
    ToSchema,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum TaxConnectors {
    Taxjar,
}

#[derive(Clone, Debug, serde::Serialize, strum::EnumString, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum BillingConnectors {
    Chargebee,
    Recurly,
    Stripebilling,
    Custombilling,
    #[cfg(feature = "dummy_connector")]
    DummyBillingConnector,
}

#[derive(Clone, Copy, Debug, serde::Serialize, strum::EnumString, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum VaultConnectors {
    Vgs,
    HyperswitchVault,
    Tokenex,
}

impl From<VaultConnectors> for Connector {
    fn from(value: VaultConnectors) -> Self {
        match value {
            VaultConnectors::Vgs => Self::Vgs,
            VaultConnectors::HyperswitchVault => Self::HyperswitchVault,
            VaultConnectors::Tokenex => Self::Tokenex,
        }
    }
}

#[derive(
    Clone, Debug, serde::Deserialize, serde::Serialize, strum::Display, strum::EnumString, ToSchema,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum FrmAction {
    CancelTxn,
    AutoRefund,
    ManualReview,
}

#[derive(
    Clone, Debug, serde::Deserialize, serde::Serialize, strum::Display, strum::EnumString, ToSchema,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum FrmPreferredFlowTypes {
    Pre,
    Post,
}
#[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
pub struct UnresolvedResponseReason {
    pub code: String,
    /// A message to merchant to give hint on next action he/she should do to resolve
    pub message: String,
}

/// Possible field type of required fields in payment_method_data
#[derive(
    Clone,
    Debug,
    Eq,
    serde::Deserialize,
    serde::Serialize,
    strum::Display,
    strum::EnumString,
    ToSchema,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum FieldType {
    UserCardNumber,
    UserCardExpiryMonth,
    UserCardExpiryYear,
    UserCardCvc,
    UserCardNetwork,
    UserFullName,
    UserEmailAddress,
    UserPhoneNumber,
    UserPhoneNumberCountryCode,           //phone number's country code
    UserCountry { options: Vec<String> }, //for country inside payment method data ex- bank redirect
    UserCurrency { options: Vec<String> },
    UserCryptoCurrencyNetwork, //for crypto network associated with the cryptopcurrency
    UserBillingName,
    UserAddressLine1,
    UserAddressLine2,
    UserAddressCity,
    UserAddressPincode,
    UserAddressState,
    UserAddressCountry { options: Vec<String> },
    UserShippingName,
    UserShippingAddressLine1,
    UserShippingAddressLine2,
    UserShippingAddressCity,
    UserShippingAddressPincode,
    UserShippingAddressState,
    UserShippingAddressCountry { options: Vec<String> },
    UserSocialSecurityNumber,
    UserBlikCode,
    UserBank,
    UserBankOptions { options: Vec<String> },
    UserBankAccountNumber,
    UserSourceBankAccountId,
    UserDestinationBankAccountId,
    Text,
    DropDown { options: Vec<String> },
    UserDateOfBirth,
    UserVpaId,
    LanguagePreference { options: Vec<String> },
    UserPixKey,
    UserCpf,
    UserCnpj,
    UserIban,
    UserBsbNumber,
    UserBankSortCode,
    UserBankRoutingNumber,
    UserBankType { options: Vec<String> },
    UserBankAccountHolderName,
    UserMsisdn,
    UserClientIdentifier,
    OrderDetailsProductName,
}

impl FieldType {
    pub fn get_billing_variants() -> Vec<Self> {
        vec![
            Self::UserBillingName,
            Self::UserAddressLine1,
            Self::UserAddressLine2,
            Self::UserAddressCity,
            Self::UserAddressPincode,
            Self::UserAddressState,
            Self::UserAddressCountry { options: vec![] },
        ]
    }

    pub fn get_shipping_variants() -> Vec<Self> {
        vec![
            Self::UserShippingName,
            Self::UserShippingAddressLine1,
            Self::UserShippingAddressLine2,
            Self::UserShippingAddressCity,
            Self::UserShippingAddressPincode,
            Self::UserShippingAddressState,
            Self::UserShippingAddressCountry { options: vec![] },
        ]
    }
}

/// This implementatiobn is to ignore the inner value of UserAddressCountry enum while comparing
impl PartialEq for FieldType {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Self::UserCardNumber, Self::UserCardNumber) => true,
            (Self::UserCardExpiryMonth, Self::UserCardExpiryMonth) => true,
            (Self::UserCardExpiryYear, Self::UserCardExpiryYear) => true,
            (Self::UserCardCvc, Self::UserCardCvc) => true,
            (Self::UserFullName, Self::UserFullName) => true,
            (Self::UserEmailAddress, Self::UserEmailAddress) => true,
            (Self::UserPhoneNumber, Self::UserPhoneNumber) => true,
            (Self::UserPhoneNumberCountryCode, Self::UserPhoneNumberCountryCode) => true,
            (
                Self::UserCountry {
                    options: options_self,
                },
                Self::UserCountry {
                    options: options_other,
                },
            ) => options_self.eq(options_other),
            (
                Self::UserCurrency {
                    options: options_self,
                },
                Self::UserCurrency {
                    options: options_other,
                },
            ) => options_self.eq(options_other),
            (Self::UserCryptoCurrencyNetwork, Self::UserCryptoCurrencyNetwork) => true,
            (Self::UserBillingName, Self::UserBillingName) => true,
            (Self::UserAddressLine1, Self::UserAddressLine1) => true,
            (Self::UserAddressLine2, Self::UserAddressLine2) => true,
            (Self::UserAddressCity, Self::UserAddressCity) => true,
            (Self::UserAddressPincode, Self::UserAddressPincode) => true,
            (Self::UserAddressState, Self::UserAddressState) => true,
            (Self::UserAddressCountry { .. }, Self::UserAddressCountry { .. }) => true,
            (Self::UserShippingName, Self::UserShippingName) => true,
            (Self::UserShippingAddressLine1, Self::UserShippingAddressLine1) => true,
            (Self::UserShippingAddressLine2, Self::UserShippingAddressLine2) => true,
            (Self::UserShippingAddressCity, Self::UserShippingAddressCity) => true,
            (Self::UserShippingAddressPincode, Self::UserShippingAddressPincode) => true,
            (Self::UserShippingAddressState, Self::UserShippingAddressState) => true,
            (Self::UserShippingAddressCountry { .. }, Self::UserShippingAddressCountry { .. }) => {
                true
            }
            (Self::UserBlikCode, Self::UserBlikCode) => true,
            (Self::UserBank, Self::UserBank) => true,
            (Self::Text, Self::Text) => true,
            (
                Self::DropDown {
                    options: options_self,
                },
                Self::DropDown {
                    options: options_other,
                },
            ) => options_self.eq(options_other),
            (Self::UserDateOfBirth, Self::UserDateOfBirth) => true,
            (Self::UserVpaId, Self::UserVpaId) => true,
            (Self::UserPixKey, Self::UserPixKey) => true,
            (Self::UserCpf, Self::UserCpf) => true,
            (Self::UserCnpj, Self::UserCnpj) => true,
            (Self::LanguagePreference { .. }, Self::LanguagePreference { .. }) => true,
            (Self::UserMsisdn, Self::UserMsisdn) => true,
            (Self::UserClientIdentifier, Self::UserClientIdentifier) => true,
            (Self::OrderDetailsProductName, Self::OrderDetailsProductName) => true,
            _unused => false,
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_partialeq_for_field_type() {
        let user_address_country_is_us = FieldType::UserAddressCountry {
            options: vec!["US".to_string()],
        };

        let user_address_country_is_all = FieldType::UserAddressCountry {
            options: vec!["ALL".to_string()],
        };

        assert!(user_address_country_is_us.eq(&user_address_country_is_all))
    }
}

/// Denotes the retry action
#[derive(
    Debug,
    serde::Deserialize,
    serde::Serialize,
    strum::Display,
    strum::EnumString,
    Clone,
    PartialEq,
    Eq,
    ToSchema,
    SmithyModel,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
#[smithy(namespace = "com.hyperswitch.smithy.types")]
pub enum RetryAction {
    /// Manual retry through request is being deprecated, now it is available through profile
    ManualRetry,
    /// Denotes that the payment is requeued
    Requeue,
}

#[derive(Clone, Copy)]
pub enum LockerChoice {
    HyperswitchCardVault,
}

#[derive(
    Clone,
    Copy,
    Debug,
    Eq,
    PartialEq,
    serde::Serialize,
    serde::Deserialize,
    strum::Display,
    strum::EnumString,
    ToSchema,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum PmAuthConnectors {
    Plaid,
}

pub fn convert_pm_auth_connector(connector_name: &str) -> Option<PmAuthConnectors> {
    PmAuthConnectors::from_str(connector_name).ok()
}

pub fn convert_authentication_connector(connector_name: &str) -> Option<AuthenticationConnectors> {
    AuthenticationConnectors::from_str(connector_name).ok()
}

pub fn convert_tax_connector(connector_name: &str) -> Option<TaxConnectors> {
    TaxConnectors::from_str(connector_name).ok()
}

pub fn convert_billing_connector(connector_name: &str) -> Option<BillingConnectors> {
    BillingConnectors::from_str(connector_name).ok()
}
#[cfg(feature = "frm")]
pub fn convert_frm_connector(connector_name: &str) -> Option<FrmConnectors> {
    FrmConnectors::from_str(connector_name).ok()
}

pub fn convert_vault_connector(connector_name: &str) -> Option<VaultConnectors> {
    VaultConnectors::from_str(connector_name).ok()
}

#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, Hash)]
pub enum ReconPermissionScope {
    #[serde(rename = "R")]
    Read = 0,
    #[serde(rename = "RW")]
    Write = 1,
}

impl From<PermissionScope> for ReconPermissionScope {
    fn from(scope: PermissionScope) -> Self {
        match scope {
            PermissionScope::Read => Self::Read,
            PermissionScope::Write => Self::Write,
        }
    }
}

#[cfg(feature = "v2")]
#[derive(
    Clone,
    Copy,
    Debug,
    Eq,
    Hash,
    PartialEq,
    ToSchema,
    serde::Deserialize,
    serde::Serialize,
    strum::Display,
    strum::EnumIter,
    strum::EnumString,
)]
#[serde(rename_all = "UPPERCASE")]
#[strum(serialize_all = "UPPERCASE")]
pub enum TokenStatus {
    /// Indicates that the token is active and can be used for payments
    Active,
    /// Indicates that the token is inactive and can't be used for payments
    Inactive,
    /// Indicates that the token is suspended from network's end for some reason and can't be used for payments until it is re-activated
    Suspended,
    /// Indicates that the token is expired and can't be used for payments
    Expired,
    /// Indicates that the token is deleted and further can't be used for payments
    Deleted,
}

Tests: crates/api_models/src/enums.rs
================================================================================
use std::str::FromStr;

pub use common_enums::*;
use smithy::SmithyModel;
use utoipa::ToSchema;

pub use super::connector_enums::Connector;

#[derive(
    Clone,
    Copy,
    Debug,
    Eq,
    PartialEq,
    serde::Deserialize,
    serde::Serialize,
    strum::Display,
    strum::EnumString,
)]

/// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom'
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum RoutingAlgorithm {
    RoundRobin,
    MaxConversion,
    MinCost,
    Custom,
}

#[cfg(feature = "payouts")]
#[derive(
    Clone,
    Copy,
    Debug,
    Eq,
    Hash,
    PartialEq,
    serde::Serialize,
    serde::Deserialize,
    strum::Display,
    strum::EnumString,
    ToSchema,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum PayoutConnectors {
    Adyen,
    Adyenplatform,
    Cybersource,
    Ebanx,
    Gigadat,
    Loonio,
    Nomupay,
    Nuvei,
    Payone,
    Paypal,
    Stripe,
    Wise,
    Worldpay,
    Worldpayxml,
}

#[cfg(feature = "v2")]
/// Whether active attempt is to be set/unset
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)]
pub enum UpdateActiveAttempt {
    /// Request to set the active attempt id
    #[schema(value_type = Option<String>)]
    Set(common_utils::id_type::GlobalAttemptId),
    /// To unset the active attempt id
    Unset,
}

#[cfg(feature = "payouts")]
impl From<PayoutConnectors> for RoutableConnectors {
    fn from(value: PayoutConnectors) -> Self {
        match value {
            PayoutConnectors::Adyen => Self::Adyen,
            PayoutConnectors::Adyenplatform => Self::Adyenplatform,
            PayoutConnectors::Cybersource => Self::Cybersource,
            PayoutConnectors::Ebanx => Self::Ebanx,
            PayoutConnectors::Gigadat => Self::Gigadat,
            PayoutConnectors::Loonio => Self::Loonio,
            PayoutConnectors::Nomupay => Self::Nomupay,
            PayoutConnectors::Nuvei => Self::Nuvei,
            PayoutConnectors::Payone => Self::Payone,
            PayoutConnectors::Paypal => Self::Paypal,
            PayoutConnectors::Stripe => Self::Stripe,
            PayoutConnectors::Wise => Self::Wise,
            PayoutConnectors::Worldpay => Self::Worldpay,
            PayoutConnectors::Worldpayxml => Self::Worldpayxml,
        }
    }
}

#[cfg(feature = "payouts")]
impl From<PayoutConnectors> for Connector {
    fn from(value: PayoutConnectors) -> Self {
        match value {
            PayoutConnectors::Adyen => Self::Adyen,
            PayoutConnectors::Adyenplatform => Self::Adyenplatform,
            PayoutConnectors::Cybersource => Self::Cybersource,
            PayoutConnectors::Ebanx => Self::Ebanx,
            PayoutConnectors::Gigadat => Self::Gigadat,
            PayoutConnectors::Loonio => Self::Loonio,
            PayoutConnectors::Nomupay => Self::Nomupay,
            PayoutConnectors::Nuvei => Self::Nuvei,
            PayoutConnectors::Payone => Self::Payone,
            PayoutConnectors::Paypal => Self::Paypal,
            PayoutConnectors::Stripe => Self::Stripe,
            PayoutConnectors::Wise => Self::Wise,
            PayoutConnectors::Worldpay => Self::Worldpay,
            PayoutConnectors::Worldpayxml => Self::Worldpayxml,
        }
    }
}

#[cfg(feature = "payouts")]
impl TryFrom<Connector> for PayoutConnectors {
    type Error = String;
    fn try_from(value: Connector) -> Result<Self, Self::Error> {
        match value {
            Connector::Adyen => Ok(Self::Adyen),
            Connector::Adyenplatform => Ok(Self::Adyenplatform),
            Connector::Cybersource => Ok(Self::Cybersource),
            Connector::Ebanx => Ok(Self::Ebanx),
            Connector::Gigadat => Ok(Self::Gigadat),
            Connector::Loonio => Ok(Self::Loonio),
            Connector::Nuvei => Ok(Self::Nuvei),
            Connector::Nomupay => Ok(Self::Nomupay),
            Connector::Payone => Ok(Self::Payone),
            Connector::Paypal => Ok(Self::Paypal),
            Connector::Stripe => Ok(Self::Stripe),
            Connector::Wise => Ok(Self::Wise),
            Connector::Worldpay => Ok(Self::Worldpay),
            Connector::Worldpayxml => Ok(Self::Worldpayxml),
            _ => Err(format!("Invalid payout connector {value}")),
        }
    }
}

#[cfg(feature = "frm")]
#[derive(
    Clone,
    Copy,
    Debug,
    Eq,
    Hash,
    PartialEq,
    serde::Serialize,
    serde::Deserialize,
    strum::Display,
    strum::EnumString,
    ToSchema,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum FrmConnectors {
    /// Signifyd Risk Manager. Official docs: https://docs.signifyd.com/
    Signifyd,
    Riskified,
}

#[derive(
    Clone,
    Copy,
    Debug,
    Eq,
    Hash,
    PartialEq,
    serde::Serialize,
    serde::Deserialize,
    strum::Display,
    strum::EnumString,
    ToSchema,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum TaxConnectors {
    Taxjar,
}

#[derive(Clone, Debug, serde::Serialize, strum::EnumString, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum BillingConnectors {
    Chargebee,
    Recurly,
    Stripebilling,
    Custombilling,
    #[cfg(feature = "dummy_connector")]
    DummyBillingConnector,
}

#[derive(Clone, Copy, Debug, serde::Serialize, strum::EnumString, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum VaultConnectors {
    Vgs,
    HyperswitchVault,
    Tokenex,
}

impl From<VaultConnectors> for Connector {
    fn from(value: VaultConnectors) -> Self {
        match value {
            VaultConnectors::Vgs => Self::Vgs,
            VaultConnectors::HyperswitchVault => Self::HyperswitchVault,
            VaultConnectors::Tokenex => Self::Tokenex,
        }
    }
}

#[derive(
    Clone, Debug, serde::Deserialize, serde::Serialize, strum::Display, strum::EnumString, ToSchema,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum FrmAction {
    CancelTxn,
    AutoRefund,
    ManualReview,
}

#[derive(
    Clone, Debug, serde::Deserialize, serde::Serialize, strum::Display, strum::EnumString, ToSchema,
)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum FrmPreferredFlowTypes {
    Pre,
    Post,
}
#[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
pub struct UnresolvedResponseReason {
    pub code: String,
    /// A message to merchant to give hint on next action he/she should do to resolve
    pub message: String,
}

/// Possible field type of required fields in payment_method_data
#[derive(
    Clone,
    Debug,
    Eq,
    serde::Deserialize,
    serde::Serialize,
    strum::Display,
    strum::EnumString,
    ToSchema,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum FieldType {
    UserCardNumber,
    UserCardExpiryMonth,
    UserCardExpiryYear,
    UserCardCvc,
    UserCardNetwork,
    UserFullName,
    UserEmailAddress,
    UserPhoneNumber,
    UserPhoneNumberCountryCode,           //phone number's country code
    UserCountry { options: Vec<String> }, //for country inside payment method data ex- bank redirect
    UserCurrency { options: Vec<String> },
    UserCryptoCurrencyNetwork, //for crypto network associated with the cryptopcurrency
    UserBillingName,
    UserAddressLine1,
    UserAddressLine2,
    UserAddressCity,
    UserAddressPincode,
    UserAddressState,
    UserAddressCountry { options: Vec<String> },
    UserShippingName,
    UserShippingAddressLine1,
    UserShippingAddressLine2,
    UserShippingAddressCity,
    UserShippingAddressPincode,
    UserShippingAddressState,
    UserShippingAddressCountry { options: Vec<String> },
    UserSocialSecurityNumber,
    UserBlikCode,
    UserBank,
    UserBankOptions { options: Vec<String> },
    UserBankAccountNumber,
    UserSourceBankAccountId,
    UserDestinationBankAccountId,
    Text,
    DropDown { options: Vec<String> },
    UserDateOfBirth,
    UserVpaId,
    LanguagePreference { options: Vec<String> },
    UserPixKey,
    UserCpf,
    UserCnpj,
    UserIban,
    UserBsbNumber,
    UserBankSortCode,
    UserBankRoutingNumber,
    UserBankType { options: Vec<String> },
    UserBankAccountHolderName,
    UserMsisdn,
    UserClientIdentifier,
    OrderDetailsProductName,
}

impl FieldType {
    pub fn get_billing_variants() -> Vec<Self> {
        vec![
            Self::UserBillingName,
            Self::UserAddressLine1,
            Self::UserAddressLine2,
            Self::UserAddressCity,
            Self::UserAddressPincode,
            Self::UserAddressState,
            Self::UserAddressCountry { options: vec![] },
        ]
    }

    pub fn get_shipping_variants() -> Vec<Self> {
        vec![
            Self::UserShippingName,
            Self::UserShippingAddressLine1,
            Self::UserShippingAddressLine2,
            Self::UserShippingAddressCity,
            Self::UserShippingAddressPincode,
            Self::UserShippingAddressState,
            Self::UserShippingAddressCountry { options: vec![] },
        ]
    }
}

/// This implementatiobn is to ignore the inner value of UserAddressCountry enum while comparing
impl PartialEq for FieldType {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Self::UserCardNumber, Self::UserCardNumber) => true,
            (Self::UserCardExpiryMonth, Self::UserCardExpiryMonth) => true,
            (Self::UserCardExpiryYear, Self::UserCardExpiryYear) => true,
            (Self::UserCardCvc, Self::UserCardCvc) => true,
            (Self::UserFullName, Self::UserFullName) => true,
            (Self::UserEmailAddress, Self::UserEmailAddress) => true,
            (Self::UserPhoneNumber, Self::UserPhoneNumber) => true,
            (Self::UserPhoneNumberCountryCode, Self::UserPhoneNumberCountryCode) => true,
            (
                Self::UserCountry {
                    options: options_self,
                },
                Self::UserCountry {
                    options: options_other,
                },
            ) => options_self.eq(options_other),
            (
                Self::UserCurrency {
                    options: options_self,
                },
                Self::UserCurrency {
                    options: options_other,
                },
            ) => options_self.eq(options_other),
            (Self::UserCryptoCurrencyNetwork, Self::UserCryptoCurrencyNetwork) => true,
            (Self::UserBillingName, Self::UserBillingName) => true,
            (Self::UserAddressLine1, Self::UserAddressLine1) => true,
            (Self::UserAddressLine2, Self::UserAddressLine2) => true,
            (Self::UserAddressCity, Self::UserAddressCity) => true,
            (Self::UserAddressPincode, Self::UserAddressPincode) => true,
            (Self::UserAddressState, Self::UserAddressState) => true,
            (Self::UserAddressCountry { .. }, Self::UserAddressCountry { .. }) => true,
            (Self::UserShippingName, Self::UserShippingName) => true,
            (Self::UserShippingAddressLine1, Self::UserShippingAddressLine1) => true,
            (Self::UserShippingAddressLine2, Self::UserShippingAddressLine2) => true,
            (Self::UserShippingAddressCity, Self::UserShippingAddressCity) => true,
            (Self::UserShippingAddressPincode, Self::UserShippingAddressPincode) => true,
            (Self::UserShippingAddressState, Self::UserShippingAddressState) => true,
            (Self::UserShippingAddressCountry { .. }, Self::UserShippingAddressCountry { .. }) => {
                true
            }
            (Self::UserBlikCode, Self::UserBlikCode) => true,
            (Self::UserBank, Self::UserBank) => true,
            (Self::Text, Self::Text) => true,
            (
                Self::DropDown {
                    options: options_self,
                },
                Self::DropDown {
                    options: options_other,
                },
            ) => options_self.eq(options_other),
            (Self::UserDateOfBirth, Self::UserDateOfBirth) => true,
            (Self::UserVpaId, Self::UserVpaId) => true,
            (Self::UserPixKey, Self::UserPixKey) => true,
            (Self::UserCpf, Self::UserCpf) => true,
            (Self::UserCnpj, Self::UserCnpj) => true,
            (Self::LanguagePreference { .. }, Self::LanguagePreference { .. }) => true,
            (Self::UserMsisdn, Self::UserMsisdn) => true,
            (Self::UserClientIdentifier, Self::UserClientIdentifier) => true,
            (Self::OrderDetailsProductName, Self::OrderDetailsProductName) => true,
            _unused => false,
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_partialeq_for_field_type() {
        let user_address_country_is_us = FieldType::UserAddressCountry {
            options: vec!["US".to_string()],
        };

        let user_address_country_is_all = FieldType::UserAddressCountry {
            options: vec!["ALL".to_string()],
        };

        assert!(user_address_country_is_us.eq(&user_address_country_is_all))
    }
}

/// Denotes the retry action
#[derive(
    Debug,
    serde::Deserialize,
    serde::Serialize,
    strum::Display,
    strum::EnumString,
    Clone,
    PartialEq,
    Eq,
    ToSchema,
    SmithyModel,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
#[smithy(namespace = "com.hyperswitch.smithy.types")]
pub enum RetryAction {
    /// Manual retry through request is being deprecated, now it is available through profile
    ManualRetry,
    /// Denotes that the payment is requeued
    Requeue,
}

#[derive(Clone, Copy)]
pub enum LockerChoice {
    HyperswitchCardVault,
}

#[derive(
    Clone,
    Copy,
    Debug,
    Eq,
    PartialEq,
    serde::Serialize,
    serde::Deserialize,
    strum::Display,
    strum::EnumString,
    ToSchema,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum PmAuthConnectors {
    Plaid,
}

pub fn convert_pm_auth_connector(connector_name: &str) -> Option<PmAuthConnectors> {
    PmAuthConnectors::from_str(connector_name).ok()
}

pub fn convert_authentication_connector(connector_name: &str) -> Option<AuthenticationConnectors> {
    AuthenticationConnectors::from_str(connector_name).ok()
}

pub fn convert_tax_connector(connector_name: &str) -> Option<TaxConnectors> {
    TaxConnectors::from_str(connector_name).ok()
}

pub fn convert_billing_connector(connector_name: &str) -> Option<BillingConnectors> {
    BillingConnectors::from_str(connector_name).ok()
}
#[cfg(feature = "frm")]
pub fn convert_frm_connector(connector_name: &str) -> Option<FrmConnectors> {
    FrmConnectors::from_str(connector_name).ok()
}

pub fn convert_vault_connector(connector_name: &str) -> Option<VaultConnectors> {
    VaultConnectors::from_str(connector_name).ok()
}

#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, Hash)]
pub enum ReconPermissionScope {
    #[serde(rename = "R")]
    Read = 0,
    #[serde(rename = "RW")]
    Write = 1,
}

impl From<PermissionScope> for ReconPermissionScope {
    fn from(scope: PermissionScope) -> Self {
        match scope {
            PermissionScope::Read => Self::Read,
            PermissionScope::Write => Self::Write,
        }
    }
}

#[cfg(feature = "v2")]
#[derive(
    Clone,
    Copy,
    Debug,
    Eq,
    Hash,
    PartialEq,
    ToSchema,
    serde::Deserialize,
    serde::Serialize,
    strum::Display,
    strum::EnumIter,
    strum::EnumString,
)]
#[serde(rename_all = "UPPERCASE")]
#[strum(serialize_all = "UPPERCASE")]
pub enum TokenStatus {
    /// Indicates that the token is active and can be used for payments
    Active,
    /// Indicates that the token is inactive and can't be used for payments
    Inactive,
    /// Indicates that the token is suspended from network's end for some reason and can't be used for payments until it is re-activated
    Suspended,
    /// Indicates that the token is expired and can't be used for payments
    Expired,
    /// Indicates that the token is deleted and further can't be used for payments
    Deleted,
}


Test-Implementation Pair

Implementation: crates/api_models/src/api_keys.rs
================================================================================
use common_utils::custom_serde;
use masking::StrongSecret;
use serde::{Deserialize, Serialize};
use time::PrimitiveDateTime;
use utoipa::ToSchema;

/// The request body for creating an API Key.
#[derive(Debug, Deserialize, ToSchema, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CreateApiKeyRequest {
    /// A unique name for the API Key to help you identify it.
    #[schema(max_length = 64, example = "Sandbox integration key")]
    pub name: String,

    /// A description to provide more context about the API Key.
    #[schema(
        max_length = 256,
        example = "Key used by our developers to integrate with the sandbox environment"
    )]
    pub description: Option<String>,

    /// An expiration date for the API Key. Although we allow keys to never expire, we recommend
    /// rotating your keys once every 6 months.
    #[schema(example = "2022-09-10T10:11:12Z")]
    pub expiration: ApiKeyExpiration,
}

/// The response body for creating an API Key.
#[derive(Debug, Serialize, ToSchema)]
pub struct CreateApiKeyResponse {
    /// The identifier for the API Key.
    #[schema(max_length = 64, example = "5hEEqkgJUyuxgSKGArHA4mWSnX", value_type = String)]
    pub key_id: common_utils::id_type::ApiKeyId,

    /// The identifier for the Merchant Account.
    #[schema(max_length = 64, example = "y3oqhf46pyzuxjbcn2giaqnb44", value_type = String)]
    pub merchant_id: common_utils::id_type::MerchantId,

    /// The unique name for the API Key to help you identify it.
    #[schema(max_length = 64, example = "Sandbox integration key")]
    pub name: String,

    /// The description to provide more context about the API Key.
    #[schema(
        max_length = 256,
        example = "Key used by our developers to integrate with the sandbox environment"
    )]
    pub description: Option<String>,

    /// The plaintext API Key used for server-side API access. Ensure you store the API Key
    /// securely as you will not be able to see it again.
    #[schema(value_type = String, max_length = 128)]
    pub api_key: StrongSecret<String>,

    /// The time at which the API Key was created.
    #[schema(example = "2022-09-10T10:11:12Z")]
    #[serde(with = "common_utils::custom_serde::iso8601")]
    pub created: PrimitiveDateTime,

    /// The expiration date for the API Key.
    #[schema(example = "2022-09-10T10:11:12Z")]
    pub expiration: ApiKeyExpiration,
    /*
    /// The date and time indicating when the API Key was last used.
    #[schema(example = "2022-09-10T10:11:12Z")]
    #[serde(with = "common_utils::custom_serde::iso8601::option")]
    pub last_used: Option<PrimitiveDateTime>,
    */
}

/// The response body for retrieving an API Key.
#[derive(Debug, Serialize, ToSchema)]
pub struct RetrieveApiKeyResponse {
    /// The identifier for the API Key.
    #[schema(max_length = 64, example = "5hEEqkgJUyuxgSKGArHA4mWSnX", value_type = String)]
    pub key_id: common_utils::id_type::ApiKeyId,

    /// The identifier for the Merchant Account.
    #[schema(max_length = 64, example = "y3oqhf46pyzuxjbcn2giaqnb44", value_type = String)]
    pub merchant_id: common_utils::id_type::MerchantId,

    /// The unique name for the API Key to help you identify it.
    #[schema(max_length = 64, example = "Sandbox integration key")]
    pub name: String,

    /// The description to provide more context about the API Key.
    #[schema(
        max_length = 256,
        example = "Key used by our developers to integrate with the sandbox environment"
    )]
    pub description: Option<String>,

    /// The first few characters of the plaintext API Key to help you identify it.
    #[schema(value_type = String, max_length = 64)]
    pub prefix: StrongSecret<String>,

    /// The time at which the API Key was created.
    #[schema(example = "2022-09-10T10:11:12Z")]
    #[serde(with = "common_utils::custom_serde::iso8601")]
    pub created: PrimitiveDateTime,

    /// The expiration date for the API Key.
    #[schema(example = "2022-09-10T10:11:12Z")]
    pub expiration: ApiKeyExpiration,
    /*
    /// The date and time indicating when the API Key was last used.
    #[schema(example = "2022-09-10T10:11:12Z")]
    #[serde(with = "common_utils::custom_serde::iso8601::option")]
    pub last_used: Option<PrimitiveDateTime>,
    */
}

/// The request body for updating an API Key.
#[derive(Debug, Deserialize, ToSchema, Serialize)]
#[serde(deny_unknown_fields)]
pub struct UpdateApiKeyRequest {
    /// A unique name for the API Key to help you identify it.
    #[schema(max_length = 64, example = "Sandbox integration key")]
    pub name: Option<String>,

    /// A description to provide more context about the API Key.
    #[schema(
        max_length = 256,
        example = "Key used by our developers to integrate with the sandbox environment"
    )]
    pub description: Option<String>,

    /// An expiration date for the API Key. Although we allow keys to never expire, we recommend
    /// rotating your keys once every 6 months.
    #[schema(example = "2022-09-10T10:11:12Z")]
    pub expiration: Option<ApiKeyExpiration>,

    #[serde(skip_deserializing)]
    #[schema(value_type = String)]
    pub key_id: common_utils::id_type::ApiKeyId,

    #[serde(skip_deserializing)]
    #[schema(value_type = String)]
    pub merchant_id: common_utils::id_type::MerchantId,
}

/// The response body for revoking an API Key.
#[derive(Debug, Serialize, ToSchema)]
pub struct RevokeApiKeyResponse {
    /// The identifier for the Merchant Account.
    #[schema(max_length = 64, example = "y3oqhf46pyzuxjbcn2giaqnb44", value_type = String)]
    pub merchant_id: common_utils::id_type::MerchantId,

    /// The identifier for the API Key.
    #[schema(max_length = 64, example = "5hEEqkgJUyuxgSKGArHA4mWSnX", value_type = String)]
    pub key_id: common_utils::id_type::ApiKeyId,
    /// Indicates whether the API key was revoked or not.
    #[schema(example = "true")]
    pub revoked: bool,
}

/// The constraints that are applicable when listing API Keys associated with a merchant account.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ListApiKeyConstraints {
    /// The maximum number of API Keys to include in the response.
    pub limit: Option<i64>,

    /// The number of API Keys to skip when retrieving the list of API keys.
    pub skip: Option<i64>,
}

/// The expiration date and time for an API Key.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum ApiKeyExpiration {
    /// The API Key does not expire.
    #[serde(with = "never")]
    Never,

    /// The API Key expires at the specified date and time.
    #[serde(with = "custom_serde::iso8601")]
    DateTime(PrimitiveDateTime),
}

impl From<ApiKeyExpiration> for Option<PrimitiveDateTime> {
    fn from(expiration: ApiKeyExpiration) -> Self {
        match expiration {
            ApiKeyExpiration::Never => None,
            ApiKeyExpiration::DateTime(date_time) => Some(date_time),
        }
    }
}

impl From<Option<PrimitiveDateTime>> for ApiKeyExpiration {
    fn from(date_time: Option<PrimitiveDateTime>) -> Self {
        date_time.map_or(Self::Never, Self::DateTime)
    }
}

// This implementation is required as otherwise, `serde` would serialize and deserialize
// `ApiKeyExpiration::Never` as `null`, which is not preferable.
// Reference: https://github.com/serde-rs/serde/issues/1560#issuecomment-506915291
mod never {
    const NEVER: &str = "never";

    pub fn serialize<S>(serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(NEVER)
    }

    pub fn deserialize<'de, D>(deserializer: D) -> Result<(), D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        struct NeverVisitor;

        impl serde::de::Visitor<'_> for NeverVisitor {
            type Value = ();

            fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                write!(f, r#""{NEVER}""#)
            }

            fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
                if value == NEVER {
                    Ok(())
                } else {
                    Err(E::invalid_value(serde::de::Unexpected::Str(value), &self))
                }
            }
        }

        deserializer.deserialize_str(NeverVisitor)
    }
}

impl<'a> ToSchema<'a> for ApiKeyExpiration {
    fn schema() -> (
        &'a str,
        utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
    ) {
        use utoipa::openapi::{KnownFormat, ObjectBuilder, OneOfBuilder, SchemaFormat, SchemaType};

        (
            "ApiKeyExpiration",
            OneOfBuilder::new()
                .item(
                    ObjectBuilder::new()
                        .schema_type(SchemaType::String)
                        .enum_values(Some(["never"])),
                )
                .item(
                    ObjectBuilder::new()
                        .schema_type(SchemaType::String)
                        .format(Some(SchemaFormat::KnownFormat(KnownFormat::DateTime))),
                )
                .into(),
        )
    }
}

#[cfg(test)]
mod api_key_expiration_tests {
    use super::*;

    #[test]
    fn test_serialization() {
        assert_eq!(
            serde_json::to_string(&ApiKeyExpiration::Never).unwrap(),
            r#""never""#
        );

        let date = time::Date::from_calendar_date(2022, time::Month::September, 10).unwrap();
        let time = time::Time::from_hms(11, 12, 13).unwrap();
        assert_eq!(
            serde_json::to_string(&ApiKeyExpiration::DateTime(PrimitiveDateTime::new(
                date, time
            )))
            .unwrap(),
            r#""2022-09-10T11:12:13.000Z""#
        );
    }

    #[test]
    fn test_deserialization() {
        assert_eq!(
            serde_json::from_str::<ApiKeyExpiration>(r#""never""#).unwrap(),
            ApiKeyExpiration::Never
        );

        let date = time::Date::from_calendar_date(2022, time::Month::September, 10).unwrap();
        let time = time::Time::from_hms(11, 12, 13).unwrap();
        assert_eq!(
            serde_json::from_str::<ApiKeyExpiration>(r#""2022-09-10T11:12:13.000Z""#).unwrap(),
            ApiKeyExpiration::DateTime(PrimitiveDateTime::new(date, time))
        );
    }

    #[test]
    fn test_null() {
        let result = serde_json::from_str::<ApiKeyExpiration>("null");
        assert!(result.is_err());

        let result = serde_json::from_str::<Option<ApiKeyExpiration>>("null").unwrap();
        assert_eq!(result, None);
    }
}

Tests: crates/api_models/src/api_keys.rs
================================================================================
use common_utils::custom_serde;
use masking::StrongSecret;
use serde::{Deserialize, Serialize};
use time::PrimitiveDateTime;
use utoipa::ToSchema;

/// The request body for creating an API Key.
#[derive(Debug, Deserialize, ToSchema, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CreateApiKeyRequest {
    /// A unique name for the API Key to help you identify it.
    #[schema(max_length = 64, example = "Sandbox integration key")]
    pub name: String,

    /// A description to provide more context about the API Key.
    #[schema(
        max_length = 256,
        example = "Key used by our developers to integrate with the sandbox environment"
    )]
    pub description: Option<String>,

    /// An expiration date for the API Key. Although we allow keys to never expire, we recommend
    /// rotating your keys once every 6 months.
    #[schema(example = "2022-09-10T10:11:12Z")]
    pub expiration: ApiKeyExpiration,
}

/// The response body for creating an API Key.
#[derive(Debug, Serialize, ToSchema)]
pub struct CreateApiKeyResponse {
    /// The identifier for the API Key.
    #[schema(max_length = 64, example = "5hEEqkgJUyuxgSKGArHA4mWSnX", value_type = String)]
    pub key_id: common_utils::id_type::ApiKeyId,

    /// The identifier for the Merchant Account.
    #[schema(max_length = 64, example = "y3oqhf46pyzuxjbcn2giaqnb44", value_type = String)]
    pub merchant_id: common_utils::id_type::MerchantId,

    /// The unique name for the API Key to help you identify it.
    #[schema(max_length = 64, example = "Sandbox integration key")]
    pub name: String,

    /// The description to provide more context about the API Key.
    #[schema(
        max_length = 256,
        example = "Key used by our developers to integrate with the sandbox environment"
    )]
    pub description: Option<String>,

    /// The plaintext API Key used for server-side API access. Ensure you store the API Key
    /// securely as you will not be able to see it again.
    #[schema(value_type = String, max_length = 128)]
    pub api_key: StrongSecret<String>,

    /// The time at which the API Key was created.
    #[schema(example = "2022-09-10T10:11:12Z")]
    #[serde(with = "common_utils::custom_serde::iso8601")]
    pub created: PrimitiveDateTime,

    /// The expiration date for the API Key.
    #[schema(example = "2022-09-10T10:11:12Z")]
    pub expiration: ApiKeyExpiration,
    /*
    /// The date and time indicating when the API Key was last used.
    #[schema(example = "2022-09-10T10:11:12Z")]
    #[serde(with = "common_utils::custom_serde::iso8601::option")]
    pub last_used: Option<PrimitiveDateTime>,
    */
}

/// The response body for retrieving an API Key.
#[derive(Debug, Serialize, ToSchema)]
pub struct RetrieveApiKeyResponse {
    /// The identifier for the API Key.
    #[schema(max_length = 64, example = "5hEEqkgJUyuxgSKGArHA4mWSnX", value_type = String)]
    pub key_id: common_utils::id_type::ApiKeyId,

    /// The identifier for the Merchant Account.
    #[schema(max_length = 64, example = "y3oqhf46pyzuxjbcn2giaqnb44", value_type = String)]
    pub merchant_id: common_utils::id_type::MerchantId,

    /// The unique name for the API Key to help you identify it.
    #[schema(max_length = 64, example = "Sandbox integration key")]
    pub name: String,

    /// The description to provide more context about the API Key.
    #[schema(
        max_length = 256,
        example = "Key used by our developers to integrate with the sandbox environment"
    )]
    pub description: Option<String>,

    /// The first few characters of the plaintext API Key to help you identify it.
    #[schema(value_type = String, max_length = 64)]
    pub prefix: StrongSecret<String>,

    /// The time at which the API Key was created.
    #[schema(example = "2022-09-10T10:11:12Z")]
    #[serde(with = "common_utils::custom_serde::iso8601")]
    pub created: PrimitiveDateTime,

    /// The expiration date for the API Key.
    #[schema(example = "2022-09-10T10:11:12Z")]
    pub expiration: ApiKeyExpiration,
    /*
    /// The date and time indicating when the API Key was last used.
    #[schema(example = "2022-09-10T10:11:12Z")]
    #[serde(with = "common_utils::custom_serde::iso8601::option")]
    pub last_used: Option<PrimitiveDateTime>,
    */
}

/// The request body for updating an API Key.
#[derive(Debug, Deserialize, ToSchema, Serialize)]
#[serde(deny_unknown_fields)]
pub struct UpdateApiKeyRequest {
    /// A unique name for the API Key to help you identify it.
    #[schema(max_length = 64, example = "Sandbox integration key")]
    pub name: Option<String>,

    /// A description to provide more context about the API Key.
    #[schema(
        max_length = 256,
        example = "Key used by our developers to integrate with the sandbox environment"
    )]
    pub description: Option<String>,

    /// An expiration date for the API Key. Although we allow keys to never expire, we recommend
    /// rotating your keys once every 6 months.
    #[schema(example = "2022-09-10T10:11:12Z")]
    pub expiration: Option<ApiKeyExpiration>,

    #[serde(skip_deserializing)]
    #[schema(value_type = String)]
    pub key_id: common_utils::id_type::ApiKeyId,

    #[serde(skip_deserializing)]
    #[schema(value_type = String)]
    pub merchant_id: common_utils::id_type::MerchantId,
}

/// The response body for revoking an API Key.
#[derive(Debug, Serialize, ToSchema)]
pub struct RevokeApiKeyResponse {
    /// The identifier for the Merchant Account.
    #[schema(max_length = 64, example = "y3oqhf46pyzuxjbcn2giaqnb44", value_type = String)]
    pub merchant_id: common_utils::id_type::MerchantId,

    /// The identifier for the API Key.
    #[schema(max_length = 64, example = "5hEEqkgJUyuxgSKGArHA4mWSnX", value_type = String)]
    pub key_id: common_utils::id_type::ApiKeyId,
    /// Indicates whether the API key was revoked or not.
    #[schema(example = "true")]
    pub revoked: bool,
}

/// The constraints that are applicable when listing API Keys associated with a merchant account.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ListApiKeyConstraints {
    /// The maximum number of API Keys to include in the response.
    pub limit: Option<i64>,

    /// The number of API Keys to skip when retrieving the list of API keys.
    pub skip: Option<i64>,
}

/// The expiration date and time for an API Key.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum ApiKeyExpiration {
    /// The API Key does not expire.
    #[serde(with = "never")]
    Never,

    /// The API Key expires at the specified date and time.
    #[serde(with = "custom_serde::iso8601")]
    DateTime(PrimitiveDateTime),
}

impl From<ApiKeyExpiration> for Option<PrimitiveDateTime> {
    fn from(expiration: ApiKeyExpiration) -> Self {
        match expiration {
            ApiKeyExpiration::Never => None,
            ApiKeyExpiration::DateTime(date_time) => Some(date_time),
        }
    }
}

impl From<Option<PrimitiveDateTime>> for ApiKeyExpiration {
    fn from(date_time: Option<PrimitiveDateTime>) -> Self {
        date_time.map_or(Self::Never, Self::DateTime)
    }
}

// This implementation is required as otherwise, `serde` would serialize and deserialize
// `ApiKeyExpiration::Never` as `null`, which is not preferable.
// Reference: https://github.com/serde-rs/serde/issues/1560#issuecomment-506915291
mod never {
    const NEVER: &str = "never";

    pub fn serialize<S>(serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(NEVER)
    }

    pub fn deserialize<'de, D>(deserializer: D) -> Result<(), D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        struct NeverVisitor;

        impl serde::de::Visitor<'_> for NeverVisitor {
            type Value = ();

            fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                write!(f, r#""{NEVER}""#)
            }

            fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
                if value == NEVER {
                    Ok(())
                } else {
                    Err(E::invalid_value(serde::de::Unexpected::Str(value), &self))
                }
            }
        }

        deserializer.deserialize_str(NeverVisitor)
    }
}

impl<'a> ToSchema<'a> for ApiKeyExpiration {
    fn schema() -> (
        &'a str,
        utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
    ) {
        use utoipa::openapi::{KnownFormat, ObjectBuilder, OneOfBuilder, SchemaFormat, SchemaType};

        (
            "ApiKeyExpiration",
            OneOfBuilder::new()
                .item(
                    ObjectBuilder::new()
                        .schema_type(SchemaType::String)
                        .enum_values(Some(["never"])),
                )
                .item(
                    ObjectBuilder::new()
                        .schema_type(SchemaType::String)
                        .format(Some(SchemaFormat::KnownFormat(KnownFormat::DateTime))),
                )
                .into(),
        )
    }
}

#[cfg(test)]
mod api_key_expiration_tests {
    use super::*;

    #[test]
    fn test_serialization() {
        assert_eq!(
            serde_json::to_string(&ApiKeyExpiration::Never).unwrap(),
            r#""never""#
        );

        let date = time::Date::from_calendar_date(2022, time::Month::September, 10).unwrap();
        let time = time::Time::from_hms(11, 12, 13).unwrap();
        assert_eq!(
            serde_json::to_string(&ApiKeyExpiration::DateTime(PrimitiveDateTime::new(
                date, time
            )))
            .unwrap(),
            r#""2022-09-10T11:12:13.000Z""#
        );
    }

    #[test]
    fn test_deserialization() {
        assert_eq!(
            serde_json::from_str::<ApiKeyExpiration>(r#""never""#).unwrap(),
            ApiKeyExpiration::Never
        );

        let date = time::Date::from_calendar_date(2022, time::Month::September, 10).unwrap();
        let time = time::Time::from_hms(11, 12, 13).unwrap();
        assert_eq!(
            serde_json::from_str::<ApiKeyExpiration>(r#""2022-09-10T11:12:13.000Z""#).unwrap(),
            ApiKeyExpiration::DateTime(PrimitiveDateTime::new(date, time))
        );
    }

    #[test]
    fn test_null() {
        let result = serde_json::from_str::<ApiKeyExpiration>("null");
        assert!(result.is_err());

        let result = serde_json::from_str::<Option<ApiKeyExpiration>>("null").unwrap();
        assert_eq!(result, None);
    }
}


Test-Implementation Pair

Implementation: crates/redis_interface/src/lib.rs
================================================================================
//! Intermediate module for encapsulate all the redis related functionality
//!
//! Provides structs to represent redis connection and all functions that redis provides and
//! are used in the `router` crate. Abstractions for creating a new connection while also facilitating
//! redis connection pool and configuration based types.
//!
//!  # Examples
//! ```
//! use redis_interface::{types::RedisSettings, RedisConnectionPool};
//!
//! #[tokio::main]
//! async fn main() {
//!     let redis_conn = RedisConnectionPool::new(&RedisSettings::default()).await;
//!     // ... redis_conn ready to use
//! }
//! ```

pub mod commands;
pub mod errors;
pub mod types;

use std::sync::{atomic, Arc};

use common_utils::errors::CustomResult;
use error_stack::ResultExt;
pub use fred::interfaces::PubsubInterface;
use fred::{
    clients::Transaction,
    interfaces::ClientLike,
    prelude::{EventInterface, TransactionInterface},
};

pub use self::types::*;

pub struct RedisConnectionPool {
    pub pool: Arc<fred::prelude::RedisPool>,
    pub key_prefix: String,
    pub config: Arc<RedisConfig>,
    pub subscriber: Arc<SubscriberClient>,
    pub publisher: Arc<RedisClient>,
    pub is_redis_available: Arc<atomic::AtomicBool>,
}

pub struct RedisClient {
    inner: fred::prelude::RedisClient,
}

impl std::ops::Deref for RedisClient {
    type Target = fred::prelude::RedisClient;
    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

impl RedisClient {
    pub async fn new(
        config: fred::types::RedisConfig,
        reconnect_policy: fred::types::ReconnectPolicy,
        perf: fred::types::PerformanceConfig,
    ) -> CustomResult<Self, errors::RedisError> {
        let client =
            fred::prelude::RedisClient::new(config, Some(perf), None, Some(reconnect_policy));
        client.connect();
        client
            .wait_for_connect()
            .await
            .change_context(errors::RedisError::RedisConnectionError)?;
        Ok(Self { inner: client })
    }
}

pub struct SubscriberClient {
    inner: fred::clients::SubscriberClient,
    pub is_subscriber_handler_spawned: Arc<atomic::AtomicBool>,
}

impl SubscriberClient {
    pub async fn new(
        config: fred::types::RedisConfig,
        reconnect_policy: fred::types::ReconnectPolicy,
        perf: fred::types::PerformanceConfig,
    ) -> CustomResult<Self, errors::RedisError> {
        let client =
            fred::clients::SubscriberClient::new(config, Some(perf), None, Some(reconnect_policy));
        client.connect();
        client
            .wait_for_connect()
            .await
            .change_context(errors::RedisError::RedisConnectionError)?;
        Ok(Self {
            inner: client,
            is_subscriber_handler_spawned: Arc::new(atomic::AtomicBool::new(false)),
        })
    }
}

impl std::ops::Deref for SubscriberClient {
    type Target = fred::clients::SubscriberClient;
    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

impl RedisConnectionPool {
    /// Create a new Redis connection
    pub async fn new(conf: &RedisSettings) -> CustomResult<Self, errors::RedisError> {
        let redis_connection_url = match conf.cluster_enabled {
            // Fred relies on this format for specifying cluster where the host port is ignored & only query parameters are used for node addresses
            // redis-cluster://username:password@host:port?node=bar.com:30002&node=baz.com:30003
            true => format!(
                "redis-cluster://{}:{}?{}",
                conf.host,
                conf.port,
                conf.cluster_urls
                    .iter()
                    .flat_map(|url| vec!["&", url])
                    .skip(1)
                    .collect::<String>()
            ),
            false => format!(
                "redis://{}:{}", //URI Schema
                conf.host, conf.port,
            ),
        };
        let mut config = fred::types::RedisConfig::from_url(&redis_connection_url)
            .change_context(errors::RedisError::RedisConnectionError)?;

        let perf = fred::types::PerformanceConfig {
            auto_pipeline: conf.auto_pipeline,
            default_command_timeout: std::time::Duration::from_secs(conf.default_command_timeout),
            max_feed_count: conf.max_feed_count,
            backpressure: fred::types::BackpressureConfig {
                disable_auto_backpressure: conf.disable_auto_backpressure,
                max_in_flight_commands: conf.max_in_flight_commands,
                policy: fred::types::BackpressurePolicy::Drain,
            },
        };

        let connection_config = fred::types::ConnectionConfig {
            unresponsive_timeout: std::time::Duration::from_secs(conf.unresponsive_timeout),
            ..fred::types::ConnectionConfig::default()
        };

        if !conf.use_legacy_version {
            config.version = fred::types::RespVersion::RESP3;
        }
        config.tracing = fred::types::TracingConfig::new(true);
        config.blocking = fred::types::Blocking::Error;
        let reconnect_policy = fred::types::ReconnectPolicy::new_constant(
            conf.reconnect_max_attempts,
            conf.reconnect_delay,
        );

        let subscriber =
            SubscriberClient::new(config.clone(), reconnect_policy.clone(), perf.clone()).await?;

        let publisher =
            RedisClient::new(config.clone(), reconnect_policy.clone(), perf.clone()).await?;

        let pool = fred::prelude::RedisPool::new(
            config,
            Some(perf),
            Some(connection_config),
            Some(reconnect_policy),
            conf.pool_size,
        )
        .change_context(errors::RedisError::RedisConnectionError)?;

        pool.connect();
        pool.wait_for_connect()
            .await
            .change_context(errors::RedisError::RedisConnectionError)?;

        let config = RedisConfig::from(conf);

        Ok(Self {
            pool: Arc::new(pool),
            config: Arc::new(config),
            is_redis_available: Arc::new(atomic::AtomicBool::new(true)),
            subscriber: Arc::new(subscriber),
            publisher: Arc::new(publisher),
            key_prefix: String::default(),
        })
    }
    pub fn clone(&self, key_prefix: &str) -> Self {
        Self {
            pool: Arc::clone(&self.pool),
            key_prefix: key_prefix.to_string(),
            config: Arc::clone(&self.config),
            subscriber: Arc::clone(&self.subscriber),
            publisher: Arc::clone(&self.publisher),
            is_redis_available: Arc::clone(&self.is_redis_available),
        }
    }
    pub async fn on_error(&self, tx: tokio::sync::oneshot::Sender<()>) {
        use futures::StreamExt;
        use tokio_stream::wrappers::BroadcastStream;

        let error_rxs: Vec<BroadcastStream<fred::error::RedisError>> = self
            .pool
            .clients()
            .iter()
            .map(|client| BroadcastStream::new(client.error_rx()))
            .collect();

        let mut error_rx = futures::stream::select_all(error_rxs);
        loop {
            if let Some(Ok(error)) = error_rx.next().await {
                tracing::error!(?error, "Redis protocol or connection error");
                if self.pool.state() == fred::types::ClientState::Disconnected {
                    if tx.send(()).is_err() {
                        tracing::error!("The redis shutdown signal sender failed to signal");
                    }
                    self.is_redis_available
                        .store(false, atomic::Ordering::SeqCst);
                    break;
                }
            }
        }
    }

    pub async fn on_unresponsive(&self) {
        let _ = self.pool.clients().iter().map(|client| {
            client.on_unresponsive(|server| {
                tracing::warn!(redis_server =?server.host, "Redis server is unresponsive");
                Ok(())
            })
        });
    }

    pub fn get_transaction(&self) -> Transaction {
        self.pool.next().multi()
    }
}

pub struct RedisConfig {
    default_ttl: u32,
    default_stream_read_count: u64,
    default_hash_ttl: u32,
    cluster_enabled: bool,
}

impl From<&RedisSettings> for RedisConfig {
    fn from(config: &RedisSettings) -> Self {
        Self {
            default_ttl: config.default_ttl,
            default_stream_read_count: config.stream_read_count,
            default_hash_ttl: config.default_hash_ttl,
            cluster_enabled: config.cluster_enabled,
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_redis_error() {
        let x = errors::RedisError::ConsumerGroupClaimFailed.to_string();

        assert_eq!(x, "Failed to set Redis stream message owner".to_string())
    }
}

Tests: crates/redis_interface/src/lib.rs
================================================================================
//! Intermediate module for encapsulate all the redis related functionality
//!
//! Provides structs to represent redis connection and all functions that redis provides and
//! are used in the `router` crate. Abstractions for creating a new connection while also facilitating
//! redis connection pool and configuration based types.
//!
//!  # Examples
//! ```
//! use redis_interface::{types::RedisSettings, RedisConnectionPool};
//!
//! #[tokio::main]
//! async fn main() {
//!     let redis_conn = RedisConnectionPool::new(&RedisSettings::default()).await;
//!     // ... redis_conn ready to use
//! }
//! ```

pub mod commands;
pub mod errors;
pub mod types;

use std::sync::{atomic, Arc};

use common_utils::errors::CustomResult;
use error_stack::ResultExt;
pub use fred::interfaces::PubsubInterface;
use fred::{
    clients::Transaction,
    interfaces::ClientLike,
    prelude::{EventInterface, TransactionInterface},
};

pub use self::types::*;

pub struct RedisConnectionPool {
    pub pool: Arc<fred::prelude::RedisPool>,
    pub key_prefix: String,
    pub config: Arc<RedisConfig>,
    pub subscriber: Arc<SubscriberClient>,
    pub publisher: Arc<RedisClient>,
    pub is_redis_available: Arc<atomic::AtomicBool>,
}

pub struct RedisClient {
    inner: fred::prelude::RedisClient,
}

impl std::ops::Deref for RedisClient {
    type Target = fred::prelude::RedisClient;
    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

impl RedisClient {
    pub async fn new(
        config: fred::types::RedisConfig,
        reconnect_policy: fred::types::ReconnectPolicy,
        perf: fred::types::PerformanceConfig,
    ) -> CustomResult<Self, errors::RedisError> {
        let client =
            fred::prelude::RedisClient::new(config, Some(perf), None, Some(reconnect_policy));
        client.connect();
        client
            .wait_for_connect()
            .await
            .change_context(errors::RedisError::RedisConnectionError)?;
        Ok(Self { inner: client })
    }
}

pub struct SubscriberClient {
    inner: fred::clients::SubscriberClient,
    pub is_subscriber_handler_spawned: Arc<atomic::AtomicBool>,
}

impl SubscriberClient {
    pub async fn new(
        config: fred::types::RedisConfig,
        reconnect_policy: fred::types::ReconnectPolicy,
        perf: fred::types::PerformanceConfig,
    ) -> CustomResult<Self, errors::RedisError> {
        let client =
            fred::clients::SubscriberClient::new(config, Some(perf), None, Some(reconnect_policy));
        client.connect();
        client
            .wait_for_connect()
            .await
            .change_context(errors::RedisError::RedisConnectionError)?;
        Ok(Self {
            inner: client,
            is_subscriber_handler_spawned: Arc::new(atomic::AtomicBool::new(false)),
        })
    }
}

impl std::ops::Deref for SubscriberClient {
    type Target = fred::clients::SubscriberClient;
    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

impl RedisConnectionPool {
    /// Create a new Redis connection
    pub async fn new(conf: &RedisSettings) -> CustomResult<Self, errors::RedisError> {
        let redis_connection_url = match conf.cluster_enabled {
            // Fred relies on this format for specifying cluster where the host port is ignored & only query parameters are used for node addresses
            // redis-cluster://username:password@host:port?node=bar.com:30002&node=baz.com:30003
            true => format!(
                "redis-cluster://{}:{}?{}",
                conf.host,
                conf.port,
                conf.cluster_urls
                    .iter()
                    .flat_map(|url| vec!["&", url])
                    .skip(1)
                    .collect::<String>()
            ),
            false => format!(
                "redis://{}:{}", //URI Schema
                conf.host, conf.port,
            ),
        };
        let mut config = fred::types::RedisConfig::from_url(&redis_connection_url)
            .change_context(errors::RedisError::RedisConnectionError)?;

        let perf = fred::types::PerformanceConfig {
            auto_pipeline: conf.auto_pipeline,
            default_command_timeout: std::time::Duration::from_secs(conf.default_command_timeout),
            max_feed_count: conf.max_feed_count,
            backpressure: fred::types::BackpressureConfig {
                disable_auto_backpressure: conf.disable_auto_backpressure,
                max_in_flight_commands: conf.max_in_flight_commands,
                policy: fred::types::BackpressurePolicy::Drain,
            },
        };

        let connection_config = fred::types::ConnectionConfig {
            unresponsive_timeout: std::time::Duration::from_secs(conf.unresponsive_timeout),
            ..fred::types::ConnectionConfig::default()
        };

        if !conf.use_legacy_version {
            config.version = fred::types::RespVersion::RESP3;
        }
        config.tracing = fred::types::TracingConfig::new(true);
        config.blocking = fred::types::Blocking::Error;
        let reconnect_policy = fred::types::ReconnectPolicy::new_constant(
            conf.reconnect_max_attempts,
            conf.reconnect_delay,
        );

        let subscriber =
            SubscriberClient::new(config.clone(), reconnect_policy.clone(), perf.clone()).await?;

        let publisher =
            RedisClient::new(config.clone(), reconnect_policy.clone(), perf.clone()).await?;

        let pool = fred::prelude::RedisPool::new(
            config,
            Some(perf),
            Some(connection_config),
            Some(reconnect_policy),
            conf.pool_size,
        )
        .change_context(errors::RedisError::RedisConnectionError)?;

        pool.connect();
        pool.wait_for_connect()
            .await
            .change_context(errors::RedisError::RedisConnectionError)?;

        let config = RedisConfig::from(conf);

        Ok(Self {
            pool: Arc::new(pool),
            config: Arc::new(config),
            is_redis_available: Arc::new(atomic::AtomicBool::new(true)),
            subscriber: Arc::new(subscriber),
            publisher: Arc::new(publisher),
            key_prefix: String::default(),
        })
    }
    pub fn clone(&self, key_prefix: &str) -> Self {
        Self {
            pool: Arc::clone(&self.pool),
            key_prefix: key_prefix.to_string(),
            config: Arc::clone(&self.config),
            subscriber: Arc::clone(&self.subscriber),
            publisher: Arc::clone(&self.publisher),
            is_redis_available: Arc::clone(&self.is_redis_available),
        }
    }
    pub async fn on_error(&self, tx: tokio::sync::oneshot::Sender<()>) {
        use futures::StreamExt;
        use tokio_stream::wrappers::BroadcastStream;

        let error_rxs: Vec<BroadcastStream<fred::error::RedisError>> = self
            .pool
            .clients()
            .iter()
            .map(|client| BroadcastStream::new(client.error_rx()))
            .collect();

        let mut error_rx = futures::stream::select_all(error_rxs);
        loop {
            if let Some(Ok(error)) = error_rx.next().await {
                tracing::error!(?error, "Redis protocol or connection error");
                if self.pool.state() == fred::types::ClientState::Disconnected {
                    if tx.send(()).is_err() {
                        tracing::error!("The redis shutdown signal sender failed to signal");
                    }
                    self.is_redis_available
                        .store(false, atomic::Ordering::SeqCst);
                    break;
                }
            }
        }
    }

    pub async fn on_unresponsive(&self) {
        let _ = self.pool.clients().iter().map(|client| {
            client.on_unresponsive(|server| {
                tracing::warn!(redis_server =?server.host, "Redis server is unresponsive");
                Ok(())
            })
        });
    }

    pub fn get_transaction(&self) -> Transaction {
        self.pool.next().multi()
    }
}

pub struct RedisConfig {
    default_ttl: u32,
    default_stream_read_count: u64,
    default_hash_ttl: u32,
    cluster_enabled: bool,
}

impl From<&RedisSettings> for RedisConfig {
    fn from(config: &RedisSettings) -> Self {
        Self {
            default_ttl: config.default_ttl,
            default_stream_read_count: config.stream_read_count,
            default_hash_ttl: config.default_hash_ttl,
            cluster_enabled: config.cluster_enabled,
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_redis_error() {
        let x = errors::RedisError::ConsumerGroupClaimFailed.to_string();

        assert_eq!(x, "Failed to set Redis stream message owner".to_string())
    }
}