1use datasynth_core::models::audit::scots::{
24 CriticalPathStage, EstimationComplexity, ProcessingMethod, ScotSignificance,
25 ScotTransactionType, SignificantClassOfTransactions,
26};
27use datasynth_core::models::JournalEntry;
28use datasynth_core::utils::seeded_rng;
29use rand::RngExt;
30use rand_chacha::ChaCha8Rng;
31use rust_decimal::Decimal;
32use rust_decimal_macros::dec;
33
34#[derive(Debug, Clone)]
40struct ScotSpec {
41 scot_name: &'static str,
42 business_process: &'static str,
43 significance_level: ScotSignificance,
44 transaction_type: ScotTransactionType,
45 processing_method: ProcessingMethod,
46 account_prefixes: &'static [&'static str],
48 relevant_assertions: &'static [&'static str],
49 related_account_areas: &'static [&'static str],
50 estimation_complexity: Option<EstimationComplexity>,
51 stages: &'static [(&'static str, &'static str, bool, Option<&'static str>)],
53 requires_ic: bool,
55}
56
57static STANDARD_SCOTS: &[ScotSpec] = &[
59 ScotSpec {
60 scot_name: "Revenue — Product Sales",
61 business_process: "O2C",
62 significance_level: ScotSignificance::High,
63 transaction_type: ScotTransactionType::Routine,
64 processing_method: ProcessingMethod::SemiAutomated,
65 account_prefixes: &["4"],
66 relevant_assertions: &["Occurrence", "Accuracy", "Cutoff"],
67 related_account_areas: &["Revenue", "Trade Receivables"],
68 estimation_complexity: None,
69 stages: &[
70 ("Initiation", "Sales order created by customer or internal sales team", false, Some("C001")),
71 ("Recording", "System records SO upon credit check approval and customer master validation", true, Some("C002")),
72 ("Processing", "Automated posting to revenue accounts upon goods delivery confirmation", true, Some("C003")),
73 ("Reporting", "Revenue aggregated into income statement via automated GL summarisation", true, None),
74 ],
75 requires_ic: false,
76 },
77 ScotSpec {
78 scot_name: "Purchases — Procurement",
79 business_process: "P2P",
80 significance_level: ScotSignificance::High,
81 transaction_type: ScotTransactionType::Routine,
82 processing_method: ProcessingMethod::SemiAutomated,
83 account_prefixes: &["5", "6", "2"],
84 relevant_assertions: &["Occurrence", "Completeness", "Accuracy"],
85 related_account_areas: &["Cost of Sales", "Trade Payables", "Inventory"],
86 estimation_complexity: None,
87 stages: &[
88 ("Initiation", "Purchase requisition raised by department, approved per authority matrix", false, Some("C010")),
89 ("Recording", "System generates purchase order from approved requisition", true, Some("C011")),
90 ("Processing", "Three-way match (PO / GR / invoice) with system tolerance checks", true, Some("C012")),
91 ("Reporting", "Accounts payable and cost postings flow to trial balance automatically", true, None),
92 ],
93 requires_ic: false,
94 },
95 ScotSpec {
96 scot_name: "Payroll",
97 business_process: "H2R",
98 significance_level: ScotSignificance::Medium,
99 transaction_type: ScotTransactionType::Routine,
100 processing_method: ProcessingMethod::SemiAutomated,
101 account_prefixes: &["5", "6"],
102 relevant_assertions: &["Occurrence", "Accuracy", "Completeness"],
103 related_account_areas: &["Cost of Sales", "Accruals"],
104 estimation_complexity: None,
105 stages: &[
106 ("Initiation", "HR confirms headcount and compensation data for the period", false, Some("C020")),
107 ("Recording", "Payroll system calculates gross pay, deductions, and net pay per employee", true, Some("C021")),
108 ("Processing", "Payroll journal entries posted to GL; bank file generated for payment", true, Some("C022")),
109 ("Reporting", "Payroll costs aggregated by cost centre into management and financial reports", true, None),
110 ],
111 requires_ic: false,
112 },
113 ScotSpec {
114 scot_name: "Fixed Asset Additions",
115 business_process: "R2R",
116 significance_level: ScotSignificance::Medium,
117 transaction_type: ScotTransactionType::NonRoutine,
118 processing_method: ProcessingMethod::SemiAutomated,
119 account_prefixes: &["1"],
120 relevant_assertions: &["Existence", "Rights & Obligations", "Accuracy"],
121 related_account_areas: &["Fixed Assets"],
122 estimation_complexity: None,
123 stages: &[
124 ("Initiation", "Capital expenditure request raised, approved per capital authorisation policy", false, Some("C030")),
125 ("Recording", "Asset created in fixed asset register with cost, category, and useful life", false, Some("C031")),
126 ("Processing", "Capitalisation journal entry posted; asset available for depreciation", true, None),
127 ("Reporting", "Fixed assets reported on balance sheet net of accumulated depreciation", true, None),
128 ],
129 requires_ic: false,
130 },
131 ScotSpec {
132 scot_name: "Depreciation",
133 business_process: "R2R",
134 significance_level: ScotSignificance::Medium,
135 transaction_type: ScotTransactionType::Estimation,
136 processing_method: ProcessingMethod::FullyAutomated,
137 account_prefixes: &["1", "5", "6"],
138 relevant_assertions: &["Accuracy", "Valuation & Allocation"],
139 related_account_areas: &["Fixed Assets", "Cost of Sales"],
140 estimation_complexity: Some(EstimationComplexity::Simple),
141 stages: &[
142 ("Initiation", "Period-end close triggers automated depreciation run in asset module", true, Some("C040")),
143 ("Recording", "System calculates depreciation per asset based on cost, method, and useful life", true, Some("C041")),
144 ("Processing", "Depreciation journal entry posted to GL (Dr: Dep Expense / Cr: Accum Dep)", true, None),
145 ("Reporting", "Depreciation charge flows to income statement; net book value updated on balance sheet", true, None),
146 ],
147 requires_ic: false,
148 },
149 ScotSpec {
150 scot_name: "Tax Provision",
151 business_process: "R2R",
152 significance_level: ScotSignificance::High,
153 transaction_type: ScotTransactionType::Estimation,
154 processing_method: ProcessingMethod::Manual,
155 account_prefixes: &["3", "2"],
156 relevant_assertions: &["Accuracy", "Valuation & Allocation", "Completeness (Balance)"],
157 related_account_areas: &["Tax", "Equity"],
158 estimation_complexity: Some(EstimationComplexity::Complex),
159 stages: &[
160 ("Initiation", "Tax team prepares provision calculation based on pre-tax income and timing differences", false, Some("C050")),
161 ("Recording", "Current and deferred tax spreadsheet reviewed and approved by tax director", false, Some("C051")),
162 ("Processing", "Manual journal entry posted for current tax payable and deferred tax asset/liability", false, Some("C052")),
163 ("Reporting", "Tax charge reported in income statement; deferred tax balance on balance sheet", true, None),
164 ],
165 requires_ic: false,
166 },
167 ScotSpec {
168 scot_name: "ECL / Bad Debt Provision",
169 business_process: "R2R",
170 significance_level: ScotSignificance::High,
171 transaction_type: ScotTransactionType::Estimation,
172 processing_method: ProcessingMethod::Manual,
173 account_prefixes: &["1"],
174 relevant_assertions: &["Valuation & Allocation", "Completeness (Balance)"],
175 related_account_areas: &["Trade Receivables", "Provisions"],
176 estimation_complexity: Some(EstimationComplexity::Moderate),
177 stages: &[
178 ("Initiation", "Finance team reviews AR aging and customer credit risk at period end", false, Some("C060")),
179 ("Recording", "ECL / provision matrix applied; individual customer assessments for significant debtors", false, Some("C061")),
180 ("Processing", "Provision journal entry posted (Dr: Bad Debt Expense / Cr: Provision for Doubtful Debts)", false, None),
181 ("Reporting", "Net receivables (after provision) reported on balance sheet; bad debt expense in P&L", true, None),
182 ],
183 requires_ic: false,
184 },
185 ScotSpec {
186 scot_name: "Period-End Adjustments",
187 business_process: "R2R",
188 significance_level: ScotSignificance::Medium,
189 transaction_type: ScotTransactionType::NonRoutine,
190 processing_method: ProcessingMethod::Manual,
191 account_prefixes: &["3", "4", "5", "6"],
192 relevant_assertions: &["Accuracy", "Cutoff", "Occurrence"],
193 related_account_areas: &["Accruals", "Revenue", "Cost of Sales"],
194 estimation_complexity: None,
195 stages: &[
196 ("Initiation", "Close checklist triggers accrual and prepayment review at period end", false, None),
197 ("Recording", "Preparer calculates and documents accruals based on invoices received / services incurred", false, Some("C070")),
198 ("Processing", "Manual journal entries posted and reviewed by controller before period close", false, Some("C071")),
199 ("Reporting", "Accruals and prepayments reported in financial statements per cut-off policy", true, None),
200 ],
201 requires_ic: false,
202 },
203 ScotSpec {
204 scot_name: "Intercompany Transactions",
205 business_process: "IC",
206 significance_level: ScotSignificance::High,
207 transaction_type: ScotTransactionType::Routine,
208 processing_method: ProcessingMethod::SemiAutomated,
209 account_prefixes: &["1", "2", "3", "4"],
210 relevant_assertions: &["Occurrence", "Accuracy", "Completeness"],
211 related_account_areas: &["Related Parties", "Revenue", "Cost of Sales"],
212 estimation_complexity: None,
213 stages: &[
214 ("Initiation", "IC transactions initiated by business units per transfer pricing agreements", false, Some("C080")),
215 ("Recording", "IC netting system captures matching transactions across entities", true, Some("C081")),
216 ("Processing", "Automated matching engine reconciles IC balances; unmatched items flagged for resolution", true, Some("C082")),
217 ("Reporting", "IC balances eliminated on consolidation; residual differences reported", true, None),
218 ],
219 requires_ic: true,
220 },
221];
222
223#[derive(Debug, Clone)]
229pub struct ScotsGeneratorConfig {
230 pub intercompany_enabled: bool,
232 pub min_volume: usize,
234 pub max_volume: usize,
236}
237
238impl Default for ScotsGeneratorConfig {
239 fn default() -> Self {
240 Self {
241 intercompany_enabled: false,
242 min_volume: 50,
243 max_volume: 10_000,
244 }
245 }
246}
247
248pub struct ScotsGenerator {
254 rng: ChaCha8Rng,
255 config: ScotsGeneratorConfig,
256}
257
258impl ScotsGenerator {
259 pub fn new(seed: u64) -> Self {
261 Self {
262 rng: seeded_rng(seed, 0x315A), config: ScotsGeneratorConfig::default(),
264 }
265 }
266
267 pub fn with_config(seed: u64, config: ScotsGeneratorConfig) -> Self {
269 Self {
270 rng: seeded_rng(seed, 0x315A),
271 config,
272 }
273 }
274
275 pub fn generate_for_entity(
280 &mut self,
281 entity_code: &str,
282 entries: &[JournalEntry],
283 ) -> Vec<SignificantClassOfTransactions> {
284 let mut scots = Vec::new();
285
286 for spec in STANDARD_SCOTS {
287 if spec.requires_ic && !self.config.intercompany_enabled {
288 continue;
289 }
290
291 let scot = self.build_scot(entity_code, spec, entries);
292 scots.push(scot);
293 }
294
295 scots
296 }
297
298 fn build_scot(
300 &mut self,
301 entity_code: &str,
302 spec: &ScotSpec,
303 entries: &[JournalEntry],
304 ) -> SignificantClassOfTransactions {
305 let (volume, monetary_value) = self.extract_volume_and_value(entity_code, spec, entries);
306
307 let id = format!(
308 "SCOT-{}-{}",
309 entity_code,
310 spec.scot_name
311 .replace([' ', '—', '-', '/'], "_")
312 .to_uppercase(),
313 );
314
315 let critical_path = spec
316 .stages
317 .iter()
318 .map(|(name, desc, is_auto, ctrl_id)| CriticalPathStage {
319 stage_name: name.to_string(),
320 description: desc.to_string(),
321 is_automated: *is_auto,
322 key_control_id: ctrl_id.map(|c| format!("{entity_code}-{c}")),
323 })
324 .collect();
325
326 SignificantClassOfTransactions {
327 id,
328 entity_code: entity_code.to_string(),
329 scot_name: spec.scot_name.to_string(),
330 business_process: spec.business_process.to_string(),
331 significance_level: spec.significance_level,
332 transaction_type: spec.transaction_type,
333 processing_method: spec.processing_method,
334 volume,
335 monetary_value,
336 critical_path,
337 relevant_assertions: spec
338 .relevant_assertions
339 .iter()
340 .map(|s| s.to_string())
341 .collect(),
342 related_account_areas: spec
343 .related_account_areas
344 .iter()
345 .map(|s| s.to_string())
346 .collect(),
347 estimation_complexity: spec.estimation_complexity,
348 }
349 }
350
351 fn extract_volume_and_value(
356 &mut self,
357 entity_code: &str,
358 spec: &ScotSpec,
359 entries: &[JournalEntry],
360 ) -> (usize, Decimal) {
361 let matching_entries: Vec<&JournalEntry> = entries
363 .iter()
364 .filter(|e| e.company_code() == entity_code)
365 .filter(|e| {
366 e.lines.iter().any(|l| {
367 spec.account_prefixes
368 .iter()
369 .any(|&p| l.account_code.starts_with(p))
370 })
371 })
372 .collect();
373
374 if !matching_entries.is_empty() {
375 let volume = matching_entries.len();
376 let value: Decimal = matching_entries
377 .iter()
378 .flat_map(|e| e.lines.iter())
379 .filter(|l| {
380 spec.account_prefixes
381 .iter()
382 .any(|&p| l.account_code.starts_with(p))
383 })
384 .map(|l| l.debit_amount + l.credit_amount)
385 .sum::<Decimal>()
386 / dec!(2); (volume.max(1), value.max(dec!(1)))
388 } else {
389 let volume = self
391 .rng
392 .random_range(self.config.min_volume..=self.config.max_volume);
393 let avg_txn = Decimal::from(self.rng.random_range(1_000_i64..=50_000_i64));
394 let value = (Decimal::from(volume as i64) * avg_txn).round_dp(0);
395 (volume, value.max(dec!(1)))
396 }
397 }
398}
399
400#[cfg(test)]
405mod tests {
406 use super::*;
407
408 #[test]
409 fn generates_standard_scots_without_ic() {
410 let mut gen = ScotsGenerator::new(42);
411 let scots = gen.generate_for_entity("C001", &[]);
412 assert_eq!(
414 scots.len(),
415 8,
416 "Expected 8 non-IC SCOTs, got {}",
417 scots.len()
418 );
419 }
420
421 #[test]
422 fn ic_scot_added_when_enabled() {
423 let config = ScotsGeneratorConfig {
424 intercompany_enabled: true,
425 ..ScotsGeneratorConfig::default()
426 };
427 let mut gen = ScotsGenerator::with_config(42, config);
428 let scots = gen.generate_for_entity("C001", &[]);
429 assert_eq!(scots.len(), 9, "Expected 9 SCOTs including IC");
430
431 let ic_scot = scots.iter().find(|s| s.business_process == "IC");
432 assert!(
433 ic_scot.is_some(),
434 "IC SCOT should be present when IC is enabled"
435 );
436 }
437
438 #[test]
439 fn estimation_scots_have_complexity() {
440 let mut gen = ScotsGenerator::new(42);
441 let scots = gen.generate_for_entity("C001", &[]);
442
443 let estimation_scots: Vec<_> = scots
444 .iter()
445 .filter(|s| s.transaction_type == ScotTransactionType::Estimation)
446 .collect();
447
448 assert!(!estimation_scots.is_empty(), "Should have estimation SCOTs");
449 for s in &estimation_scots {
450 assert!(
451 s.estimation_complexity.is_some(),
452 "Estimation SCOT '{}' must have estimation_complexity",
453 s.scot_name
454 );
455 }
456 }
457
458 #[test]
459 fn non_estimation_scots_have_no_complexity() {
460 let mut gen = ScotsGenerator::new(42);
461 let scots = gen.generate_for_entity("C001", &[]);
462
463 for s in &scots {
464 if s.transaction_type != ScotTransactionType::Estimation {
465 assert!(
466 s.estimation_complexity.is_none(),
467 "Non-estimation SCOT '{}' should not have estimation_complexity",
468 s.scot_name
469 );
470 }
471 }
472 }
473
474 #[test]
475 fn all_scots_have_four_critical_path_stages() {
476 let mut gen = ScotsGenerator::new(42);
477 let scots = gen.generate_for_entity("C001", &[]);
478
479 for s in &scots {
480 assert_eq!(
481 s.critical_path.len(),
482 4,
483 "SCOT '{}' should have exactly 4 critical path stages",
484 s.scot_name
485 );
486 }
487 }
488
489 #[test]
490 fn scot_ids_are_unique() {
491 let mut gen = ScotsGenerator::new(42);
492 let scots = gen.generate_for_entity("C001", &[]);
493
494 let ids: std::collections::HashSet<&str> = scots.iter().map(|s| s.id.as_str()).collect();
495 assert_eq!(ids.len(), scots.len(), "SCOT IDs should be unique");
496 }
497
498 #[test]
499 fn volume_and_value_are_positive() {
500 let mut gen = ScotsGenerator::new(42);
501 let scots = gen.generate_for_entity("C001", &[]);
502
503 for s in &scots {
504 assert!(s.volume > 0, "SCOT '{}' volume must be > 0", s.scot_name);
505 assert!(
506 s.monetary_value > Decimal::ZERO,
507 "SCOT '{}' monetary_value must be > 0",
508 s.scot_name
509 );
510 }
511 }
512
513 #[test]
514 fn tax_provision_is_high_significance_estimation() {
515 let mut gen = ScotsGenerator::new(42);
516 let scots = gen.generate_for_entity("C001", &[]);
517
518 let tax = scots
519 .iter()
520 .find(|s| s.scot_name == "Tax Provision")
521 .unwrap();
522 assert_eq!(tax.significance_level, ScotSignificance::High);
523 assert_eq!(tax.transaction_type, ScotTransactionType::Estimation);
524 assert_eq!(
525 tax.estimation_complexity,
526 Some(EstimationComplexity::Complex)
527 );
528 }
529
530 #[test]
531 fn revenue_scot_is_o2c_routine_high() {
532 let mut gen = ScotsGenerator::new(42);
533 let scots = gen.generate_for_entity("C001", &[]);
534
535 let rev = scots
536 .iter()
537 .find(|s| s.scot_name == "Revenue — Product Sales")
538 .unwrap();
539 assert_eq!(rev.business_process, "O2C");
540 assert_eq!(rev.transaction_type, ScotTransactionType::Routine);
541 assert_eq!(rev.significance_level, ScotSignificance::High);
542 }
543}