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.gen_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.gen_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 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 pub fn with_amount(mut self, amount: Decimal) -> Self {
234 self.amount = Some(amount);
235 self
236 }
237
238 pub fn with_account(mut self, account: impl Into<String>) -> Self {
240 self.target_account = Some(account.into());
241 self
242 }
243
244 pub fn with_counterparty(mut self, counterparty: impl Into<String>) -> Self {
246 self.counterparty = Some(counterparty.into());
247 self
248 }
249
250 pub fn with_user(mut self, user: impl Into<String>) -> Self {
252 self.user_id = Some(user.into());
253 self
254 }
255
256 pub fn with_description(mut self, description: impl Into<String>) -> Self {
258 self.description = description.into();
259 self
260 }
261
262 pub fn with_difficulty(mut self, difficulty: AnomalyDetectionDifficulty) -> Self {
264 self.detection_difficulty = difficulty;
265 self
266 }
267
268 pub fn with_technique(mut self, technique: ConcealmentTechnique) -> Self {
270 self.concealment_techniques.push(technique);
271 self
272 }
273
274 pub fn mark_executed(&mut self) {
276 self.executed = true;
277 }
278}
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
282pub enum SchemeActionType {
283 CreateFraudulentEntry,
285 CreateFraudulentPayment,
287 CreateFictitiousVendor,
289 InflateInvoice,
291 MakeKickbackPayment,
293 ManipulateRevenue,
295 DeferExpense,
297 ReleaseReserves,
299 ChannelStuff,
301 Conceal,
303 CoverUp,
305}
306
307pub trait FraudScheme: Send + Sync {
309 fn scheme_type(&self) -> SchemeType;
311
312 fn scheme_id(&self) -> Uuid;
314
315 fn current_stage(&self) -> &SchemeStage;
317
318 fn current_stage_number(&self) -> u32;
320
321 fn stages(&self) -> &[SchemeStage];
323
324 fn status(&self) -> SchemeStatus;
326
327 fn detection_status(&self) -> SchemeDetectionStatus;
329
330 fn advance(
332 &mut self,
333 context: &SchemeContext,
334 rng: &mut dyn rand::RngCore,
335 ) -> Vec<SchemeAction>;
336
337 fn detection_probability(&self) -> f64;
339
340 fn total_impact(&self) -> Decimal;
342
343 fn should_terminate(&self, context: &SchemeContext) -> bool;
345
346 fn perpetrator_id(&self) -> &str;
348
349 fn start_date(&self) -> Option<NaiveDate>;
351
352 fn transaction_refs(&self) -> &[crate::anomaly::schemes::scheme::SchemeTransactionRef];
354
355 fn record_transaction(&mut self, transaction: SchemeTransactionRef);
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct SchemeTransactionRef {
362 pub document_id: String,
364 pub date: NaiveDate,
366 pub amount: Decimal,
368 pub stage: u32,
370 pub anomaly_id: Option<String>,
372 pub action_id: Option<Uuid>,
374}
375
376impl SchemeTransactionRef {
377 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 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 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}