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.gen_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.gen_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        Self {
216            action_id: Uuid::new_v4(),
217            scheme_id,
218            stage,
219            action_type,
220            target_date,
221            amount: None,
222            target_account: None,
223            counterparty: None,
224            user_id: None,
225            description: String::new(),
226            detection_difficulty: AnomalyDetectionDifficulty::Moderate,
227            concealment_techniques: Vec::new(),
228            executed: false,
229        }
230    }
231
232    /// Sets the amount.
233    pub fn with_amount(mut self, amount: Decimal) -> Self {
234        self.amount = Some(amount);
235        self
236    }
237
238    /// Sets the target account.
239    pub fn with_account(mut self, account: impl Into<String>) -> Self {
240        self.target_account = Some(account.into());
241        self
242    }
243
244    /// Sets the counterparty.
245    pub fn with_counterparty(mut self, counterparty: impl Into<String>) -> Self {
246        self.counterparty = Some(counterparty.into());
247        self
248    }
249
250    /// Sets the user.
251    pub fn with_user(mut self, user: impl Into<String>) -> Self {
252        self.user_id = Some(user.into());
253        self
254    }
255
256    /// Sets the description.
257    pub fn with_description(mut self, description: impl Into<String>) -> Self {
258        self.description = description.into();
259        self
260    }
261
262    /// Sets the detection difficulty.
263    pub fn with_difficulty(mut self, difficulty: AnomalyDetectionDifficulty) -> Self {
264        self.detection_difficulty = difficulty;
265        self
266    }
267
268    /// Adds a concealment technique.
269    pub fn with_technique(mut self, technique: ConcealmentTechnique) -> Self {
270        self.concealment_techniques.push(technique);
271        self
272    }
273
274    /// Marks the action as executed.
275    pub fn mark_executed(&mut self) {
276        self.executed = true;
277    }
278}
279
280/// Type of action within a scheme.
281#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
282pub enum SchemeActionType {
283    /// Create a fraudulent journal entry.
284    CreateFraudulentEntry,
285    /// Create a fraudulent payment.
286    CreateFraudulentPayment,
287    /// Create a fictitious vendor.
288    CreateFictitiousVendor,
289    /// Inflate an invoice amount.
290    InflateInvoice,
291    /// Make a kickback payment.
292    MakeKickbackPayment,
293    /// Manipulate revenue recognition.
294    ManipulateRevenue,
295    /// Defer expense recognition.
296    DeferExpense,
297    /// Release reserves.
298    ReleaseReserves,
299    /// Create channel stuffing transaction.
300    ChannelStuff,
301    /// Conceal prior fraud.
302    Conceal,
303    /// Cover up tracks.
304    CoverUp,
305}
306
307/// Trait for fraud schemes.
308pub trait FraudScheme: Send + Sync {
309    /// Returns the scheme type.
310    fn scheme_type(&self) -> SchemeType;
311
312    /// Returns the unique scheme ID.
313    fn scheme_id(&self) -> Uuid;
314
315    /// Returns the current stage.
316    fn current_stage(&self) -> &SchemeStage;
317
318    /// Returns the current stage number.
319    fn current_stage_number(&self) -> u32;
320
321    /// Returns all stages.
322    fn stages(&self) -> &[SchemeStage];
323
324    /// Returns the current status.
325    fn status(&self) -> SchemeStatus;
326
327    /// Returns the detection status.
328    fn detection_status(&self) -> SchemeDetectionStatus;
329
330    /// Advances the scheme and returns actions to execute.
331    fn advance(
332        &mut self,
333        context: &SchemeContext,
334        rng: &mut dyn rand::RngCore,
335    ) -> Vec<SchemeAction>;
336
337    /// Returns the cumulative detection probability.
338    fn detection_probability(&self) -> f64;
339
340    /// Returns the total financial impact so far.
341    fn total_impact(&self) -> Decimal;
342
343    /// Returns whether the scheme should terminate.
344    fn should_terminate(&self, context: &SchemeContext) -> bool;
345
346    /// Gets the perpetrator ID.
347    fn perpetrator_id(&self) -> &str;
348
349    /// Gets the start date.
350    fn start_date(&self) -> Option<NaiveDate>;
351
352    /// Gets all transaction references.
353    fn transaction_refs(&self) -> &[crate::anomaly::schemes::scheme::SchemeTransactionRef];
354
355    /// Records a transaction.
356    fn record_transaction(&mut self, transaction: SchemeTransactionRef);
357}
358
359/// Reference to a transaction within a scheme.
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct SchemeTransactionRef {
362    /// Document ID.
363    pub document_id: String,
364    /// Transaction date.
365    pub date: NaiveDate,
366    /// Transaction amount.
367    pub amount: Decimal,
368    /// Stage number.
369    pub stage: u32,
370    /// Anomaly ID if labeled.
371    pub anomaly_id: Option<String>,
372    /// Action ID that generated this transaction.
373    pub action_id: Option<Uuid>,
374}
375
376impl SchemeTransactionRef {
377    /// Creates a new transaction reference.
378    pub fn new(
379        document_id: impl Into<String>,
380        date: NaiveDate,
381        amount: Decimal,
382        stage: u32,
383    ) -> Self {
384        Self {
385            document_id: document_id.into(),
386            date,
387            amount,
388            stage,
389            anomaly_id: None,
390            action_id: None,
391        }
392    }
393
394    /// Sets the anomaly ID.
395    pub fn with_anomaly(mut self, anomaly_id: impl Into<String>) -> Self {
396        self.anomaly_id = Some(anomaly_id.into());
397        self
398    }
399
400    /// Sets the action ID.
401    pub fn with_action(mut self, action_id: Uuid) -> Self {
402        self.action_id = Some(action_id);
403        self
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410    use rust_decimal_macros::dec;
411
412    #[test]
413    fn test_scheme_stage() {
414        let stage = SchemeStage::new(
415            1,
416            "testing",
417            2,
418            (dec!(100), dec!(500)),
419            (2, 5),
420            AnomalyDetectionDifficulty::Hard,
421        )
422        .with_description("Initial testing phase")
423        .with_technique(ConcealmentTechnique::TransactionSplitting);
424
425        assert_eq!(stage.stage_number, 1);
426        assert_eq!(stage.name, "testing");
427        assert_eq!(stage.duration_months, 2);
428        assert_eq!(stage.detection_difficulty, AnomalyDetectionDifficulty::Hard);
429        assert_eq!(stage.concealment_techniques.len(), 1);
430    }
431
432    #[test]
433    fn test_scheme_stage_random_amount() {
434        use rand::SeedableRng;
435        use rand_chacha::ChaCha8Rng;
436
437        let stage = SchemeStage::new(
438            1,
439            "test",
440            2,
441            (dec!(100), dec!(500)),
442            (2, 5),
443            AnomalyDetectionDifficulty::Moderate,
444        );
445
446        let mut rng = ChaCha8Rng::seed_from_u64(42);
447        let amount = stage.random_amount(&mut rng);
448
449        assert!(amount >= dec!(100));
450        assert!(amount <= dec!(500));
451    }
452
453    #[test]
454    fn test_scheme_context() {
455        let context = SchemeContext::new(NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(), "1000")
456            .with_audit(true)
457            .with_detection_activity(0.3)
458            .with_accounts(vec!["5000".to_string(), "6000".to_string()])
459            .with_users(vec!["USER001".to_string()]);
460
461        assert!(context.audit_in_progress);
462        assert!((context.detection_activity - 0.3).abs() < 0.01);
463        assert_eq!(context.available_accounts.len(), 2);
464    }
465
466    #[test]
467    fn test_scheme_action() {
468        let action = SchemeAction::new(
469            Uuid::new_v4(),
470            1,
471            SchemeActionType::CreateFraudulentEntry,
472            NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(),
473        )
474        .with_amount(dec!(5000))
475        .with_account("5000")
476        .with_user("USER001")
477        .with_description("Test fraudulent entry")
478        .with_technique(ConcealmentTechnique::DocumentManipulation);
479
480        assert_eq!(action.amount, Some(dec!(5000)));
481        assert_eq!(action.target_account, Some("5000".to_string()));
482        assert!(!action.executed);
483    }
484
485    #[test]
486    fn test_scheme_transaction_ref() {
487        let tx_ref = SchemeTransactionRef::new(
488            "JE001",
489            NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(),
490            dec!(1000),
491            1,
492        )
493        .with_anomaly("ANO001");
494
495        assert_eq!(tx_ref.document_id, "JE001");
496        assert_eq!(tx_ref.anomaly_id, Some("ANO001".to_string()));
497    }
498}