use super::{AccountType, Decimal128, HybridTimestamp};
use rkyv::{Archive, Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Archive, Serialize, Deserialize)]
#[archive(compare(PartialEq))]
#[repr(u8)]
pub enum FraudPatternType {
CircularFlow = 0,
SelfLoop = 1,
BenfordViolation = 2,
ThresholdClustering = 3,
AfterHoursEntry = 4,
HighVelocity = 5,
UnusualPairing = 6,
DormantActivation = 7,
RoundAmounts = 8,
DuplicateTransaction = 9,
StructuredTransactions = 10,
ReversalAnomaly = 11,
}
impl FraudPatternType {
pub fn risk_weight(&self) -> f32 {
match self {
FraudPatternType::CircularFlow => 0.95,
FraudPatternType::HighVelocity => 0.90,
FraudPatternType::ThresholdClustering => 0.85,
FraudPatternType::StructuredTransactions => 0.85,
FraudPatternType::DormantActivation => 0.80,
FraudPatternType::UnusualPairing => 0.75,
FraudPatternType::BenfordViolation => 0.70,
FraudPatternType::AfterHoursEntry => 0.60,
FraudPatternType::RoundAmounts => 0.50,
FraudPatternType::SelfLoop => 0.65,
FraudPatternType::DuplicateTransaction => 0.55,
FraudPatternType::ReversalAnomaly => 0.60,
}
}
pub fn description(&self) -> &'static str {
match self {
FraudPatternType::CircularFlow => "Circular money flow detected (A→B→C→A)",
FraudPatternType::SelfLoop => "Bidirectional flow between accounts",
FraudPatternType::BenfordViolation => "Amount distribution violates Benford's Law",
FraudPatternType::ThresholdClustering => "Amounts clustered below approval threshold",
FraudPatternType::AfterHoursEntry => "Entry posted outside business hours",
FraudPatternType::HighVelocity => "Rapid multi-hop money movement",
FraudPatternType::UnusualPairing => "Implausible account combination",
FraudPatternType::DormantActivation => "Dormant account suddenly activated",
FraudPatternType::RoundAmounts => "Suspicious round-number amounts",
FraudPatternType::DuplicateTransaction => "Potential duplicate transaction",
FraudPatternType::StructuredTransactions => "Structured to avoid detection",
FraudPatternType::ReversalAnomaly => "Unusual reversal pattern",
}
}
pub fn icon(&self) -> &'static str {
match self {
FraudPatternType::CircularFlow => "🔄",
FraudPatternType::SelfLoop => "↔️",
FraudPatternType::BenfordViolation => "📊",
FraudPatternType::ThresholdClustering => "📍",
FraudPatternType::AfterHoursEntry => "🌙",
FraudPatternType::HighVelocity => "⚡",
FraudPatternType::UnusualPairing => "❓",
FraudPatternType::DormantActivation => "💤",
FraudPatternType::RoundAmounts => "🔢",
FraudPatternType::DuplicateTransaction => "📋",
FraudPatternType::StructuredTransactions => "✂️",
FraudPatternType::ReversalAnomaly => "↩️",
}
}
}
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
#[repr(C)]
pub struct FraudPattern {
pub id: Uuid,
pub pattern_type: FraudPatternType,
pub risk_score: f32,
pub amount: Decimal128,
pub account_count: u16,
pub transaction_count: u16,
pub timeframe_days: u16,
pub _pad: u16,
pub first_seen: HybridTimestamp,
pub last_seen: HybridTimestamp,
pub involved_accounts: [u16; 8],
}
impl FraudPattern {
pub fn new(pattern_type: FraudPatternType) -> Self {
Self {
id: Uuid::new_v4(),
pattern_type,
risk_score: pattern_type.risk_weight(),
amount: Decimal128::ZERO,
account_count: 0,
transaction_count: 0,
timeframe_days: 0,
_pad: 0,
first_seen: HybridTimestamp::zero(),
last_seen: HybridTimestamp::zero(),
involved_accounts: [u16::MAX; 8],
}
}
pub fn add_account(&mut self, account_index: u16) {
for i in 0..8 {
if self.involved_accounts[i] == u16::MAX {
self.involved_accounts[i] = account_index;
self.account_count += 1;
break;
}
}
}
pub fn get_involved_accounts(&self) -> Vec<u16> {
self.involved_accounts
.iter()
.filter(|&&idx| idx != u16::MAX)
.copied()
.collect()
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Archive, Serialize, Deserialize,
)]
#[archive(compare(PartialEq))]
#[repr(u8)]
pub enum ViolationSeverity {
Low = 0,
Medium = 1,
High = 2,
Critical = 3,
}
impl ViolationSeverity {
pub fn color(&self) -> [u8; 3] {
match self {
ViolationSeverity::Low => [255, 235, 59], ViolationSeverity::Medium => [255, 152, 0], ViolationSeverity::High => [244, 67, 54], ViolationSeverity::Critical => [183, 28, 28], }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Archive, Serialize, Deserialize)]
#[archive(compare(PartialEq))]
#[repr(u8)]
pub enum GaapViolationType {
RevenueToCashDirect = 0,
RevenueToExpense = 1,
CashToRevenue = 2,
ExpenseToAsset = 3,
LiabilityToRevenue = 4,
CogsWithoutInventory = 5,
AccumDepreciationIncrease = 6,
RetainedEarningsModification = 7,
IntercompanyImbalance = 8,
UnbalancedEntry = 9,
}
impl GaapViolationType {
pub fn default_severity(&self) -> ViolationSeverity {
match self {
GaapViolationType::RevenueToExpense => ViolationSeverity::Critical,
GaapViolationType::UnbalancedEntry => ViolationSeverity::Critical,
GaapViolationType::RetainedEarningsModification => ViolationSeverity::High,
GaapViolationType::AccumDepreciationIncrease => ViolationSeverity::High,
GaapViolationType::RevenueToCashDirect => ViolationSeverity::Medium,
GaapViolationType::CashToRevenue => ViolationSeverity::Medium,
GaapViolationType::LiabilityToRevenue => ViolationSeverity::High,
GaapViolationType::ExpenseToAsset => ViolationSeverity::Medium,
GaapViolationType::CogsWithoutInventory => ViolationSeverity::Medium,
GaapViolationType::IntercompanyImbalance => ViolationSeverity::Low,
}
}
pub fn description(&self) -> &'static str {
match self {
GaapViolationType::RevenueToCashDirect => "Revenue directly to Cash (bypass A/R)",
GaapViolationType::RevenueToExpense => {
"Revenue to Expense (accounting equation violation)"
}
GaapViolationType::CashToRevenue => "Cash to Revenue (backward flow)",
GaapViolationType::ExpenseToAsset => "Expense to Asset (improper capitalization)",
GaapViolationType::LiabilityToRevenue => "Liability to Revenue (misclassification)",
GaapViolationType::CogsWithoutInventory => "COGS without Inventory movement",
GaapViolationType::AccumDepreciationIncrease => "Direct Accum. Depreciation increase",
GaapViolationType::RetainedEarningsModification => {
"Direct Retained Earnings modification"
}
GaapViolationType::IntercompanyImbalance => "Intercompany accounts don't balance",
GaapViolationType::UnbalancedEntry => "Debits ≠ Credits",
}
}
pub fn matches(&self, source_type: AccountType, target_type: AccountType) -> bool {
match self {
GaapViolationType::RevenueToCashDirect => {
source_type == AccountType::Revenue && target_type == AccountType::Asset
}
GaapViolationType::RevenueToExpense => {
source_type == AccountType::Revenue && target_type == AccountType::Expense
}
GaapViolationType::CashToRevenue => {
source_type == AccountType::Asset && target_type == AccountType::Revenue
}
GaapViolationType::ExpenseToAsset => {
source_type == AccountType::Expense && target_type == AccountType::Asset
}
GaapViolationType::LiabilityToRevenue => {
source_type == AccountType::Liability && target_type == AccountType::Revenue
}
_ => false, }
}
}
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
#[repr(C)]
pub struct GaapViolationRule {
pub rule_id: u32,
pub violation_type: GaapViolationType,
pub source_type: Option<AccountType>,
pub target_type: Option<AccountType>,
pub severity: ViolationSeverity,
pub min_amount: f64,
pub rule_name_hash: u64,
}
#[derive(Debug, Clone)]
pub struct GaapViolation {
pub id: Uuid,
pub violation_type: GaapViolationType,
pub severity: ViolationSeverity,
pub source_account: u16,
pub target_account: u16,
pub amount: Decimal128,
pub journal_entry_id: Uuid,
pub detected_at: HybridTimestamp,
pub description: String,
}
impl GaapViolation {
pub fn new(
violation_type: GaapViolationType,
source: u16,
target: u16,
amount: Decimal128,
journal_entry_id: Uuid,
) -> Self {
Self {
id: Uuid::new_v4(),
violation_type,
severity: violation_type.default_severity(),
source_account: source,
target_account: target,
amount,
journal_entry_id,
detected_at: HybridTimestamp::now(),
description: violation_type.description().to_string(),
}
}
}
pub const BENFORD_EXPECTED: [f64; 9] = [
0.301, 0.176, 0.125, 0.097, 0.079, 0.067, 0.058, 0.051, 0.046, ];
pub fn benford_chi_squared(observed_counts: &[u32; 9], total: u32) -> f64 {
if total == 0 {
return 0.0;
}
let mut chi_sq = 0.0;
for i in 0..9 {
let expected = BENFORD_EXPECTED[i] * total as f64;
let observed = observed_counts[i] as f64;
if expected > 0.0 {
chi_sq += (observed - expected).powi(2) / expected;
}
}
chi_sq
}
pub const BENFORD_CHI_SQ_CRITICAL: f64 = 15.507;
pub fn is_benford_violation(chi_squared: f64) -> bool {
chi_squared > BENFORD_CHI_SQ_CRITICAL
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_benford_perfect_distribution() {
let observed = [301, 176, 125, 97, 79, 67, 58, 51, 46];
let chi_sq = benford_chi_squared(&observed, 1000);
assert!(chi_sq < BENFORD_CHI_SQ_CRITICAL);
}
#[test]
fn test_benford_uniform_violation() {
let observed = [111, 111, 111, 111, 111, 111, 111, 111, 112];
let chi_sq = benford_chi_squared(&observed, 1000);
assert!(is_benford_violation(chi_sq));
}
#[test]
fn test_gaap_violation_matching() {
assert!(
GaapViolationType::RevenueToExpense.matches(AccountType::Revenue, AccountType::Expense)
);
assert!(
!GaapViolationType::RevenueToExpense.matches(AccountType::Asset, AccountType::Expense)
);
}
#[test]
fn test_fraud_pattern_accounts() {
let mut pattern = FraudPattern::new(FraudPatternType::CircularFlow);
pattern.add_account(0);
pattern.add_account(1);
pattern.add_account(2);
let accounts = pattern.get_involved_accounts();
assert_eq!(accounts, vec![0, 1, 2]);
}
}