Skip to main content

datasynth_core/models/
anomaly.rs

1//! Anomaly types and labels for synthetic data generation.
2//!
3//! This module provides comprehensive anomaly classification for:
4//! - Fraud detection training
5//! - Error detection systems
6//! - Process compliance monitoring
7//! - Statistical anomaly detection
8//! - Graph-based anomaly detection
9
10use chrono::{NaiveDate, NaiveDateTime};
11use rust_decimal::Decimal;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14
15/// Causal reason explaining why an anomaly was injected.
16///
17/// This enables provenance tracking for understanding the "why" behind each anomaly.
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub enum AnomalyCausalReason {
20    /// Injected due to random rate selection.
21    RandomRate {
22        /// Base rate used for selection.
23        base_rate: f64,
24    },
25    /// Injected due to temporal pattern matching.
26    TemporalPattern {
27        /// Name of the temporal pattern (e.g., "year_end_spike", "month_end").
28        pattern_name: String,
29    },
30    /// Injected based on entity targeting rules.
31    EntityTargeting {
32        /// Type of entity targeted (e.g., "vendor", "user", "account").
33        target_type: String,
34        /// ID of the targeted entity.
35        target_id: String,
36    },
37    /// Part of an anomaly cluster.
38    ClusterMembership {
39        /// ID of the cluster this anomaly belongs to.
40        cluster_id: String,
41    },
42    /// Part of a multi-step scenario.
43    ScenarioStep {
44        /// Type of scenario (e.g., "kickback_scheme", "round_tripping").
45        scenario_type: String,
46        /// Step number within the scenario.
47        step_number: u32,
48    },
49    /// Injected based on data quality profile.
50    DataQualityProfile {
51        /// Profile name (e.g., "noisy", "legacy", "clean").
52        profile: String,
53    },
54    /// Injected for ML training balance.
55    MLTrainingBalance {
56        /// Target class being balanced.
57        target_class: String,
58    },
59}
60
61/// Structured injection strategy with captured parameters.
62///
63/// Unlike the string-based `injection_strategy` field, this enum captures
64/// the exact parameters used during injection for full reproducibility.
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66pub enum InjectionStrategy {
67    /// Amount was manipulated by a factor.
68    AmountManipulation {
69        /// Original amount before manipulation.
70        original: Decimal,
71        /// Multiplication factor applied.
72        factor: f64,
73    },
74    /// Amount adjusted to avoid a threshold.
75    ThresholdAvoidance {
76        /// Threshold being avoided.
77        threshold: Decimal,
78        /// Final amount after adjustment.
79        adjusted_amount: Decimal,
80    },
81    /// Date was backdated or forward-dated.
82    DateShift {
83        /// Number of days shifted (negative = backdated).
84        days_shifted: i32,
85        /// Original date before shift.
86        original_date: NaiveDate,
87    },
88    /// User approved their own transaction.
89    SelfApproval {
90        /// User who created and approved.
91        user_id: String,
92    },
93    /// Segregation of duties violation.
94    SoDViolation {
95        /// First duty involved.
96        duty1: String,
97        /// Second duty involved.
98        duty2: String,
99        /// User who performed both duties.
100        violating_user: String,
101    },
102    /// Exact duplicate of another document.
103    ExactDuplicate {
104        /// ID of the original document.
105        original_doc_id: String,
106    },
107    /// Near-duplicate with small variations.
108    NearDuplicate {
109        /// ID of the original document.
110        original_doc_id: String,
111        /// Fields that were varied.
112        varied_fields: Vec<String>,
113    },
114    /// Circular flow of funds/goods.
115    CircularFlow {
116        /// Chain of entities involved.
117        entity_chain: Vec<String>,
118    },
119    /// Split transaction to avoid threshold.
120    SplitTransaction {
121        /// Original total amount.
122        original_amount: Decimal,
123        /// Number of splits.
124        split_count: u32,
125        /// IDs of the split documents.
126        split_doc_ids: Vec<String>,
127    },
128    /// Round number manipulation.
129    RoundNumbering {
130        /// Original precise amount.
131        original_amount: Decimal,
132        /// Rounded amount.
133        rounded_amount: Decimal,
134    },
135    /// Timing manipulation (weekend, after-hours, etc.).
136    TimingManipulation {
137        /// Type of timing issue.
138        timing_type: String,
139        /// Original timestamp.
140        original_time: Option<NaiveDateTime>,
141    },
142    /// Account misclassification.
143    AccountMisclassification {
144        /// Correct account.
145        correct_account: String,
146        /// Incorrect account used.
147        incorrect_account: String,
148    },
149    /// Missing required field.
150    MissingField {
151        /// Name of the missing field.
152        field_name: String,
153    },
154    /// Custom injection strategy.
155    Custom {
156        /// Strategy name.
157        name: String,
158        /// Additional parameters.
159        parameters: HashMap<String, String>,
160    },
161}
162
163impl InjectionStrategy {
164    /// Returns a human-readable description of the strategy.
165    pub fn description(&self) -> String {
166        match self {
167            InjectionStrategy::AmountManipulation { factor, .. } => {
168                format!("Amount multiplied by {:.2}", factor)
169            }
170            InjectionStrategy::ThresholdAvoidance { threshold, .. } => {
171                format!("Amount adjusted to avoid {} threshold", threshold)
172            }
173            InjectionStrategy::DateShift { days_shifted, .. } => {
174                if *days_shifted < 0 {
175                    format!("Date backdated by {} days", days_shifted.abs())
176                } else {
177                    format!("Date forward-dated by {} days", days_shifted)
178                }
179            }
180            InjectionStrategy::SelfApproval { user_id } => {
181                format!("Self-approval by user {}", user_id)
182            }
183            InjectionStrategy::SoDViolation { duty1, duty2, .. } => {
184                format!("SoD violation: {} and {}", duty1, duty2)
185            }
186            InjectionStrategy::ExactDuplicate { original_doc_id } => {
187                format!("Exact duplicate of {}", original_doc_id)
188            }
189            InjectionStrategy::NearDuplicate {
190                original_doc_id,
191                varied_fields,
192            } => {
193                format!(
194                    "Near-duplicate of {} (varied: {:?})",
195                    original_doc_id, varied_fields
196                )
197            }
198            InjectionStrategy::CircularFlow { entity_chain } => {
199                format!("Circular flow through {} entities", entity_chain.len())
200            }
201            InjectionStrategy::SplitTransaction { split_count, .. } => {
202                format!("Split into {} transactions", split_count)
203            }
204            InjectionStrategy::RoundNumbering { .. } => "Amount rounded to even number".to_string(),
205            InjectionStrategy::TimingManipulation { timing_type, .. } => {
206                format!("Timing manipulation: {}", timing_type)
207            }
208            InjectionStrategy::AccountMisclassification {
209                correct_account,
210                incorrect_account,
211            } => {
212                format!(
213                    "Misclassified from {} to {}",
214                    correct_account, incorrect_account
215                )
216            }
217            InjectionStrategy::MissingField { field_name } => {
218                format!("Missing required field: {}", field_name)
219            }
220            InjectionStrategy::Custom { name, .. } => format!("Custom: {}", name),
221        }
222    }
223
224    /// Returns the strategy type name.
225    pub fn strategy_type(&self) -> &'static str {
226        match self {
227            InjectionStrategy::AmountManipulation { .. } => "AmountManipulation",
228            InjectionStrategy::ThresholdAvoidance { .. } => "ThresholdAvoidance",
229            InjectionStrategy::DateShift { .. } => "DateShift",
230            InjectionStrategy::SelfApproval { .. } => "SelfApproval",
231            InjectionStrategy::SoDViolation { .. } => "SoDViolation",
232            InjectionStrategy::ExactDuplicate { .. } => "ExactDuplicate",
233            InjectionStrategy::NearDuplicate { .. } => "NearDuplicate",
234            InjectionStrategy::CircularFlow { .. } => "CircularFlow",
235            InjectionStrategy::SplitTransaction { .. } => "SplitTransaction",
236            InjectionStrategy::RoundNumbering { .. } => "RoundNumbering",
237            InjectionStrategy::TimingManipulation { .. } => "TimingManipulation",
238            InjectionStrategy::AccountMisclassification { .. } => "AccountMisclassification",
239            InjectionStrategy::MissingField { .. } => "MissingField",
240            InjectionStrategy::Custom { .. } => "Custom",
241        }
242    }
243}
244
245/// Primary anomaly classification.
246#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
247pub enum AnomalyType {
248    /// Fraudulent activity.
249    Fraud(FraudType),
250    /// Data entry or processing error.
251    Error(ErrorType),
252    /// Process or control issue.
253    ProcessIssue(ProcessIssueType),
254    /// Statistical anomaly.
255    Statistical(StatisticalAnomalyType),
256    /// Relational/graph anomaly.
257    Relational(RelationalAnomalyType),
258    /// Custom anomaly type.
259    Custom(String),
260}
261
262impl AnomalyType {
263    /// Returns the category name.
264    pub fn category(&self) -> &'static str {
265        match self {
266            AnomalyType::Fraud(_) => "Fraud",
267            AnomalyType::Error(_) => "Error",
268            AnomalyType::ProcessIssue(_) => "ProcessIssue",
269            AnomalyType::Statistical(_) => "Statistical",
270            AnomalyType::Relational(_) => "Relational",
271            AnomalyType::Custom(_) => "Custom",
272        }
273    }
274
275    /// Returns the specific type name.
276    pub fn type_name(&self) -> String {
277        match self {
278            AnomalyType::Fraud(t) => format!("{:?}", t),
279            AnomalyType::Error(t) => format!("{:?}", t),
280            AnomalyType::ProcessIssue(t) => format!("{:?}", t),
281            AnomalyType::Statistical(t) => format!("{:?}", t),
282            AnomalyType::Relational(t) => format!("{:?}", t),
283            AnomalyType::Custom(s) => s.clone(),
284        }
285    }
286
287    /// Returns the severity level (1-5, 5 being most severe).
288    pub fn severity(&self) -> u8 {
289        match self {
290            AnomalyType::Fraud(t) => t.severity(),
291            AnomalyType::Error(t) => t.severity(),
292            AnomalyType::ProcessIssue(t) => t.severity(),
293            AnomalyType::Statistical(t) => t.severity(),
294            AnomalyType::Relational(t) => t.severity(),
295            AnomalyType::Custom(_) => 3,
296        }
297    }
298
299    /// Returns whether this anomaly is typically intentional.
300    pub fn is_intentional(&self) -> bool {
301        matches!(self, AnomalyType::Fraud(_))
302    }
303}
304
305/// Fraud types for detection training.
306#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
307pub enum FraudType {
308    // Journal Entry Fraud
309    /// Fictitious journal entry with no business purpose.
310    FictitiousEntry,
311    /// Fictitious transaction (alias for FictitiousEntry).
312    FictitiousTransaction,
313    /// Round-dollar amounts suggesting manual manipulation.
314    RoundDollarManipulation,
315    /// Entry posted just below approval threshold.
316    JustBelowThreshold,
317    /// Revenue recognition manipulation.
318    RevenueManipulation,
319    /// Expense capitalization fraud.
320    ImproperCapitalization,
321    /// Improperly capitalizing expenses as assets.
322    ExpenseCapitalization,
323    /// Cookie jar reserves manipulation.
324    ReserveManipulation,
325    /// Round-tripping funds through suspense/clearing accounts.
326    SuspenseAccountAbuse,
327    /// Splitting transactions to stay below approval thresholds.
328    SplitTransaction,
329    /// Unusual timing (weekend, holiday, after-hours postings).
330    TimingAnomaly,
331    /// Posting to unauthorized accounts.
332    UnauthorizedAccess,
333
334    // Approval Fraud
335    /// User approving their own request.
336    SelfApproval,
337    /// Approval beyond authorized limit.
338    ExceededApprovalLimit,
339    /// Segregation of duties violation.
340    SegregationOfDutiesViolation,
341    /// Approval by unauthorized user.
342    UnauthorizedApproval,
343    /// Collusion between approver and requester.
344    CollusiveApproval,
345
346    // Vendor/Payment Fraud
347    /// Fictitious vendor.
348    FictitiousVendor,
349    /// Duplicate payment to vendor.
350    DuplicatePayment,
351    /// Payment to shell company.
352    ShellCompanyPayment,
353    /// Kickback scheme.
354    Kickback,
355    /// Kickback scheme (alias).
356    KickbackScheme,
357    /// Invoice manipulation.
358    InvoiceManipulation,
359
360    // Asset Fraud
361    /// Misappropriation of assets.
362    AssetMisappropriation,
363    /// Inventory theft.
364    InventoryTheft,
365    /// Ghost employee.
366    GhostEmployee,
367
368    // Financial Statement Fraud
369    /// Premature revenue recognition.
370    PrematureRevenue,
371    /// Understated liabilities.
372    UnderstatedLiabilities,
373    /// Overstated assets.
374    OverstatedAssets,
375    /// Channel stuffing.
376    ChannelStuffing,
377
378    // Accounting Standards Violations (ASC 606 / IFRS 15 - Revenue)
379    /// Improper revenue recognition timing (ASC 606/IFRS 15).
380    ImproperRevenueRecognition,
381    /// Multiple performance obligations not properly separated.
382    ImproperPoAllocation,
383    /// Variable consideration not properly estimated.
384    VariableConsiderationManipulation,
385    /// Contract modifications not properly accounted for.
386    ContractModificationMisstatement,
387
388    // Accounting Standards Violations (ASC 842 / IFRS 16 - Leases)
389    /// Lease classification manipulation (operating vs finance).
390    LeaseClassificationManipulation,
391    /// Off-balance sheet lease fraud.
392    OffBalanceSheetLease,
393    /// Lease liability understatement.
394    LeaseLiabilityUnderstatement,
395    /// ROU asset misstatement.
396    RouAssetMisstatement,
397
398    // Accounting Standards Violations (ASC 820 / IFRS 13 - Fair Value)
399    /// Fair value hierarchy misclassification.
400    FairValueHierarchyManipulation,
401    /// Level 3 input manipulation.
402    Level3InputManipulation,
403    /// Valuation technique manipulation.
404    ValuationTechniqueManipulation,
405
406    // Accounting Standards Violations (ASC 360 / IAS 36 - Impairment)
407    /// Delayed impairment recognition.
408    DelayedImpairment,
409    /// Improperly avoiding impairment testing.
410    ImpairmentTestAvoidance,
411    /// Cash flow projection manipulation for impairment.
412    CashFlowProjectionManipulation,
413    /// Improper impairment reversal (IFRS only).
414    ImproperImpairmentReversal,
415}
416
417impl FraudType {
418    /// Returns severity level (1-5).
419    pub fn severity(&self) -> u8 {
420        match self {
421            FraudType::RoundDollarManipulation => 2,
422            FraudType::JustBelowThreshold => 3,
423            FraudType::SelfApproval => 3,
424            FraudType::ExceededApprovalLimit => 3,
425            FraudType::DuplicatePayment => 3,
426            FraudType::FictitiousEntry => 4,
427            FraudType::RevenueManipulation => 5,
428            FraudType::FictitiousVendor => 5,
429            FraudType::ShellCompanyPayment => 5,
430            FraudType::AssetMisappropriation => 5,
431            FraudType::SegregationOfDutiesViolation => 4,
432            FraudType::CollusiveApproval => 5,
433            // Accounting Standards Violations (Revenue - ASC 606/IFRS 15)
434            FraudType::ImproperRevenueRecognition => 5,
435            FraudType::ImproperPoAllocation => 4,
436            FraudType::VariableConsiderationManipulation => 4,
437            FraudType::ContractModificationMisstatement => 3,
438            // Accounting Standards Violations (Leases - ASC 842/IFRS 16)
439            FraudType::LeaseClassificationManipulation => 4,
440            FraudType::OffBalanceSheetLease => 5,
441            FraudType::LeaseLiabilityUnderstatement => 4,
442            FraudType::RouAssetMisstatement => 3,
443            // Accounting Standards Violations (Fair Value - ASC 820/IFRS 13)
444            FraudType::FairValueHierarchyManipulation => 4,
445            FraudType::Level3InputManipulation => 5,
446            FraudType::ValuationTechniqueManipulation => 4,
447            // Accounting Standards Violations (Impairment - ASC 360/IAS 36)
448            FraudType::DelayedImpairment => 4,
449            FraudType::ImpairmentTestAvoidance => 4,
450            FraudType::CashFlowProjectionManipulation => 5,
451            FraudType::ImproperImpairmentReversal => 3,
452            _ => 4,
453        }
454    }
455}
456
457/// Error types for error detection.
458#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
459pub enum ErrorType {
460    // Data Entry Errors
461    /// Duplicate document entry.
462    DuplicateEntry,
463    /// Reversed debit/credit amounts.
464    ReversedAmount,
465    /// Transposed digits in amount.
466    TransposedDigits,
467    /// Wrong decimal placement.
468    DecimalError,
469    /// Missing required field.
470    MissingField,
471    /// Invalid account code.
472    InvalidAccount,
473
474    // Timing Errors
475    /// Posted to wrong period.
476    WrongPeriod,
477    /// Backdated entry.
478    BackdatedEntry,
479    /// Future-dated entry.
480    FutureDatedEntry,
481    /// Cutoff error.
482    CutoffError,
483
484    // Classification Errors
485    /// Wrong account classification.
486    MisclassifiedAccount,
487    /// Wrong cost center.
488    WrongCostCenter,
489    /// Wrong company code.
490    WrongCompanyCode,
491
492    // Calculation Errors
493    /// Unbalanced journal entry.
494    UnbalancedEntry,
495    /// Rounding error.
496    RoundingError,
497    /// Currency conversion error.
498    CurrencyError,
499    /// Tax calculation error.
500    TaxCalculationError,
501
502    // Accounting Standards Errors (Non-Fraudulent)
503    /// Wrong revenue recognition timing (honest mistake).
504    RevenueTimingError,
505    /// Performance obligation allocation error.
506    PoAllocationError,
507    /// Lease classification error (operating vs finance).
508    LeaseClassificationError,
509    /// Lease calculation error (PV, amortization).
510    LeaseCalculationError,
511    /// Fair value measurement error.
512    FairValueError,
513    /// Impairment calculation error.
514    ImpairmentCalculationError,
515    /// Discount rate error.
516    DiscountRateError,
517    /// Framework application error (IFRS vs GAAP).
518    FrameworkApplicationError,
519}
520
521impl ErrorType {
522    /// Returns severity level (1-5).
523    pub fn severity(&self) -> u8 {
524        match self {
525            ErrorType::RoundingError => 1,
526            ErrorType::MissingField => 2,
527            ErrorType::TransposedDigits => 2,
528            ErrorType::DecimalError => 3,
529            ErrorType::DuplicateEntry => 3,
530            ErrorType::ReversedAmount => 3,
531            ErrorType::WrongPeriod => 4,
532            ErrorType::UnbalancedEntry => 5,
533            ErrorType::CurrencyError => 4,
534            // Accounting Standards Errors
535            ErrorType::RevenueTimingError => 4,
536            ErrorType::PoAllocationError => 3,
537            ErrorType::LeaseClassificationError => 3,
538            ErrorType::LeaseCalculationError => 3,
539            ErrorType::FairValueError => 4,
540            ErrorType::ImpairmentCalculationError => 4,
541            ErrorType::DiscountRateError => 3,
542            ErrorType::FrameworkApplicationError => 4,
543            _ => 3,
544        }
545    }
546}
547
548/// Process issue types.
549#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
550pub enum ProcessIssueType {
551    // Approval Issues
552    /// Approval skipped entirely.
553    SkippedApproval,
554    /// Late approval (after posting).
555    LateApproval,
556    /// Missing supporting documentation.
557    MissingDocumentation,
558    /// Incomplete approval chain.
559    IncompleteApprovalChain,
560
561    // Timing Issues
562    /// Late posting.
563    LatePosting,
564    /// Posting outside business hours.
565    AfterHoursPosting,
566    /// Weekend/holiday posting.
567    WeekendPosting,
568    /// Rushed period-end posting.
569    RushedPeriodEnd,
570
571    // Control Issues
572    /// Manual override of system control.
573    ManualOverride,
574    /// Unusual user access pattern.
575    UnusualAccess,
576    /// System bypass.
577    SystemBypass,
578    /// Batch processing anomaly.
579    BatchAnomaly,
580
581    // Documentation Issues
582    /// Vague or missing description.
583    VagueDescription,
584    /// Changed after posting.
585    PostFactoChange,
586    /// Incomplete audit trail.
587    IncompleteAuditTrail,
588}
589
590impl ProcessIssueType {
591    /// Returns severity level (1-5).
592    pub fn severity(&self) -> u8 {
593        match self {
594            ProcessIssueType::VagueDescription => 1,
595            ProcessIssueType::LatePosting => 2,
596            ProcessIssueType::AfterHoursPosting => 2,
597            ProcessIssueType::WeekendPosting => 2,
598            ProcessIssueType::SkippedApproval => 4,
599            ProcessIssueType::ManualOverride => 4,
600            ProcessIssueType::SystemBypass => 5,
601            ProcessIssueType::IncompleteAuditTrail => 4,
602            _ => 3,
603        }
604    }
605}
606
607/// Statistical anomaly types.
608#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
609pub enum StatisticalAnomalyType {
610    // Amount Anomalies
611    /// Amount significantly above normal.
612    UnusuallyHighAmount,
613    /// Amount significantly below normal.
614    UnusuallyLowAmount,
615    /// Violates Benford's Law distribution.
616    BenfordViolation,
617    /// Exact duplicate amount (suspicious).
618    ExactDuplicateAmount,
619    /// Repeating pattern in amounts.
620    RepeatingAmount,
621
622    // Frequency Anomalies
623    /// Unusual transaction frequency.
624    UnusualFrequency,
625    /// Burst of transactions.
626    TransactionBurst,
627    /// Unusual time of day.
628    UnusualTiming,
629
630    // Trend Anomalies
631    /// Break in historical trend.
632    TrendBreak,
633    /// Sudden level shift.
634    LevelShift,
635    /// Seasonal pattern violation.
636    SeasonalAnomaly,
637
638    // Distribution Anomalies
639    /// Outlier in distribution.
640    StatisticalOutlier,
641    /// Change in variance.
642    VarianceChange,
643    /// Distribution shift.
644    DistributionShift,
645}
646
647impl StatisticalAnomalyType {
648    /// Returns severity level (1-5).
649    pub fn severity(&self) -> u8 {
650        match self {
651            StatisticalAnomalyType::UnusualTiming => 1,
652            StatisticalAnomalyType::UnusualFrequency => 2,
653            StatisticalAnomalyType::BenfordViolation => 2,
654            StatisticalAnomalyType::UnusuallyHighAmount => 3,
655            StatisticalAnomalyType::TrendBreak => 3,
656            StatisticalAnomalyType::TransactionBurst => 4,
657            StatisticalAnomalyType::ExactDuplicateAmount => 3,
658            _ => 3,
659        }
660    }
661}
662
663/// Relational/graph anomaly types.
664#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
665pub enum RelationalAnomalyType {
666    // Transaction Pattern Anomalies
667    /// Circular transaction pattern.
668    CircularTransaction,
669    /// Unusual account combination.
670    UnusualAccountPair,
671    /// New trading partner.
672    NewCounterparty,
673    /// Dormant account suddenly active.
674    DormantAccountActivity,
675
676    // Network Anomalies
677    /// Unusual network centrality.
678    CentralityAnomaly,
679    /// Isolated transaction cluster.
680    IsolatedCluster,
681    /// Bridge node anomaly.
682    BridgeNodeAnomaly,
683    /// Community structure change.
684    CommunityAnomaly,
685
686    // Relationship Anomalies
687    /// Missing expected relationship.
688    MissingRelationship,
689    /// Unexpected relationship.
690    UnexpectedRelationship,
691    /// Relationship strength change.
692    RelationshipStrengthChange,
693
694    // Intercompany Anomalies
695    /// Unmatched intercompany transaction.
696    UnmatchedIntercompany,
697    /// Circular intercompany flow.
698    CircularIntercompany,
699    /// Transfer pricing anomaly.
700    TransferPricingAnomaly,
701}
702
703impl RelationalAnomalyType {
704    /// Returns severity level (1-5).
705    pub fn severity(&self) -> u8 {
706        match self {
707            RelationalAnomalyType::NewCounterparty => 1,
708            RelationalAnomalyType::DormantAccountActivity => 2,
709            RelationalAnomalyType::UnusualAccountPair => 2,
710            RelationalAnomalyType::CircularTransaction => 4,
711            RelationalAnomalyType::CircularIntercompany => 4,
712            RelationalAnomalyType::TransferPricingAnomaly => 4,
713            RelationalAnomalyType::UnmatchedIntercompany => 3,
714            _ => 3,
715        }
716    }
717}
718
719/// A labeled anomaly for supervised learning.
720#[derive(Debug, Clone, Serialize, Deserialize)]
721pub struct LabeledAnomaly {
722    /// Unique anomaly identifier.
723    pub anomaly_id: String,
724    /// Type of anomaly.
725    pub anomaly_type: AnomalyType,
726    /// Document or entity that contains the anomaly.
727    pub document_id: String,
728    /// Document type (JE, PO, Invoice, etc.).
729    pub document_type: String,
730    /// Company code.
731    pub company_code: String,
732    /// Date the anomaly occurred.
733    pub anomaly_date: NaiveDate,
734    /// Timestamp when detected/injected.
735    pub detection_timestamp: NaiveDateTime,
736    /// Confidence score (0.0 - 1.0) for injected anomalies.
737    pub confidence: f64,
738    /// Severity (1-5).
739    pub severity: u8,
740    /// Description of the anomaly.
741    pub description: String,
742    /// Related entities (user IDs, account codes, etc.).
743    pub related_entities: Vec<String>,
744    /// Monetary impact if applicable.
745    pub monetary_impact: Option<Decimal>,
746    /// Additional metadata.
747    pub metadata: HashMap<String, String>,
748    /// Whether this was injected (true) or naturally occurring (false).
749    pub is_injected: bool,
750    /// Injection strategy used (if injected) - legacy string field.
751    pub injection_strategy: Option<String>,
752    /// Cluster ID if part of an anomaly cluster.
753    pub cluster_id: Option<String>,
754
755    // ========================================
756    // PROVENANCE TRACKING FIELDS (Phase 1.2)
757    // ========================================
758    /// Hash of the original document before modification.
759    /// Enables tracking what the document looked like pre-injection.
760    #[serde(default, skip_serializing_if = "Option::is_none")]
761    pub original_document_hash: Option<String>,
762
763    /// Causal reason explaining why this anomaly was injected.
764    /// Provides "why" tracking for each anomaly.
765    #[serde(default, skip_serializing_if = "Option::is_none")]
766    pub causal_reason: Option<AnomalyCausalReason>,
767
768    /// Structured injection strategy with parameters.
769    /// More detailed than the legacy string-based injection_strategy field.
770    #[serde(default, skip_serializing_if = "Option::is_none")]
771    pub structured_strategy: Option<InjectionStrategy>,
772
773    /// Parent anomaly ID if this was derived from another anomaly.
774    /// Enables anomaly transformation chains.
775    #[serde(default, skip_serializing_if = "Option::is_none")]
776    pub parent_anomaly_id: Option<String>,
777
778    /// Child anomaly IDs that were derived from this anomaly.
779    #[serde(default, skip_serializing_if = "Vec::is_empty")]
780    pub child_anomaly_ids: Vec<String>,
781
782    /// Scenario ID if this anomaly is part of a multi-step scenario.
783    #[serde(default, skip_serializing_if = "Option::is_none")]
784    pub scenario_id: Option<String>,
785
786    /// Generation run ID that produced this anomaly.
787    /// Enables tracing anomalies back to their generation run.
788    #[serde(default, skip_serializing_if = "Option::is_none")]
789    pub run_id: Option<String>,
790
791    /// Seed used for RNG during generation.
792    /// Enables reproducibility.
793    #[serde(default, skip_serializing_if = "Option::is_none")]
794    pub generation_seed: Option<u64>,
795}
796
797impl LabeledAnomaly {
798    /// Creates a new labeled anomaly.
799    pub fn new(
800        anomaly_id: String,
801        anomaly_type: AnomalyType,
802        document_id: String,
803        document_type: String,
804        company_code: String,
805        anomaly_date: NaiveDate,
806    ) -> Self {
807        let severity = anomaly_type.severity();
808        let description = format!(
809            "{} - {} in document {}",
810            anomaly_type.category(),
811            anomaly_type.type_name(),
812            document_id
813        );
814
815        Self {
816            anomaly_id,
817            anomaly_type,
818            document_id,
819            document_type,
820            company_code,
821            anomaly_date,
822            detection_timestamp: chrono::Local::now().naive_local(),
823            confidence: 1.0,
824            severity,
825            description,
826            related_entities: Vec::new(),
827            monetary_impact: None,
828            metadata: HashMap::new(),
829            is_injected: true,
830            injection_strategy: None,
831            cluster_id: None,
832            // Provenance fields
833            original_document_hash: None,
834            causal_reason: None,
835            structured_strategy: None,
836            parent_anomaly_id: None,
837            child_anomaly_ids: Vec::new(),
838            scenario_id: None,
839            run_id: None,
840            generation_seed: None,
841        }
842    }
843
844    /// Sets the description.
845    pub fn with_description(mut self, description: &str) -> Self {
846        self.description = description.to_string();
847        self
848    }
849
850    /// Sets the monetary impact.
851    pub fn with_monetary_impact(mut self, impact: Decimal) -> Self {
852        self.monetary_impact = Some(impact);
853        self
854    }
855
856    /// Adds a related entity.
857    pub fn with_related_entity(mut self, entity: &str) -> Self {
858        self.related_entities.push(entity.to_string());
859        self
860    }
861
862    /// Adds metadata.
863    pub fn with_metadata(mut self, key: &str, value: &str) -> Self {
864        self.metadata.insert(key.to_string(), value.to_string());
865        self
866    }
867
868    /// Sets the injection strategy (legacy string).
869    pub fn with_injection_strategy(mut self, strategy: &str) -> Self {
870        self.injection_strategy = Some(strategy.to_string());
871        self
872    }
873
874    /// Sets the cluster ID.
875    pub fn with_cluster(mut self, cluster_id: &str) -> Self {
876        self.cluster_id = Some(cluster_id.to_string());
877        self
878    }
879
880    // ========================================
881    // PROVENANCE BUILDER METHODS (Phase 1.2)
882    // ========================================
883
884    /// Sets the original document hash for provenance tracking.
885    pub fn with_original_document_hash(mut self, hash: &str) -> Self {
886        self.original_document_hash = Some(hash.to_string());
887        self
888    }
889
890    /// Sets the causal reason for this anomaly.
891    pub fn with_causal_reason(mut self, reason: AnomalyCausalReason) -> Self {
892        self.causal_reason = Some(reason);
893        self
894    }
895
896    /// Sets the structured injection strategy.
897    pub fn with_structured_strategy(mut self, strategy: InjectionStrategy) -> Self {
898        // Also set the legacy string field for backward compatibility
899        self.injection_strategy = Some(strategy.strategy_type().to_string());
900        self.structured_strategy = Some(strategy);
901        self
902    }
903
904    /// Sets the parent anomaly ID (for anomaly derivation chains).
905    pub fn with_parent_anomaly(mut self, parent_id: &str) -> Self {
906        self.parent_anomaly_id = Some(parent_id.to_string());
907        self
908    }
909
910    /// Adds a child anomaly ID.
911    pub fn with_child_anomaly(mut self, child_id: &str) -> Self {
912        self.child_anomaly_ids.push(child_id.to_string());
913        self
914    }
915
916    /// Sets the scenario ID for multi-step scenario tracking.
917    pub fn with_scenario(mut self, scenario_id: &str) -> Self {
918        self.scenario_id = Some(scenario_id.to_string());
919        self
920    }
921
922    /// Sets the generation run ID.
923    pub fn with_run_id(mut self, run_id: &str) -> Self {
924        self.run_id = Some(run_id.to_string());
925        self
926    }
927
928    /// Sets the generation seed for reproducibility.
929    pub fn with_generation_seed(mut self, seed: u64) -> Self {
930        self.generation_seed = Some(seed);
931        self
932    }
933
934    /// Sets multiple provenance fields at once for convenience.
935    pub fn with_provenance(
936        mut self,
937        run_id: Option<&str>,
938        seed: Option<u64>,
939        causal_reason: Option<AnomalyCausalReason>,
940    ) -> Self {
941        if let Some(id) = run_id {
942            self.run_id = Some(id.to_string());
943        }
944        self.generation_seed = seed;
945        self.causal_reason = causal_reason;
946        self
947    }
948
949    /// Converts to a feature vector for ML.
950    ///
951    /// Returns a vector of 15 features:
952    /// - 6 features: Category one-hot encoding (Fraud, Error, ProcessIssue, Statistical, Relational, Custom)
953    /// - 1 feature: Severity (normalized 0-1)
954    /// - 1 feature: Confidence
955    /// - 1 feature: Has monetary impact (0/1)
956    /// - 1 feature: Monetary impact (log-scaled)
957    /// - 1 feature: Is intentional (0/1)
958    /// - 1 feature: Number of related entities
959    /// - 1 feature: Is part of cluster (0/1)
960    /// - 1 feature: Is part of scenario (0/1)
961    /// - 1 feature: Has parent anomaly (0/1) - indicates derivation
962    pub fn to_features(&self) -> Vec<f64> {
963        let mut features = Vec::new();
964
965        // Category one-hot encoding
966        let categories = [
967            "Fraud",
968            "Error",
969            "ProcessIssue",
970            "Statistical",
971            "Relational",
972            "Custom",
973        ];
974        for cat in &categories {
975            features.push(if self.anomaly_type.category() == *cat {
976                1.0
977            } else {
978                0.0
979            });
980        }
981
982        // Severity (normalized)
983        features.push(self.severity as f64 / 5.0);
984
985        // Confidence
986        features.push(self.confidence);
987
988        // Has monetary impact
989        features.push(if self.monetary_impact.is_some() {
990            1.0
991        } else {
992            0.0
993        });
994
995        // Monetary impact (log-scaled)
996        if let Some(impact) = self.monetary_impact {
997            let impact_f64: f64 = impact.try_into().unwrap_or(0.0);
998            features.push((impact_f64.abs() + 1.0).ln());
999        } else {
1000            features.push(0.0);
1001        }
1002
1003        // Is intentional
1004        features.push(if self.anomaly_type.is_intentional() {
1005            1.0
1006        } else {
1007            0.0
1008        });
1009
1010        // Number of related entities
1011        features.push(self.related_entities.len() as f64);
1012
1013        // Is part of cluster
1014        features.push(if self.cluster_id.is_some() { 1.0 } else { 0.0 });
1015
1016        // Provenance features
1017        // Is part of scenario
1018        features.push(if self.scenario_id.is_some() { 1.0 } else { 0.0 });
1019
1020        // Has parent anomaly (indicates this is a derived anomaly)
1021        features.push(if self.parent_anomaly_id.is_some() {
1022            1.0
1023        } else {
1024            0.0
1025        });
1026
1027        features
1028    }
1029
1030    /// Returns the number of features in the feature vector.
1031    pub fn feature_count() -> usize {
1032        15 // 6 category + 9 other features
1033    }
1034
1035    /// Returns feature names for documentation/ML metadata.
1036    pub fn feature_names() -> Vec<&'static str> {
1037        vec![
1038            "category_fraud",
1039            "category_error",
1040            "category_process_issue",
1041            "category_statistical",
1042            "category_relational",
1043            "category_custom",
1044            "severity_normalized",
1045            "confidence",
1046            "has_monetary_impact",
1047            "monetary_impact_log",
1048            "is_intentional",
1049            "related_entity_count",
1050            "is_clustered",
1051            "is_scenario_part",
1052            "is_derived",
1053        ]
1054    }
1055}
1056
1057/// Summary of anomalies for reporting.
1058#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1059pub struct AnomalySummary {
1060    /// Total anomaly count.
1061    pub total_count: usize,
1062    /// Count by category.
1063    pub by_category: HashMap<String, usize>,
1064    /// Count by specific type.
1065    pub by_type: HashMap<String, usize>,
1066    /// Count by severity.
1067    pub by_severity: HashMap<u8, usize>,
1068    /// Count by company.
1069    pub by_company: HashMap<String, usize>,
1070    /// Total monetary impact.
1071    pub total_monetary_impact: Decimal,
1072    /// Date range.
1073    pub date_range: Option<(NaiveDate, NaiveDate)>,
1074    /// Number of clusters.
1075    pub cluster_count: usize,
1076}
1077
1078impl AnomalySummary {
1079    /// Creates a summary from a list of anomalies.
1080    pub fn from_anomalies(anomalies: &[LabeledAnomaly]) -> Self {
1081        let mut summary = AnomalySummary {
1082            total_count: anomalies.len(),
1083            ..Default::default()
1084        };
1085
1086        let mut min_date: Option<NaiveDate> = None;
1087        let mut max_date: Option<NaiveDate> = None;
1088        let mut clusters = std::collections::HashSet::new();
1089
1090        for anomaly in anomalies {
1091            // By category
1092            *summary
1093                .by_category
1094                .entry(anomaly.anomaly_type.category().to_string())
1095                .or_insert(0) += 1;
1096
1097            // By type
1098            *summary
1099                .by_type
1100                .entry(anomaly.anomaly_type.type_name())
1101                .or_insert(0) += 1;
1102
1103            // By severity
1104            *summary.by_severity.entry(anomaly.severity).or_insert(0) += 1;
1105
1106            // By company
1107            *summary
1108                .by_company
1109                .entry(anomaly.company_code.clone())
1110                .or_insert(0) += 1;
1111
1112            // Monetary impact
1113            if let Some(impact) = anomaly.monetary_impact {
1114                summary.total_monetary_impact += impact;
1115            }
1116
1117            // Date range
1118            match min_date {
1119                None => min_date = Some(anomaly.anomaly_date),
1120                Some(d) if anomaly.anomaly_date < d => min_date = Some(anomaly.anomaly_date),
1121                _ => {}
1122            }
1123            match max_date {
1124                None => max_date = Some(anomaly.anomaly_date),
1125                Some(d) if anomaly.anomaly_date > d => max_date = Some(anomaly.anomaly_date),
1126                _ => {}
1127            }
1128
1129            // Clusters
1130            if let Some(cluster_id) = &anomaly.cluster_id {
1131                clusters.insert(cluster_id.clone());
1132            }
1133        }
1134
1135        summary.date_range = min_date.zip(max_date);
1136        summary.cluster_count = clusters.len();
1137
1138        summary
1139    }
1140}
1141
1142// ============================================================================
1143// ENHANCED ANOMALY TAXONOMY (FR-003)
1144// ============================================================================
1145
1146/// High-level anomaly category for multi-class classification.
1147///
1148/// These categories provide a more granular classification than the base
1149/// AnomalyType enum, enabling better ML model training and audit reporting.
1150#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1151pub enum AnomalyCategory {
1152    // Vendor-related anomalies
1153    /// Fictitious or shell vendor.
1154    FictitiousVendor,
1155    /// Kickback or collusion with vendor.
1156    VendorKickback,
1157    /// Related party vendor transactions.
1158    RelatedPartyVendor,
1159
1160    // Transaction-related anomalies
1161    /// Duplicate payment or invoice.
1162    DuplicatePayment,
1163    /// Unauthorized transaction.
1164    UnauthorizedTransaction,
1165    /// Structured transactions to avoid thresholds.
1166    StructuredTransaction,
1167
1168    // Pattern-based anomalies
1169    /// Circular flow of funds.
1170    CircularFlow,
1171    /// Behavioral anomaly (deviation from normal patterns).
1172    BehavioralAnomaly,
1173    /// Timing-based anomaly.
1174    TimingAnomaly,
1175
1176    // Journal entry anomalies
1177    /// Manual journal entry anomaly.
1178    JournalAnomaly,
1179    /// Manual override of controls.
1180    ManualOverride,
1181    /// Missing approval in chain.
1182    MissingApproval,
1183
1184    // Statistical anomalies
1185    /// Statistical outlier.
1186    StatisticalOutlier,
1187    /// Distribution anomaly (Benford, etc.).
1188    DistributionAnomaly,
1189
1190    // Custom category
1191    /// User-defined category.
1192    Custom(String),
1193}
1194
1195impl AnomalyCategory {
1196    /// Derives an AnomalyCategory from an AnomalyType.
1197    pub fn from_anomaly_type(anomaly_type: &AnomalyType) -> Self {
1198        match anomaly_type {
1199            AnomalyType::Fraud(fraud_type) => match fraud_type {
1200                FraudType::FictitiousVendor | FraudType::ShellCompanyPayment => {
1201                    AnomalyCategory::FictitiousVendor
1202                }
1203                FraudType::Kickback | FraudType::KickbackScheme => AnomalyCategory::VendorKickback,
1204                FraudType::DuplicatePayment => AnomalyCategory::DuplicatePayment,
1205                FraudType::SplitTransaction | FraudType::JustBelowThreshold => {
1206                    AnomalyCategory::StructuredTransaction
1207                }
1208                FraudType::SelfApproval
1209                | FraudType::UnauthorizedApproval
1210                | FraudType::CollusiveApproval => AnomalyCategory::UnauthorizedTransaction,
1211                FraudType::TimingAnomaly
1212                | FraudType::RoundDollarManipulation
1213                | FraudType::SuspenseAccountAbuse => AnomalyCategory::JournalAnomaly,
1214                _ => AnomalyCategory::BehavioralAnomaly,
1215            },
1216            AnomalyType::Error(error_type) => match error_type {
1217                ErrorType::DuplicateEntry => AnomalyCategory::DuplicatePayment,
1218                ErrorType::WrongPeriod
1219                | ErrorType::BackdatedEntry
1220                | ErrorType::FutureDatedEntry => AnomalyCategory::TimingAnomaly,
1221                _ => AnomalyCategory::JournalAnomaly,
1222            },
1223            AnomalyType::ProcessIssue(process_type) => match process_type {
1224                ProcessIssueType::SkippedApproval | ProcessIssueType::IncompleteApprovalChain => {
1225                    AnomalyCategory::MissingApproval
1226                }
1227                ProcessIssueType::ManualOverride | ProcessIssueType::SystemBypass => {
1228                    AnomalyCategory::ManualOverride
1229                }
1230                ProcessIssueType::AfterHoursPosting | ProcessIssueType::WeekendPosting => {
1231                    AnomalyCategory::TimingAnomaly
1232                }
1233                _ => AnomalyCategory::BehavioralAnomaly,
1234            },
1235            AnomalyType::Statistical(stat_type) => match stat_type {
1236                StatisticalAnomalyType::BenfordViolation
1237                | StatisticalAnomalyType::DistributionShift => AnomalyCategory::DistributionAnomaly,
1238                _ => AnomalyCategory::StatisticalOutlier,
1239            },
1240            AnomalyType::Relational(rel_type) => match rel_type {
1241                RelationalAnomalyType::CircularTransaction
1242                | RelationalAnomalyType::CircularIntercompany => AnomalyCategory::CircularFlow,
1243                _ => AnomalyCategory::BehavioralAnomaly,
1244            },
1245            AnomalyType::Custom(s) => AnomalyCategory::Custom(s.clone()),
1246        }
1247    }
1248
1249    /// Returns the category name as a string.
1250    pub fn name(&self) -> &str {
1251        match self {
1252            AnomalyCategory::FictitiousVendor => "fictitious_vendor",
1253            AnomalyCategory::VendorKickback => "vendor_kickback",
1254            AnomalyCategory::RelatedPartyVendor => "related_party_vendor",
1255            AnomalyCategory::DuplicatePayment => "duplicate_payment",
1256            AnomalyCategory::UnauthorizedTransaction => "unauthorized_transaction",
1257            AnomalyCategory::StructuredTransaction => "structured_transaction",
1258            AnomalyCategory::CircularFlow => "circular_flow",
1259            AnomalyCategory::BehavioralAnomaly => "behavioral_anomaly",
1260            AnomalyCategory::TimingAnomaly => "timing_anomaly",
1261            AnomalyCategory::JournalAnomaly => "journal_anomaly",
1262            AnomalyCategory::ManualOverride => "manual_override",
1263            AnomalyCategory::MissingApproval => "missing_approval",
1264            AnomalyCategory::StatisticalOutlier => "statistical_outlier",
1265            AnomalyCategory::DistributionAnomaly => "distribution_anomaly",
1266            AnomalyCategory::Custom(s) => s.as_str(),
1267        }
1268    }
1269
1270    /// Returns the ordinal value for ML encoding.
1271    pub fn ordinal(&self) -> u8 {
1272        match self {
1273            AnomalyCategory::FictitiousVendor => 0,
1274            AnomalyCategory::VendorKickback => 1,
1275            AnomalyCategory::RelatedPartyVendor => 2,
1276            AnomalyCategory::DuplicatePayment => 3,
1277            AnomalyCategory::UnauthorizedTransaction => 4,
1278            AnomalyCategory::StructuredTransaction => 5,
1279            AnomalyCategory::CircularFlow => 6,
1280            AnomalyCategory::BehavioralAnomaly => 7,
1281            AnomalyCategory::TimingAnomaly => 8,
1282            AnomalyCategory::JournalAnomaly => 9,
1283            AnomalyCategory::ManualOverride => 10,
1284            AnomalyCategory::MissingApproval => 11,
1285            AnomalyCategory::StatisticalOutlier => 12,
1286            AnomalyCategory::DistributionAnomaly => 13,
1287            AnomalyCategory::Custom(_) => 14,
1288        }
1289    }
1290
1291    /// Returns the total number of categories (excluding Custom).
1292    pub fn category_count() -> usize {
1293        15 // 14 fixed categories + Custom
1294    }
1295}
1296
1297/// Type of contributing factor for anomaly confidence/severity calculation.
1298#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1299pub enum FactorType {
1300    /// Amount deviation from expected value.
1301    AmountDeviation,
1302    /// Proximity to approval/reporting threshold.
1303    ThresholdProximity,
1304    /// Timing-related anomaly indicator.
1305    TimingAnomaly,
1306    /// Entity risk score contribution.
1307    EntityRisk,
1308    /// Pattern match confidence.
1309    PatternMatch,
1310    /// Frequency deviation from normal.
1311    FrequencyDeviation,
1312    /// Relationship-based anomaly indicator.
1313    RelationshipAnomaly,
1314    /// Control bypass indicator.
1315    ControlBypass,
1316    /// Benford's Law violation.
1317    BenfordViolation,
1318    /// Duplicate indicator.
1319    DuplicateIndicator,
1320    /// Approval chain issue.
1321    ApprovalChainIssue,
1322    /// Documentation gap.
1323    DocumentationGap,
1324    /// Custom factor type.
1325    Custom,
1326}
1327
1328impl FactorType {
1329    /// Returns the factor type name.
1330    pub fn name(&self) -> &'static str {
1331        match self {
1332            FactorType::AmountDeviation => "amount_deviation",
1333            FactorType::ThresholdProximity => "threshold_proximity",
1334            FactorType::TimingAnomaly => "timing_anomaly",
1335            FactorType::EntityRisk => "entity_risk",
1336            FactorType::PatternMatch => "pattern_match",
1337            FactorType::FrequencyDeviation => "frequency_deviation",
1338            FactorType::RelationshipAnomaly => "relationship_anomaly",
1339            FactorType::ControlBypass => "control_bypass",
1340            FactorType::BenfordViolation => "benford_violation",
1341            FactorType::DuplicateIndicator => "duplicate_indicator",
1342            FactorType::ApprovalChainIssue => "approval_chain_issue",
1343            FactorType::DocumentationGap => "documentation_gap",
1344            FactorType::Custom => "custom",
1345        }
1346    }
1347}
1348
1349/// Evidence supporting a contributing factor.
1350#[derive(Debug, Clone, Serialize, Deserialize)]
1351pub struct FactorEvidence {
1352    /// Source of the evidence (e.g., "transaction_history", "entity_registry").
1353    pub source: String,
1354    /// Raw evidence data.
1355    pub data: HashMap<String, String>,
1356}
1357
1358/// A contributing factor to anomaly confidence/severity.
1359#[derive(Debug, Clone, Serialize, Deserialize)]
1360pub struct ContributingFactor {
1361    /// Type of factor.
1362    pub factor_type: FactorType,
1363    /// Observed value.
1364    pub value: f64,
1365    /// Threshold or expected value.
1366    pub threshold: f64,
1367    /// Direction of comparison (true = value > threshold is anomalous).
1368    pub direction_greater: bool,
1369    /// Weight of this factor in overall calculation (0.0 - 1.0).
1370    pub weight: f64,
1371    /// Human-readable description.
1372    pub description: String,
1373    /// Optional supporting evidence.
1374    pub evidence: Option<FactorEvidence>,
1375}
1376
1377impl ContributingFactor {
1378    /// Creates a new contributing factor.
1379    pub fn new(
1380        factor_type: FactorType,
1381        value: f64,
1382        threshold: f64,
1383        direction_greater: bool,
1384        weight: f64,
1385        description: &str,
1386    ) -> Self {
1387        Self {
1388            factor_type,
1389            value,
1390            threshold,
1391            direction_greater,
1392            weight,
1393            description: description.to_string(),
1394            evidence: None,
1395        }
1396    }
1397
1398    /// Adds evidence to the factor.
1399    pub fn with_evidence(mut self, source: &str, data: HashMap<String, String>) -> Self {
1400        self.evidence = Some(FactorEvidence {
1401            source: source.to_string(),
1402            data,
1403        });
1404        self
1405    }
1406
1407    /// Calculates the factor's contribution to anomaly score.
1408    pub fn contribution(&self) -> f64 {
1409        let deviation = if self.direction_greater {
1410            (self.value - self.threshold).max(0.0)
1411        } else {
1412            (self.threshold - self.value).max(0.0)
1413        };
1414
1415        // Normalize by threshold to get relative deviation
1416        let relative_deviation = if self.threshold.abs() > 0.001 {
1417            deviation / self.threshold.abs()
1418        } else {
1419            deviation
1420        };
1421
1422        // Apply weight and cap at 1.0
1423        (relative_deviation * self.weight).min(1.0)
1424    }
1425}
1426
1427/// Enhanced anomaly label with dynamic confidence and severity.
1428#[derive(Debug, Clone, Serialize, Deserialize)]
1429pub struct EnhancedAnomalyLabel {
1430    /// Base labeled anomaly (backward compatible).
1431    pub base: LabeledAnomaly,
1432    /// Enhanced category classification.
1433    pub category: AnomalyCategory,
1434    /// Dynamically calculated confidence (0.0 - 1.0).
1435    pub enhanced_confidence: f64,
1436    /// Contextually calculated severity (0.0 - 1.0).
1437    pub enhanced_severity: f64,
1438    /// Factors contributing to confidence/severity.
1439    pub contributing_factors: Vec<ContributingFactor>,
1440    /// Secondary categories (for multi-label classification).
1441    pub secondary_categories: Vec<AnomalyCategory>,
1442}
1443
1444impl EnhancedAnomalyLabel {
1445    /// Creates an enhanced label from a base labeled anomaly.
1446    pub fn from_base(base: LabeledAnomaly) -> Self {
1447        let category = AnomalyCategory::from_anomaly_type(&base.anomaly_type);
1448        let enhanced_confidence = base.confidence;
1449        let enhanced_severity = base.severity as f64 / 5.0;
1450
1451        Self {
1452            base,
1453            category,
1454            enhanced_confidence,
1455            enhanced_severity,
1456            contributing_factors: Vec::new(),
1457            secondary_categories: Vec::new(),
1458        }
1459    }
1460
1461    /// Sets the enhanced confidence.
1462    pub fn with_confidence(mut self, confidence: f64) -> Self {
1463        self.enhanced_confidence = confidence.clamp(0.0, 1.0);
1464        self
1465    }
1466
1467    /// Sets the enhanced severity.
1468    pub fn with_severity(mut self, severity: f64) -> Self {
1469        self.enhanced_severity = severity.clamp(0.0, 1.0);
1470        self
1471    }
1472
1473    /// Adds a contributing factor.
1474    pub fn with_factor(mut self, factor: ContributingFactor) -> Self {
1475        self.contributing_factors.push(factor);
1476        self
1477    }
1478
1479    /// Adds a secondary category.
1480    pub fn with_secondary_category(mut self, category: AnomalyCategory) -> Self {
1481        if !self.secondary_categories.contains(&category) && category != self.category {
1482            self.secondary_categories.push(category);
1483        }
1484        self
1485    }
1486
1487    /// Converts to an extended feature vector.
1488    ///
1489    /// Returns base features (15) + enhanced features (10) = 25 features.
1490    pub fn to_features(&self) -> Vec<f64> {
1491        let mut features = self.base.to_features();
1492
1493        // Enhanced features
1494        features.push(self.enhanced_confidence);
1495        features.push(self.enhanced_severity);
1496        features.push(self.category.ordinal() as f64 / AnomalyCategory::category_count() as f64);
1497        features.push(self.secondary_categories.len() as f64);
1498        features.push(self.contributing_factors.len() as f64);
1499
1500        // Max factor weight
1501        let max_weight = self
1502            .contributing_factors
1503            .iter()
1504            .map(|f| f.weight)
1505            .fold(0.0, f64::max);
1506        features.push(max_weight);
1507
1508        // Factor type indicators (binary flags for key factor types)
1509        let has_control_bypass = self
1510            .contributing_factors
1511            .iter()
1512            .any(|f| f.factor_type == FactorType::ControlBypass);
1513        features.push(if has_control_bypass { 1.0 } else { 0.0 });
1514
1515        let has_amount_deviation = self
1516            .contributing_factors
1517            .iter()
1518            .any(|f| f.factor_type == FactorType::AmountDeviation);
1519        features.push(if has_amount_deviation { 1.0 } else { 0.0 });
1520
1521        let has_timing = self
1522            .contributing_factors
1523            .iter()
1524            .any(|f| f.factor_type == FactorType::TimingAnomaly);
1525        features.push(if has_timing { 1.0 } else { 0.0 });
1526
1527        let has_pattern_match = self
1528            .contributing_factors
1529            .iter()
1530            .any(|f| f.factor_type == FactorType::PatternMatch);
1531        features.push(if has_pattern_match { 1.0 } else { 0.0 });
1532
1533        features
1534    }
1535
1536    /// Returns the number of features in the enhanced feature vector.
1537    pub fn feature_count() -> usize {
1538        25 // 15 base + 10 enhanced
1539    }
1540
1541    /// Returns feature names for the enhanced feature vector.
1542    pub fn feature_names() -> Vec<&'static str> {
1543        let mut names = LabeledAnomaly::feature_names();
1544        names.extend(vec![
1545            "enhanced_confidence",
1546            "enhanced_severity",
1547            "category_ordinal",
1548            "secondary_category_count",
1549            "contributing_factor_count",
1550            "max_factor_weight",
1551            "has_control_bypass",
1552            "has_amount_deviation",
1553            "has_timing_factor",
1554            "has_pattern_match",
1555        ]);
1556        names
1557    }
1558}
1559
1560// ============================================================================
1561// MULTI-DIMENSIONAL LABELING (Anomaly Pattern Enhancements)
1562// ============================================================================
1563
1564/// Severity level classification for anomalies.
1565#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1566pub enum SeverityLevel {
1567    /// Minor issue, low impact.
1568    Low,
1569    /// Moderate issue, noticeable impact.
1570    #[default]
1571    Medium,
1572    /// Significant issue, substantial impact.
1573    High,
1574    /// Critical issue, severe impact requiring immediate attention.
1575    Critical,
1576}
1577
1578impl SeverityLevel {
1579    /// Returns the numeric value (1-4) for the severity level.
1580    pub fn numeric(&self) -> u8 {
1581        match self {
1582            SeverityLevel::Low => 1,
1583            SeverityLevel::Medium => 2,
1584            SeverityLevel::High => 3,
1585            SeverityLevel::Critical => 4,
1586        }
1587    }
1588
1589    /// Creates a severity level from a numeric value.
1590    pub fn from_numeric(value: u8) -> Self {
1591        match value {
1592            1 => SeverityLevel::Low,
1593            2 => SeverityLevel::Medium,
1594            3 => SeverityLevel::High,
1595            _ => SeverityLevel::Critical,
1596        }
1597    }
1598
1599    /// Creates a severity level from a normalized score (0.0-1.0).
1600    pub fn from_score(score: f64) -> Self {
1601        match score {
1602            s if s < 0.25 => SeverityLevel::Low,
1603            s if s < 0.50 => SeverityLevel::Medium,
1604            s if s < 0.75 => SeverityLevel::High,
1605            _ => SeverityLevel::Critical,
1606        }
1607    }
1608
1609    /// Returns a normalized score (0.0-1.0) for this severity level.
1610    pub fn to_score(&self) -> f64 {
1611        match self {
1612            SeverityLevel::Low => 0.125,
1613            SeverityLevel::Medium => 0.375,
1614            SeverityLevel::High => 0.625,
1615            SeverityLevel::Critical => 0.875,
1616        }
1617    }
1618}
1619
1620/// Structured severity scoring for anomalies.
1621#[derive(Debug, Clone, Serialize, Deserialize)]
1622pub struct AnomalySeverity {
1623    /// Severity level classification.
1624    pub level: SeverityLevel,
1625    /// Continuous severity score (0.0-1.0).
1626    pub score: f64,
1627    /// Absolute financial impact amount.
1628    pub financial_impact: Decimal,
1629    /// Whether this exceeds materiality threshold.
1630    pub is_material: bool,
1631    /// Materiality threshold used for determination.
1632    #[serde(default, skip_serializing_if = "Option::is_none")]
1633    pub materiality_threshold: Option<Decimal>,
1634}
1635
1636impl AnomalySeverity {
1637    /// Creates a new severity assessment.
1638    pub fn new(level: SeverityLevel, financial_impact: Decimal) -> Self {
1639        Self {
1640            level,
1641            score: level.to_score(),
1642            financial_impact,
1643            is_material: false,
1644            materiality_threshold: None,
1645        }
1646    }
1647
1648    /// Creates severity from a score, auto-determining level.
1649    pub fn from_score(score: f64, financial_impact: Decimal) -> Self {
1650        Self {
1651            level: SeverityLevel::from_score(score),
1652            score: score.clamp(0.0, 1.0),
1653            financial_impact,
1654            is_material: false,
1655            materiality_threshold: None,
1656        }
1657    }
1658
1659    /// Sets the materiality assessment.
1660    pub fn with_materiality(mut self, threshold: Decimal) -> Self {
1661        self.materiality_threshold = Some(threshold);
1662        self.is_material = self.financial_impact.abs() >= threshold;
1663        self
1664    }
1665}
1666
1667impl Default for AnomalySeverity {
1668    fn default() -> Self {
1669        Self {
1670            level: SeverityLevel::Medium,
1671            score: 0.5,
1672            financial_impact: Decimal::ZERO,
1673            is_material: false,
1674            materiality_threshold: None,
1675        }
1676    }
1677}
1678
1679/// Detection difficulty classification for anomalies.
1680///
1681/// Categorizes how difficult an anomaly is to detect, which is useful
1682/// for ML model benchmarking and audit procedure selection.
1683///
1684/// Note: This is distinct from `drift_events::AnomalyDetectionDifficulty` which
1685/// is used for drift event classification and has different variants.
1686#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1687pub enum AnomalyDetectionDifficulty {
1688    /// Obvious anomaly, easily caught by basic rules (expected detection rate: 99%).
1689    Trivial,
1690    /// Relatively easy to detect with standard procedures (expected detection rate: 90%).
1691    Easy,
1692    /// Requires moderate effort or specialized analysis (expected detection rate: 70%).
1693    #[default]
1694    Moderate,
1695    /// Difficult to detect, requires advanced techniques (expected detection rate: 40%).
1696    Hard,
1697    /// Expert-level difficulty, requires forensic analysis (expected detection rate: 15%).
1698    Expert,
1699}
1700
1701impl AnomalyDetectionDifficulty {
1702    /// Returns the expected detection rate for this difficulty level.
1703    pub fn expected_detection_rate(&self) -> f64 {
1704        match self {
1705            AnomalyDetectionDifficulty::Trivial => 0.99,
1706            AnomalyDetectionDifficulty::Easy => 0.90,
1707            AnomalyDetectionDifficulty::Moderate => 0.70,
1708            AnomalyDetectionDifficulty::Hard => 0.40,
1709            AnomalyDetectionDifficulty::Expert => 0.15,
1710        }
1711    }
1712
1713    /// Returns a numeric difficulty score (0.0-1.0).
1714    pub fn difficulty_score(&self) -> f64 {
1715        match self {
1716            AnomalyDetectionDifficulty::Trivial => 0.05,
1717            AnomalyDetectionDifficulty::Easy => 0.25,
1718            AnomalyDetectionDifficulty::Moderate => 0.50,
1719            AnomalyDetectionDifficulty::Hard => 0.75,
1720            AnomalyDetectionDifficulty::Expert => 0.95,
1721        }
1722    }
1723
1724    /// Creates a difficulty level from a score (0.0-1.0).
1725    pub fn from_score(score: f64) -> Self {
1726        match score {
1727            s if s < 0.15 => AnomalyDetectionDifficulty::Trivial,
1728            s if s < 0.35 => AnomalyDetectionDifficulty::Easy,
1729            s if s < 0.55 => AnomalyDetectionDifficulty::Moderate,
1730            s if s < 0.75 => AnomalyDetectionDifficulty::Hard,
1731            _ => AnomalyDetectionDifficulty::Expert,
1732        }
1733    }
1734
1735    /// Returns the name of this difficulty level.
1736    pub fn name(&self) -> &'static str {
1737        match self {
1738            AnomalyDetectionDifficulty::Trivial => "trivial",
1739            AnomalyDetectionDifficulty::Easy => "easy",
1740            AnomalyDetectionDifficulty::Moderate => "moderate",
1741            AnomalyDetectionDifficulty::Hard => "hard",
1742            AnomalyDetectionDifficulty::Expert => "expert",
1743        }
1744    }
1745}
1746
1747/// Ground truth certainty level for anomaly labels.
1748///
1749/// Indicates how certain we are that the label is correct.
1750#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1751pub enum GroundTruthCertainty {
1752    /// Definitively known (injected anomaly with full provenance).
1753    #[default]
1754    Definite,
1755    /// Highly probable based on strong evidence.
1756    Probable,
1757    /// Possibly an anomaly based on indirect evidence.
1758    Possible,
1759}
1760
1761impl GroundTruthCertainty {
1762    /// Returns a certainty score (0.0-1.0).
1763    pub fn certainty_score(&self) -> f64 {
1764        match self {
1765            GroundTruthCertainty::Definite => 1.0,
1766            GroundTruthCertainty::Probable => 0.8,
1767            GroundTruthCertainty::Possible => 0.5,
1768        }
1769    }
1770
1771    /// Returns the name of this certainty level.
1772    pub fn name(&self) -> &'static str {
1773        match self {
1774            GroundTruthCertainty::Definite => "definite",
1775            GroundTruthCertainty::Probable => "probable",
1776            GroundTruthCertainty::Possible => "possible",
1777        }
1778    }
1779}
1780
1781/// Detection method classification.
1782///
1783/// Indicates which detection methods are recommended or effective for an anomaly.
1784#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1785pub enum DetectionMethod {
1786    /// Simple rule-based detection (thresholds, filters).
1787    RuleBased,
1788    /// Statistical analysis (distributions, outlier detection).
1789    Statistical,
1790    /// Machine learning models (classification, anomaly detection).
1791    MachineLearning,
1792    /// Graph-based analysis (network patterns, relationships).
1793    GraphBased,
1794    /// Manual forensic audit procedures.
1795    ForensicAudit,
1796    /// Combination of multiple methods.
1797    Hybrid,
1798}
1799
1800impl DetectionMethod {
1801    /// Returns the name of this detection method.
1802    pub fn name(&self) -> &'static str {
1803        match self {
1804            DetectionMethod::RuleBased => "rule_based",
1805            DetectionMethod::Statistical => "statistical",
1806            DetectionMethod::MachineLearning => "machine_learning",
1807            DetectionMethod::GraphBased => "graph_based",
1808            DetectionMethod::ForensicAudit => "forensic_audit",
1809            DetectionMethod::Hybrid => "hybrid",
1810        }
1811    }
1812
1813    /// Returns a description of this detection method.
1814    pub fn description(&self) -> &'static str {
1815        match self {
1816            DetectionMethod::RuleBased => "Simple threshold and filter rules",
1817            DetectionMethod::Statistical => "Statistical distribution analysis",
1818            DetectionMethod::MachineLearning => "ML classification models",
1819            DetectionMethod::GraphBased => "Network and relationship analysis",
1820            DetectionMethod::ForensicAudit => "Manual forensic procedures",
1821            DetectionMethod::Hybrid => "Combined multi-method approach",
1822        }
1823    }
1824}
1825
1826/// Extended anomaly label with comprehensive multi-dimensional classification.
1827///
1828/// This extends the base `EnhancedAnomalyLabel` with additional fields for
1829/// severity scoring, detection difficulty, recommended methods, and ground truth.
1830#[derive(Debug, Clone, Serialize, Deserialize)]
1831pub struct ExtendedAnomalyLabel {
1832    /// Base labeled anomaly.
1833    pub base: LabeledAnomaly,
1834    /// Enhanced category classification.
1835    pub category: AnomalyCategory,
1836    /// Structured severity assessment.
1837    pub severity: AnomalySeverity,
1838    /// Detection difficulty classification.
1839    pub detection_difficulty: AnomalyDetectionDifficulty,
1840    /// Recommended detection methods for this anomaly.
1841    pub recommended_methods: Vec<DetectionMethod>,
1842    /// Key indicators that should trigger detection.
1843    pub key_indicators: Vec<String>,
1844    /// Ground truth certainty level.
1845    pub ground_truth_certainty: GroundTruthCertainty,
1846    /// Contributing factors to confidence/severity.
1847    pub contributing_factors: Vec<ContributingFactor>,
1848    /// Related entity IDs (vendors, customers, employees, etc.).
1849    pub related_entity_ids: Vec<String>,
1850    /// Secondary categories for multi-label classification.
1851    pub secondary_categories: Vec<AnomalyCategory>,
1852    /// Scheme ID if part of a multi-stage fraud scheme.
1853    #[serde(default, skip_serializing_if = "Option::is_none")]
1854    pub scheme_id: Option<String>,
1855    /// Stage number within a scheme (1-indexed).
1856    #[serde(default, skip_serializing_if = "Option::is_none")]
1857    pub scheme_stage: Option<u32>,
1858    /// Whether this is a near-miss (suspicious but legitimate).
1859    #[serde(default)]
1860    pub is_near_miss: bool,
1861    /// Explanation if this is a near-miss.
1862    #[serde(default, skip_serializing_if = "Option::is_none")]
1863    pub near_miss_explanation: Option<String>,
1864}
1865
1866impl ExtendedAnomalyLabel {
1867    /// Creates an extended label from a base labeled anomaly.
1868    pub fn from_base(base: LabeledAnomaly) -> Self {
1869        let category = AnomalyCategory::from_anomaly_type(&base.anomaly_type);
1870        let severity = AnomalySeverity {
1871            level: SeverityLevel::from_numeric(base.severity),
1872            score: base.severity as f64 / 5.0,
1873            financial_impact: base.monetary_impact.unwrap_or(Decimal::ZERO),
1874            is_material: false,
1875            materiality_threshold: None,
1876        };
1877
1878        Self {
1879            base,
1880            category,
1881            severity,
1882            detection_difficulty: AnomalyDetectionDifficulty::Moderate,
1883            recommended_methods: vec![DetectionMethod::RuleBased],
1884            key_indicators: Vec::new(),
1885            ground_truth_certainty: GroundTruthCertainty::Definite,
1886            contributing_factors: Vec::new(),
1887            related_entity_ids: Vec::new(),
1888            secondary_categories: Vec::new(),
1889            scheme_id: None,
1890            scheme_stage: None,
1891            is_near_miss: false,
1892            near_miss_explanation: None,
1893        }
1894    }
1895
1896    /// Sets the severity assessment.
1897    pub fn with_severity(mut self, severity: AnomalySeverity) -> Self {
1898        self.severity = severity;
1899        self
1900    }
1901
1902    /// Sets the detection difficulty.
1903    pub fn with_difficulty(mut self, difficulty: AnomalyDetectionDifficulty) -> Self {
1904        self.detection_difficulty = difficulty;
1905        self
1906    }
1907
1908    /// Adds a recommended detection method.
1909    pub fn with_method(mut self, method: DetectionMethod) -> Self {
1910        if !self.recommended_methods.contains(&method) {
1911            self.recommended_methods.push(method);
1912        }
1913        self
1914    }
1915
1916    /// Sets the recommended detection methods.
1917    pub fn with_methods(mut self, methods: Vec<DetectionMethod>) -> Self {
1918        self.recommended_methods = methods;
1919        self
1920    }
1921
1922    /// Adds a key indicator.
1923    pub fn with_indicator(mut self, indicator: impl Into<String>) -> Self {
1924        self.key_indicators.push(indicator.into());
1925        self
1926    }
1927
1928    /// Sets the ground truth certainty.
1929    pub fn with_certainty(mut self, certainty: GroundTruthCertainty) -> Self {
1930        self.ground_truth_certainty = certainty;
1931        self
1932    }
1933
1934    /// Adds a contributing factor.
1935    pub fn with_factor(mut self, factor: ContributingFactor) -> Self {
1936        self.contributing_factors.push(factor);
1937        self
1938    }
1939
1940    /// Adds a related entity ID.
1941    pub fn with_entity(mut self, entity_id: impl Into<String>) -> Self {
1942        self.related_entity_ids.push(entity_id.into());
1943        self
1944    }
1945
1946    /// Adds a secondary category.
1947    pub fn with_secondary_category(mut self, category: AnomalyCategory) -> Self {
1948        if category != self.category && !self.secondary_categories.contains(&category) {
1949            self.secondary_categories.push(category);
1950        }
1951        self
1952    }
1953
1954    /// Sets scheme information.
1955    pub fn with_scheme(mut self, scheme_id: impl Into<String>, stage: u32) -> Self {
1956        self.scheme_id = Some(scheme_id.into());
1957        self.scheme_stage = Some(stage);
1958        self
1959    }
1960
1961    /// Marks this as a near-miss with explanation.
1962    pub fn as_near_miss(mut self, explanation: impl Into<String>) -> Self {
1963        self.is_near_miss = true;
1964        self.near_miss_explanation = Some(explanation.into());
1965        self
1966    }
1967
1968    /// Converts to an extended feature vector for ML.
1969    ///
1970    /// Returns base features (15) + extended features (15) = 30 features.
1971    pub fn to_features(&self) -> Vec<f64> {
1972        let mut features = self.base.to_features();
1973
1974        // Extended features
1975        features.push(self.severity.score);
1976        features.push(self.severity.level.to_score());
1977        features.push(if self.severity.is_material { 1.0 } else { 0.0 });
1978        features.push(self.detection_difficulty.difficulty_score());
1979        features.push(self.detection_difficulty.expected_detection_rate());
1980        features.push(self.ground_truth_certainty.certainty_score());
1981        features.push(self.category.ordinal() as f64 / AnomalyCategory::category_count() as f64);
1982        features.push(self.secondary_categories.len() as f64);
1983        features.push(self.contributing_factors.len() as f64);
1984        features.push(self.key_indicators.len() as f64);
1985        features.push(self.recommended_methods.len() as f64);
1986        features.push(self.related_entity_ids.len() as f64);
1987        features.push(if self.scheme_id.is_some() { 1.0 } else { 0.0 });
1988        features.push(self.scheme_stage.unwrap_or(0) as f64);
1989        features.push(if self.is_near_miss { 1.0 } else { 0.0 });
1990
1991        features
1992    }
1993
1994    /// Returns the number of features in the extended feature vector.
1995    pub fn feature_count() -> usize {
1996        30 // 15 base + 15 extended
1997    }
1998
1999    /// Returns feature names for the extended feature vector.
2000    pub fn feature_names() -> Vec<&'static str> {
2001        let mut names = LabeledAnomaly::feature_names();
2002        names.extend(vec![
2003            "severity_score",
2004            "severity_level_score",
2005            "is_material",
2006            "difficulty_score",
2007            "expected_detection_rate",
2008            "ground_truth_certainty",
2009            "category_ordinal",
2010            "secondary_category_count",
2011            "contributing_factor_count",
2012            "key_indicator_count",
2013            "recommended_method_count",
2014            "related_entity_count",
2015            "is_part_of_scheme",
2016            "scheme_stage",
2017            "is_near_miss",
2018        ]);
2019        names
2020    }
2021}
2022
2023// ============================================================================
2024// MULTI-STAGE FRAUD SCHEME TYPES
2025// ============================================================================
2026
2027/// Type of multi-stage fraud scheme.
2028#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2029pub enum SchemeType {
2030    /// Gradual embezzlement over time.
2031    GradualEmbezzlement,
2032    /// Revenue manipulation across periods.
2033    RevenueManipulation,
2034    /// Vendor kickback scheme.
2035    VendorKickback,
2036    /// Round-tripping funds through multiple entities.
2037    RoundTripping,
2038    /// Ghost employee scheme.
2039    GhostEmployee,
2040    /// Expense reimbursement fraud.
2041    ExpenseReimbursement,
2042    /// Inventory theft scheme.
2043    InventoryTheft,
2044    /// Custom scheme type.
2045    Custom,
2046}
2047
2048impl SchemeType {
2049    /// Returns the name of this scheme type.
2050    pub fn name(&self) -> &'static str {
2051        match self {
2052            SchemeType::GradualEmbezzlement => "gradual_embezzlement",
2053            SchemeType::RevenueManipulation => "revenue_manipulation",
2054            SchemeType::VendorKickback => "vendor_kickback",
2055            SchemeType::RoundTripping => "round_tripping",
2056            SchemeType::GhostEmployee => "ghost_employee",
2057            SchemeType::ExpenseReimbursement => "expense_reimbursement",
2058            SchemeType::InventoryTheft => "inventory_theft",
2059            SchemeType::Custom => "custom",
2060        }
2061    }
2062
2063    /// Returns the typical number of stages for this scheme type.
2064    pub fn typical_stages(&self) -> u32 {
2065        match self {
2066            SchemeType::GradualEmbezzlement => 4, // testing, escalation, acceleration, desperation
2067            SchemeType::RevenueManipulation => 4, // Q4->Q1->Q2->Q4
2068            SchemeType::VendorKickback => 4,      // setup, inflation, kickback, concealment
2069            SchemeType::RoundTripping => 3,       // setup, execution, reversal
2070            SchemeType::GhostEmployee => 3,       // creation, payroll, concealment
2071            SchemeType::ExpenseReimbursement => 3, // submission, approval, payment
2072            SchemeType::InventoryTheft => 3,      // access, theft, cover-up
2073            SchemeType::Custom => 4,
2074        }
2075    }
2076}
2077
2078/// Status of detection for a fraud scheme.
2079#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2080pub enum SchemeDetectionStatus {
2081    /// Scheme is undetected.
2082    #[default]
2083    Undetected,
2084    /// Under investigation but not confirmed.
2085    UnderInvestigation,
2086    /// Partially detected (some transactions flagged).
2087    PartiallyDetected,
2088    /// Fully detected and confirmed.
2089    FullyDetected,
2090}
2091
2092/// Reference to a transaction within a scheme.
2093#[derive(Debug, Clone, Serialize, Deserialize)]
2094pub struct SchemeTransactionRef {
2095    /// Document ID of the transaction.
2096    pub document_id: String,
2097    /// Transaction date.
2098    pub date: chrono::NaiveDate,
2099    /// Transaction amount.
2100    pub amount: Decimal,
2101    /// Stage this transaction belongs to.
2102    pub stage: u32,
2103    /// Anomaly ID if labeled.
2104    #[serde(default, skip_serializing_if = "Option::is_none")]
2105    pub anomaly_id: Option<String>,
2106}
2107
2108/// Concealment technique used in fraud.
2109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2110pub enum ConcealmentTechnique {
2111    /// Document manipulation or forgery.
2112    DocumentManipulation,
2113    /// Circumventing approval processes.
2114    ApprovalCircumvention,
2115    /// Exploiting timing (period-end, holidays).
2116    TimingExploitation,
2117    /// Transaction splitting to avoid thresholds.
2118    TransactionSplitting,
2119    /// Account misclassification.
2120    AccountMisclassification,
2121    /// Collusion with other employees.
2122    Collusion,
2123    /// Data alteration or deletion.
2124    DataAlteration,
2125    /// Creating false documentation.
2126    FalseDocumentation,
2127}
2128
2129impl ConcealmentTechnique {
2130    /// Returns the difficulty bonus this technique adds.
2131    pub fn difficulty_bonus(&self) -> f64 {
2132        match self {
2133            ConcealmentTechnique::DocumentManipulation => 0.20,
2134            ConcealmentTechnique::ApprovalCircumvention => 0.15,
2135            ConcealmentTechnique::TimingExploitation => 0.10,
2136            ConcealmentTechnique::TransactionSplitting => 0.15,
2137            ConcealmentTechnique::AccountMisclassification => 0.10,
2138            ConcealmentTechnique::Collusion => 0.25,
2139            ConcealmentTechnique::DataAlteration => 0.20,
2140            ConcealmentTechnique::FalseDocumentation => 0.15,
2141        }
2142    }
2143}
2144
2145// ============================================================================
2146// ACFE-ALIGNED FRAUD TAXONOMY
2147// ============================================================================
2148//
2149// Based on the Association of Certified Fraud Examiners (ACFE) Report to the
2150// Nations: Occupational Fraud Classification System. This taxonomy provides
2151// ACFE-aligned categories, schemes, and calibration data.
2152
2153/// ACFE-aligned fraud categories based on the Occupational Fraud Tree.
2154///
2155/// ACFE Report to the Nations statistics (typical):
2156/// - Asset Misappropriation: 86% of cases, $100k median loss
2157/// - Corruption: 33% of cases, $150k median loss
2158/// - Financial Statement Fraud: 10% of cases, $954k median loss
2159///
2160/// Note: Percentages sum to >100% because some schemes fall into multiple categories.
2161#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2162pub enum AcfeFraudCategory {
2163    /// Theft of organizational assets (cash, inventory, equipment).
2164    /// Most common (86% of cases) but typically lowest median loss ($100k).
2165    #[default]
2166    AssetMisappropriation,
2167    /// Abuse of position for personal gain through bribery, kickbacks, conflicts of interest.
2168    /// Medium frequency (33% of cases), medium median loss ($150k).
2169    Corruption,
2170    /// Intentional misstatement of financial statements.
2171    /// Least common (10% of cases) but highest median loss ($954k).
2172    FinancialStatementFraud,
2173}
2174
2175impl AcfeFraudCategory {
2176    /// Returns the name of this category.
2177    pub fn name(&self) -> &'static str {
2178        match self {
2179            AcfeFraudCategory::AssetMisappropriation => "asset_misappropriation",
2180            AcfeFraudCategory::Corruption => "corruption",
2181            AcfeFraudCategory::FinancialStatementFraud => "financial_statement_fraud",
2182        }
2183    }
2184
2185    /// Returns the typical percentage of occupational fraud cases (from ACFE reports).
2186    pub fn typical_occurrence_rate(&self) -> f64 {
2187        match self {
2188            AcfeFraudCategory::AssetMisappropriation => 0.86,
2189            AcfeFraudCategory::Corruption => 0.33,
2190            AcfeFraudCategory::FinancialStatementFraud => 0.10,
2191        }
2192    }
2193
2194    /// Returns the typical median loss amount (from ACFE reports).
2195    pub fn typical_median_loss(&self) -> Decimal {
2196        match self {
2197            AcfeFraudCategory::AssetMisappropriation => Decimal::new(100_000, 0),
2198            AcfeFraudCategory::Corruption => Decimal::new(150_000, 0),
2199            AcfeFraudCategory::FinancialStatementFraud => Decimal::new(954_000, 0),
2200        }
2201    }
2202
2203    /// Returns the typical detection time in months (from ACFE reports).
2204    pub fn typical_detection_months(&self) -> u32 {
2205        match self {
2206            AcfeFraudCategory::AssetMisappropriation => 12,
2207            AcfeFraudCategory::Corruption => 18,
2208            AcfeFraudCategory::FinancialStatementFraud => 24,
2209        }
2210    }
2211}
2212
2213/// Cash-based fraud schemes under Asset Misappropriation.
2214///
2215/// Organized according to the ACFE Fraud Tree:
2216/// - Theft of Cash on Hand
2217/// - Theft of Cash Receipts
2218/// - Fraudulent Disbursements
2219#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2220pub enum CashFraudScheme {
2221    // ========== Theft of Cash on Hand ==========
2222    /// Stealing cash from cash drawers or safes after it has been recorded.
2223    Larceny,
2224    /// Stealing cash before it is recorded in the books (intercepts receipts).
2225    Skimming,
2226
2227    // ========== Theft of Cash Receipts ==========
2228    /// Skimming from sales transactions before recording.
2229    SalesSkimming,
2230    /// Intercepting customer payments on accounts receivable.
2231    ReceivablesSkimming,
2232    /// Creating false refunds to pocket the difference.
2233    RefundSchemes,
2234
2235    // ========== Fraudulent Disbursements - Billing Schemes ==========
2236    /// Creating fictitious vendors to invoice and pay.
2237    ShellCompany,
2238    /// Manipulating payments to legitimate vendors for personal gain.
2239    NonAccompliceVendor,
2240    /// Using company funds for personal purchases.
2241    PersonalPurchases,
2242
2243    // ========== Fraudulent Disbursements - Payroll Schemes ==========
2244    /// Creating fake employees to collect wages.
2245    GhostEmployee,
2246    /// Falsifying hours worked, sales commissions, or salary rates.
2247    FalsifiedWages,
2248    /// Manipulating commission calculations.
2249    CommissionSchemes,
2250
2251    // ========== Fraudulent Disbursements - Expense Reimbursement ==========
2252    /// Claiming non-business expenses as business expenses.
2253    MischaracterizedExpenses,
2254    /// Inflating legitimate expense amounts.
2255    OverstatedExpenses,
2256    /// Creating completely fictitious expenses.
2257    FictitiousExpenses,
2258
2259    // ========== Fraudulent Disbursements - Check/Payment Tampering ==========
2260    /// Forging the signature of an authorized check signer.
2261    ForgedMaker,
2262    /// Intercepting and altering the endorsement on legitimate checks.
2263    ForgedEndorsement,
2264    /// Altering the payee on a legitimate check.
2265    AlteredPayee,
2266    /// Authorized signer writing checks for personal benefit.
2267    AuthorizedMaker,
2268
2269    // ========== Fraudulent Disbursements - Register/POS Schemes ==========
2270    /// Creating false voided transactions.
2271    FalseVoids,
2272    /// Processing fictitious refunds.
2273    FalseRefunds,
2274}
2275
2276impl CashFraudScheme {
2277    /// Returns the ACFE category this scheme belongs to.
2278    pub fn category(&self) -> AcfeFraudCategory {
2279        AcfeFraudCategory::AssetMisappropriation
2280    }
2281
2282    /// Returns the subcategory within the ACFE Fraud Tree.
2283    pub fn subcategory(&self) -> &'static str {
2284        match self {
2285            CashFraudScheme::Larceny | CashFraudScheme::Skimming => "theft_of_cash_on_hand",
2286            CashFraudScheme::SalesSkimming
2287            | CashFraudScheme::ReceivablesSkimming
2288            | CashFraudScheme::RefundSchemes => "theft_of_cash_receipts",
2289            CashFraudScheme::ShellCompany
2290            | CashFraudScheme::NonAccompliceVendor
2291            | CashFraudScheme::PersonalPurchases => "billing_schemes",
2292            CashFraudScheme::GhostEmployee
2293            | CashFraudScheme::FalsifiedWages
2294            | CashFraudScheme::CommissionSchemes => "payroll_schemes",
2295            CashFraudScheme::MischaracterizedExpenses
2296            | CashFraudScheme::OverstatedExpenses
2297            | CashFraudScheme::FictitiousExpenses => "expense_reimbursement",
2298            CashFraudScheme::ForgedMaker
2299            | CashFraudScheme::ForgedEndorsement
2300            | CashFraudScheme::AlteredPayee
2301            | CashFraudScheme::AuthorizedMaker => "check_tampering",
2302            CashFraudScheme::FalseVoids | CashFraudScheme::FalseRefunds => "register_schemes",
2303        }
2304    }
2305
2306    /// Returns the typical severity (1-5) for this scheme.
2307    pub fn severity(&self) -> u8 {
2308        match self {
2309            // Lower severity - often small amounts, easier to detect
2310            CashFraudScheme::FalseVoids
2311            | CashFraudScheme::FalseRefunds
2312            | CashFraudScheme::MischaracterizedExpenses => 3,
2313            // Medium severity
2314            CashFraudScheme::OverstatedExpenses
2315            | CashFraudScheme::Skimming
2316            | CashFraudScheme::Larceny
2317            | CashFraudScheme::PersonalPurchases
2318            | CashFraudScheme::FalsifiedWages => 4,
2319            // Higher severity - larger amounts, harder to detect
2320            CashFraudScheme::ShellCompany
2321            | CashFraudScheme::GhostEmployee
2322            | CashFraudScheme::FictitiousExpenses
2323            | CashFraudScheme::ForgedMaker
2324            | CashFraudScheme::AuthorizedMaker => 5,
2325            _ => 4,
2326        }
2327    }
2328
2329    /// Returns the typical detection difficulty.
2330    pub fn detection_difficulty(&self) -> AnomalyDetectionDifficulty {
2331        match self {
2332            // Easy to detect with basic controls
2333            CashFraudScheme::FalseVoids | CashFraudScheme::FalseRefunds => {
2334                AnomalyDetectionDifficulty::Easy
2335            }
2336            // Moderate - requires reconciliation
2337            CashFraudScheme::Larceny | CashFraudScheme::OverstatedExpenses => {
2338                AnomalyDetectionDifficulty::Moderate
2339            }
2340            // Hard - requires sophisticated analysis
2341            CashFraudScheme::Skimming
2342            | CashFraudScheme::ShellCompany
2343            | CashFraudScheme::GhostEmployee => AnomalyDetectionDifficulty::Hard,
2344            // Expert level
2345            CashFraudScheme::SalesSkimming | CashFraudScheme::ReceivablesSkimming => {
2346                AnomalyDetectionDifficulty::Expert
2347            }
2348            _ => AnomalyDetectionDifficulty::Moderate,
2349        }
2350    }
2351
2352    /// Returns all variants for iteration.
2353    pub fn all_variants() -> &'static [CashFraudScheme] {
2354        &[
2355            CashFraudScheme::Larceny,
2356            CashFraudScheme::Skimming,
2357            CashFraudScheme::SalesSkimming,
2358            CashFraudScheme::ReceivablesSkimming,
2359            CashFraudScheme::RefundSchemes,
2360            CashFraudScheme::ShellCompany,
2361            CashFraudScheme::NonAccompliceVendor,
2362            CashFraudScheme::PersonalPurchases,
2363            CashFraudScheme::GhostEmployee,
2364            CashFraudScheme::FalsifiedWages,
2365            CashFraudScheme::CommissionSchemes,
2366            CashFraudScheme::MischaracterizedExpenses,
2367            CashFraudScheme::OverstatedExpenses,
2368            CashFraudScheme::FictitiousExpenses,
2369            CashFraudScheme::ForgedMaker,
2370            CashFraudScheme::ForgedEndorsement,
2371            CashFraudScheme::AlteredPayee,
2372            CashFraudScheme::AuthorizedMaker,
2373            CashFraudScheme::FalseVoids,
2374            CashFraudScheme::FalseRefunds,
2375        ]
2376    }
2377}
2378
2379/// Inventory and Other Asset fraud schemes under Asset Misappropriation.
2380#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2381pub enum AssetFraudScheme {
2382    // ========== Inventory Schemes ==========
2383    /// Misusing or converting inventory for personal benefit.
2384    InventoryMisuse,
2385    /// Stealing physical inventory items.
2386    InventoryTheft,
2387    /// Manipulating purchasing to facilitate theft.
2388    InventoryPurchasingScheme,
2389    /// Manipulating receiving/shipping to steal inventory.
2390    InventoryReceivingScheme,
2391
2392    // ========== Other Asset Schemes ==========
2393    /// Misusing company equipment or vehicles.
2394    EquipmentMisuse,
2395    /// Theft of company equipment, tools, or supplies.
2396    EquipmentTheft,
2397    /// Unauthorized access to or theft of intellectual property.
2398    IntellectualPropertyTheft,
2399    /// Using company time/resources for personal business.
2400    TimeTheft,
2401}
2402
2403impl AssetFraudScheme {
2404    /// Returns the ACFE category this scheme belongs to.
2405    pub fn category(&self) -> AcfeFraudCategory {
2406        AcfeFraudCategory::AssetMisappropriation
2407    }
2408
2409    /// Returns the subcategory within the ACFE Fraud Tree.
2410    pub fn subcategory(&self) -> &'static str {
2411        match self {
2412            AssetFraudScheme::InventoryMisuse
2413            | AssetFraudScheme::InventoryTheft
2414            | AssetFraudScheme::InventoryPurchasingScheme
2415            | AssetFraudScheme::InventoryReceivingScheme => "inventory",
2416            _ => "other_assets",
2417        }
2418    }
2419
2420    /// Returns the typical severity (1-5) for this scheme.
2421    pub fn severity(&self) -> u8 {
2422        match self {
2423            AssetFraudScheme::TimeTheft | AssetFraudScheme::EquipmentMisuse => 2,
2424            AssetFraudScheme::InventoryMisuse | AssetFraudScheme::EquipmentTheft => 3,
2425            AssetFraudScheme::InventoryTheft
2426            | AssetFraudScheme::InventoryPurchasingScheme
2427            | AssetFraudScheme::InventoryReceivingScheme => 4,
2428            AssetFraudScheme::IntellectualPropertyTheft => 5,
2429        }
2430    }
2431}
2432
2433/// Corruption schemes under the ACFE Fraud Tree.
2434///
2435/// Corruption schemes involve the wrongful use of influence in a business
2436/// transaction to procure personal benefit.
2437#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2438pub enum CorruptionScheme {
2439    // ========== Conflicts of Interest ==========
2440    /// Employee has undisclosed financial interest in purchasing decisions.
2441    PurchasingConflict,
2442    /// Employee has undisclosed relationship with customer/vendor.
2443    SalesConflict,
2444    /// Employee owns or has interest in competing business.
2445    OutsideBusinessInterest,
2446    /// Employee makes decisions benefiting family members.
2447    NepotismConflict,
2448
2449    // ========== Bribery ==========
2450    /// Kickback payments from vendors for favorable treatment.
2451    InvoiceKickback,
2452    /// Collusion among vendors to inflate prices.
2453    BidRigging,
2454    /// Other cash payments for favorable decisions.
2455    CashBribery,
2456    /// Bribery of government officials.
2457    PublicOfficial,
2458
2459    // ========== Illegal Gratuities ==========
2460    /// Gifts given after favorable decisions (not agreed in advance).
2461    IllegalGratuity,
2462
2463    // ========== Economic Extortion ==========
2464    /// Demanding payment under threat of adverse action.
2465    EconomicExtortion,
2466}
2467
2468impl CorruptionScheme {
2469    /// Returns the ACFE category this scheme belongs to.
2470    pub fn category(&self) -> AcfeFraudCategory {
2471        AcfeFraudCategory::Corruption
2472    }
2473
2474    /// Returns the subcategory within the ACFE Fraud Tree.
2475    pub fn subcategory(&self) -> &'static str {
2476        match self {
2477            CorruptionScheme::PurchasingConflict
2478            | CorruptionScheme::SalesConflict
2479            | CorruptionScheme::OutsideBusinessInterest
2480            | CorruptionScheme::NepotismConflict => "conflicts_of_interest",
2481            CorruptionScheme::InvoiceKickback
2482            | CorruptionScheme::BidRigging
2483            | CorruptionScheme::CashBribery
2484            | CorruptionScheme::PublicOfficial => "bribery",
2485            CorruptionScheme::IllegalGratuity => "illegal_gratuities",
2486            CorruptionScheme::EconomicExtortion => "economic_extortion",
2487        }
2488    }
2489
2490    /// Returns the typical severity (1-5) for this scheme.
2491    pub fn severity(&self) -> u8 {
2492        match self {
2493            // Lower severity conflicts of interest
2494            CorruptionScheme::NepotismConflict => 3,
2495            // Medium severity
2496            CorruptionScheme::PurchasingConflict
2497            | CorruptionScheme::SalesConflict
2498            | CorruptionScheme::OutsideBusinessInterest
2499            | CorruptionScheme::IllegalGratuity => 4,
2500            // High severity - active corruption
2501            CorruptionScheme::InvoiceKickback
2502            | CorruptionScheme::BidRigging
2503            | CorruptionScheme::CashBribery
2504            | CorruptionScheme::EconomicExtortion => 5,
2505            // Highest severity - involves public officials
2506            CorruptionScheme::PublicOfficial => 5,
2507        }
2508    }
2509
2510    /// Returns the typical detection difficulty.
2511    pub fn detection_difficulty(&self) -> AnomalyDetectionDifficulty {
2512        match self {
2513            // Easier to detect with proper disclosure requirements
2514            CorruptionScheme::NepotismConflict | CorruptionScheme::OutsideBusinessInterest => {
2515                AnomalyDetectionDifficulty::Moderate
2516            }
2517            // Hard - requires transaction pattern analysis
2518            CorruptionScheme::PurchasingConflict
2519            | CorruptionScheme::SalesConflict
2520            | CorruptionScheme::BidRigging => AnomalyDetectionDifficulty::Hard,
2521            // Expert level - deliberate concealment
2522            CorruptionScheme::InvoiceKickback
2523            | CorruptionScheme::CashBribery
2524            | CorruptionScheme::PublicOfficial
2525            | CorruptionScheme::IllegalGratuity
2526            | CorruptionScheme::EconomicExtortion => AnomalyDetectionDifficulty::Expert,
2527        }
2528    }
2529
2530    /// Returns all variants for iteration.
2531    pub fn all_variants() -> &'static [CorruptionScheme] {
2532        &[
2533            CorruptionScheme::PurchasingConflict,
2534            CorruptionScheme::SalesConflict,
2535            CorruptionScheme::OutsideBusinessInterest,
2536            CorruptionScheme::NepotismConflict,
2537            CorruptionScheme::InvoiceKickback,
2538            CorruptionScheme::BidRigging,
2539            CorruptionScheme::CashBribery,
2540            CorruptionScheme::PublicOfficial,
2541            CorruptionScheme::IllegalGratuity,
2542            CorruptionScheme::EconomicExtortion,
2543        ]
2544    }
2545}
2546
2547/// Financial Statement Fraud schemes under the ACFE Fraud Tree.
2548///
2549/// Financial statement fraud involves the intentional misstatement or omission
2550/// of material information in financial reports.
2551#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2552pub enum FinancialStatementScheme {
2553    // ========== Asset/Revenue Overstatement ==========
2554    /// Recording revenue before it is earned.
2555    PrematureRevenue,
2556    /// Deferring expenses to future periods.
2557    DelayedExpenses,
2558    /// Recording revenue for transactions that never occurred.
2559    FictitiousRevenues,
2560    /// Failing to record known liabilities.
2561    ConcealedLiabilities,
2562    /// Overstating the value of assets.
2563    ImproperAssetValuations,
2564    /// Omitting or misstating required disclosures.
2565    ImproperDisclosures,
2566    /// Manipulating timing of revenue recognition (channel stuffing).
2567    ChannelStuffing,
2568    /// Recognizing bill-and-hold revenue improperly.
2569    BillAndHold,
2570    /// Capitalizing expenses that should be expensed.
2571    ImproperCapitalization,
2572
2573    // ========== Asset/Revenue Understatement ==========
2574    /// Understating revenue (often for tax purposes).
2575    UnderstatedRevenues,
2576    /// Recording excessive expenses.
2577    OverstatedExpenses,
2578    /// Recording excessive liabilities or reserves.
2579    OverstatedLiabilities,
2580    /// Undervaluing assets for writedowns/reserves.
2581    ImproperAssetWritedowns,
2582}
2583
2584impl FinancialStatementScheme {
2585    /// Returns the ACFE category this scheme belongs to.
2586    pub fn category(&self) -> AcfeFraudCategory {
2587        AcfeFraudCategory::FinancialStatementFraud
2588    }
2589
2590    /// Returns the subcategory within the ACFE Fraud Tree.
2591    pub fn subcategory(&self) -> &'static str {
2592        match self {
2593            FinancialStatementScheme::UnderstatedRevenues
2594            | FinancialStatementScheme::OverstatedExpenses
2595            | FinancialStatementScheme::OverstatedLiabilities
2596            | FinancialStatementScheme::ImproperAssetWritedowns => "understatement",
2597            _ => "overstatement",
2598        }
2599    }
2600
2601    /// Returns the typical severity (1-5) for this scheme.
2602    pub fn severity(&self) -> u8 {
2603        // All financial statement fraud is high severity
2604        5
2605    }
2606
2607    /// Returns the typical detection difficulty.
2608    pub fn detection_difficulty(&self) -> AnomalyDetectionDifficulty {
2609        match self {
2610            // Easier to detect with good analytics
2611            FinancialStatementScheme::ChannelStuffing
2612            | FinancialStatementScheme::DelayedExpenses => AnomalyDetectionDifficulty::Moderate,
2613            // Hard - requires deep analysis
2614            FinancialStatementScheme::PrematureRevenue
2615            | FinancialStatementScheme::ImproperCapitalization
2616            | FinancialStatementScheme::ImproperAssetWritedowns => AnomalyDetectionDifficulty::Hard,
2617            // Expert level
2618            FinancialStatementScheme::FictitiousRevenues
2619            | FinancialStatementScheme::ConcealedLiabilities
2620            | FinancialStatementScheme::ImproperAssetValuations
2621            | FinancialStatementScheme::ImproperDisclosures
2622            | FinancialStatementScheme::BillAndHold => AnomalyDetectionDifficulty::Expert,
2623            _ => AnomalyDetectionDifficulty::Hard,
2624        }
2625    }
2626
2627    /// Returns all variants for iteration.
2628    pub fn all_variants() -> &'static [FinancialStatementScheme] {
2629        &[
2630            FinancialStatementScheme::PrematureRevenue,
2631            FinancialStatementScheme::DelayedExpenses,
2632            FinancialStatementScheme::FictitiousRevenues,
2633            FinancialStatementScheme::ConcealedLiabilities,
2634            FinancialStatementScheme::ImproperAssetValuations,
2635            FinancialStatementScheme::ImproperDisclosures,
2636            FinancialStatementScheme::ChannelStuffing,
2637            FinancialStatementScheme::BillAndHold,
2638            FinancialStatementScheme::ImproperCapitalization,
2639            FinancialStatementScheme::UnderstatedRevenues,
2640            FinancialStatementScheme::OverstatedExpenses,
2641            FinancialStatementScheme::OverstatedLiabilities,
2642            FinancialStatementScheme::ImproperAssetWritedowns,
2643        ]
2644    }
2645}
2646
2647/// Unified ACFE scheme type that encompasses all fraud schemes.
2648#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2649pub enum AcfeScheme {
2650    /// Cash-based fraud schemes.
2651    Cash(CashFraudScheme),
2652    /// Inventory and other asset fraud schemes.
2653    Asset(AssetFraudScheme),
2654    /// Corruption schemes.
2655    Corruption(CorruptionScheme),
2656    /// Financial statement fraud schemes.
2657    FinancialStatement(FinancialStatementScheme),
2658}
2659
2660impl AcfeScheme {
2661    /// Returns the ACFE category this scheme belongs to.
2662    pub fn category(&self) -> AcfeFraudCategory {
2663        match self {
2664            AcfeScheme::Cash(s) => s.category(),
2665            AcfeScheme::Asset(s) => s.category(),
2666            AcfeScheme::Corruption(s) => s.category(),
2667            AcfeScheme::FinancialStatement(s) => s.category(),
2668        }
2669    }
2670
2671    /// Returns the severity (1-5) for this scheme.
2672    pub fn severity(&self) -> u8 {
2673        match self {
2674            AcfeScheme::Cash(s) => s.severity(),
2675            AcfeScheme::Asset(s) => s.severity(),
2676            AcfeScheme::Corruption(s) => s.severity(),
2677            AcfeScheme::FinancialStatement(s) => s.severity(),
2678        }
2679    }
2680
2681    /// Returns the detection difficulty for this scheme.
2682    pub fn detection_difficulty(&self) -> AnomalyDetectionDifficulty {
2683        match self {
2684            AcfeScheme::Cash(s) => s.detection_difficulty(),
2685            AcfeScheme::Asset(_) => AnomalyDetectionDifficulty::Moderate,
2686            AcfeScheme::Corruption(s) => s.detection_difficulty(),
2687            AcfeScheme::FinancialStatement(s) => s.detection_difficulty(),
2688        }
2689    }
2690}
2691
2692/// How a fraud was detected (from ACFE statistics).
2693#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2694pub enum AcfeDetectionMethod {
2695    /// Tip from employee, customer, vendor, or anonymous source.
2696    Tip,
2697    /// Internal audit procedures.
2698    InternalAudit,
2699    /// Management review and oversight.
2700    ManagementReview,
2701    /// External audit procedures.
2702    ExternalAudit,
2703    /// Account reconciliation discrepancies.
2704    AccountReconciliation,
2705    /// Document examination.
2706    DocumentExamination,
2707    /// Discovered by accident.
2708    ByAccident,
2709    /// Automated monitoring/IT controls.
2710    ItControls,
2711    /// Surveillance or investigation.
2712    Surveillance,
2713    /// Confession by perpetrator.
2714    Confession,
2715    /// Law enforcement notification.
2716    LawEnforcement,
2717    /// Other detection method.
2718    Other,
2719}
2720
2721impl AcfeDetectionMethod {
2722    /// Returns the typical percentage of frauds detected by this method (from ACFE reports).
2723    pub fn typical_detection_rate(&self) -> f64 {
2724        match self {
2725            AcfeDetectionMethod::Tip => 0.42,
2726            AcfeDetectionMethod::InternalAudit => 0.16,
2727            AcfeDetectionMethod::ManagementReview => 0.12,
2728            AcfeDetectionMethod::ExternalAudit => 0.04,
2729            AcfeDetectionMethod::AccountReconciliation => 0.05,
2730            AcfeDetectionMethod::DocumentExamination => 0.04,
2731            AcfeDetectionMethod::ByAccident => 0.06,
2732            AcfeDetectionMethod::ItControls => 0.03,
2733            AcfeDetectionMethod::Surveillance => 0.02,
2734            AcfeDetectionMethod::Confession => 0.02,
2735            AcfeDetectionMethod::LawEnforcement => 0.01,
2736            AcfeDetectionMethod::Other => 0.03,
2737        }
2738    }
2739
2740    /// Returns all variants for iteration.
2741    pub fn all_variants() -> &'static [AcfeDetectionMethod] {
2742        &[
2743            AcfeDetectionMethod::Tip,
2744            AcfeDetectionMethod::InternalAudit,
2745            AcfeDetectionMethod::ManagementReview,
2746            AcfeDetectionMethod::ExternalAudit,
2747            AcfeDetectionMethod::AccountReconciliation,
2748            AcfeDetectionMethod::DocumentExamination,
2749            AcfeDetectionMethod::ByAccident,
2750            AcfeDetectionMethod::ItControls,
2751            AcfeDetectionMethod::Surveillance,
2752            AcfeDetectionMethod::Confession,
2753            AcfeDetectionMethod::LawEnforcement,
2754            AcfeDetectionMethod::Other,
2755        ]
2756    }
2757}
2758
2759/// Department/position of perpetrator (from ACFE statistics).
2760#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2761pub enum PerpetratorDepartment {
2762    /// Accounting, finance, or bookkeeping.
2763    Accounting,
2764    /// Operations or manufacturing.
2765    Operations,
2766    /// Executive/upper management.
2767    Executive,
2768    /// Sales.
2769    Sales,
2770    /// Customer service.
2771    CustomerService,
2772    /// Purchasing/procurement.
2773    Purchasing,
2774    /// Information technology.
2775    It,
2776    /// Human resources.
2777    HumanResources,
2778    /// Administrative/clerical.
2779    Administrative,
2780    /// Warehouse/inventory.
2781    Warehouse,
2782    /// Board of directors.
2783    BoardOfDirectors,
2784    /// Other department.
2785    Other,
2786}
2787
2788impl PerpetratorDepartment {
2789    /// Returns the typical percentage of frauds by department (from ACFE reports).
2790    pub fn typical_occurrence_rate(&self) -> f64 {
2791        match self {
2792            PerpetratorDepartment::Accounting => 0.21,
2793            PerpetratorDepartment::Operations => 0.17,
2794            PerpetratorDepartment::Executive => 0.12,
2795            PerpetratorDepartment::Sales => 0.11,
2796            PerpetratorDepartment::CustomerService => 0.07,
2797            PerpetratorDepartment::Purchasing => 0.06,
2798            PerpetratorDepartment::It => 0.05,
2799            PerpetratorDepartment::HumanResources => 0.04,
2800            PerpetratorDepartment::Administrative => 0.04,
2801            PerpetratorDepartment::Warehouse => 0.03,
2802            PerpetratorDepartment::BoardOfDirectors => 0.02,
2803            PerpetratorDepartment::Other => 0.08,
2804        }
2805    }
2806
2807    /// Returns the typical median loss by perpetrator department.
2808    pub fn typical_median_loss(&self) -> Decimal {
2809        match self {
2810            PerpetratorDepartment::Executive => Decimal::new(600_000, 0),
2811            PerpetratorDepartment::BoardOfDirectors => Decimal::new(500_000, 0),
2812            PerpetratorDepartment::Sales => Decimal::new(150_000, 0),
2813            PerpetratorDepartment::Accounting => Decimal::new(130_000, 0),
2814            PerpetratorDepartment::Purchasing => Decimal::new(120_000, 0),
2815            PerpetratorDepartment::Operations => Decimal::new(100_000, 0),
2816            PerpetratorDepartment::It => Decimal::new(100_000, 0),
2817            _ => Decimal::new(80_000, 0),
2818        }
2819    }
2820}
2821
2822/// Perpetrator position level (from ACFE statistics).
2823#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2824pub enum PerpetratorLevel {
2825    /// Entry-level employee.
2826    Employee,
2827    /// Manager or supervisor.
2828    Manager,
2829    /// Owner, executive, or C-level.
2830    OwnerExecutive,
2831}
2832
2833impl PerpetratorLevel {
2834    /// Returns the typical percentage of frauds by position level.
2835    pub fn typical_occurrence_rate(&self) -> f64 {
2836        match self {
2837            PerpetratorLevel::Employee => 0.42,
2838            PerpetratorLevel::Manager => 0.36,
2839            PerpetratorLevel::OwnerExecutive => 0.22,
2840        }
2841    }
2842
2843    /// Returns the typical median loss by position level.
2844    pub fn typical_median_loss(&self) -> Decimal {
2845        match self {
2846            PerpetratorLevel::Employee => Decimal::new(50_000, 0),
2847            PerpetratorLevel::Manager => Decimal::new(125_000, 0),
2848            PerpetratorLevel::OwnerExecutive => Decimal::new(337_000, 0),
2849        }
2850    }
2851}
2852
2853/// ACFE Calibration data for fraud generation.
2854///
2855/// Contains statistical parameters based on ACFE Report to the Nations
2856/// for realistic fraud pattern generation.
2857#[derive(Debug, Clone, Serialize, Deserialize)]
2858pub struct AcfeCalibration {
2859    /// Overall median loss for occupational fraud ($117,000 typical).
2860    pub median_loss: Decimal,
2861    /// Median duration in months before detection (12 months typical).
2862    pub median_duration_months: u32,
2863    /// Distribution of fraud by category.
2864    pub category_distribution: HashMap<String, f64>,
2865    /// Distribution of detection methods.
2866    pub detection_method_distribution: HashMap<String, f64>,
2867    /// Distribution by perpetrator department.
2868    pub department_distribution: HashMap<String, f64>,
2869    /// Distribution by perpetrator level.
2870    pub level_distribution: HashMap<String, f64>,
2871    /// Average number of red flags per fraud case.
2872    pub avg_red_flags_per_case: f64,
2873    /// Percentage of frauds involving collusion.
2874    pub collusion_rate: f64,
2875}
2876
2877impl Default for AcfeCalibration {
2878    fn default() -> Self {
2879        let mut category_distribution = HashMap::new();
2880        category_distribution.insert("asset_misappropriation".to_string(), 0.86);
2881        category_distribution.insert("corruption".to_string(), 0.33);
2882        category_distribution.insert("financial_statement_fraud".to_string(), 0.10);
2883
2884        let mut detection_method_distribution = HashMap::new();
2885        for method in AcfeDetectionMethod::all_variants() {
2886            detection_method_distribution.insert(
2887                format!("{:?}", method).to_lowercase(),
2888                method.typical_detection_rate(),
2889            );
2890        }
2891
2892        let mut department_distribution = HashMap::new();
2893        department_distribution.insert("accounting".to_string(), 0.21);
2894        department_distribution.insert("operations".to_string(), 0.17);
2895        department_distribution.insert("executive".to_string(), 0.12);
2896        department_distribution.insert("sales".to_string(), 0.11);
2897        department_distribution.insert("customer_service".to_string(), 0.07);
2898        department_distribution.insert("purchasing".to_string(), 0.06);
2899        department_distribution.insert("other".to_string(), 0.26);
2900
2901        let mut level_distribution = HashMap::new();
2902        level_distribution.insert("employee".to_string(), 0.42);
2903        level_distribution.insert("manager".to_string(), 0.36);
2904        level_distribution.insert("owner_executive".to_string(), 0.22);
2905
2906        Self {
2907            median_loss: Decimal::new(117_000, 0),
2908            median_duration_months: 12,
2909            category_distribution,
2910            detection_method_distribution,
2911            department_distribution,
2912            level_distribution,
2913            avg_red_flags_per_case: 2.8,
2914            collusion_rate: 0.50,
2915        }
2916    }
2917}
2918
2919impl AcfeCalibration {
2920    /// Creates a new ACFE calibration with the given parameters.
2921    pub fn new(median_loss: Decimal, median_duration_months: u32) -> Self {
2922        Self {
2923            median_loss,
2924            median_duration_months,
2925            ..Self::default()
2926        }
2927    }
2928
2929    /// Returns the median loss for a specific category.
2930    pub fn median_loss_for_category(&self, category: AcfeFraudCategory) -> Decimal {
2931        category.typical_median_loss()
2932    }
2933
2934    /// Returns the median duration for a specific category.
2935    pub fn median_duration_for_category(&self, category: AcfeFraudCategory) -> u32 {
2936        category.typical_detection_months()
2937    }
2938
2939    /// Validates the calibration data.
2940    pub fn validate(&self) -> Result<(), String> {
2941        if self.median_loss <= Decimal::ZERO {
2942            return Err("Median loss must be positive".to_string());
2943        }
2944        if self.median_duration_months == 0 {
2945            return Err("Median duration must be at least 1 month".to_string());
2946        }
2947        if self.collusion_rate < 0.0 || self.collusion_rate > 1.0 {
2948            return Err("Collusion rate must be between 0.0 and 1.0".to_string());
2949        }
2950        Ok(())
2951    }
2952}
2953
2954/// Fraud Triangle components (Pressure, Opportunity, Rationalization).
2955///
2956/// The fraud triangle is a model for explaining the factors that cause
2957/// someone to commit occupational fraud.
2958#[derive(Debug, Clone, Serialize, Deserialize)]
2959pub struct FraudTriangle {
2960    /// Pressure or incentive to commit fraud.
2961    pub pressure: PressureType,
2962    /// Opportunity factors that enable fraud.
2963    pub opportunities: Vec<OpportunityFactor>,
2964    /// Rationalization used to justify the fraud.
2965    pub rationalization: Rationalization,
2966}
2967
2968impl FraudTriangle {
2969    /// Creates a new fraud triangle.
2970    pub fn new(
2971        pressure: PressureType,
2972        opportunities: Vec<OpportunityFactor>,
2973        rationalization: Rationalization,
2974    ) -> Self {
2975        Self {
2976            pressure,
2977            opportunities,
2978            rationalization,
2979        }
2980    }
2981
2982    /// Returns a risk score based on the fraud triangle components.
2983    pub fn risk_score(&self) -> f64 {
2984        let pressure_score = self.pressure.risk_weight();
2985        let opportunity_score: f64 = self
2986            .opportunities
2987            .iter()
2988            .map(|o| o.risk_weight())
2989            .sum::<f64>()
2990            / self.opportunities.len().max(1) as f64;
2991        let rationalization_score = self.rationalization.risk_weight();
2992
2993        (pressure_score + opportunity_score + rationalization_score) / 3.0
2994    }
2995}
2996
2997/// Types of pressure/incentive that can lead to fraud.
2998#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2999pub enum PressureType {
3000    // Financial Pressures
3001    /// Personal financial difficulties (debt, lifestyle beyond means).
3002    PersonalFinancialDifficulties,
3003    /// Pressure to meet financial targets/earnings expectations.
3004    FinancialTargets,
3005    /// Market or analyst expectations.
3006    MarketExpectations,
3007    /// Debt covenant compliance requirements.
3008    CovenantCompliance,
3009    /// Credit rating maintenance.
3010    CreditRatingMaintenance,
3011    /// Acquisition/merger valuation pressure.
3012    AcquisitionValuation,
3013
3014    // Non-Financial Pressures
3015    /// Fear of job loss.
3016    JobSecurity,
3017    /// Pressure to maintain status or image.
3018    StatusMaintenance,
3019    /// Gambling addiction.
3020    GamblingAddiction,
3021    /// Substance abuse issues.
3022    SubstanceAbuse,
3023    /// Family pressure or obligations.
3024    FamilyPressure,
3025    /// Greed or desire for more.
3026    Greed,
3027}
3028
3029impl PressureType {
3030    /// Returns the risk weight (0.0-1.0) for this pressure type.
3031    pub fn risk_weight(&self) -> f64 {
3032        match self {
3033            PressureType::PersonalFinancialDifficulties => 0.80,
3034            PressureType::FinancialTargets => 0.75,
3035            PressureType::MarketExpectations => 0.70,
3036            PressureType::CovenantCompliance => 0.85,
3037            PressureType::CreditRatingMaintenance => 0.70,
3038            PressureType::AcquisitionValuation => 0.75,
3039            PressureType::JobSecurity => 0.65,
3040            PressureType::StatusMaintenance => 0.55,
3041            PressureType::GamblingAddiction => 0.90,
3042            PressureType::SubstanceAbuse => 0.85,
3043            PressureType::FamilyPressure => 0.60,
3044            PressureType::Greed => 0.70,
3045        }
3046    }
3047}
3048
3049/// Opportunity factors that enable fraud.
3050#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3051pub enum OpportunityFactor {
3052    /// Weak internal controls.
3053    WeakInternalControls,
3054    /// Lack of segregation of duties.
3055    LackOfSegregation,
3056    /// Override capability.
3057    ManagementOverride,
3058    /// Complex or unusual transactions.
3059    ComplexTransactions,
3060    /// Related party transactions.
3061    RelatedPartyTransactions,
3062    /// Poor tone at the top.
3063    PoorToneAtTop,
3064    /// Inadequate supervision.
3065    InadequateSupervision,
3066    /// Access to assets without accountability.
3067    AssetAccess,
3068    /// Inadequate record keeping.
3069    PoorRecordKeeping,
3070    /// Failure to discipline fraud perpetrators.
3071    LackOfDiscipline,
3072    /// Lack of independent checks.
3073    LackOfIndependentChecks,
3074}
3075
3076impl OpportunityFactor {
3077    /// Returns the risk weight (0.0-1.0) for this opportunity factor.
3078    pub fn risk_weight(&self) -> f64 {
3079        match self {
3080            OpportunityFactor::WeakInternalControls => 0.85,
3081            OpportunityFactor::LackOfSegregation => 0.80,
3082            OpportunityFactor::ManagementOverride => 0.90,
3083            OpportunityFactor::ComplexTransactions => 0.70,
3084            OpportunityFactor::RelatedPartyTransactions => 0.75,
3085            OpportunityFactor::PoorToneAtTop => 0.85,
3086            OpportunityFactor::InadequateSupervision => 0.75,
3087            OpportunityFactor::AssetAccess => 0.70,
3088            OpportunityFactor::PoorRecordKeeping => 0.65,
3089            OpportunityFactor::LackOfDiscipline => 0.60,
3090            OpportunityFactor::LackOfIndependentChecks => 0.75,
3091        }
3092    }
3093}
3094
3095/// Rationalizations used by fraud perpetrators.
3096#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3097pub enum Rationalization {
3098    /// "I'm just borrowing; I'll pay it back."
3099    TemporaryBorrowing,
3100    /// "Everyone does it."
3101    EveryoneDoesIt,
3102    /// "It's for the good of the company."
3103    ForTheCompanyGood,
3104    /// "I deserve this; the company owes me."
3105    Entitlement,
3106    /// "I was just following orders."
3107    FollowingOrders,
3108    /// "They won't miss it; they have plenty."
3109    TheyWontMissIt,
3110    /// "I need it more than they do."
3111    NeedItMore,
3112    /// "It's not really stealing."
3113    NotReallyStealing,
3114    /// "I'm underpaid for what I do."
3115    Underpaid,
3116    /// "It's a victimless crime."
3117    VictimlessCrime,
3118}
3119
3120impl Rationalization {
3121    /// Returns the risk weight (0.0-1.0) for this rationalization.
3122    pub fn risk_weight(&self) -> f64 {
3123        match self {
3124            // More dangerous rationalizations
3125            Rationalization::Entitlement => 0.85,
3126            Rationalization::EveryoneDoesIt => 0.80,
3127            Rationalization::NotReallyStealing => 0.80,
3128            Rationalization::TheyWontMissIt => 0.75,
3129            // Medium risk
3130            Rationalization::Underpaid => 0.70,
3131            Rationalization::ForTheCompanyGood => 0.65,
3132            Rationalization::NeedItMore => 0.65,
3133            // Lower risk (still indicates fraud)
3134            Rationalization::TemporaryBorrowing => 0.60,
3135            Rationalization::FollowingOrders => 0.55,
3136            Rationalization::VictimlessCrime => 0.60,
3137        }
3138    }
3139}
3140
3141// ============================================================================
3142// NEAR-MISS TYPES
3143// ============================================================================
3144
3145/// Type of near-miss pattern (suspicious but legitimate).
3146#[derive(Debug, Clone, Serialize, Deserialize)]
3147pub enum NearMissPattern {
3148    /// Transaction very similar to another (possible duplicate but legitimate).
3149    NearDuplicate {
3150        /// Date difference from similar transaction.
3151        date_difference_days: u32,
3152        /// Original transaction ID.
3153        similar_transaction_id: String,
3154    },
3155    /// Amount just below approval threshold (but legitimate).
3156    ThresholdProximity {
3157        /// The threshold being approached.
3158        threshold: Decimal,
3159        /// Percentage of threshold (0.0-1.0).
3160        proximity: f64,
3161    },
3162    /// Unusual but legitimate business pattern.
3163    UnusualLegitimate {
3164        /// Type of legitimate pattern.
3165        pattern_type: LegitimatePatternType,
3166        /// Business justification.
3167        justification: String,
3168    },
3169    /// Error that was caught and corrected.
3170    CorrectedError {
3171        /// Days until correction.
3172        correction_lag_days: u32,
3173        /// Correction document ID.
3174        correction_document_id: String,
3175    },
3176}
3177
3178/// Types of unusual but legitimate business patterns.
3179#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3180pub enum LegitimatePatternType {
3181    /// Year-end bonus payment.
3182    YearEndBonus,
3183    /// Contract prepayment.
3184    ContractPrepayment,
3185    /// Settlement payment.
3186    SettlementPayment,
3187    /// Insurance claim.
3188    InsuranceClaim,
3189    /// One-time vendor payment.
3190    OneTimePayment,
3191    /// Asset disposal.
3192    AssetDisposal,
3193    /// Seasonal inventory buildup.
3194    SeasonalInventory,
3195    /// Promotional spending.
3196    PromotionalSpending,
3197}
3198
3199impl LegitimatePatternType {
3200    /// Returns a description of this pattern type.
3201    pub fn description(&self) -> &'static str {
3202        match self {
3203            LegitimatePatternType::YearEndBonus => "Year-end bonus payment",
3204            LegitimatePatternType::ContractPrepayment => "Contract prepayment per terms",
3205            LegitimatePatternType::SettlementPayment => "Legal settlement payment",
3206            LegitimatePatternType::InsuranceClaim => "Insurance claim reimbursement",
3207            LegitimatePatternType::OneTimePayment => "One-time vendor payment",
3208            LegitimatePatternType::AssetDisposal => "Fixed asset disposal",
3209            LegitimatePatternType::SeasonalInventory => "Seasonal inventory buildup",
3210            LegitimatePatternType::PromotionalSpending => "Promotional campaign spending",
3211        }
3212    }
3213}
3214
3215/// What might trigger a false positive for this near-miss.
3216#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3217pub enum FalsePositiveTrigger {
3218    /// Amount is near threshold.
3219    AmountNearThreshold,
3220    /// Timing is unusual.
3221    UnusualTiming,
3222    /// Similar to existing transaction.
3223    SimilarTransaction,
3224    /// New counterparty.
3225    NewCounterparty,
3226    /// Account combination unusual.
3227    UnusualAccountCombination,
3228    /// Volume spike.
3229    VolumeSpike,
3230    /// Round amount.
3231    RoundAmount,
3232}
3233
3234/// Label for a near-miss case.
3235#[derive(Debug, Clone, Serialize, Deserialize)]
3236pub struct NearMissLabel {
3237    /// Document ID.
3238    pub document_id: String,
3239    /// The near-miss pattern.
3240    pub pattern: NearMissPattern,
3241    /// How suspicious it appears (0.0-1.0).
3242    pub suspicion_score: f64,
3243    /// What would trigger a false positive.
3244    pub false_positive_trigger: FalsePositiveTrigger,
3245    /// Why this is actually legitimate.
3246    pub explanation: String,
3247}
3248
3249impl NearMissLabel {
3250    /// Creates a new near-miss label.
3251    pub fn new(
3252        document_id: impl Into<String>,
3253        pattern: NearMissPattern,
3254        suspicion_score: f64,
3255        trigger: FalsePositiveTrigger,
3256        explanation: impl Into<String>,
3257    ) -> Self {
3258        Self {
3259            document_id: document_id.into(),
3260            pattern,
3261            suspicion_score: suspicion_score.clamp(0.0, 1.0),
3262            false_positive_trigger: trigger,
3263            explanation: explanation.into(),
3264        }
3265    }
3266}
3267
3268/// Configuration for anomaly rates.
3269#[derive(Debug, Clone, Serialize, Deserialize)]
3270pub struct AnomalyRateConfig {
3271    /// Overall anomaly rate (0.0 - 1.0).
3272    pub total_rate: f64,
3273    /// Fraud rate as proportion of anomalies.
3274    pub fraud_rate: f64,
3275    /// Error rate as proportion of anomalies.
3276    pub error_rate: f64,
3277    /// Process issue rate as proportion of anomalies.
3278    pub process_issue_rate: f64,
3279    /// Statistical anomaly rate as proportion of anomalies.
3280    pub statistical_rate: f64,
3281    /// Relational anomaly rate as proportion of anomalies.
3282    pub relational_rate: f64,
3283}
3284
3285impl Default for AnomalyRateConfig {
3286    fn default() -> Self {
3287        Self {
3288            total_rate: 0.02,         // 2% of transactions are anomalous
3289            fraud_rate: 0.25,         // 25% of anomalies are fraud
3290            error_rate: 0.35,         // 35% of anomalies are errors
3291            process_issue_rate: 0.20, // 20% are process issues
3292            statistical_rate: 0.15,   // 15% are statistical
3293            relational_rate: 0.05,    // 5% are relational
3294        }
3295    }
3296}
3297
3298impl AnomalyRateConfig {
3299    /// Validates that rates sum to approximately 1.0.
3300    pub fn validate(&self) -> Result<(), String> {
3301        let sum = self.fraud_rate
3302            + self.error_rate
3303            + self.process_issue_rate
3304            + self.statistical_rate
3305            + self.relational_rate;
3306
3307        if (sum - 1.0).abs() > 0.01 {
3308            return Err(format!(
3309                "Anomaly category rates must sum to 1.0, got {}",
3310                sum
3311            ));
3312        }
3313
3314        if self.total_rate < 0.0 || self.total_rate > 1.0 {
3315            return Err(format!(
3316                "Total rate must be between 0.0 and 1.0, got {}",
3317                self.total_rate
3318            ));
3319        }
3320
3321        Ok(())
3322    }
3323}
3324
3325#[cfg(test)]
3326mod tests {
3327    use super::*;
3328    use rust_decimal_macros::dec;
3329
3330    #[test]
3331    fn test_anomaly_type_category() {
3332        let fraud = AnomalyType::Fraud(FraudType::SelfApproval);
3333        assert_eq!(fraud.category(), "Fraud");
3334        assert!(fraud.is_intentional());
3335
3336        let error = AnomalyType::Error(ErrorType::DuplicateEntry);
3337        assert_eq!(error.category(), "Error");
3338        assert!(!error.is_intentional());
3339    }
3340
3341    #[test]
3342    fn test_labeled_anomaly() {
3343        let anomaly = LabeledAnomaly::new(
3344            "ANO001".to_string(),
3345            AnomalyType::Fraud(FraudType::SelfApproval),
3346            "JE001".to_string(),
3347            "JE".to_string(),
3348            "1000".to_string(),
3349            NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
3350        )
3351        .with_description("User approved their own expense report")
3352        .with_related_entity("USER001");
3353
3354        assert_eq!(anomaly.severity, 3);
3355        assert!(anomaly.is_injected);
3356        assert_eq!(anomaly.related_entities.len(), 1);
3357    }
3358
3359    #[test]
3360    fn test_labeled_anomaly_with_provenance() {
3361        let anomaly = LabeledAnomaly::new(
3362            "ANO001".to_string(),
3363            AnomalyType::Fraud(FraudType::SelfApproval),
3364            "JE001".to_string(),
3365            "JE".to_string(),
3366            "1000".to_string(),
3367            NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
3368        )
3369        .with_run_id("run-123")
3370        .with_generation_seed(42)
3371        .with_causal_reason(AnomalyCausalReason::RandomRate { base_rate: 0.02 })
3372        .with_structured_strategy(InjectionStrategy::SelfApproval {
3373            user_id: "USER001".to_string(),
3374        })
3375        .with_scenario("scenario-001")
3376        .with_original_document_hash("abc123");
3377
3378        assert_eq!(anomaly.run_id, Some("run-123".to_string()));
3379        assert_eq!(anomaly.generation_seed, Some(42));
3380        assert!(anomaly.causal_reason.is_some());
3381        assert!(anomaly.structured_strategy.is_some());
3382        assert_eq!(anomaly.scenario_id, Some("scenario-001".to_string()));
3383        assert_eq!(anomaly.original_document_hash, Some("abc123".to_string()));
3384
3385        // Check that legacy injection_strategy is also set
3386        assert_eq!(anomaly.injection_strategy, Some("SelfApproval".to_string()));
3387    }
3388
3389    #[test]
3390    fn test_labeled_anomaly_derivation_chain() {
3391        let parent = LabeledAnomaly::new(
3392            "ANO001".to_string(),
3393            AnomalyType::Fraud(FraudType::DuplicatePayment),
3394            "JE001".to_string(),
3395            "JE".to_string(),
3396            "1000".to_string(),
3397            NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
3398        );
3399
3400        let child = LabeledAnomaly::new(
3401            "ANO002".to_string(),
3402            AnomalyType::Error(ErrorType::DuplicateEntry),
3403            "JE002".to_string(),
3404            "JE".to_string(),
3405            "1000".to_string(),
3406            NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
3407        )
3408        .with_parent_anomaly(&parent.anomaly_id);
3409
3410        assert_eq!(child.parent_anomaly_id, Some("ANO001".to_string()));
3411    }
3412
3413    #[test]
3414    fn test_injection_strategy_description() {
3415        let strategy = InjectionStrategy::AmountManipulation {
3416            original: dec!(1000),
3417            factor: 2.5,
3418        };
3419        assert_eq!(strategy.description(), "Amount multiplied by 2.50");
3420        assert_eq!(strategy.strategy_type(), "AmountManipulation");
3421
3422        let strategy = InjectionStrategy::ThresholdAvoidance {
3423            threshold: dec!(10000),
3424            adjusted_amount: dec!(9999),
3425        };
3426        assert_eq!(
3427            strategy.description(),
3428            "Amount adjusted to avoid 10000 threshold"
3429        );
3430
3431        let strategy = InjectionStrategy::DateShift {
3432            days_shifted: -5,
3433            original_date: NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
3434        };
3435        assert_eq!(strategy.description(), "Date backdated by 5 days");
3436
3437        let strategy = InjectionStrategy::DateShift {
3438            days_shifted: 3,
3439            original_date: NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
3440        };
3441        assert_eq!(strategy.description(), "Date forward-dated by 3 days");
3442    }
3443
3444    #[test]
3445    fn test_causal_reason_variants() {
3446        let reason = AnomalyCausalReason::RandomRate { base_rate: 0.02 };
3447        if let AnomalyCausalReason::RandomRate { base_rate } = reason {
3448            assert!((base_rate - 0.02).abs() < 0.001);
3449        }
3450
3451        let reason = AnomalyCausalReason::TemporalPattern {
3452            pattern_name: "year_end_spike".to_string(),
3453        };
3454        if let AnomalyCausalReason::TemporalPattern { pattern_name } = reason {
3455            assert_eq!(pattern_name, "year_end_spike");
3456        }
3457
3458        let reason = AnomalyCausalReason::ScenarioStep {
3459            scenario_type: "kickback".to_string(),
3460            step_number: 3,
3461        };
3462        if let AnomalyCausalReason::ScenarioStep {
3463            scenario_type,
3464            step_number,
3465        } = reason
3466        {
3467            assert_eq!(scenario_type, "kickback");
3468            assert_eq!(step_number, 3);
3469        }
3470    }
3471
3472    #[test]
3473    fn test_feature_vector_length() {
3474        let anomaly = LabeledAnomaly::new(
3475            "ANO001".to_string(),
3476            AnomalyType::Fraud(FraudType::SelfApproval),
3477            "JE001".to_string(),
3478            "JE".to_string(),
3479            "1000".to_string(),
3480            NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
3481        );
3482
3483        let features = anomaly.to_features();
3484        assert_eq!(features.len(), LabeledAnomaly::feature_count());
3485        assert_eq!(features.len(), LabeledAnomaly::feature_names().len());
3486    }
3487
3488    #[test]
3489    fn test_feature_vector_with_provenance() {
3490        let anomaly = LabeledAnomaly::new(
3491            "ANO001".to_string(),
3492            AnomalyType::Fraud(FraudType::SelfApproval),
3493            "JE001".to_string(),
3494            "JE".to_string(),
3495            "1000".to_string(),
3496            NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
3497        )
3498        .with_scenario("scenario-001")
3499        .with_parent_anomaly("ANO000");
3500
3501        let features = anomaly.to_features();
3502
3503        // Last two features should be 1.0 (has scenario, has parent)
3504        assert_eq!(features[features.len() - 2], 1.0); // is_scenario_part
3505        assert_eq!(features[features.len() - 1], 1.0); // is_derived
3506    }
3507
3508    #[test]
3509    fn test_anomaly_summary() {
3510        let anomalies = vec![
3511            LabeledAnomaly::new(
3512                "ANO001".to_string(),
3513                AnomalyType::Fraud(FraudType::SelfApproval),
3514                "JE001".to_string(),
3515                "JE".to_string(),
3516                "1000".to_string(),
3517                NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
3518            ),
3519            LabeledAnomaly::new(
3520                "ANO002".to_string(),
3521                AnomalyType::Error(ErrorType::DuplicateEntry),
3522                "JE002".to_string(),
3523                "JE".to_string(),
3524                "1000".to_string(),
3525                NaiveDate::from_ymd_opt(2024, 1, 16).unwrap(),
3526            ),
3527        ];
3528
3529        let summary = AnomalySummary::from_anomalies(&anomalies);
3530
3531        assert_eq!(summary.total_count, 2);
3532        assert_eq!(summary.by_category.get("Fraud"), Some(&1));
3533        assert_eq!(summary.by_category.get("Error"), Some(&1));
3534    }
3535
3536    #[test]
3537    fn test_rate_config_validation() {
3538        let config = AnomalyRateConfig::default();
3539        assert!(config.validate().is_ok());
3540
3541        let bad_config = AnomalyRateConfig {
3542            fraud_rate: 0.5,
3543            error_rate: 0.5,
3544            process_issue_rate: 0.5, // Sum > 1.0
3545            ..Default::default()
3546        };
3547        assert!(bad_config.validate().is_err());
3548    }
3549
3550    #[test]
3551    fn test_injection_strategy_serialization() {
3552        let strategy = InjectionStrategy::SoDViolation {
3553            duty1: "CreatePO".to_string(),
3554            duty2: "ApprovePO".to_string(),
3555            violating_user: "USER001".to_string(),
3556        };
3557
3558        let json = serde_json::to_string(&strategy).unwrap();
3559        let deserialized: InjectionStrategy = serde_json::from_str(&json).unwrap();
3560
3561        assert_eq!(strategy, deserialized);
3562    }
3563
3564    #[test]
3565    fn test_labeled_anomaly_serialization_with_provenance() {
3566        let anomaly = LabeledAnomaly::new(
3567            "ANO001".to_string(),
3568            AnomalyType::Fraud(FraudType::SelfApproval),
3569            "JE001".to_string(),
3570            "JE".to_string(),
3571            "1000".to_string(),
3572            NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
3573        )
3574        .with_run_id("run-123")
3575        .with_generation_seed(42)
3576        .with_causal_reason(AnomalyCausalReason::RandomRate { base_rate: 0.02 });
3577
3578        let json = serde_json::to_string(&anomaly).unwrap();
3579        let deserialized: LabeledAnomaly = serde_json::from_str(&json).unwrap();
3580
3581        assert_eq!(anomaly.run_id, deserialized.run_id);
3582        assert_eq!(anomaly.generation_seed, deserialized.generation_seed);
3583    }
3584
3585    // ========================================
3586    // FR-003 ENHANCED TAXONOMY TESTS
3587    // ========================================
3588
3589    #[test]
3590    fn test_anomaly_category_from_anomaly_type() {
3591        // Fraud mappings
3592        let fraud_vendor = AnomalyType::Fraud(FraudType::FictitiousVendor);
3593        assert_eq!(
3594            AnomalyCategory::from_anomaly_type(&fraud_vendor),
3595            AnomalyCategory::FictitiousVendor
3596        );
3597
3598        let fraud_kickback = AnomalyType::Fraud(FraudType::KickbackScheme);
3599        assert_eq!(
3600            AnomalyCategory::from_anomaly_type(&fraud_kickback),
3601            AnomalyCategory::VendorKickback
3602        );
3603
3604        let fraud_structured = AnomalyType::Fraud(FraudType::SplitTransaction);
3605        assert_eq!(
3606            AnomalyCategory::from_anomaly_type(&fraud_structured),
3607            AnomalyCategory::StructuredTransaction
3608        );
3609
3610        // Error mappings
3611        let error_duplicate = AnomalyType::Error(ErrorType::DuplicateEntry);
3612        assert_eq!(
3613            AnomalyCategory::from_anomaly_type(&error_duplicate),
3614            AnomalyCategory::DuplicatePayment
3615        );
3616
3617        // Process issue mappings
3618        let process_skip = AnomalyType::ProcessIssue(ProcessIssueType::SkippedApproval);
3619        assert_eq!(
3620            AnomalyCategory::from_anomaly_type(&process_skip),
3621            AnomalyCategory::MissingApproval
3622        );
3623
3624        // Relational mappings
3625        let relational_circular =
3626            AnomalyType::Relational(RelationalAnomalyType::CircularTransaction);
3627        assert_eq!(
3628            AnomalyCategory::from_anomaly_type(&relational_circular),
3629            AnomalyCategory::CircularFlow
3630        );
3631    }
3632
3633    #[test]
3634    fn test_anomaly_category_ordinal() {
3635        assert_eq!(AnomalyCategory::FictitiousVendor.ordinal(), 0);
3636        assert_eq!(AnomalyCategory::VendorKickback.ordinal(), 1);
3637        assert_eq!(AnomalyCategory::Custom("test".to_string()).ordinal(), 14);
3638    }
3639
3640    #[test]
3641    fn test_contributing_factor() {
3642        let factor = ContributingFactor::new(
3643            FactorType::AmountDeviation,
3644            15000.0,
3645            10000.0,
3646            true,
3647            0.5,
3648            "Amount exceeds threshold",
3649        );
3650
3651        assert_eq!(factor.factor_type, FactorType::AmountDeviation);
3652        assert_eq!(factor.value, 15000.0);
3653        assert_eq!(factor.threshold, 10000.0);
3654        assert!(factor.direction_greater);
3655
3656        // Contribution: (15000 - 10000) / 10000 * 0.5 = 0.25
3657        let contribution = factor.contribution();
3658        assert!((contribution - 0.25).abs() < 0.01);
3659    }
3660
3661    #[test]
3662    fn test_contributing_factor_with_evidence() {
3663        let mut data = HashMap::new();
3664        data.insert("expected".to_string(), "10000".to_string());
3665        data.insert("actual".to_string(), "15000".to_string());
3666
3667        let factor = ContributingFactor::new(
3668            FactorType::AmountDeviation,
3669            15000.0,
3670            10000.0,
3671            true,
3672            0.5,
3673            "Amount deviation detected",
3674        )
3675        .with_evidence("transaction_history", data);
3676
3677        assert!(factor.evidence.is_some());
3678        let evidence = factor.evidence.unwrap();
3679        assert_eq!(evidence.source, "transaction_history");
3680        assert_eq!(evidence.data.get("expected"), Some(&"10000".to_string()));
3681    }
3682
3683    #[test]
3684    fn test_enhanced_anomaly_label() {
3685        let base = LabeledAnomaly::new(
3686            "ANO001".to_string(),
3687            AnomalyType::Fraud(FraudType::DuplicatePayment),
3688            "JE001".to_string(),
3689            "JE".to_string(),
3690            "1000".to_string(),
3691            NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
3692        );
3693
3694        let enhanced = EnhancedAnomalyLabel::from_base(base)
3695            .with_confidence(0.85)
3696            .with_severity(0.7)
3697            .with_factor(ContributingFactor::new(
3698                FactorType::DuplicateIndicator,
3699                1.0,
3700                0.5,
3701                true,
3702                0.4,
3703                "Duplicate payment detected",
3704            ))
3705            .with_secondary_category(AnomalyCategory::StructuredTransaction);
3706
3707        assert_eq!(enhanced.category, AnomalyCategory::DuplicatePayment);
3708        assert_eq!(enhanced.enhanced_confidence, 0.85);
3709        assert_eq!(enhanced.enhanced_severity, 0.7);
3710        assert_eq!(enhanced.contributing_factors.len(), 1);
3711        assert_eq!(enhanced.secondary_categories.len(), 1);
3712    }
3713
3714    #[test]
3715    fn test_enhanced_anomaly_label_features() {
3716        let base = LabeledAnomaly::new(
3717            "ANO001".to_string(),
3718            AnomalyType::Fraud(FraudType::SelfApproval),
3719            "JE001".to_string(),
3720            "JE".to_string(),
3721            "1000".to_string(),
3722            NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
3723        );
3724
3725        let enhanced = EnhancedAnomalyLabel::from_base(base)
3726            .with_confidence(0.9)
3727            .with_severity(0.8)
3728            .with_factor(ContributingFactor::new(
3729                FactorType::ControlBypass,
3730                1.0,
3731                0.0,
3732                true,
3733                0.5,
3734                "Control bypass detected",
3735            ));
3736
3737        let features = enhanced.to_features();
3738
3739        // Should have 25 features (15 base + 10 enhanced)
3740        assert_eq!(features.len(), EnhancedAnomalyLabel::feature_count());
3741        assert_eq!(features.len(), 25);
3742
3743        // Check enhanced confidence is in features
3744        assert_eq!(features[15], 0.9); // enhanced_confidence
3745
3746        // Check has_control_bypass flag
3747        assert_eq!(features[21], 1.0); // has_control_bypass
3748    }
3749
3750    #[test]
3751    fn test_enhanced_anomaly_label_feature_names() {
3752        let names = EnhancedAnomalyLabel::feature_names();
3753        assert_eq!(names.len(), 25);
3754        assert!(names.contains(&"enhanced_confidence"));
3755        assert!(names.contains(&"enhanced_severity"));
3756        assert!(names.contains(&"has_control_bypass"));
3757    }
3758
3759    #[test]
3760    fn test_factor_type_names() {
3761        assert_eq!(FactorType::AmountDeviation.name(), "amount_deviation");
3762        assert_eq!(FactorType::ThresholdProximity.name(), "threshold_proximity");
3763        assert_eq!(FactorType::ControlBypass.name(), "control_bypass");
3764    }
3765
3766    #[test]
3767    fn test_anomaly_category_serialization() {
3768        let category = AnomalyCategory::CircularFlow;
3769        let json = serde_json::to_string(&category).unwrap();
3770        let deserialized: AnomalyCategory = serde_json::from_str(&json).unwrap();
3771        assert_eq!(category, deserialized);
3772
3773        let custom = AnomalyCategory::Custom("custom_type".to_string());
3774        let json = serde_json::to_string(&custom).unwrap();
3775        let deserialized: AnomalyCategory = serde_json::from_str(&json).unwrap();
3776        assert_eq!(custom, deserialized);
3777    }
3778
3779    #[test]
3780    fn test_enhanced_label_secondary_category_dedup() {
3781        let base = LabeledAnomaly::new(
3782            "ANO001".to_string(),
3783            AnomalyType::Fraud(FraudType::DuplicatePayment),
3784            "JE001".to_string(),
3785            "JE".to_string(),
3786            "1000".to_string(),
3787            NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
3788        );
3789
3790        let enhanced = EnhancedAnomalyLabel::from_base(base)
3791            // Try to add the primary category as secondary (should be ignored)
3792            .with_secondary_category(AnomalyCategory::DuplicatePayment)
3793            // Add a valid secondary
3794            .with_secondary_category(AnomalyCategory::TimingAnomaly)
3795            // Try to add duplicate secondary (should be ignored)
3796            .with_secondary_category(AnomalyCategory::TimingAnomaly);
3797
3798        // Should only have 1 secondary category (TimingAnomaly)
3799        assert_eq!(enhanced.secondary_categories.len(), 1);
3800        assert_eq!(
3801            enhanced.secondary_categories[0],
3802            AnomalyCategory::TimingAnomaly
3803        );
3804    }
3805
3806    // ==========================================================================
3807    // Accounting Standards Fraud Type Tests
3808    // ==========================================================================
3809
3810    #[test]
3811    fn test_revenue_recognition_fraud_types() {
3812        // Test ASC 606/IFRS 15 related fraud types
3813        let fraud_types = [
3814            FraudType::ImproperRevenueRecognition,
3815            FraudType::ImproperPoAllocation,
3816            FraudType::VariableConsiderationManipulation,
3817            FraudType::ContractModificationMisstatement,
3818        ];
3819
3820        for fraud_type in fraud_types {
3821            let anomaly_type = AnomalyType::Fraud(fraud_type);
3822            assert_eq!(anomaly_type.category(), "Fraud");
3823            assert!(anomaly_type.is_intentional());
3824            assert!(anomaly_type.severity() >= 3);
3825        }
3826    }
3827
3828    #[test]
3829    fn test_lease_accounting_fraud_types() {
3830        // Test ASC 842/IFRS 16 related fraud types
3831        let fraud_types = [
3832            FraudType::LeaseClassificationManipulation,
3833            FraudType::OffBalanceSheetLease,
3834            FraudType::LeaseLiabilityUnderstatement,
3835            FraudType::RouAssetMisstatement,
3836        ];
3837
3838        for fraud_type in fraud_types {
3839            let anomaly_type = AnomalyType::Fraud(fraud_type);
3840            assert_eq!(anomaly_type.category(), "Fraud");
3841            assert!(anomaly_type.is_intentional());
3842            assert!(anomaly_type.severity() >= 3);
3843        }
3844
3845        // Off-balance sheet lease fraud should be high severity
3846        assert_eq!(FraudType::OffBalanceSheetLease.severity(), 5);
3847    }
3848
3849    #[test]
3850    fn test_fair_value_fraud_types() {
3851        // Test ASC 820/IFRS 13 related fraud types
3852        let fraud_types = [
3853            FraudType::FairValueHierarchyManipulation,
3854            FraudType::Level3InputManipulation,
3855            FraudType::ValuationTechniqueManipulation,
3856        ];
3857
3858        for fraud_type in fraud_types {
3859            let anomaly_type = AnomalyType::Fraud(fraud_type);
3860            assert_eq!(anomaly_type.category(), "Fraud");
3861            assert!(anomaly_type.is_intentional());
3862            assert!(anomaly_type.severity() >= 4);
3863        }
3864
3865        // Level 3 manipulation is highest severity (unobservable inputs)
3866        assert_eq!(FraudType::Level3InputManipulation.severity(), 5);
3867    }
3868
3869    #[test]
3870    fn test_impairment_fraud_types() {
3871        // Test ASC 360/IAS 36 related fraud types
3872        let fraud_types = [
3873            FraudType::DelayedImpairment,
3874            FraudType::ImpairmentTestAvoidance,
3875            FraudType::CashFlowProjectionManipulation,
3876            FraudType::ImproperImpairmentReversal,
3877        ];
3878
3879        for fraud_type in fraud_types {
3880            let anomaly_type = AnomalyType::Fraud(fraud_type);
3881            assert_eq!(anomaly_type.category(), "Fraud");
3882            assert!(anomaly_type.is_intentional());
3883            assert!(anomaly_type.severity() >= 3);
3884        }
3885
3886        // Cash flow manipulation has highest severity
3887        assert_eq!(FraudType::CashFlowProjectionManipulation.severity(), 5);
3888    }
3889
3890    // ==========================================================================
3891    // Accounting Standards Error Type Tests
3892    // ==========================================================================
3893
3894    #[test]
3895    fn test_standards_error_types() {
3896        // Test non-fraudulent accounting standards errors
3897        let error_types = [
3898            ErrorType::RevenueTimingError,
3899            ErrorType::PoAllocationError,
3900            ErrorType::LeaseClassificationError,
3901            ErrorType::LeaseCalculationError,
3902            ErrorType::FairValueError,
3903            ErrorType::ImpairmentCalculationError,
3904            ErrorType::DiscountRateError,
3905            ErrorType::FrameworkApplicationError,
3906        ];
3907
3908        for error_type in error_types {
3909            let anomaly_type = AnomalyType::Error(error_type);
3910            assert_eq!(anomaly_type.category(), "Error");
3911            assert!(!anomaly_type.is_intentional());
3912            assert!(anomaly_type.severity() >= 3);
3913        }
3914    }
3915
3916    #[test]
3917    fn test_framework_application_error() {
3918        // Test IFRS vs GAAP confusion errors
3919        let error_type = ErrorType::FrameworkApplicationError;
3920        assert_eq!(error_type.severity(), 4);
3921
3922        let anomaly = LabeledAnomaly::new(
3923            "ERR001".to_string(),
3924            AnomalyType::Error(error_type),
3925            "JE100".to_string(),
3926            "JE".to_string(),
3927            "1000".to_string(),
3928            NaiveDate::from_ymd_opt(2024, 6, 30).unwrap(),
3929        )
3930        .with_description("LIFO inventory method used under IFRS (not permitted)")
3931        .with_metadata("framework", "IFRS")
3932        .with_metadata("standard_violated", "IAS 2");
3933
3934        assert_eq!(anomaly.anomaly_type.category(), "Error");
3935        assert_eq!(
3936            anomaly.metadata.get("standard_violated"),
3937            Some(&"IAS 2".to_string())
3938        );
3939    }
3940
3941    #[test]
3942    fn test_standards_anomaly_serialization() {
3943        // Test that new fraud types serialize/deserialize correctly
3944        let fraud_types = [
3945            FraudType::ImproperRevenueRecognition,
3946            FraudType::LeaseClassificationManipulation,
3947            FraudType::FairValueHierarchyManipulation,
3948            FraudType::DelayedImpairment,
3949        ];
3950
3951        for fraud_type in fraud_types {
3952            let json = serde_json::to_string(&fraud_type).expect("Failed to serialize");
3953            let deserialized: FraudType =
3954                serde_json::from_str(&json).expect("Failed to deserialize");
3955            assert_eq!(fraud_type, deserialized);
3956        }
3957
3958        // Test error types
3959        let error_types = [
3960            ErrorType::RevenueTimingError,
3961            ErrorType::LeaseCalculationError,
3962            ErrorType::FairValueError,
3963            ErrorType::FrameworkApplicationError,
3964        ];
3965
3966        for error_type in error_types {
3967            let json = serde_json::to_string(&error_type).expect("Failed to serialize");
3968            let deserialized: ErrorType =
3969                serde_json::from_str(&json).expect("Failed to deserialize");
3970            assert_eq!(error_type, deserialized);
3971        }
3972    }
3973
3974    #[test]
3975    fn test_standards_labeled_anomaly() {
3976        // Test creating a labeled anomaly for a standards violation
3977        let anomaly = LabeledAnomaly::new(
3978            "STD001".to_string(),
3979            AnomalyType::Fraud(FraudType::ImproperRevenueRecognition),
3980            "CONTRACT-2024-001".to_string(),
3981            "Revenue".to_string(),
3982            "1000".to_string(),
3983            NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
3984        )
3985        .with_description("Revenue recognized before performance obligation satisfied (ASC 606)")
3986        .with_monetary_impact(dec!(500000))
3987        .with_metadata("standard", "ASC 606")
3988        .with_metadata("paragraph", "606-10-25-1")
3989        .with_metadata("contract_id", "C-2024-001")
3990        .with_related_entity("CONTRACT-2024-001")
3991        .with_related_entity("CUSTOMER-500");
3992
3993        assert_eq!(anomaly.severity, 5); // ImproperRevenueRecognition has severity 5
3994        assert!(anomaly.is_injected);
3995        assert_eq!(anomaly.monetary_impact, Some(dec!(500000)));
3996        assert_eq!(anomaly.related_entities.len(), 2);
3997        assert_eq!(
3998            anomaly.metadata.get("standard"),
3999            Some(&"ASC 606".to_string())
4000        );
4001    }
4002
4003    // ==========================================================================
4004    // Multi-Dimensional Labeling Tests
4005    // ==========================================================================
4006
4007    #[test]
4008    fn test_severity_level() {
4009        assert_eq!(SeverityLevel::Low.numeric(), 1);
4010        assert_eq!(SeverityLevel::Critical.numeric(), 4);
4011
4012        assert_eq!(SeverityLevel::from_numeric(1), SeverityLevel::Low);
4013        assert_eq!(SeverityLevel::from_numeric(4), SeverityLevel::Critical);
4014
4015        assert_eq!(SeverityLevel::from_score(0.1), SeverityLevel::Low);
4016        assert_eq!(SeverityLevel::from_score(0.9), SeverityLevel::Critical);
4017
4018        assert!((SeverityLevel::Medium.to_score() - 0.375).abs() < 0.01);
4019    }
4020
4021    #[test]
4022    fn test_anomaly_severity() {
4023        let severity =
4024            AnomalySeverity::new(SeverityLevel::High, dec!(50000)).with_materiality(dec!(10000));
4025
4026        assert_eq!(severity.level, SeverityLevel::High);
4027        assert!(severity.is_material);
4028        assert_eq!(severity.materiality_threshold, Some(dec!(10000)));
4029
4030        // Not material
4031        let low_severity =
4032            AnomalySeverity::new(SeverityLevel::Low, dec!(5000)).with_materiality(dec!(10000));
4033        assert!(!low_severity.is_material);
4034    }
4035
4036    #[test]
4037    fn test_detection_difficulty() {
4038        assert!(
4039            (AnomalyDetectionDifficulty::Trivial.expected_detection_rate() - 0.99).abs() < 0.01
4040        );
4041        assert!((AnomalyDetectionDifficulty::Expert.expected_detection_rate() - 0.15).abs() < 0.01);
4042
4043        assert_eq!(
4044            AnomalyDetectionDifficulty::from_score(0.05),
4045            AnomalyDetectionDifficulty::Trivial
4046        );
4047        assert_eq!(
4048            AnomalyDetectionDifficulty::from_score(0.90),
4049            AnomalyDetectionDifficulty::Expert
4050        );
4051
4052        assert_eq!(AnomalyDetectionDifficulty::Moderate.name(), "moderate");
4053    }
4054
4055    #[test]
4056    fn test_ground_truth_certainty() {
4057        assert_eq!(GroundTruthCertainty::Definite.certainty_score(), 1.0);
4058        assert_eq!(GroundTruthCertainty::Probable.certainty_score(), 0.8);
4059        assert_eq!(GroundTruthCertainty::Possible.certainty_score(), 0.5);
4060    }
4061
4062    #[test]
4063    fn test_detection_method() {
4064        assert_eq!(DetectionMethod::RuleBased.name(), "rule_based");
4065        assert_eq!(DetectionMethod::MachineLearning.name(), "machine_learning");
4066    }
4067
4068    #[test]
4069    fn test_extended_anomaly_label() {
4070        let base = LabeledAnomaly::new(
4071            "ANO001".to_string(),
4072            AnomalyType::Fraud(FraudType::FictitiousVendor),
4073            "JE001".to_string(),
4074            "JE".to_string(),
4075            "1000".to_string(),
4076            NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(),
4077        )
4078        .with_monetary_impact(dec!(100000));
4079
4080        let extended = ExtendedAnomalyLabel::from_base(base)
4081            .with_severity(AnomalySeverity::new(SeverityLevel::Critical, dec!(100000)))
4082            .with_difficulty(AnomalyDetectionDifficulty::Hard)
4083            .with_method(DetectionMethod::GraphBased)
4084            .with_method(DetectionMethod::ForensicAudit)
4085            .with_indicator("New vendor with no history")
4086            .with_indicator("Large first transaction")
4087            .with_certainty(GroundTruthCertainty::Definite)
4088            .with_entity("V001")
4089            .with_secondary_category(AnomalyCategory::BehavioralAnomaly)
4090            .with_scheme("SCHEME001", 2);
4091
4092        assert_eq!(extended.severity.level, SeverityLevel::Critical);
4093        assert_eq!(
4094            extended.detection_difficulty,
4095            AnomalyDetectionDifficulty::Hard
4096        );
4097        // from_base adds RuleBased, then we add 2 more (GraphBased, ForensicAudit)
4098        assert_eq!(extended.recommended_methods.len(), 3);
4099        assert_eq!(extended.key_indicators.len(), 2);
4100        assert_eq!(extended.scheme_id, Some("SCHEME001".to_string()));
4101        assert_eq!(extended.scheme_stage, Some(2));
4102    }
4103
4104    #[test]
4105    fn test_extended_anomaly_label_features() {
4106        let base = LabeledAnomaly::new(
4107            "ANO001".to_string(),
4108            AnomalyType::Fraud(FraudType::SelfApproval),
4109            "JE001".to_string(),
4110            "JE".to_string(),
4111            "1000".to_string(),
4112            NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(),
4113        );
4114
4115        let extended =
4116            ExtendedAnomalyLabel::from_base(base).with_difficulty(AnomalyDetectionDifficulty::Hard);
4117
4118        let features = extended.to_features();
4119        assert_eq!(features.len(), ExtendedAnomalyLabel::feature_count());
4120        assert_eq!(features.len(), 30);
4121
4122        // Check difficulty score is in features
4123        let difficulty_idx = 18; // Position of difficulty_score
4124        assert!((features[difficulty_idx] - 0.75).abs() < 0.01);
4125    }
4126
4127    #[test]
4128    fn test_extended_label_near_miss() {
4129        let base = LabeledAnomaly::new(
4130            "ANO001".to_string(),
4131            AnomalyType::Statistical(StatisticalAnomalyType::UnusuallyHighAmount),
4132            "JE001".to_string(),
4133            "JE".to_string(),
4134            "1000".to_string(),
4135            NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(),
4136        );
4137
4138        let extended = ExtendedAnomalyLabel::from_base(base)
4139            .as_near_miss("Year-end bonus payment, legitimately high");
4140
4141        assert!(extended.is_near_miss);
4142        assert!(extended.near_miss_explanation.is_some());
4143    }
4144
4145    #[test]
4146    fn test_scheme_type() {
4147        assert_eq!(
4148            SchemeType::GradualEmbezzlement.name(),
4149            "gradual_embezzlement"
4150        );
4151        assert_eq!(SchemeType::GradualEmbezzlement.typical_stages(), 4);
4152        assert_eq!(SchemeType::VendorKickback.typical_stages(), 4);
4153    }
4154
4155    #[test]
4156    fn test_concealment_technique() {
4157        assert!(ConcealmentTechnique::Collusion.difficulty_bonus() > 0.0);
4158        assert!(
4159            ConcealmentTechnique::Collusion.difficulty_bonus()
4160                > ConcealmentTechnique::TimingExploitation.difficulty_bonus()
4161        );
4162    }
4163
4164    #[test]
4165    fn test_near_miss_label() {
4166        let near_miss = NearMissLabel::new(
4167            "JE001",
4168            NearMissPattern::ThresholdProximity {
4169                threshold: dec!(10000),
4170                proximity: 0.95,
4171            },
4172            0.7,
4173            FalsePositiveTrigger::AmountNearThreshold,
4174            "Transaction is 95% of threshold but business justified",
4175        );
4176
4177        assert_eq!(near_miss.document_id, "JE001");
4178        assert_eq!(near_miss.suspicion_score, 0.7);
4179        assert_eq!(
4180            near_miss.false_positive_trigger,
4181            FalsePositiveTrigger::AmountNearThreshold
4182        );
4183    }
4184
4185    #[test]
4186    fn test_legitimate_pattern_type() {
4187        assert_eq!(
4188            LegitimatePatternType::YearEndBonus.description(),
4189            "Year-end bonus payment"
4190        );
4191        assert_eq!(
4192            LegitimatePatternType::InsuranceClaim.description(),
4193            "Insurance claim reimbursement"
4194        );
4195    }
4196
4197    #[test]
4198    fn test_severity_detection_difficulty_serialization() {
4199        let severity = AnomalySeverity::new(SeverityLevel::High, dec!(50000));
4200        let json = serde_json::to_string(&severity).expect("Failed to serialize");
4201        let deserialized: AnomalySeverity =
4202            serde_json::from_str(&json).expect("Failed to deserialize");
4203        assert_eq!(severity.level, deserialized.level);
4204
4205        let difficulty = AnomalyDetectionDifficulty::Hard;
4206        let json = serde_json::to_string(&difficulty).expect("Failed to serialize");
4207        let deserialized: AnomalyDetectionDifficulty =
4208            serde_json::from_str(&json).expect("Failed to deserialize");
4209        assert_eq!(difficulty, deserialized);
4210    }
4211
4212    // ========================================
4213    // ACFE Taxonomy Tests
4214    // ========================================
4215
4216    #[test]
4217    fn test_acfe_fraud_category() {
4218        let asset = AcfeFraudCategory::AssetMisappropriation;
4219        assert_eq!(asset.name(), "asset_misappropriation");
4220        assert!((asset.typical_occurrence_rate() - 0.86).abs() < 0.01);
4221        assert_eq!(asset.typical_median_loss(), Decimal::new(100_000, 0));
4222        assert_eq!(asset.typical_detection_months(), 12);
4223
4224        let corruption = AcfeFraudCategory::Corruption;
4225        assert_eq!(corruption.name(), "corruption");
4226        assert!((corruption.typical_occurrence_rate() - 0.33).abs() < 0.01);
4227
4228        let fs_fraud = AcfeFraudCategory::FinancialStatementFraud;
4229        assert_eq!(fs_fraud.typical_median_loss(), Decimal::new(954_000, 0));
4230        assert_eq!(fs_fraud.typical_detection_months(), 24);
4231    }
4232
4233    #[test]
4234    fn test_cash_fraud_scheme() {
4235        let shell = CashFraudScheme::ShellCompany;
4236        assert_eq!(shell.category(), AcfeFraudCategory::AssetMisappropriation);
4237        assert_eq!(shell.subcategory(), "billing_schemes");
4238        assert_eq!(shell.severity(), 5);
4239        assert_eq!(
4240            shell.detection_difficulty(),
4241            AnomalyDetectionDifficulty::Hard
4242        );
4243
4244        let ghost = CashFraudScheme::GhostEmployee;
4245        assert_eq!(ghost.subcategory(), "payroll_schemes");
4246        assert_eq!(ghost.severity(), 5);
4247
4248        // Test all variants exist
4249        assert_eq!(CashFraudScheme::all_variants().len(), 20);
4250    }
4251
4252    #[test]
4253    fn test_asset_fraud_scheme() {
4254        let ip_theft = AssetFraudScheme::IntellectualPropertyTheft;
4255        assert_eq!(
4256            ip_theft.category(),
4257            AcfeFraudCategory::AssetMisappropriation
4258        );
4259        assert_eq!(ip_theft.subcategory(), "other_assets");
4260        assert_eq!(ip_theft.severity(), 5);
4261
4262        let inv_theft = AssetFraudScheme::InventoryTheft;
4263        assert_eq!(inv_theft.subcategory(), "inventory");
4264        assert_eq!(inv_theft.severity(), 4);
4265    }
4266
4267    #[test]
4268    fn test_corruption_scheme() {
4269        let kickback = CorruptionScheme::InvoiceKickback;
4270        assert_eq!(kickback.category(), AcfeFraudCategory::Corruption);
4271        assert_eq!(kickback.subcategory(), "bribery");
4272        assert_eq!(kickback.severity(), 5);
4273        assert_eq!(
4274            kickback.detection_difficulty(),
4275            AnomalyDetectionDifficulty::Expert
4276        );
4277
4278        let bid_rigging = CorruptionScheme::BidRigging;
4279        assert_eq!(bid_rigging.subcategory(), "bribery");
4280        assert_eq!(
4281            bid_rigging.detection_difficulty(),
4282            AnomalyDetectionDifficulty::Hard
4283        );
4284
4285        let purchasing = CorruptionScheme::PurchasingConflict;
4286        assert_eq!(purchasing.subcategory(), "conflicts_of_interest");
4287
4288        // Test all variants exist
4289        assert_eq!(CorruptionScheme::all_variants().len(), 10);
4290    }
4291
4292    #[test]
4293    fn test_financial_statement_scheme() {
4294        let fictitious = FinancialStatementScheme::FictitiousRevenues;
4295        assert_eq!(
4296            fictitious.category(),
4297            AcfeFraudCategory::FinancialStatementFraud
4298        );
4299        assert_eq!(fictitious.subcategory(), "overstatement");
4300        assert_eq!(fictitious.severity(), 5);
4301        assert_eq!(
4302            fictitious.detection_difficulty(),
4303            AnomalyDetectionDifficulty::Expert
4304        );
4305
4306        let understated = FinancialStatementScheme::UnderstatedRevenues;
4307        assert_eq!(understated.subcategory(), "understatement");
4308
4309        // Test all variants exist
4310        assert_eq!(FinancialStatementScheme::all_variants().len(), 13);
4311    }
4312
4313    #[test]
4314    fn test_acfe_scheme_unified() {
4315        let cash_scheme = AcfeScheme::Cash(CashFraudScheme::ShellCompany);
4316        assert_eq!(
4317            cash_scheme.category(),
4318            AcfeFraudCategory::AssetMisappropriation
4319        );
4320        assert_eq!(cash_scheme.severity(), 5);
4321
4322        let corruption_scheme = AcfeScheme::Corruption(CorruptionScheme::BidRigging);
4323        assert_eq!(corruption_scheme.category(), AcfeFraudCategory::Corruption);
4324
4325        let fs_scheme = AcfeScheme::FinancialStatement(FinancialStatementScheme::PrematureRevenue);
4326        assert_eq!(
4327            fs_scheme.category(),
4328            AcfeFraudCategory::FinancialStatementFraud
4329        );
4330    }
4331
4332    #[test]
4333    fn test_acfe_detection_method() {
4334        let tip = AcfeDetectionMethod::Tip;
4335        assert!((tip.typical_detection_rate() - 0.42).abs() < 0.01);
4336
4337        let internal_audit = AcfeDetectionMethod::InternalAudit;
4338        assert!((internal_audit.typical_detection_rate() - 0.16).abs() < 0.01);
4339
4340        let external_audit = AcfeDetectionMethod::ExternalAudit;
4341        assert!((external_audit.typical_detection_rate() - 0.04).abs() < 0.01);
4342
4343        // Test all variants exist
4344        assert_eq!(AcfeDetectionMethod::all_variants().len(), 12);
4345    }
4346
4347    #[test]
4348    fn test_perpetrator_department() {
4349        let accounting = PerpetratorDepartment::Accounting;
4350        assert!((accounting.typical_occurrence_rate() - 0.21).abs() < 0.01);
4351        assert_eq!(accounting.typical_median_loss(), Decimal::new(130_000, 0));
4352
4353        let executive = PerpetratorDepartment::Executive;
4354        assert_eq!(executive.typical_median_loss(), Decimal::new(600_000, 0));
4355    }
4356
4357    #[test]
4358    fn test_perpetrator_level() {
4359        let employee = PerpetratorLevel::Employee;
4360        assert!((employee.typical_occurrence_rate() - 0.42).abs() < 0.01);
4361        assert_eq!(employee.typical_median_loss(), Decimal::new(50_000, 0));
4362
4363        let exec = PerpetratorLevel::OwnerExecutive;
4364        assert_eq!(exec.typical_median_loss(), Decimal::new(337_000, 0));
4365    }
4366
4367    #[test]
4368    fn test_acfe_calibration() {
4369        let cal = AcfeCalibration::default();
4370        assert_eq!(cal.median_loss, Decimal::new(117_000, 0));
4371        assert_eq!(cal.median_duration_months, 12);
4372        assert!((cal.collusion_rate - 0.50).abs() < 0.01);
4373        assert!(cal.validate().is_ok());
4374
4375        // Test custom calibration
4376        let custom_cal = AcfeCalibration::new(Decimal::new(200_000, 0), 18);
4377        assert_eq!(custom_cal.median_loss, Decimal::new(200_000, 0));
4378        assert_eq!(custom_cal.median_duration_months, 18);
4379
4380        // Test validation failure
4381        let bad_cal = AcfeCalibration {
4382            collusion_rate: 1.5,
4383            ..Default::default()
4384        };
4385        assert!(bad_cal.validate().is_err());
4386    }
4387
4388    #[test]
4389    fn test_fraud_triangle() {
4390        let triangle = FraudTriangle::new(
4391            PressureType::FinancialTargets,
4392            vec![
4393                OpportunityFactor::WeakInternalControls,
4394                OpportunityFactor::ManagementOverride,
4395            ],
4396            Rationalization::ForTheCompanyGood,
4397        );
4398
4399        // Risk score should be between 0 and 1
4400        let risk = triangle.risk_score();
4401        assert!((0.0..=1.0).contains(&risk));
4402        // Should be relatively high given the components
4403        assert!(risk > 0.5);
4404    }
4405
4406    #[test]
4407    fn test_pressure_types() {
4408        let financial = PressureType::FinancialTargets;
4409        assert!(financial.risk_weight() > 0.5);
4410
4411        let gambling = PressureType::GamblingAddiction;
4412        assert_eq!(gambling.risk_weight(), 0.90);
4413    }
4414
4415    #[test]
4416    fn test_opportunity_factors() {
4417        let override_factor = OpportunityFactor::ManagementOverride;
4418        assert_eq!(override_factor.risk_weight(), 0.90);
4419
4420        let weak_controls = OpportunityFactor::WeakInternalControls;
4421        assert!(weak_controls.risk_weight() > 0.8);
4422    }
4423
4424    #[test]
4425    fn test_rationalizations() {
4426        let entitlement = Rationalization::Entitlement;
4427        assert!(entitlement.risk_weight() > 0.8);
4428
4429        let borrowing = Rationalization::TemporaryBorrowing;
4430        assert!(borrowing.risk_weight() < entitlement.risk_weight());
4431    }
4432
4433    #[test]
4434    fn test_acfe_scheme_serialization() {
4435        let scheme = AcfeScheme::Corruption(CorruptionScheme::BidRigging);
4436        let json = serde_json::to_string(&scheme).expect("Failed to serialize");
4437        let deserialized: AcfeScheme = serde_json::from_str(&json).expect("Failed to deserialize");
4438        assert_eq!(scheme, deserialized);
4439
4440        let calibration = AcfeCalibration::default();
4441        let json = serde_json::to_string(&calibration).expect("Failed to serialize");
4442        let deserialized: AcfeCalibration =
4443            serde_json::from_str(&json).expect("Failed to deserialize");
4444        assert_eq!(calibration.median_loss, deserialized.median_loss);
4445    }
4446}