1use 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#[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(
350 &mut self,
351 context: &SchemeContext,
352 rng: &mut dyn rand::RngCore,
353 ) -> Vec<SchemeAction>;
354
355 fn detection_probability(&self) -> f64;
357
358 fn total_impact(&self) -> Decimal;
360
361 fn should_terminate(&self, context: &SchemeContext) -> bool;
363
364 fn perpetrator_id(&self) -> &str;
366
367 fn start_date(&self) -> Option<NaiveDate>;
369
370 fn transaction_refs(&self) -> &[crate::anomaly::schemes::scheme::SchemeTransactionRef];
372
373 fn record_transaction(&mut self, transaction: SchemeTransactionRef);
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct SchemeTransactionRef {
380 pub document_id: String,
382 pub date: NaiveDate,
384 pub amount: Decimal,
386 pub stage: u32,
388 pub anomaly_id: Option<String>,
390 pub action_id: Option<Uuid>,
392}
393
394impl SchemeTransactionRef {
395 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 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 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}