1use chrono::NaiveDate;
4use datasynth_core::models::banking::{AmlTypology, LaunderingStage, Sophistication};
5use rand::prelude::*;
6use rand_chacha::ChaCha8Rng;
7use serde::{Deserialize, Serialize};
8
9use crate::models::{
10 AmlScenario, CaseNarrative, CaseRecommendation, RedFlag, RedFlagCategory, ViolatedExpectation,
11};
12
13pub struct NarrativeGenerator {
15 rng: ChaCha8Rng,
16}
17
18impl NarrativeGenerator {
19 pub fn new(seed: u64) -> Self {
21 Self {
22 rng: ChaCha8Rng::seed_from_u64(seed.wrapping_add(7000)),
23 }
24 }
25
26 pub fn generate(&mut self, scenario: &AmlScenario) -> CaseNarrative {
28 let storyline = self.generate_storyline(scenario);
29 let mut narrative = CaseNarrative::new(&storyline);
30
31 for evidence in self.generate_evidence_points(scenario) {
33 narrative.add_evidence(&evidence);
34 }
35
36 for ve in self.generate_violated_expectations(scenario) {
38 narrative.add_violated_expectation(ve);
39 }
40
41 let today = chrono::Utc::now().date_naive();
43 for rf in self.generate_red_flags(scenario, today) {
44 narrative.add_red_flag(rf);
45 }
46
47 let recommendation = self.recommend_action(scenario);
49 narrative.with_recommendation(recommendation)
50 }
51
52 fn generate_storyline(&mut self, scenario: &AmlScenario) -> String {
54 let typology_desc = self.typology_description(scenario.typology);
55 let sophistication_desc = self.sophistication_description(scenario.sophistication);
56 let stage_desc = self.stages_description(&scenario.stages);
57
58 format!(
59 "Investigation identified {} activity pattern involving {} sophistication level. \
60 The activity appears consistent with the {} stage(s) of money laundering. \
61 Analysis period: {} to {}. \
62 Total {} accounts involved in the suspicious activity cluster.",
63 typology_desc,
64 sophistication_desc,
65 stage_desc,
66 scenario.start_date.format("%Y-%m-%d"),
67 scenario.end_date.format("%Y-%m-%d"),
68 scenario.involved_accounts.len()
69 )
70 }
71
72 fn typology_description(&self, typology: AmlTypology) -> &'static str {
74 match typology {
75 AmlTypology::Structuring => "cash deposit structuring",
76 AmlTypology::Smurfing => "smurfing/structuring",
77 AmlTypology::CuckooSmurfing => "cuckoo smurfing",
78 AmlTypology::FunnelAccount => "funnel account aggregation",
79 AmlTypology::ConcentrationAccount => "concentration account abuse",
80 AmlTypology::PouchActivity => "pouch activity",
81 AmlTypology::Layering => "complex layering chain",
82 AmlTypology::RapidMovement => "rapid fund movement",
83 AmlTypology::ShellCompany => "shell company network",
84 AmlTypology::RoundTripping => "round-tripping fund movement",
85 AmlTypology::TradeBasedML => "trade-based money laundering",
86 AmlTypology::InvoiceManipulation => "invoice manipulation",
87 AmlTypology::MoneyMule => "money mule operation",
88 AmlTypology::RomanceScam => "romance scam activity",
89 AmlTypology::AdvanceFeeFraud => "advance fee fraud",
90 AmlTypology::RealEstateIntegration => "real estate-based integration",
91 AmlTypology::LuxuryGoods => "luxury goods integration",
92 AmlTypology::CasinoIntegration => "casino-based integration",
93 AmlTypology::CryptoIntegration => "cryptocurrency integration",
94 AmlTypology::AccountTakeover => "account takeover fraud",
95 AmlTypology::SyntheticIdentity => "synthetic identity fraud",
96 AmlTypology::FirstPartyFraud => "first-party fraud",
97 AmlTypology::AuthorizedPushPayment => "authorized push payment fraud",
98 AmlTypology::BusinessEmailCompromise => "business email compromise",
99 AmlTypology::FakeVendor => "fake vendor fraud",
100 AmlTypology::TerroristFinancing => "potential terrorist financing",
101 AmlTypology::SanctionsEvasion => "potential sanctions evasion",
102 AmlTypology::TaxEvasion => "tax evasion",
103 AmlTypology::HumanTrafficking => "human trafficking related",
104 AmlTypology::DrugTrafficking => "drug trafficking related",
105 AmlTypology::Corruption => "corruption/PEP-related activity",
106 AmlTypology::Custom(_) => "custom suspicious pattern",
107 }
108 }
109
110 fn sophistication_description(&self, sophistication: Sophistication) -> &'static str {
112 match sophistication {
113 Sophistication::Basic => "basic/amateur",
114 Sophistication::Standard => "standard/organized",
115 Sophistication::Professional => "professional/systematic",
116 Sophistication::Advanced => "advanced/coordinated network",
117 Sophistication::StateLevel => "state-level/highly sophisticated",
118 }
119 }
120
121 fn stages_description(&self, stages: &[LaunderingStage]) -> String {
123 if stages.is_empty() {
124 return "unclassified".to_string();
125 }
126
127 stages
128 .iter()
129 .map(|s| match s {
130 LaunderingStage::Placement => "placement",
131 LaunderingStage::Layering => "layering",
132 LaunderingStage::Integration => "integration",
133 LaunderingStage::NotApplicable => "N/A",
134 })
135 .collect::<Vec<_>>()
136 .join("/")
137 }
138
139 fn generate_evidence_points(&mut self, scenario: &AmlScenario) -> Vec<String> {
141 let mut points = Vec::new();
142
143 match scenario.typology {
145 AmlTypology::Structuring | AmlTypology::Smurfing => {
146 let deposit_count = self.rng.gen_range(5..20);
147 let threshold = 10_000;
148 points.push(format!(
149 "{} cash deposits below ${} reporting threshold within {} days",
150 deposit_count,
151 threshold,
152 (scenario.end_date - scenario.start_date).num_days()
153 ));
154 points.push("Deposits made at multiple branch locations".to_string());
155 points.push("Immediate consolidation transfer following deposits".to_string());
156 }
157 AmlTypology::FunnelAccount => {
158 let source_count = self.rng.gen_range(8..25);
159 points.push(format!(
160 "{} unrelated inbound transfers from different sources",
161 source_count
162 ));
163 points.push("Rapid outward transfers within 24-48 hours of receipt".to_string());
164 points.push("No business relationship with senders documented".to_string());
165 }
166 AmlTypology::Layering => {
167 let hop_count = self.rng.gen_range(3..8);
168 points.push(format!(
169 "Funds traced through {} intermediary accounts",
170 hop_count
171 ));
172 points.push("Systematic splitting and recombination of amounts".to_string());
173 points.push("Time delays inserted between hops to avoid detection".to_string());
174 }
175 AmlTypology::MoneyMule => {
176 points.push("New account with limited prior transaction history".to_string());
177 points.push("Pattern of receive-and-forward within short timeframe".to_string());
178 points.push(
179 "Cash withdrawals/wire transfers immediately following deposits".to_string(),
180 );
181 points.push("Small retention amount consistent with mule compensation".to_string());
182 }
183 _ => {
184 points.push("Unusual transaction pattern identified".to_string());
185 points.push("Activity inconsistent with stated account purpose".to_string());
186 }
187 }
188
189 if matches!(
191 scenario.sophistication,
192 Sophistication::Professional | Sophistication::Advanced | Sophistication::StateLevel
193 ) {
194 points.push("Use of intermediary entities to obscure beneficial ownership".to_string());
195 }
196 if matches!(
197 scenario.sophistication,
198 Sophistication::Advanced | Sophistication::StateLevel
199 ) {
200 points.push(
201 "Coordinated activity across multiple accounts and jurisdictions".to_string(),
202 );
203 }
204
205 points
206 }
207
208 fn generate_violated_expectations(
210 &mut self,
211 scenario: &AmlScenario,
212 ) -> Vec<ViolatedExpectation> {
213 let mut violations = Vec::new();
214
215 let expected_freq = self.rng.gen_range(5..15);
217 let actual_freq = self.rng.gen_range(25..100);
218 violations.push(ViolatedExpectation::new(
219 "Monthly transaction count",
220 &format!("{}", expected_freq),
221 &format!("{}", actual_freq),
222 (actual_freq as f64 - expected_freq as f64) / expected_freq as f64 * 100.0,
223 ));
224
225 if matches!(
227 scenario.typology,
228 AmlTypology::Structuring | AmlTypology::Smurfing | AmlTypology::MoneyMule
229 ) {
230 let expected_cash = self.rng.gen_range(5..15);
231 let actual_cash = self.rng.gen_range(40..80);
232 violations.push(ViolatedExpectation::new(
233 "Cash activity percentage",
234 &format!("{}%", expected_cash),
235 &format!("{}%", actual_cash),
236 (actual_cash - expected_cash) as f64,
237 ));
238 }
239
240 let expected_vol = self.rng.gen_range(5000..15000);
242 let actual_vol = self.rng.gen_range(50000..250000);
243 violations.push(ViolatedExpectation::new(
244 "Monthly transaction volume",
245 &format!("${}", expected_vol),
246 &format!("${}", actual_vol),
247 (actual_vol as f64 - expected_vol as f64) / expected_vol as f64 * 100.0,
248 ));
249
250 violations
251 }
252
253 fn generate_red_flags(&mut self, scenario: &AmlScenario, date: NaiveDate) -> Vec<RedFlag> {
255 let mut flags = Vec::new();
256
257 flags.push(RedFlag::new(
259 RedFlagCategory::ActivityPattern,
260 "Funds moved rapidly through account with minimal dwell time",
261 8,
262 date,
263 ));
264
265 match scenario.typology {
267 AmlTypology::Structuring | AmlTypology::Smurfing => {
268 flags.push(RedFlag::new(
269 RedFlagCategory::TransactionCharacteristic,
270 "Multiple transactions just below $10,000 reporting threshold",
271 9,
272 date,
273 ));
274 }
275 AmlTypology::FunnelAccount => {
276 flags.push(RedFlag::new(
277 RedFlagCategory::ThirdParty,
278 "Multiple unrelated senders with no apparent business connection",
279 7,
280 date,
281 ));
282 }
283 AmlTypology::MoneyMule => {
284 flags.push(RedFlag::new(
285 RedFlagCategory::AccountCharacteristic,
286 "New account with unusually high activity",
287 7,
288 date,
289 ));
290 flags.push(RedFlag::new(
291 RedFlagCategory::ActivityPattern,
292 "Immediate cash withdrawals following electronic deposits",
293 9,
294 date,
295 ));
296 }
297 _ => {}
298 }
299
300 if matches!(
302 scenario.sophistication,
303 Sophistication::Professional | Sophistication::Advanced | Sophistication::StateLevel
304 ) {
305 flags.push(RedFlag::new(
306 RedFlagCategory::CustomerBehavior,
307 "Complex ownership structure obscures beneficial owner",
308 6,
309 date,
310 ));
311 }
312
313 flags
314 }
315
316 fn recommend_action(&self, scenario: &AmlScenario) -> CaseRecommendation {
318 if matches!(
320 scenario.typology,
321 AmlTypology::SanctionsEvasion
322 | AmlTypology::TerroristFinancing
323 | AmlTypology::Corruption
324 ) {
325 return CaseRecommendation::ReportLawEnforcement;
326 }
327
328 if matches!(
330 scenario.sophistication,
331 Sophistication::Professional | Sophistication::Advanced | Sophistication::StateLevel
332 ) {
333 return CaseRecommendation::FileSar;
334 }
335
336 if scenario.typology == AmlTypology::MoneyMule {
338 return CaseRecommendation::CloseAccount;
339 }
340
341 if matches!(scenario.sophistication, Sophistication::Standard) {
343 return CaseRecommendation::FileSar;
344 }
345
346 CaseRecommendation::EnhancedMonitoring
348 }
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct ExportedNarrative {
354 pub case_id: String,
356 pub storyline: String,
358 pub evidence_points: Vec<String>,
360 pub violated_expectations: Vec<ExportedViolation>,
362 pub red_flags: Vec<ExportedRedFlag>,
364 pub recommendation: String,
366 pub metadata: NarrativeMetadata,
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct ExportedViolation {
373 pub expectation_type: String,
374 pub expected: String,
375 pub actual: String,
376 pub deviation_percent: f64,
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct ExportedRedFlag {
382 pub category: String,
383 pub description: String,
384 pub severity: u8,
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct NarrativeMetadata {
390 pub typology: String,
392 pub sophistication: String,
394 pub stages: Vec<String>,
396 pub start_date: String,
398 pub end_date: String,
400 pub account_count: usize,
402 pub detectability: f64,
404}
405
406impl ExportedNarrative {
407 pub fn from_scenario(scenario: &AmlScenario, narrative: &CaseNarrative) -> Self {
409 Self {
410 case_id: scenario.scenario_id.clone(),
411 storyline: narrative.storyline.clone(),
412 evidence_points: narrative.evidence_points.clone(),
413 violated_expectations: narrative
414 .violated_expectations
415 .iter()
416 .map(|ve| ExportedViolation {
417 expectation_type: ve.expectation_type.clone(),
418 expected: ve.expected_value.clone(),
419 actual: ve.actual_value.clone(),
420 deviation_percent: ve.deviation_percentage,
421 })
422 .collect(),
423 red_flags: narrative
424 .red_flags
425 .iter()
426 .map(|rf| ExportedRedFlag {
427 category: format!("{:?}", rf.category),
428 description: rf.description.clone(),
429 severity: rf.severity,
430 })
431 .collect(),
432 recommendation: format!("{:?}", narrative.recommendation),
433 metadata: NarrativeMetadata {
434 typology: format!("{:?}", scenario.typology),
435 sophistication: format!("{:?}", scenario.sophistication),
436 stages: scenario.stages.iter().map(|s| format!("{:?}", s)).collect(),
437 start_date: scenario.start_date.format("%Y-%m-%d").to_string(),
438 end_date: scenario.end_date.format("%Y-%m-%d").to_string(),
439 account_count: scenario.involved_accounts.len(),
440 detectability: scenario.detectability,
441 },
442 }
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449
450 #[test]
451 fn test_narrative_generation() {
452 let mut generator = NarrativeGenerator::new(12345);
453
454 let scenario = AmlScenario::new(
455 "TEST-001",
456 AmlTypology::Structuring,
457 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
458 NaiveDate::from_ymd_opt(2024, 1, 31).unwrap(),
459 )
460 .with_sophistication(Sophistication::Standard);
461
462 let narrative = generator.generate(&scenario);
463
464 assert!(!narrative.storyline.is_empty());
465 assert!(!narrative.evidence_points.is_empty());
466 assert!(!narrative.violated_expectations.is_empty());
467 assert!(!narrative.red_flags.is_empty());
468 }
469
470 #[test]
471 fn test_recommendation() {
472 let generator = NarrativeGenerator::new(12345);
473
474 let scenario = AmlScenario::new(
476 "TEST-001",
477 AmlTypology::Structuring,
478 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
479 NaiveDate::from_ymd_opt(2024, 1, 31).unwrap(),
480 )
481 .with_sophistication(Sophistication::Professional);
482
483 let rec = generator.recommend_action(&scenario);
484 assert_eq!(rec, CaseRecommendation::FileSar);
485
486 let scenario = AmlScenario::new(
488 "TEST-002",
489 AmlTypology::MoneyMule,
490 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
491 NaiveDate::from_ymd_opt(2024, 1, 31).unwrap(),
492 )
493 .with_sophistication(Sophistication::Basic);
494
495 let rec = generator.recommend_action(&scenario);
496 assert_eq!(rec, CaseRecommendation::CloseAccount);
497 }
498}