use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AmlTypology {
Structuring,
Smurfing,
CuckooSmurfing,
FunnelAccount,
ConcentrationAccount,
PouchActivity,
Layering,
RapidMovement,
ShellCompany,
RoundTripping,
TradeBasedML,
InvoiceManipulation,
MoneyMule,
RomanceScam,
AdvanceFeeFraud,
RealEstateIntegration,
LuxuryGoods,
CasinoIntegration,
CryptoIntegration,
AccountTakeover,
SyntheticIdentity,
FirstPartyFraud,
AuthorizedPushPayment,
BusinessEmailCompromise,
FakeVendor,
TerroristFinancing,
SanctionsEvasion,
TaxEvasion,
HumanTrafficking,
DrugTrafficking,
Corruption,
Custom(u16),
}
impl AmlTypology {
pub fn category(&self) -> &'static str {
match self {
Self::Structuring | Self::Smurfing | Self::CuckooSmurfing => "Structuring",
Self::FunnelAccount | Self::ConcentrationAccount | Self::PouchActivity => "Funnel",
Self::Layering | Self::RapidMovement | Self::ShellCompany => "Layering",
Self::RoundTripping | Self::TradeBasedML | Self::InvoiceManipulation => {
"Round-Tripping"
}
Self::MoneyMule | Self::RomanceScam | Self::AdvanceFeeFraud => "Mule/Scam",
Self::RealEstateIntegration
| Self::LuxuryGoods
| Self::CasinoIntegration
| Self::CryptoIntegration => "Integration",
Self::AccountTakeover
| Self::SyntheticIdentity
| Self::FirstPartyFraud
| Self::AuthorizedPushPayment
| Self::BusinessEmailCompromise
| Self::FakeVendor => "Fraud",
Self::TerroristFinancing
| Self::SanctionsEvasion
| Self::TaxEvasion
| Self::HumanTrafficking
| Self::DrugTrafficking
| Self::Corruption => "Predicate Crime",
Self::Custom(_) => "Custom",
}
}
pub fn severity(&self) -> u8 {
match self {
Self::TerroristFinancing | Self::SanctionsEvasion | Self::HumanTrafficking => 10,
Self::DrugTrafficking | Self::Corruption => 9,
Self::AccountTakeover | Self::BusinessEmailCompromise => 8,
Self::MoneyMule | Self::SyntheticIdentity | Self::ShellCompany => 7,
Self::Structuring | Self::Layering | Self::RoundTripping => 6,
Self::FunnelAccount | Self::RapidMovement => 5,
Self::TaxEvasion | Self::FirstPartyFraud => 5,
Self::Smurfing | Self::CuckooSmurfing => 5,
Self::TradeBasedML | Self::InvoiceManipulation => 6,
Self::CryptoIntegration | Self::CasinoIntegration => 5,
Self::RealEstateIntegration | Self::LuxuryGoods => 4,
Self::RomanceScam | Self::AdvanceFeeFraud => 6,
Self::AuthorizedPushPayment | Self::FakeVendor => 7,
Self::ConcentrationAccount | Self::PouchActivity => 5,
Self::Custom(_) => 5,
}
}
pub fn is_fraud(&self) -> bool {
matches!(
self,
Self::AccountTakeover
| Self::SyntheticIdentity
| Self::FirstPartyFraud
| Self::AuthorizedPushPayment
| Self::BusinessEmailCompromise
| Self::FakeVendor
| Self::RomanceScam
| Self::AdvanceFeeFraud
)
}
pub fn typical_duration_days(&self) -> (u32, u32) {
match self {
Self::Structuring => (1, 30),
Self::Smurfing | Self::CuckooSmurfing => (1, 7),
Self::FunnelAccount => (7, 90),
Self::ConcentrationAccount => (30, 180),
Self::PouchActivity => (1, 3),
Self::Layering => (3, 14),
Self::RapidMovement => (1, 3),
Self::ShellCompany => (30, 365),
Self::RoundTripping => (7, 60),
Self::TradeBasedML | Self::InvoiceManipulation => (30, 180),
Self::MoneyMule => (1, 30),
Self::RomanceScam => (30, 180),
Self::AdvanceFeeFraud => (7, 60),
Self::RealEstateIntegration => (60, 180),
Self::LuxuryGoods => (1, 7),
Self::CasinoIntegration => (1, 30),
Self::CryptoIntegration => (1, 14),
Self::AccountTakeover => (1, 7),
Self::SyntheticIdentity => (30, 365),
Self::FirstPartyFraud => (30, 180),
Self::AuthorizedPushPayment => (1, 3),
Self::BusinessEmailCompromise => (1, 14),
Self::FakeVendor => (30, 180),
_ => (7, 90),
}
}
pub fn typical_entity_count(&self) -> (u32, u32) {
match self {
Self::Structuring => (1, 1),
Self::Smurfing => (3, 20),
Self::CuckooSmurfing => (5, 50),
Self::FunnelAccount => (10, 100),
Self::MoneyMule => (3, 10),
Self::Layering => (3, 20),
Self::ShellCompany => (2, 10),
Self::RoundTripping => (2, 5),
_ => (1, 5),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LaunderingStage {
Placement,
Layering,
Integration,
NotApplicable,
}
impl LaunderingStage {
pub fn description(&self) -> &'static str {
match self {
Self::Placement => "Introducing illicit funds into the financial system",
Self::Layering => "Disguising the source through complex transactions",
Self::Integration => "Making funds appear legitimate through business",
Self::NotApplicable => "Not a laundering pattern",
}
}
pub fn detection_difficulty(&self) -> u8 {
match self {
Self::Placement => 4,
Self::Layering => 7,
Self::Integration => 9,
Self::NotApplicable => 5,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum Sophistication {
Basic,
#[default]
Standard,
Professional,
Advanced,
StateLevel,
}
impl Sophistication {
pub fn detectability_modifier(&self) -> f64 {
match self {
Self::Basic => 1.0,
Self::Standard => 0.7,
Self::Professional => 0.4,
Self::Advanced => 0.2,
Self::StateLevel => 0.1,
}
}
pub fn spoofing_intensity(&self) -> f64 {
match self {
Self::Basic => 0.0,
Self::Standard => 0.2,
Self::Professional => 0.5,
Self::Advanced => 0.8,
Self::StateLevel => 0.95,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvasionTactic {
ThresholdAvoidance,
TimeJitter,
AccountSplitting,
ChannelDiversification,
PatternMimicry,
CoverTraffic,
Commingling,
NestedCorrespondent,
RegulatoryArbitrage,
PrivacyTechnology,
}
impl EvasionTactic {
pub fn difficulty_modifier(&self) -> f64 {
match self {
Self::ThresholdAvoidance => 1.2,
Self::TimeJitter => 1.3,
Self::AccountSplitting => 1.4,
Self::ChannelDiversification => 1.3,
Self::PatternMimicry => 1.8,
Self::CoverTraffic => 1.6,
Self::Commingling => 1.5,
Self::NestedCorrespondent => 1.7,
Self::RegulatoryArbitrage => 1.4,
Self::PrivacyTechnology => 2.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum TurnoverBand {
VeryLow,
#[default]
Low,
Medium,
High,
VeryHigh,
UltraHigh,
}
impl TurnoverBand {
pub fn range(&self) -> (u64, u64) {
match self {
Self::VeryLow => (0, 1_000),
Self::Low => (1_000, 5_000),
Self::Medium => (5_000, 25_000),
Self::High => (25_000, 100_000),
Self::VeryHigh => (100_000, 500_000),
Self::UltraHigh => (500_000, 10_000_000),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum FrequencyBand {
VeryLow,
#[default]
Low,
Medium,
High,
VeryHigh,
}
impl FrequencyBand {
pub fn range(&self) -> (u32, u32) {
match self {
Self::VeryLow => (0, 10),
Self::Low => (10, 30),
Self::Medium => (30, 100),
Self::High => (100, 300),
Self::VeryHigh => (300, 10_000),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_aml_typology_severity() {
assert!(AmlTypology::TerroristFinancing.severity() >= 10);
assert!(AmlTypology::Structuring.severity() < AmlTypology::TerroristFinancing.severity());
}
#[test]
fn test_aml_typology_category() {
assert_eq!(AmlTypology::Structuring.category(), "Structuring");
assert_eq!(AmlTypology::Smurfing.category(), "Structuring");
assert_eq!(AmlTypology::FunnelAccount.category(), "Funnel");
assert_eq!(AmlTypology::AccountTakeover.category(), "Fraud");
}
#[test]
fn test_laundering_stage() {
assert!(
LaunderingStage::Integration.detection_difficulty()
> LaunderingStage::Placement.detection_difficulty()
);
}
#[test]
fn test_sophistication_detectability() {
assert!(
Sophistication::Basic.detectability_modifier()
> Sophistication::Professional.detectability_modifier()
);
}
#[test]
fn test_turnover_band_range() {
let (min, max) = TurnoverBand::Medium.range();
assert!(min < max);
assert!(min >= 5_000);
assert!(max <= 25_000);
}
}