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