1use chrono::NaiveDate;
4use rand::{Rng, RngExt};
5use rust_decimal::Decimal;
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9use datasynth_core::models::{
10 AnomalyDetectionDifficulty, ConcealmentTechnique, SchemeDetectionStatus, SchemeType,
11};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SchemeStage {
16 pub stage_number: u32,
18 pub name: String,
20 pub description: String,
22 pub duration_months: u32,
24 pub amount_min: Decimal,
26 pub amount_max: Decimal,
28 pub transaction_count_min: u32,
30 pub transaction_count_max: u32,
32 pub detection_difficulty: AnomalyDetectionDifficulty,
34 pub concealment_techniques: Vec<ConcealmentTechnique>,
36}
37
38impl SchemeStage {
39 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 pub fn with_description(mut self, description: impl Into<String>) -> Self {
64 self.description = description.into();
65 self
66 }
67
68 pub fn with_technique(mut self, technique: ConcealmentTechnique) -> Self {
70 self.concealment_techniques.push(technique);
71 self
72 }
73
74 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
96pub enum SchemeStatus {
97 #[default]
99 NotStarted,
100 Active,
102 Paused,
104 Terminated,
106 Detected,
108 Completed,
110}
111
112#[derive(Debug, Clone)]
114pub struct SchemeContext {
115 pub current_date: NaiveDate,
117 pub audit_in_progress: bool,
119 pub detection_activity: f64,
121 pub available_accounts: Vec<String>,
123 pub available_counterparties: Vec<String>,
125 pub available_users: Vec<String>,
127 pub company_code: String,
129}
130
131impl SchemeContext {
132 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 pub fn with_audit(mut self, in_progress: bool) -> Self {
147 self.audit_in_progress = in_progress;
148 self
149 }
150
151 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 pub fn with_accounts(mut self, accounts: Vec<String>) -> Self {
159 self.available_accounts = accounts;
160 self
161 }
162
163 pub fn with_counterparties(mut self, counterparties: Vec<String>) -> Self {
165 self.available_counterparties = counterparties;
166 self
167 }
168
169 pub fn with_users(mut self, users: Vec<String>) -> Self {
171 self.available_users = users;
172 self
173 }
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct SchemeAction {
179 pub action_id: Uuid,
181 pub scheme_id: Uuid,
183 pub stage: u32,
185 pub action_type: SchemeActionType,
187 pub target_date: NaiveDate,
189 pub amount: Option<Decimal>,
191 pub target_account: Option<String>,
193 pub counterparty: Option<String>,
195 pub user_id: Option<String>,
197 pub description: String,
199 pub detection_difficulty: AnomalyDetectionDifficulty,
201 pub concealment_techniques: Vec<ConcealmentTechnique>,
203 pub executed: bool,
205}
206
207impl SchemeAction {
208 pub fn new(
210 scheme_id: Uuid,
211 stage: u32,
212 action_type: SchemeActionType,
213 target_date: NaiveDate,
214 ) -> Self {
215 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 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 pub fn with_amount(mut self, amount: Decimal) -> Self {
252 self.amount = Some(amount);
253 self
254 }
255
256 pub fn with_account(mut self, account: impl Into<String>) -> Self {
258 self.target_account = Some(account.into());
259 self
260 }
261
262 pub fn with_counterparty(mut self, counterparty: impl Into<String>) -> Self {
264 self.counterparty = Some(counterparty.into());
265 self
266 }
267
268 pub fn with_user(mut self, user: impl Into<String>) -> Self {
270 self.user_id = Some(user.into());
271 self
272 }
273
274 pub fn with_description(mut self, description: impl Into<String>) -> Self {
276 self.description = description.into();
277 self
278 }
279
280 pub fn with_difficulty(mut self, difficulty: AnomalyDetectionDifficulty) -> Self {
282 self.detection_difficulty = difficulty;
283 self
284 }
285
286 pub fn with_technique(mut self, technique: ConcealmentTechnique) -> Self {
288 self.concealment_techniques.push(technique);
289 self
290 }
291
292 pub fn mark_executed(&mut self) {
294 self.executed = true;
295 }
296}
297
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
300pub enum SchemeActionType {
301 CreateFraudulentEntry,
303 CreateFraudulentPayment,
305 CreateFictitiousVendor,
307 InflateInvoice,
309 MakeKickbackPayment,
311 ManipulateRevenue,
313 DeferExpense,
315 ReleaseReserves,
317 ChannelStuff,
319 Conceal,
321 CoverUp,
323}
324
325pub trait FraudScheme: Send + Sync {
327 fn scheme_type(&self) -> SchemeType;
329
330 fn scheme_id(&self) -> Uuid;
332
333 fn current_stage(&self) -> &SchemeStage;
335
336 fn current_stage_number(&self) -> u32;
338
339 fn stages(&self) -> &[SchemeStage];
341
342 fn status(&self) -> SchemeStatus;
344
345 fn detection_status(&self) -> SchemeDetectionStatus;
347
348 fn advance(&mut self, context: &SchemeContext, rng: &mut dyn rand::Rng) -> Vec<SchemeAction>;
350
351 fn detection_probability(&self) -> f64;
353
354 fn total_impact(&self) -> Decimal;
356
357 fn should_terminate(&self, context: &SchemeContext) -> bool;
359
360 fn perpetrator_id(&self) -> &str;
362
363 fn start_date(&self) -> Option<NaiveDate>;
365
366 fn transaction_refs(&self) -> &[crate::anomaly::schemes::scheme::SchemeTransactionRef];
368
369 fn record_transaction(&mut self, transaction: SchemeTransactionRef);
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct SchemeTransactionRef {
376 pub document_id: String,
378 pub date: NaiveDate,
380 pub amount: Decimal,
382 pub stage: u32,
384 pub anomaly_id: Option<String>,
386 pub action_id: Option<Uuid>,
388}
389
390impl SchemeTransactionRef {
391 pub fn new(
393 document_id: impl Into<String>,
394 date: NaiveDate,
395 amount: Decimal,
396 stage: u32,
397 ) -> Self {
398 Self {
399 document_id: document_id.into(),
400 date,
401 amount,
402 stage,
403 anomaly_id: None,
404 action_id: None,
405 }
406 }
407
408 pub fn with_anomaly(mut self, anomaly_id: impl Into<String>) -> Self {
410 self.anomaly_id = Some(anomaly_id.into());
411 self
412 }
413
414 pub fn with_action(mut self, action_id: Uuid) -> Self {
416 self.action_id = Some(action_id);
417 self
418 }
419}
420
421#[cfg(test)]
422#[allow(clippy::unwrap_used)]
423mod tests {
424 use super::*;
425 use rust_decimal_macros::dec;
426
427 #[test]
428 fn test_scheme_stage() {
429 let stage = SchemeStage::new(
430 1,
431 "testing",
432 2,
433 (dec!(100), dec!(500)),
434 (2, 5),
435 AnomalyDetectionDifficulty::Hard,
436 )
437 .with_description("Initial testing phase")
438 .with_technique(ConcealmentTechnique::TransactionSplitting);
439
440 assert_eq!(stage.stage_number, 1);
441 assert_eq!(stage.name, "testing");
442 assert_eq!(stage.duration_months, 2);
443 assert_eq!(stage.detection_difficulty, AnomalyDetectionDifficulty::Hard);
444 assert_eq!(stage.concealment_techniques.len(), 1);
445 }
446
447 #[test]
448 fn test_scheme_stage_random_amount() {
449 use rand::SeedableRng;
450 use rand_chacha::ChaCha8Rng;
451
452 let stage = SchemeStage::new(
453 1,
454 "test",
455 2,
456 (dec!(100), dec!(500)),
457 (2, 5),
458 AnomalyDetectionDifficulty::Moderate,
459 );
460
461 let mut rng = ChaCha8Rng::seed_from_u64(42);
462 let amount = stage.random_amount(&mut rng);
463
464 assert!(amount >= dec!(100));
465 assert!(amount <= dec!(500));
466 }
467
468 #[test]
469 fn test_scheme_context() {
470 let context = SchemeContext::new(NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(), "1000")
471 .with_audit(true)
472 .with_detection_activity(0.3)
473 .with_accounts(vec!["5000".to_string(), "6000".to_string()])
474 .with_users(vec!["USER001".to_string()]);
475
476 assert!(context.audit_in_progress);
477 assert!((context.detection_activity - 0.3).abs() < 0.01);
478 assert_eq!(context.available_accounts.len(), 2);
479 }
480
481 #[test]
482 fn test_scheme_action() {
483 let action = SchemeAction::new(
484 Uuid::new_v4(),
485 1,
486 SchemeActionType::CreateFraudulentEntry,
487 NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(),
488 )
489 .with_amount(dec!(5000))
490 .with_account("5000")
491 .with_user("USER001")
492 .with_description("Test fraudulent entry")
493 .with_technique(ConcealmentTechnique::DocumentManipulation);
494
495 assert_eq!(action.amount, Some(dec!(5000)));
496 assert_eq!(action.target_account, Some("5000".to_string()));
497 assert!(!action.executed);
498 }
499
500 #[test]
501 fn test_scheme_transaction_ref() {
502 let tx_ref = SchemeTransactionRef::new(
503 "JE001",
504 NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(),
505 dec!(1000),
506 1,
507 )
508 .with_anomaly("ANO001");
509
510 assert_eq!(tx_ref.document_id, "JE001");
511 assert_eq!(tx_ref.anomaly_id, Some("ANO001".to_string()));
512 }
513}