1use chrono::{DateTime, Datelike, NaiveDate, Utc};
4use datasynth_core::models::banking::{
5 AmlTypology, Direction, LaunderingStage, Sophistication, TransactionCategory,
6 TransactionChannel,
7};
8use datasynth_core::DeterministicUuidFactory;
9use rand::prelude::*;
10use rand_chacha::ChaCha8Rng;
11use rust_decimal::Decimal;
12
13use crate::models::{BankAccount, BankTransaction, BankingCustomer, CounterpartyRef};
14
15pub struct FraudInjector {
23 rng: ChaCha8Rng,
24 uuid_factory: DeterministicUuidFactory,
25}
26
27impl FraudInjector {
28 pub fn new(seed: u64) -> Self {
30 Self {
31 rng: ChaCha8Rng::seed_from_u64(seed.wrapping_add(7200)),
32 uuid_factory: DeterministicUuidFactory::new(
33 seed,
34 datasynth_core::GeneratorType::Anomaly,
35 ),
36 }
37 }
38
39 pub fn generate_account_takeover(
43 &mut self,
44 _customer: &BankingCustomer,
45 account: &BankAccount,
46 start_date: NaiveDate,
47 _end_date: NaiveDate,
48 sophistication: Sophistication,
49 ) -> Vec<BankTransaction> {
50 let mut transactions = Vec::new();
51
52 let (num_extractions, max_amount, time_window_hours) = match sophistication {
54 Sophistication::Basic => (1..3, 5_000.0, 1..4),
55 Sophistication::Standard => (2..4, 15_000.0, 1..8),
56 Sophistication::Professional => (3..6, 50_000.0, 2..12),
57 Sophistication::Advanced => (4..8, 100_000.0, 4..24),
58 Sophistication::StateLevel => (5..10, 250_000.0, 8..48),
59 };
60
61 let extractions = self.rng.gen_range(num_extractions);
62 let scenario_id = format!("ATO-{:06}", self.rng.gen::<u32>());
63
64 let takeover_date = start_date;
66 let mut current_hour = self.rng.gen_range(0..12);
67
68 for i in 0..extractions {
69 let hour_offset = self.rng.gen_range(time_window_hours.clone());
71 current_hour = (current_hour + hour_offset) % 24;
72
73 let timestamp = takeover_date
74 .and_hms_opt(
75 current_hour as u32,
76 self.rng.gen_range(0..60),
77 self.rng.gen_range(0..60),
78 )
79 .map(|dt| DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc))
80 .unwrap_or_else(Utc::now);
81
82 let amount = self.rng.gen_range(500.0..max_amount);
84
85 let (channel, category, counterparty, reference) = self.random_ato_extraction(i);
87
88 let txn = BankTransaction::new(
89 self.uuid_factory.next(),
90 account.account_id,
91 Decimal::from_f64_retain(amount).unwrap_or(Decimal::ZERO),
92 &account.currency,
93 Direction::Outbound,
94 channel,
95 category,
96 counterparty,
97 &reference,
98 timestamp,
99 )
100 .mark_suspicious(AmlTypology::AccountTakeover, &scenario_id)
101 .with_laundering_stage(LaunderingStage::NotApplicable)
102 .with_scenario(&scenario_id, i as u32);
103
104 transactions.push(txn);
105 }
106
107 if matches!(
109 sophistication,
110 Sophistication::Professional | Sophistication::Advanced | Sophistication::StateLevel
111 ) {
112 let test_timestamp = takeover_date
114 .and_hms_opt(self.rng.gen_range(6..12), self.rng.gen_range(0..60), 0)
115 .map(|dt| DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc))
116 .unwrap_or_else(Utc::now);
117
118 let test_txn = BankTransaction::new(
119 self.uuid_factory.next(),
120 account.account_id,
121 Decimal::from_f64_retain(self.rng.gen_range(1.0..10.0)).unwrap_or(Decimal::ONE),
122 &account.currency,
123 Direction::Outbound,
124 TransactionChannel::CardNotPresent,
125 TransactionCategory::Shopping,
126 CounterpartyRef::merchant_by_name("Test Merchant", "5999"),
127 "Small test purchase",
128 test_timestamp,
129 )
130 .mark_suspicious(AmlTypology::AccountTakeover, &scenario_id)
131 .with_laundering_stage(LaunderingStage::NotApplicable)
132 .with_scenario(&scenario_id, extractions as u32);
133
134 transactions.insert(0, test_txn);
135 }
136
137 transactions
138 }
139
140 pub fn generate_fake_vendor(
144 &mut self,
145 _customer: &BankingCustomer,
146 account: &BankAccount,
147 start_date: NaiveDate,
148 end_date: NaiveDate,
149 sophistication: Sophistication,
150 ) -> Vec<BankTransaction> {
151 let mut transactions = Vec::new();
152
153 let (num_payments, payment_range, interval_days) = match sophistication {
155 Sophistication::Basic => (2..4, 1_000.0..10_000.0, 7..14),
156 Sophistication::Standard => (3..6, 5_000.0..30_000.0, 14..30),
157 Sophistication::Professional => (4..8, 10_000.0..75_000.0, 21..45),
158 Sophistication::Advanced => (6..12, 25_000.0..150_000.0, 30..60),
159 Sophistication::StateLevel => (8..20, 50_000.0..500_000.0, 45..90),
160 };
161
162 let payments = self.rng.gen_range(num_payments);
163 let scenario_id = format!("FKV-{:06}", self.rng.gen::<u32>());
164
165 let fake_vendor = self.random_fake_vendor();
167 let available_days = (end_date - start_date).num_days().max(1);
168 let mut current_date = start_date;
169
170 for i in 0..payments {
171 let timestamp = self.random_timestamp(current_date);
172 let amount = self.rng.gen_range(payment_range.clone());
173
174 let invoice_ref = format!(
176 "INV-{:04}-{:06}",
177 current_date.year() % 100,
178 self.rng.gen::<u32>() % 1_000_000
179 );
180
181 let txn = BankTransaction::new(
182 self.uuid_factory.next(),
183 account.account_id,
184 Decimal::from_f64_retain(amount).unwrap_or(Decimal::ZERO),
185 &account.currency,
186 Direction::Outbound,
187 TransactionChannel::Ach,
188 TransactionCategory::Other,
189 CounterpartyRef::business(&fake_vendor.0),
190 &format!("{} - {}", fake_vendor.1, invoice_ref),
191 timestamp,
192 )
193 .mark_suspicious(AmlTypology::FakeVendor, &scenario_id)
194 .with_laundering_stage(LaunderingStage::Placement)
195 .with_scenario(&scenario_id, i as u32);
196
197 transactions.push(txn);
198
199 let interval = self.rng.gen_range(interval_days.clone()) as i64;
201 current_date += chrono::Duration::days(interval);
202
203 if current_date > end_date || (current_date - start_date).num_days() > available_days {
204 break;
205 }
206 }
207
208 if matches!(
210 sophistication,
211 Sophistication::Professional | Sophistication::Advanced | Sophistication::StateLevel
212 ) {
213 for txn in &mut transactions {
214 txn.is_spoofed = true;
215 txn.spoofing_intensity = Some(sophistication.spoofing_intensity());
216 }
217 }
218
219 transactions
220 }
221
222 pub fn generate_bec(
226 &mut self,
227 _customer: &BankingCustomer,
228 account: &BankAccount,
229 start_date: NaiveDate,
230 _end_date: NaiveDate,
231 sophistication: Sophistication,
232 ) -> Vec<BankTransaction> {
233 let mut transactions = Vec::new();
234
235 let (num_payments, amount_range) = match sophistication {
237 Sophistication::Basic => (1..2, 25_000.0..75_000.0),
238 Sophistication::Standard => (1..2, 50_000.0..150_000.0),
239 Sophistication::Professional => (1..3, 100_000.0..500_000.0),
240 Sophistication::Advanced => (2..3, 250_000.0..1_000_000.0),
241 Sophistication::StateLevel => (2..4, 500_000.0..5_000_000.0),
242 };
243
244 let payments = self.rng.gen_range(num_payments);
245 let scenario_id = format!("BEC-{:06}", self.rng.gen::<u32>());
246
247 let mut current_date = start_date;
249
250 for i in 0..payments {
251 let timestamp = self.random_timestamp(current_date);
252 let amount = self.rng.gen_range(amount_range.clone());
253
254 let (recipient, reference) = self.random_bec_recipient();
255
256 let txn = BankTransaction::new(
257 self.uuid_factory.next(),
258 account.account_id,
259 Decimal::from_f64_retain(amount).unwrap_or(Decimal::ZERO),
260 &account.currency,
261 Direction::Outbound,
262 TransactionChannel::Swift, TransactionCategory::Other,
264 CounterpartyRef::business(&recipient),
265 &reference,
266 timestamp,
267 )
268 .mark_suspicious(AmlTypology::BusinessEmailCompromise, &scenario_id)
269 .with_laundering_stage(LaunderingStage::Placement)
270 .with_scenario(&scenario_id, i as u32);
271
272 transactions.push(txn);
273
274 current_date += chrono::Duration::days(self.rng.gen_range(1..3));
276 }
277
278 transactions
279 }
280
281 pub fn generate_app_fraud(
285 &mut self,
286 _customer: &BankingCustomer,
287 account: &BankAccount,
288 start_date: NaiveDate,
289 end_date: NaiveDate,
290 sophistication: Sophistication,
291 ) -> Vec<BankTransaction> {
292 let mut transactions = Vec::new();
293
294 let (num_payments, amount_range, urgency_factor) = match sophistication {
296 Sophistication::Basic => (1..2, 500.0..5_000.0, 0.8),
297 Sophistication::Standard => (2..4, 1_000.0..15_000.0, 0.7),
298 Sophistication::Professional => (3..6, 5_000.0..50_000.0, 0.6),
299 Sophistication::Advanced => (4..8, 10_000.0..100_000.0, 0.5),
300 Sophistication::StateLevel => (5..10, 25_000.0..250_000.0, 0.4),
301 };
302
303 let payments = self.rng.gen_range(num_payments);
304 let scenario_id = format!("APP-{:06}", self.rng.gen::<u32>());
305
306 let scam_type = self.random_app_scam_type();
307 let available_days = (end_date - start_date).num_days().max(1);
308 let mut current_date = start_date;
309
310 for i in 0..payments {
311 let timestamp = self.random_timestamp(current_date);
312 let amount = self.rng.gen_range(amount_range.clone());
313
314 let txn = BankTransaction::new(
315 self.uuid_factory.next(),
316 account.account_id,
317 Decimal::from_f64_retain(amount).unwrap_or(Decimal::ZERO),
318 &account.currency,
319 Direction::Outbound,
320 TransactionChannel::RealTimePayment,
321 TransactionCategory::TransferOut,
322 CounterpartyRef::person(&scam_type.0),
323 &scam_type.1,
324 timestamp,
325 )
326 .mark_suspicious(AmlTypology::AuthorizedPushPayment, &scenario_id)
327 .with_laundering_stage(LaunderingStage::NotApplicable)
328 .with_scenario(&scenario_id, i as u32);
329
330 transactions.push(txn);
331
332 let base_interval = self.rng.gen_range(1..7) as f64;
334 let interval = (base_interval * urgency_factor).max(1.0) as i64;
335 current_date += chrono::Duration::days(interval);
336
337 if current_date > end_date || (current_date - start_date).num_days() > available_days {
338 break;
339 }
340 }
341
342 transactions
343 }
344
345 fn random_ato_extraction(
347 &mut self,
348 index: usize,
349 ) -> (
350 TransactionChannel,
351 TransactionCategory,
352 CounterpartyRef,
353 String,
354 ) {
355 let extractions = [
356 (
357 TransactionChannel::Wire,
358 TransactionCategory::TransferOut,
359 CounterpartyRef::person("External Account"),
360 "External transfer".to_string(),
361 ),
362 (
363 TransactionChannel::Ach,
364 TransactionCategory::TransferOut,
365 CounterpartyRef::person("Linked Account"),
366 "ACH transfer out".to_string(),
367 ),
368 (
369 TransactionChannel::CardNotPresent,
370 TransactionCategory::Shopping,
371 CounterpartyRef::merchant_by_name("Online Store", "5999"),
372 "Online purchase".to_string(),
373 ),
374 (
375 TransactionChannel::Atm,
376 TransactionCategory::AtmWithdrawal,
377 CounterpartyRef::atm("ATM"),
378 "ATM withdrawal".to_string(),
379 ),
380 (
381 TransactionChannel::CardNotPresent,
382 TransactionCategory::Shopping,
383 CounterpartyRef::merchant_by_name("Gift Card Vendor", "5815"),
384 "Gift card purchase".to_string(),
385 ),
386 ];
387
388 let idx = (index + self.rng.gen_range(0..extractions.len())) % extractions.len();
389 extractions[idx].clone()
390 }
391
392 fn random_fake_vendor(&mut self) -> (String, String) {
394 let vendors = [
395 ("ABC Consulting Services LLC", "Consulting services"),
396 ("Generic Supplies Inc", "Office supplies"),
397 ("Tech Solutions Partners", "IT services"),
398 ("Professional Services Group", "Professional fees"),
399 ("Strategic Advisory LLC", "Advisory services"),
400 ("Business Support Services", "Business support"),
401 ("Enterprise Solutions Corp", "Enterprise solutions"),
402 ("Market Research Associates", "Research services"),
403 ("Quality Assurance Partners", "QA services"),
404 ("Operational Excellence LLC", "Operations consulting"),
405 ];
406
407 let idx = self.rng.gen_range(0..vendors.len());
408 (vendors[idx].0.to_string(), vendors[idx].1.to_string())
409 }
410
411 fn random_bec_recipient(&mut self) -> (String, String) {
413 let recipients = [
414 (
415 "International Trade Co Ltd",
416 "URGENT: Updated payment details - Invoice payment",
417 ),
418 (
419 "Overseas Partner Holdings",
420 "Wire transfer - NEW BANK DETAILS",
421 ),
422 (
423 "Foreign Supplier Pte Ltd",
424 "Payment for goods - UPDATED ACCOUNT",
425 ),
426 (
427 "Global Trading Services",
428 "URGENT: Supplier payment - new instructions",
429 ),
430 (
431 "Asian Manufacturing Ltd",
432 "Invoice settlement - REVISED BANK",
433 ),
434 ];
435
436 let idx = self.rng.gen_range(0..recipients.len());
437 (recipients[idx].0.to_string(), recipients[idx].1.to_string())
438 }
439
440 fn random_app_scam_type(&mut self) -> (String, String) {
442 let scam_types = [
443 ("HMRC Tax Department", "Tax refund processing fee"),
444 ("Investment Advisor", "Investment opportunity"),
445 ("Tech Support Services", "Computer repair services"),
446 ("Romantic Partner", "Emergency funds needed"),
447 ("Police Officer", "Safe account transfer"),
448 ("Bank Security", "Account protection transfer"),
449 ("Lottery Commission", "Prize claim fee"),
450 ("Crypto Investment", "Cryptocurrency investment"),
451 ];
452
453 let idx = self.rng.gen_range(0..scam_types.len());
454 (scam_types[idx].0.to_string(), scam_types[idx].1.to_string())
455 }
456
457 fn random_timestamp(&mut self, date: NaiveDate) -> DateTime<Utc> {
459 let hour: u32 = self.rng.gen_range(6..23);
460 let minute: u32 = self.rng.gen_range(0..60);
461 let second: u32 = self.rng.gen_range(0..60);
462
463 date.and_hms_opt(hour, minute, second)
464 .map(|dt| DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc))
465 .unwrap_or_else(Utc::now)
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472 use uuid::Uuid;
473
474 fn create_test_customer() -> BankingCustomer {
475 BankingCustomer::new_retail(
476 Uuid::new_v4(),
477 "Test",
478 "User",
479 "US",
480 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
481 )
482 }
483
484 fn create_test_account(customer: &BankingCustomer) -> BankAccount {
485 BankAccount::new(
486 Uuid::new_v4(),
487 "****1234".to_string(),
488 datasynth_core::models::banking::BankAccountType::Checking,
489 customer.customer_id,
490 "USD",
491 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
492 )
493 }
494
495 #[test]
496 fn test_account_takeover_generation() {
497 let mut injector = FraudInjector::new(12345);
498 let customer = create_test_customer();
499 let account = create_test_account(&customer);
500
501 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
502 let end = NaiveDate::from_ymd_opt(2024, 1, 7).unwrap();
503
504 let transactions = injector.generate_account_takeover(
505 &customer,
506 &account,
507 start,
508 end,
509 Sophistication::Standard,
510 );
511
512 assert!(!transactions.is_empty());
513
514 for txn in &transactions {
516 assert!(txn.is_suspicious);
517 assert_eq!(txn.suspicion_reason, Some(AmlTypology::AccountTakeover));
518 assert_eq!(txn.direction, Direction::Outbound);
519 }
520 }
521
522 #[test]
523 fn test_fake_vendor_generation() {
524 let mut injector = FraudInjector::new(54321);
525 let customer = create_test_customer();
526 let account = create_test_account(&customer);
527
528 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
529 let end = NaiveDate::from_ymd_opt(2024, 6, 30).unwrap();
530
531 let transactions = injector.generate_fake_vendor(
532 &customer,
533 &account,
534 start,
535 end,
536 Sophistication::Professional,
537 );
538
539 assert!(!transactions.is_empty());
540 assert!(transactions.len() >= 4); for txn in &transactions {
543 assert!(txn.is_suspicious);
544 assert_eq!(txn.suspicion_reason, Some(AmlTypology::FakeVendor));
545 }
546 }
547
548 #[test]
549 fn test_bec_generation() {
550 let mut injector = FraudInjector::new(11111);
551 let customer = create_test_customer();
552 let account = create_test_account(&customer);
553
554 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
555 let end = NaiveDate::from_ymd_opt(2024, 1, 14).unwrap();
556
557 let transactions =
558 injector.generate_bec(&customer, &account, start, end, Sophistication::Advanced);
559
560 assert!(!transactions.is_empty());
561
562 for txn in &transactions {
563 assert!(txn.is_suspicious);
564 assert_eq!(
565 txn.suspicion_reason,
566 Some(AmlTypology::BusinessEmailCompromise)
567 );
568 assert_eq!(txn.channel, TransactionChannel::Swift);
570 }
571 }
572
573 #[test]
574 fn test_app_fraud_generation() {
575 let mut injector = FraudInjector::new(99999);
576 let customer = create_test_customer();
577 let account = create_test_account(&customer);
578
579 let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
580 let end = NaiveDate::from_ymd_opt(2024, 3, 31).unwrap();
581
582 let transactions =
583 injector.generate_app_fraud(&customer, &account, start, end, Sophistication::Standard);
584
585 assert!(!transactions.is_empty());
586
587 for txn in &transactions {
588 assert!(txn.is_suspicious);
589 assert_eq!(
590 txn.suspicion_reason,
591 Some(AmlTypology::AuthorizedPushPayment)
592 );
593 }
594 }
595}