use serde::{Deserialize, Serialize};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default, PartialOrd, Ord,
)]
#[serde(rename_all = "snake_case")]
pub enum RiskTier {
Low,
#[default]
Medium,
High,
VeryHigh,
Prohibited,
}
impl RiskTier {
pub fn score(&self) -> u8 {
match self {
Self::Low => 20,
Self::Medium => 40,
Self::High => 65,
Self::VeryHigh => 85,
Self::Prohibited => 100,
}
}
pub fn requires_enhanced_dd(&self) -> bool {
matches!(self, Self::High | Self::VeryHigh)
}
pub fn requires_senior_approval(&self) -> bool {
matches!(self, Self::VeryHigh)
}
pub fn review_frequency_months(&self) -> u8 {
match self {
Self::Low => 36,
Self::Medium => 24,
Self::High => 12,
Self::VeryHigh => 6,
Self::Prohibited => 0,
}
}
pub fn monitoring_intensity(&self) -> MonitoringIntensity {
match self {
Self::Low => MonitoringIntensity::Standard,
Self::Medium => MonitoringIntensity::Standard,
Self::High => MonitoringIntensity::Enhanced,
Self::VeryHigh => MonitoringIntensity::Intensive,
Self::Prohibited => MonitoringIntensity::Intensive,
}
}
pub fn from_score(score: u8) -> Self {
match score {
0..=30 => Self::Low,
31..=50 => Self::Medium,
51..=75 => Self::High,
76..=95 => Self::VeryHigh,
_ => Self::Prohibited,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum MonitoringIntensity {
#[default]
Standard,
Enhanced,
Intensive,
}
impl MonitoringIntensity {
pub fn threshold_multiplier(&self) -> f64 {
match self {
Self::Standard => 1.0,
Self::Enhanced => 0.7,
Self::Intensive => 0.5,
}
}
pub fn requires_manual_review(&self) -> bool {
matches!(self, Self::Intensive)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SourceOfFunds {
Employment,
SelfEmployment,
Investments,
Inheritance,
Gift,
PropertySale,
Pension,
GovernmentBenefits,
GamblingWinnings,
LegalSettlement,
Loan,
Insurance,
CryptoAssets,
Other,
Unknown,
}
impl SourceOfFunds {
pub fn risk_weight(&self) -> f64 {
match self {
Self::Employment | Self::Pension | Self::GovernmentBenefits => 0.7,
Self::SelfEmployment => 1.2,
Self::Investments => 1.0,
Self::Inheritance | Self::Gift => 1.3,
Self::PropertySale => 1.1,
Self::GamblingWinnings => 2.0,
Self::LegalSettlement => 1.5,
Self::Loan => 1.0,
Self::Insurance => 0.9,
Self::CryptoAssets => 2.0,
Self::Other => 1.5,
Self::Unknown => 2.5,
}
}
pub fn requires_documentation(&self) -> bool {
!matches!(
self,
Self::Employment | Self::Pension | Self::GovernmentBenefits
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SourceOfWealth {
CareerEarnings,
Inheritance,
BusinessOwnership,
Investments,
RealEstate,
BusinessSale,
EquityEvent,
ProfessionalPractice,
Entertainment,
CryptoAssets,
Other,
}
impl SourceOfWealth {
pub fn risk_weight(&self) -> f64 {
match self {
Self::CareerEarnings | Self::ProfessionalPractice => 0.8,
Self::Inheritance => 1.2,
Self::BusinessOwnership | Self::BusinessSale => 1.3,
Self::Investments | Self::RealEstate => 1.0,
Self::EquityEvent => 1.1,
Self::Entertainment => 1.4,
Self::CryptoAssets => 2.0,
Self::Other => 1.5,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CountryExposure {
pub country_code: String,
pub exposure_type: CountryExposureType,
pub risk_category: CountryRiskCategory,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CountryExposureType {
Residence,
Citizenship,
Birth,
BusinessOperations,
TransactionHistory,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum CountryRiskCategory {
Low,
#[default]
Medium,
High,
VeryHigh,
Sanctioned,
}
impl CountryRiskCategory {
pub fn risk_weight(&self) -> f64 {
match self {
Self::Low => 0.7,
Self::Medium => 1.0,
Self::High => 1.5,
Self::VeryHigh => 2.5,
Self::Sanctioned => 10.0,
}
}
pub fn is_prohibited(&self) -> bool {
matches!(self, Self::Sanctioned)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum CashIntensity {
VeryLow,
#[default]
Low,
Moderate,
High,
VeryHigh,
}
impl CashIntensity {
pub fn risk_weight(&self) -> f64 {
match self {
Self::VeryLow => 0.8,
Self::Low => 1.0,
Self::Moderate => 1.3,
Self::High => 1.8,
Self::VeryHigh => 2.5,
}
}
pub fn expected_percentage(&self) -> (f64, f64) {
match self {
Self::VeryLow => (0.0, 0.05),
Self::Low => (0.05, 0.15),
Self::Moderate => (0.15, 0.30),
Self::High => (0.30, 0.50),
Self::VeryHigh => (0.50, 1.0),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_risk_tier_ordering() {
assert!(RiskTier::Low < RiskTier::Medium);
assert!(RiskTier::Medium < RiskTier::High);
assert!(RiskTier::High < RiskTier::VeryHigh);
}
#[test]
fn test_risk_tier_from_score() {
assert_eq!(RiskTier::from_score(10), RiskTier::Low);
assert_eq!(RiskTier::from_score(40), RiskTier::Medium);
assert_eq!(RiskTier::from_score(60), RiskTier::High);
assert_eq!(RiskTier::from_score(90), RiskTier::VeryHigh);
}
#[test]
fn test_source_of_funds_risk() {
assert!(
SourceOfFunds::CryptoAssets.risk_weight() > SourceOfFunds::Employment.risk_weight()
);
assert!(SourceOfFunds::Employment.risk_weight() < 1.0);
}
#[test]
fn test_country_risk_category() {
assert!(CountryRiskCategory::Sanctioned.is_prohibited());
assert!(!CountryRiskCategory::High.is_prohibited());
}
#[test]
fn test_cash_intensity() {
let (min, max) = CashIntensity::High.expected_percentage();
assert!(min >= 0.30);
assert!(max <= 0.50);
}
}