use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
use datasynth_core::uuid_factory::{DeterministicUuidFactory, GeneratorType};
use datasynth_core::{
AcfeFraudCategory, AnomalyDetectionDifficulty, FraudTriangle, OpportunityFactor, PressureType,
Rationalization,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ManagementLevel {
SeniorManagement,
CSuite,
Board,
}
impl ManagementLevel {
pub fn detection_difficulty(&self) -> AnomalyDetectionDifficulty {
match self {
ManagementLevel::SeniorManagement => AnomalyDetectionDifficulty::Hard,
ManagementLevel::CSuite => AnomalyDetectionDifficulty::Expert,
ManagementLevel::Board => AnomalyDetectionDifficulty::Expert,
}
}
pub fn typical_median_loss(&self) -> Decimal {
match self {
ManagementLevel::SeniorManagement => Decimal::new(150_000, 0),
ManagementLevel::CSuite => Decimal::new(600_000, 0),
ManagementLevel::Board => Decimal::new(500_000, 0),
}
}
pub fn concealment_probability(&self) -> f64 {
match self {
ManagementLevel::SeniorManagement => 0.70,
ManagementLevel::CSuite => 0.85,
ManagementLevel::Board => 0.80,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum OverrideType {
Revenue(Vec<RevenueOverrideTechnique>),
Expense(Vec<ExpenseOverrideTechnique>),
Asset(Vec<AssetOverrideTechnique>),
Reserve(Vec<ReserveOverrideTechnique>),
}
impl OverrideType {
pub fn acfe_category(&self) -> AcfeFraudCategory {
AcfeFraudCategory::FinancialStatementFraud
}
pub fn description(&self) -> String {
match self {
OverrideType::Revenue(techniques) => {
format!("Revenue override: {techniques:?}")
}
OverrideType::Expense(techniques) => {
format!("Expense override: {techniques:?}")
}
OverrideType::Asset(techniques) => {
format!("Asset valuation override: {techniques:?}")
}
OverrideType::Reserve(techniques) => {
format!("Reserve manipulation: {techniques:?}")
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum RevenueOverrideTechnique {
JournalEntryOverride,
RevenueRecognitionAcceleration,
AllowanceReduction,
SideAgreementConcealment,
ChannelStuffingWithSideLetters,
ImproperBillAndHold,
PercentageOfCompletionOverstatement,
}
impl RevenueOverrideTechnique {
pub fn detection_difficulty(&self) -> AnomalyDetectionDifficulty {
match self {
RevenueOverrideTechnique::JournalEntryOverride => AnomalyDetectionDifficulty::Moderate,
RevenueOverrideTechnique::RevenueRecognitionAcceleration => {
AnomalyDetectionDifficulty::Hard
}
RevenueOverrideTechnique::AllowanceReduction => AnomalyDetectionDifficulty::Moderate,
RevenueOverrideTechnique::SideAgreementConcealment => {
AnomalyDetectionDifficulty::Expert
}
RevenueOverrideTechnique::ChannelStuffingWithSideLetters => {
AnomalyDetectionDifficulty::Expert
}
RevenueOverrideTechnique::ImproperBillAndHold => AnomalyDetectionDifficulty::Hard,
RevenueOverrideTechnique::PercentageOfCompletionOverstatement => {
AnomalyDetectionDifficulty::Hard
}
}
}
pub fn indicators(&self) -> Vec<&'static str> {
match self {
RevenueOverrideTechnique::JournalEntryOverride => {
vec!["manual_je_at_period_end", "unusual_revenue_account_entries"]
}
RevenueOverrideTechnique::RevenueRecognitionAcceleration => {
vec![
"revenue_spike_at_period_end",
"reversals_in_subsequent_period",
]
}
RevenueOverrideTechnique::AllowanceReduction => {
vec![
"allowance_ratio_decline",
"aging_profile_deterioration_without_allowance_increase",
]
}
RevenueOverrideTechnique::SideAgreementConcealment => {
vec!["unusual_return_rates", "credit_memos_post_period"]
}
RevenueOverrideTechnique::ChannelStuffingWithSideLetters => {
vec![
"distributor_inventory_buildup",
"quarter_end_shipment_spike",
]
}
RevenueOverrideTechnique::ImproperBillAndHold => {
vec!["inventory_not_shipped", "unusual_storage_arrangements"]
}
RevenueOverrideTechnique::PercentageOfCompletionOverstatement => {
vec![
"cost_to_complete_estimates_declining",
"revenue_recognized_exceeds_billing",
]
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ExpenseOverrideTechnique {
CapitalizationAbuse,
ExpenseDeferral,
CostAllocationManipulation,
AccrualOmission,
UsefulLifeExtension,
DepreciationMethodChange,
}
impl ExpenseOverrideTechnique {
pub fn detection_difficulty(&self) -> AnomalyDetectionDifficulty {
match self {
ExpenseOverrideTechnique::CapitalizationAbuse => AnomalyDetectionDifficulty::Hard,
ExpenseOverrideTechnique::ExpenseDeferral => AnomalyDetectionDifficulty::Moderate,
ExpenseOverrideTechnique::CostAllocationManipulation => {
AnomalyDetectionDifficulty::Hard
}
ExpenseOverrideTechnique::AccrualOmission => AnomalyDetectionDifficulty::Moderate,
ExpenseOverrideTechnique::UsefulLifeExtension => AnomalyDetectionDifficulty::Moderate,
ExpenseOverrideTechnique::DepreciationMethodChange => AnomalyDetectionDifficulty::Easy,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AssetOverrideTechnique {
ImpairmentAvoidance,
FairValueManipulation,
InventoryOverstatement,
ObsolescenceConcealment,
ReceivablesAging,
}
impl AssetOverrideTechnique {
pub fn detection_difficulty(&self) -> AnomalyDetectionDifficulty {
match self {
AssetOverrideTechnique::ImpairmentAvoidance => AnomalyDetectionDifficulty::Hard,
AssetOverrideTechnique::FairValueManipulation => AnomalyDetectionDifficulty::Expert,
AssetOverrideTechnique::InventoryOverstatement => AnomalyDetectionDifficulty::Moderate,
AssetOverrideTechnique::ObsolescenceConcealment => AnomalyDetectionDifficulty::Hard,
AssetOverrideTechnique::ReceivablesAging => AnomalyDetectionDifficulty::Moderate,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ReserveOverrideTechnique {
CookieJarReserves,
ReserveRelease,
WarrantyReserveUnderstatement,
RestructuringReserveManipulation,
}
impl ReserveOverrideTechnique {
pub fn detection_difficulty(&self) -> AnomalyDetectionDifficulty {
match self {
ReserveOverrideTechnique::CookieJarReserves => AnomalyDetectionDifficulty::Hard,
ReserveOverrideTechnique::ReserveRelease => AnomalyDetectionDifficulty::Moderate,
ReserveOverrideTechnique::WarrantyReserveUnderstatement => {
AnomalyDetectionDifficulty::Hard
}
ReserveOverrideTechnique::RestructuringReserveManipulation => {
AnomalyDetectionDifficulty::Hard
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ManagementConcealment {
pub false_documentation: bool,
pub intimidation_of_subordinates: bool,
pub auditor_deception: bool,
pub board_oversight_circumvention: bool,
pub information_control: bool,
pub transaction_complexity: bool,
pub related_party_concealment: bool,
}
impl ManagementConcealment {
pub fn active_count(&self) -> u32 {
[
self.false_documentation,
self.intimidation_of_subordinates,
self.auditor_deception,
self.board_oversight_circumvention,
self.information_control,
self.transaction_complexity,
self.related_party_concealment,
]
.iter()
.filter(|&&x| x)
.count() as u32
}
pub fn difficulty_modifier(&self) -> f64 {
1.0 + (self.active_count() as f64 * 0.1)
}
pub fn potential_indicators(&self) -> Vec<&'static str> {
let mut indicators = Vec::new();
if self.false_documentation {
indicators.push("document_inconsistencies");
}
if self.intimidation_of_subordinates {
indicators.push("employee_turnover_in_accounting");
indicators.push("anonymous_hotline_tips");
}
if self.auditor_deception {
indicators.push("limited_information_to_auditors");
indicators.push("auditor_scope_limitations");
}
if self.board_oversight_circumvention {
indicators.push("limited_board_information");
indicators.push("audit_committee_turnover");
}
indicators
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManagementOverrideScheme {
pub scheme_id: Uuid,
pub perpetrator_level: ManagementLevel,
pub perpetrator_id: String,
pub override_type: OverrideType,
pub fraud_triangle: FraudTriangle,
pub concealment: ManagementConcealment,
pub start_date: NaiveDate,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end_date: Option<NaiveDate>,
pub financial_impact: Decimal,
pub periods_affected: u32,
pub is_detected: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detection_method: Option<String>,
pub transaction_ids: Vec<String>,
#[serde(default)]
pub metadata: HashMap<String, String>,
}
impl ManagementOverrideScheme {
pub fn new(
perpetrator_level: ManagementLevel,
perpetrator_id: impl Into<String>,
override_type: OverrideType,
start_date: NaiveDate,
) -> Self {
let fraud_triangle = FraudTriangle::new(
PressureType::FinancialTargets,
vec![OpportunityFactor::ManagementOverride],
Rationalization::ForTheCompanyGood,
);
let uuid_factory = DeterministicUuidFactory::new(0, GeneratorType::Anomaly);
Self {
scheme_id: uuid_factory.next(),
perpetrator_level,
perpetrator_id: perpetrator_id.into(),
override_type,
fraud_triangle,
concealment: ManagementConcealment::default(),
start_date,
end_date: None,
financial_impact: Decimal::ZERO,
periods_affected: 0,
is_detected: false,
detection_method: None,
transaction_ids: Vec::new(),
metadata: HashMap::new(),
}
}
pub fn with_fraud_triangle(mut self, triangle: FraudTriangle) -> Self {
self.fraud_triangle = triangle;
self
}
pub fn with_concealment(mut self, concealment: ManagementConcealment) -> Self {
self.concealment = concealment;
self
}
pub fn record_transaction(&mut self, amount: Decimal, transaction_id: impl Into<String>) {
self.financial_impact += amount;
self.transaction_ids.push(transaction_id.into());
}
pub fn record_period(&mut self) {
self.periods_affected += 1;
}
pub fn mark_detected(&mut self, end_date: NaiveDate, method: impl Into<String>) {
self.is_detected = true;
self.end_date = Some(end_date);
self.detection_method = Some(method.into());
}
pub fn detection_difficulty(&self) -> AnomalyDetectionDifficulty {
let base = self.perpetrator_level.detection_difficulty();
let concealment_modifier = self.concealment.difficulty_modifier();
let score = base.difficulty_score() * concealment_modifier;
AnomalyDetectionDifficulty::from_score(score.min(1.0))
}
pub fn risk_indicators(&self) -> Vec<String> {
let mut indicators = Vec::new();
if let OverrideType::Revenue(techniques) = &self.override_type {
for tech in techniques {
indicators.extend(tech.indicators().into_iter().map(String::from));
}
}
indicators.extend(
self.concealment
.potential_indicators()
.into_iter()
.map(String::from),
);
indicators.push("manual_period_end_entries".to_string());
indicators.push("entries_without_supporting_documentation".to_string());
indicators.push("overridden_system_controls".to_string());
indicators
}
pub fn description(&self) -> String {
format!(
"{:?} level override: {}, impact: {}, {} periods affected",
self.perpetrator_level,
self.override_type.description(),
self.financial_impact,
self.periods_affected
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManagementOverrideGenerator {
pub override_rate: f64,
pub type_weights: HashMap<String, f64>,
pub level_weights: HashMap<String, f64>,
}
impl Default for ManagementOverrideGenerator {
fn default() -> Self {
let mut type_weights = HashMap::new();
type_weights.insert("revenue".to_string(), 0.40);
type_weights.insert("expense".to_string(), 0.25);
type_weights.insert("asset".to_string(), 0.20);
type_weights.insert("reserve".to_string(), 0.15);
let mut level_weights = HashMap::new();
level_weights.insert("senior_management".to_string(), 0.50);
level_weights.insert("c_suite".to_string(), 0.35);
level_weights.insert("board".to_string(), 0.15);
Self {
override_rate: 0.70, type_weights,
level_weights,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_management_level() {
let cfo = ManagementLevel::CSuite;
assert_eq!(
cfo.detection_difficulty(),
AnomalyDetectionDifficulty::Expert
);
assert_eq!(cfo.typical_median_loss(), Decimal::new(600_000, 0));
}
#[test]
fn test_revenue_override_technique() {
let tech = RevenueOverrideTechnique::SideAgreementConcealment;
assert_eq!(
tech.detection_difficulty(),
AnomalyDetectionDifficulty::Expert
);
assert!(!tech.indicators().is_empty());
}
#[test]
fn test_management_concealment() {
let mut concealment = ManagementConcealment::default();
assert_eq!(concealment.active_count(), 0);
assert_eq!(concealment.difficulty_modifier(), 1.0);
concealment.false_documentation = true;
concealment.auditor_deception = true;
concealment.intimidation_of_subordinates = true;
assert_eq!(concealment.active_count(), 3);
assert!((concealment.difficulty_modifier() - 1.3).abs() < 0.01);
assert!(!concealment.potential_indicators().is_empty());
}
#[test]
fn test_management_override_scheme() {
let scheme = ManagementOverrideScheme::new(
ManagementLevel::CSuite,
"CFO001",
OverrideType::Revenue(vec![
RevenueOverrideTechnique::RevenueRecognitionAcceleration,
RevenueOverrideTechnique::ChannelStuffingWithSideLetters,
]),
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
);
assert_eq!(scheme.perpetrator_level, ManagementLevel::CSuite);
assert!(!scheme.is_detected);
assert!(!scheme.risk_indicators().is_empty());
}
#[test]
fn test_scheme_transaction_recording() {
let mut scheme = ManagementOverrideScheme::new(
ManagementLevel::SeniorManagement,
"VP001",
OverrideType::Expense(vec![ExpenseOverrideTechnique::CapitalizationAbuse]),
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
);
scheme.record_transaction(Decimal::new(100_000, 0), "JE001");
scheme.record_transaction(Decimal::new(50_000, 0), "JE002");
scheme.record_period();
assert_eq!(scheme.financial_impact, Decimal::new(150_000, 0));
assert_eq!(scheme.transaction_ids.len(), 2);
assert_eq!(scheme.periods_affected, 1);
}
#[test]
fn test_scheme_detection() {
let mut scheme = ManagementOverrideScheme::new(
ManagementLevel::SeniorManagement,
"VP001",
OverrideType::Asset(vec![AssetOverrideTechnique::ImpairmentAvoidance]),
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
);
scheme.mark_detected(
NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(),
"internal_audit",
);
assert!(scheme.is_detected);
assert_eq!(
scheme.end_date,
Some(NaiveDate::from_ymd_opt(2024, 6, 15).unwrap())
);
assert_eq!(scheme.detection_method, Some("internal_audit".to_string()));
}
#[test]
fn test_fraud_triangle_integration() {
let triangle = FraudTriangle::new(
PressureType::MarketExpectations,
vec![
OpportunityFactor::ManagementOverride,
OpportunityFactor::WeakInternalControls,
],
Rationalization::ForTheCompanyGood,
);
let scheme = ManagementOverrideScheme::new(
ManagementLevel::CSuite,
"CEO001",
OverrideType::Revenue(vec![RevenueOverrideTechnique::JournalEntryOverride]),
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
)
.with_fraud_triangle(triangle);
assert_eq!(
scheme.fraud_triangle.pressure,
PressureType::MarketExpectations
);
assert!(scheme.fraud_triangle.risk_score() > 0.5);
}
}