Skip to main content

datasynth_generators/anomaly/schemes/
scheme.rs

1//! Core fraud scheme trait and types.
2
3use chrono::NaiveDate;
4use rand::Rng;
5use rust_decimal::Decimal;
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9use datasynth_core::models::{
10    AnomalyDetectionDifficulty, ConcealmentTechnique, SchemeDetectionStatus, SchemeType,
11};
12
13/// A stage within a multi-stage fraud scheme.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SchemeStage {
16    /// Stage number (1-indexed).
17    pub stage_number: u32,
18    /// Name of the stage (e.g., "testing", "escalation").
19    pub name: String,
20    /// Description of what happens in this stage.
21    pub description: String,
22    /// Duration in months.
23    pub duration_months: u32,
24    /// Minimum transaction amount for this stage.
25    pub amount_min: Decimal,
26    /// Maximum transaction amount for this stage.
27    pub amount_max: Decimal,
28    /// Minimum number of transactions in this stage.
29    pub transaction_count_min: u32,
30    /// Maximum number of transactions in this stage.
31    pub transaction_count_max: u32,
32    /// Detection difficulty for this stage.
33    pub detection_difficulty: AnomalyDetectionDifficulty,
34    /// Concealment techniques typically used in this stage.
35    pub concealment_techniques: Vec<ConcealmentTechnique>,
36}
37
38impl SchemeStage {
39    /// Creates a new scheme stage.
40    pub fn new(
41        stage_number: u32,
42        name: impl Into<String>,
43        duration_months: u32,
44        amount_range: (Decimal, Decimal),
45        transaction_range: (u32, u32),
46        difficulty: AnomalyDetectionDifficulty,
47    ) -> Self {
48        Self {
49            stage_number,
50            name: name.into(),
51            description: String::new(),
52            duration_months,
53            amount_min: amount_range.0,
54            amount_max: amount_range.1,
55            transaction_count_min: transaction_range.0,
56            transaction_count_max: transaction_range.1,
57            detection_difficulty: difficulty,
58            concealment_techniques: Vec::new(),
59        }
60    }
61
62    /// Sets the description.
63    pub fn with_description(mut self, description: impl Into<String>) -> Self {
64        self.description = description.into();
65        self
66    }
67
68    /// Adds a concealment technique.
69    pub fn with_technique(mut self, technique: ConcealmentTechnique) -> Self {
70        self.concealment_techniques.push(technique);
71        self
72    }
73
74    /// Generates a random amount within the stage range.
75    pub fn random_amount<R: Rng + ?Sized>(&self, rng: &mut R) -> Decimal {
76        if self.amount_min == self.amount_max {
77            return self.amount_min;
78        }
79        let min_f64: f64 = self.amount_min.try_into().unwrap_or(0.0);
80        let max_f64: f64 = self.amount_max.try_into().unwrap_or(min_f64 + 1000.0);
81        let value = rng.random_range(min_f64..=max_f64);
82        Decimal::from_f64_retain(value).unwrap_or(self.amount_min)
83    }
84
85    /// Generates a random transaction count within the stage range.
86    pub fn random_transaction_count<R: Rng + ?Sized>(&self, rng: &mut R) -> u32 {
87        if self.transaction_count_min == self.transaction_count_max {
88            return self.transaction_count_min;
89        }
90        rng.random_range(self.transaction_count_min..=self.transaction_count_max)
91    }
92}
93
94/// Current status of a fraud scheme.
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
96pub enum SchemeStatus {
97    /// Scheme has not started yet.
98    #[default]
99    NotStarted,
100    /// Scheme is actively ongoing.
101    Active,
102    /// Scheme is temporarily paused (e.g., due to audit).
103    Paused,
104    /// Scheme has been terminated by perpetrator.
105    Terminated,
106    /// Scheme has been detected.
107    Detected,
108    /// Scheme has completed its full lifecycle.
109    Completed,
110}
111
112/// Context provided to a scheme for decision-making.
113#[derive(Debug, Clone)]
114pub struct SchemeContext {
115    /// Current date.
116    pub current_date: NaiveDate,
117    /// Whether an audit is currently in progress.
118    pub audit_in_progress: bool,
119    /// Recent detection activity level (0.0-1.0).
120    pub detection_activity: f64,
121    /// Available accounts for transactions.
122    pub available_accounts: Vec<String>,
123    /// Available vendors/customers.
124    pub available_counterparties: Vec<String>,
125    /// Available users who could be perpetrators.
126    pub available_users: Vec<String>,
127    /// Company code.
128    pub company_code: String,
129}
130
131impl SchemeContext {
132    /// Creates a new scheme context.
133    pub fn new(current_date: NaiveDate, company_code: impl Into<String>) -> Self {
134        Self {
135            current_date,
136            audit_in_progress: false,
137            detection_activity: 0.0,
138            available_accounts: Vec::new(),
139            available_counterparties: Vec::new(),
140            available_users: Vec::new(),
141            company_code: company_code.into(),
142        }
143    }
144
145    /// Sets audit in progress flag.
146    pub fn with_audit(mut self, in_progress: bool) -> Self {
147        self.audit_in_progress = in_progress;
148        self
149    }
150
151    /// Sets detection activity level.
152    pub fn with_detection_activity(mut self, level: f64) -> Self {
153        self.detection_activity = level.clamp(0.0, 1.0);
154        self
155    }
156
157    /// Sets available accounts.
158    pub fn with_accounts(mut self, accounts: Vec<String>) -> Self {
159        self.available_accounts = accounts;
160        self
161    }
162
163    /// Sets available counterparties.
164    pub fn with_counterparties(mut self, counterparties: Vec<String>) -> Self {
165        self.available_counterparties = counterparties;
166        self
167    }
168
169    /// Sets available users.
170    pub fn with_users(mut self, users: Vec<String>) -> Self {
171        self.available_users = users;
172        self
173    }
174}
175
176/// An action generated by a scheme to be executed.
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct SchemeAction {
179    /// Unique action ID.
180    pub action_id: Uuid,
181    /// Scheme ID this action belongs to.
182    pub scheme_id: Uuid,
183    /// Stage this action belongs to.
184    pub stage: u32,
185    /// Type of action.
186    pub action_type: SchemeActionType,
187    /// Target date for the action.
188    pub target_date: NaiveDate,
189    /// Amount involved (if applicable).
190    pub amount: Option<Decimal>,
191    /// Target account.
192    pub target_account: Option<String>,
193    /// Counterparty involved.
194    pub counterparty: Option<String>,
195    /// User to perform action.
196    pub user_id: Option<String>,
197    /// Description.
198    pub description: String,
199    /// Detection difficulty for this specific action.
200    pub detection_difficulty: AnomalyDetectionDifficulty,
201    /// Concealment techniques to apply.
202    pub concealment_techniques: Vec<ConcealmentTechnique>,
203    /// Whether this action has been executed.
204    pub executed: bool,
205}
206
207impl SchemeAction {
208    /// Creates a new scheme action.
209    pub fn new(
210        scheme_id: Uuid,
211        stage: u32,
212        action_type: SchemeActionType,
213        target_date: NaiveDate,
214    ) -> Self {
215        // Derive deterministic action_id from scheme_id + stage using FNV-1a hash
216        let action_id = {
217            let scheme_bytes = scheme_id.as_bytes();
218            let stage_bytes = stage.to_le_bytes();
219            let mut hash: u64 = 0xcbf29ce484222325;
220            for &b in scheme_bytes.iter().chain(stage_bytes.iter()) {
221                hash ^= b as u64;
222                hash = hash.wrapping_mul(0x100000001b3);
223            }
224            let bytes = hash.to_le_bytes();
225            let mut uuid_bytes = [0u8; 16];
226            uuid_bytes[..8].copy_from_slice(&bytes);
227            uuid_bytes[8..16].copy_from_slice(&bytes);
228            // Set version 4 bits for compatibility
229            uuid_bytes[6] = (uuid_bytes[6] & 0x0f) | 0x40;
230            uuid_bytes[8] = (uuid_bytes[8] & 0x3f) | 0x80;
231            Uuid::from_bytes(uuid_bytes)
232        };
233        Self {
234            action_id,
235            scheme_id,
236            stage,
237            action_type,
238            target_date,
239            amount: None,
240            target_account: None,
241            counterparty: None,
242            user_id: None,
243            description: String::new(),
244            detection_difficulty: AnomalyDetectionDifficulty::Moderate,
245            concealment_techniques: Vec::new(),
246            executed: false,
247        }
248    }
249
250    /// Sets the amount.
251    pub fn with_amount(mut self, amount: Decimal) -> Self {
252        self.amount = Some(amount);
253        self
254    }
255
256    /// Sets the target account.
257    pub fn with_account(mut self, account: impl Into<String>) -> Self {
258        self.target_account = Some(account.into());
259        self
260    }
261
262    /// Sets the counterparty.
263    pub fn with_counterparty(mut self, counterparty: impl Into<String>) -> Self {
264        self.counterparty = Some(counterparty.into());
265        self
266    }
267
268    /// Sets the user.
269    pub fn with_user(mut self, user: impl Into<String>) -> Self {
270        self.user_id = Some(user.into());
271        self
272    }
273
274    /// Sets the description.
275    pub fn with_description(mut self, description: impl Into<String>) -> Self {
276        self.description = description.into();
277        self
278    }
279
280    /// Sets the detection difficulty.
281    pub fn with_difficulty(mut self, difficulty: AnomalyDetectionDifficulty) -> Self {
282        self.detection_difficulty = difficulty;
283        self
284    }
285
286    /// Adds a concealment technique.
287    pub fn with_technique(mut self, technique: ConcealmentTechnique) -> Self {
288        self.concealment_techniques.push(technique);
289        self
290    }
291
292    /// Marks the action as executed.
293    pub fn mark_executed(&mut self) {
294        self.executed = true;
295    }
296}
297
298/// Type of action within a scheme.
299#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
300pub enum SchemeActionType {
301    /// Create a fraudulent journal entry.
302    CreateFraudulentEntry,
303    /// Create a fraudulent payment.
304    CreateFraudulentPayment,
305    /// Create a fictitious vendor.
306    CreateFictitiousVendor,
307    /// Inflate an invoice amount.
308    InflateInvoice,
309    /// Make a kickback payment.
310    MakeKickbackPayment,
311    /// Manipulate revenue recognition.
312    ManipulateRevenue,
313    /// Defer expense recognition.
314    DeferExpense,
315    /// Release reserves.
316    ReleaseReserves,
317    /// Create channel stuffing transaction.
318    ChannelStuff,
319    /// Conceal prior fraud.
320    Conceal,
321    /// Cover up tracks.
322    CoverUp,
323}
324
325/// Trait for fraud schemes.
326pub trait FraudScheme: Send + Sync {
327    /// Returns the scheme type.
328    fn scheme_type(&self) -> SchemeType;
329
330    /// Returns the unique scheme ID.
331    fn scheme_id(&self) -> Uuid;
332
333    /// Returns the current stage.
334    fn current_stage(&self) -> &SchemeStage;
335
336    /// Returns the current stage number.
337    fn current_stage_number(&self) -> u32;
338
339    /// Returns all stages.
340    fn stages(&self) -> &[SchemeStage];
341
342    /// Returns the current status.
343    fn status(&self) -> SchemeStatus;
344
345    /// Returns the detection status.
346    fn detection_status(&self) -> SchemeDetectionStatus;
347
348    /// Advances the scheme and returns actions to execute.
349    fn advance(
350        &mut self,
351        context: &SchemeContext,
352        rng: &mut dyn rand::RngCore,
353    ) -> Vec<SchemeAction>;
354
355    /// Returns the cumulative detection probability.
356    fn detection_probability(&self) -> f64;
357
358    /// Returns the total financial impact so far.
359    fn total_impact(&self) -> Decimal;
360
361    /// Returns whether the scheme should terminate.
362    fn should_terminate(&self, context: &SchemeContext) -> bool;
363
364    /// Gets the perpetrator ID.
365    fn perpetrator_id(&self) -> &str;
366
367    /// Gets the start date.
368    fn start_date(&self) -> Option<NaiveDate>;
369
370    /// Gets all transaction references.
371    fn transaction_refs(&self) -> &[crate::anomaly::schemes::scheme::SchemeTransactionRef];
372
373    /// Records a transaction.
374    fn record_transaction(&mut self, transaction: SchemeTransactionRef);
375}
376
377/// Reference to a transaction within a scheme.
378#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct SchemeTransactionRef {
380    /// Document ID.
381    pub document_id: String,
382    /// Transaction date.
383    pub date: NaiveDate,
384    /// Transaction amount.
385    pub amount: Decimal,
386    /// Stage number.
387    pub stage: u32,
388    /// Anomaly ID if labeled.
389    pub anomaly_id: Option<String>,
390    /// Action ID that generated this transaction.
391    pub action_id: Option<Uuid>,
392}
393
394impl SchemeTransactionRef {
395    /// Creates a new transaction reference.
396    pub fn new(
397        document_id: impl Into<String>,
398        date: NaiveDate,
399        amount: Decimal,
400        stage: u32,
401    ) -> Self {
402        Self {
403            document_id: document_id.into(),
404            date,
405            amount,
406            stage,
407            anomaly_id: None,
408            action_id: None,
409        }
410    }
411
412    /// Sets the anomaly ID.
413    pub fn with_anomaly(mut self, anomaly_id: impl Into<String>) -> Self {
414        self.anomaly_id = Some(anomaly_id.into());
415        self
416    }
417
418    /// Sets the action ID.
419    pub fn with_action(mut self, action_id: Uuid) -> Self {
420        self.action_id = Some(action_id);
421        self
422    }
423}
424
425#[cfg(test)]
426#[allow(clippy::unwrap_used)]
427mod tests {
428    use super::*;
429    use rust_decimal_macros::dec;
430
431    #[test]
432    fn test_scheme_stage() {
433        let stage = SchemeStage::new(
434            1,
435            "testing",
436            2,
437            (dec!(100), dec!(500)),
438            (2, 5),
439            AnomalyDetectionDifficulty::Hard,
440        )
441        .with_description("Initial testing phase")
442        .with_technique(ConcealmentTechnique::TransactionSplitting);
443
444        assert_eq!(stage.stage_number, 1);
445        assert_eq!(stage.name, "testing");
446        assert_eq!(stage.duration_months, 2);
447        assert_eq!(stage.detection_difficulty, AnomalyDetectionDifficulty::Hard);
448        assert_eq!(stage.concealment_techniques.len(), 1);
449    }
450
451    #[test]
452    fn test_scheme_stage_random_amount() {
453        use rand::SeedableRng;
454        use rand_chacha::ChaCha8Rng;
455
456        let stage = SchemeStage::new(
457            1,
458            "test",
459            2,
460            (dec!(100), dec!(500)),
461            (2, 5),
462            AnomalyDetectionDifficulty::Moderate,
463        );
464
465        let mut rng = ChaCha8Rng::seed_from_u64(42);
466        let amount = stage.random_amount(&mut rng);
467
468        assert!(amount >= dec!(100));
469        assert!(amount <= dec!(500));
470    }
471
472    #[test]
473    fn test_scheme_context() {
474        let context = SchemeContext::new(NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(), "1000")
475            .with_audit(true)
476            .with_detection_activity(0.3)
477            .with_accounts(vec!["5000".to_string(), "6000".to_string()])
478            .with_users(vec!["USER001".to_string()]);
479
480        assert!(context.audit_in_progress);
481        assert!((context.detection_activity - 0.3).abs() < 0.01);
482        assert_eq!(context.available_accounts.len(), 2);
483    }
484
485    #[test]
486    fn test_scheme_action() {
487        let action = SchemeAction::new(
488            Uuid::new_v4(),
489            1,
490            SchemeActionType::CreateFraudulentEntry,
491            NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(),
492        )
493        .with_amount(dec!(5000))
494        .with_account("5000")
495        .with_user("USER001")
496        .with_description("Test fraudulent entry")
497        .with_technique(ConcealmentTechnique::DocumentManipulation);
498
499        assert_eq!(action.amount, Some(dec!(5000)));
500        assert_eq!(action.target_account, Some("5000".to_string()));
501        assert!(!action.executed);
502    }
503
504    #[test]
505    fn test_scheme_transaction_ref() {
506        let tx_ref = SchemeTransactionRef::new(
507            "JE001",
508            NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(),
509            dec!(1000),
510            1,
511        )
512        .with_anomaly("ANO001");
513
514        assert_eq!(tx_ref.document_id, "JE001");
515        assert_eq!(tx_ref.anomaly_id, Some("ANO001".to_string()));
516    }
517}