1use crate::schema::GeneratorConfig;
4use datasynth_core::error::{SynthError, SynthResult};
5
6const MAX_PERIOD_MONTHS: u32 = 120;
8
9pub fn validate_config(config: &GeneratorConfig) -> SynthResult<()> {
11 validate_global_settings(config)?;
12 validate_companies(config)?;
13 validate_transactions(config)?;
14 validate_output(config)?;
15 validate_fraud(config)?;
16 validate_internal_controls(config)?;
17 validate_approval(config)?;
18 validate_master_data(config)?;
19 validate_document_flows(config)?;
20 validate_intercompany(config)?;
21 validate_balance(config)?;
22 validate_accounting_standards(config)?;
23 validate_audit_standards(config)?;
24 Ok(())
25}
26
27fn validate_global_settings(config: &GeneratorConfig) -> SynthResult<()> {
29 if config.global.period_months == 0 {
30 return Err(SynthError::validation(
31 "period_months must be greater than 0",
32 ));
33 }
34 if config.global.period_months > MAX_PERIOD_MONTHS {
35 return Err(SynthError::validation(format!(
36 "period_months must be at most {} (10 years), got {}",
37 MAX_PERIOD_MONTHS, config.global.period_months
38 )));
39 }
40 Ok(())
41}
42
43fn validate_companies(config: &GeneratorConfig) -> SynthResult<()> {
45 if config.companies.is_empty() {
46 return Err(SynthError::validation(
47 "At least one company must be configured",
48 ));
49 }
50
51 for company in &config.companies {
52 if company.code.is_empty() {
53 return Err(SynthError::validation("Company code cannot be empty"));
54 }
55 if company.currency.len() != 3 {
56 return Err(SynthError::validation(format!(
57 "Invalid currency code '{}' for company '{}'",
58 company.currency, company.code
59 )));
60 }
61 if company.volume_weight < 0.0 {
62 return Err(SynthError::validation(format!(
63 "volume_weight must be non-negative for company '{}'",
64 company.code
65 )));
66 }
67 }
68 Ok(())
69}
70
71fn validate_transactions(config: &GeneratorConfig) -> SynthResult<()> {
73 let line_dist = &config.transactions.line_item_distribution;
75 if let Err(e) = line_dist.validate() {
76 return Err(SynthError::validation(e));
77 }
78
79 let source_sum = config.transactions.source_distribution.manual
81 + config.transactions.source_distribution.automated
82 + config.transactions.source_distribution.recurring
83 + config.transactions.source_distribution.adjustment;
84 if (source_sum - 1.0).abs() > 0.01 {
85 return Err(SynthError::validation(format!(
86 "Source distribution must sum to 1.0, got {}",
87 source_sum
88 )));
89 }
90
91 let bp_sum = config.business_processes.o2c_weight
93 + config.business_processes.p2p_weight
94 + config.business_processes.r2r_weight
95 + config.business_processes.h2r_weight
96 + config.business_processes.a2r_weight;
97 if (bp_sum - 1.0).abs() > 0.01 {
98 return Err(SynthError::validation(format!(
99 "Business process weights must sum to 1.0, got {}",
100 bp_sum
101 )));
102 }
103
104 let tolerance = config.transactions.benford.tolerance;
106 if !(0.0..=1.0).contains(&tolerance) {
107 return Err(SynthError::validation(format!(
108 "benford.tolerance must be between 0.0 and 1.0, got {}",
109 tolerance
110 )));
111 }
112
113 Ok(())
114}
115
116fn validate_output(config: &GeneratorConfig) -> SynthResult<()> {
118 let level = config.output.compression.level;
119 if config.output.compression.enabled && !(1..=9).contains(&level) {
120 return Err(SynthError::validation(format!(
121 "compression.level must be between 1 and 9, got {}",
122 level
123 )));
124 }
125
126 if config.output.batch_size == 0 {
127 return Err(SynthError::validation("batch_size must be greater than 0"));
128 }
129
130 Ok(())
131}
132
133fn validate_fraud(config: &GeneratorConfig) -> SynthResult<()> {
135 if !config.fraud.enabled {
136 return Ok(());
137 }
138
139 if config.fraud.fraud_rate < 0.0 || config.fraud.fraud_rate > 1.0 {
140 return Err(SynthError::validation(
141 "fraud_rate must be between 0.0 and 1.0",
142 ));
143 }
144
145 if config.fraud.clustering_factor < 0.0 {
146 return Err(SynthError::validation(
147 "clustering_factor must be non-negative",
148 ));
149 }
150
151 let thresholds = &config.fraud.approval_thresholds;
153 for i in 1..thresholds.len() {
154 if thresholds[i] <= thresholds[i - 1] {
155 return Err(SynthError::validation(format!(
156 "fraud.approval_thresholds must be in strictly ascending order: {} is not greater than {}",
157 thresholds[i], thresholds[i - 1]
158 )));
159 }
160 }
161
162 let dist = &config.fraud.fraud_type_distribution;
164 let sum = dist.suspense_account_abuse
165 + dist.fictitious_transaction
166 + dist.revenue_manipulation
167 + dist.expense_capitalization
168 + dist.split_transaction
169 + dist.timing_anomaly
170 + dist.unauthorized_access
171 + dist.duplicate_payment;
172 if (sum - 1.0).abs() > 0.01 {
173 return Err(SynthError::validation(format!(
174 "fraud_type_distribution must sum to 1.0, got {}",
175 sum
176 )));
177 }
178
179 Ok(())
180}
181
182fn validate_internal_controls(config: &GeneratorConfig) -> SynthResult<()> {
184 if !config.internal_controls.enabled {
185 return Ok(());
186 }
187
188 let exception_rate = config.internal_controls.exception_rate;
189 if !(0.0..=1.0).contains(&exception_rate) {
190 return Err(SynthError::validation(format!(
191 "exception_rate must be between 0.0 and 1.0, got {}",
192 exception_rate
193 )));
194 }
195
196 let sod_rate = config.internal_controls.sod_violation_rate;
197 if !(0.0..=1.0).contains(&sod_rate) {
198 return Err(SynthError::validation(format!(
199 "sod_violation_rate must be between 0.0 and 1.0, got {}",
200 sod_rate
201 )));
202 }
203
204 if config.internal_controls.sox_materiality_threshold < 0.0 {
205 return Err(SynthError::validation(
206 "sox_materiality_threshold must be non-negative",
207 ));
208 }
209
210 Ok(())
211}
212
213fn validate_approval(config: &GeneratorConfig) -> SynthResult<()> {
215 if !config.approval.enabled {
216 return Ok(());
217 }
218
219 if config.approval.auto_approve_threshold < 0.0 {
220 return Err(SynthError::validation(
221 "auto_approve_threshold must be non-negative",
222 ));
223 }
224
225 let rejection_rate = config.approval.rejection_rate;
226 if !(0.0..=1.0).contains(&rejection_rate) {
227 return Err(SynthError::validation(format!(
228 "rejection_rate must be between 0.0 and 1.0, got {}",
229 rejection_rate
230 )));
231 }
232
233 let revision_rate = config.approval.revision_rate;
234 if !(0.0..=1.0).contains(&revision_rate) {
235 return Err(SynthError::validation(format!(
236 "revision_rate must be between 0.0 and 1.0, got {}",
237 revision_rate
238 )));
239 }
240
241 if rejection_rate + revision_rate > 1.0 {
243 return Err(SynthError::validation(format!(
244 "rejection_rate + revision_rate must not exceed 1.0, got {}",
245 rejection_rate + revision_rate
246 )));
247 }
248
249 let thresholds = &config.approval.thresholds;
251 for i in 1..thresholds.len() {
252 if thresholds[i].amount <= thresholds[i - 1].amount {
253 return Err(SynthError::validation(format!(
254 "approval.thresholds must have strictly ascending amounts: {} is not greater than {}",
255 thresholds[i].amount, thresholds[i - 1].amount
256 )));
257 }
258 }
259
260 Ok(())
261}
262
263fn validate_master_data(config: &GeneratorConfig) -> SynthResult<()> {
265 let vendor_ic = config.master_data.vendors.intercompany_percent;
267 if !(0.0..=1.0).contains(&vendor_ic) {
268 return Err(SynthError::validation(format!(
269 "vendors.intercompany_percent must be between 0.0 and 1.0, got {}",
270 vendor_ic
271 )));
272 }
273
274 let customer_ic = config.master_data.customers.intercompany_percent;
276 if !(0.0..=1.0).contains(&customer_ic) {
277 return Err(SynthError::validation(format!(
278 "customers.intercompany_percent must be between 0.0 and 1.0, got {}",
279 customer_ic
280 )));
281 }
282
283 let bom_percent = config.master_data.materials.bom_percent;
285 if !(0.0..=1.0).contains(&bom_percent) {
286 return Err(SynthError::validation(format!(
287 "materials.bom_percent must be between 0.0 and 1.0, got {}",
288 bom_percent
289 )));
290 }
291
292 let fully_dep = config.master_data.fixed_assets.fully_depreciated_percent;
294 if !(0.0..=1.0).contains(&fully_dep) {
295 return Err(SynthError::validation(format!(
296 "fixed_assets.fully_depreciated_percent must be between 0.0 and 1.0, got {}",
297 fully_dep
298 )));
299 }
300
301 Ok(())
302}
303
304fn validate_document_flows(config: &GeneratorConfig) -> SynthResult<()> {
306 let p2p = &config.document_flows.p2p;
308 if p2p.enabled {
309 validate_rate("p2p.three_way_match_rate", p2p.three_way_match_rate)?;
310 validate_rate("p2p.partial_delivery_rate", p2p.partial_delivery_rate)?;
311 validate_rate("p2p.price_variance_rate", p2p.price_variance_rate)?;
312 validate_rate("p2p.quantity_variance_rate", p2p.quantity_variance_rate)?;
313
314 if p2p.max_price_variance_percent < 0.0 {
315 return Err(SynthError::validation(
316 "p2p.max_price_variance_percent must be non-negative",
317 ));
318 }
319
320 validate_p2p_payment_behavior(&p2p.payment_behavior)?;
322 }
323
324 let o2c = &config.document_flows.o2c;
326 if o2c.enabled {
327 validate_rate(
328 "o2c.credit_check_failure_rate",
329 o2c.credit_check_failure_rate,
330 )?;
331 validate_rate("o2c.partial_shipment_rate", o2c.partial_shipment_rate)?;
332 validate_rate("o2c.return_rate", o2c.return_rate)?;
333 validate_rate("o2c.bad_debt_rate", o2c.bad_debt_rate)?;
334
335 validate_rate(
337 "o2c.cash_discount.eligible_rate",
338 o2c.cash_discount.eligible_rate,
339 )?;
340 validate_rate("o2c.cash_discount.taken_rate", o2c.cash_discount.taken_rate)?;
341 validate_rate(
342 "o2c.cash_discount.discount_percent",
343 o2c.cash_discount.discount_percent,
344 )?;
345
346 validate_o2c_payment_behavior(&o2c.payment_behavior)?;
348 }
349
350 Ok(())
351}
352
353fn validate_p2p_payment_behavior(
355 config: &crate::schema::P2PPaymentBehaviorConfig,
356) -> SynthResult<()> {
357 validate_rate(
358 "p2p.payment_behavior.late_payment_rate",
359 config.late_payment_rate,
360 )?;
361 validate_rate(
362 "p2p.payment_behavior.partial_payment_rate",
363 config.partial_payment_rate,
364 )?;
365 validate_rate(
366 "p2p.payment_behavior.payment_correction_rate",
367 config.payment_correction_rate,
368 )?;
369
370 let late_dist = &config.late_payment_days_distribution;
372 let late_sum = late_dist.slightly_late_1_to_7
373 + late_dist.late_8_to_14
374 + late_dist.very_late_15_to_30
375 + late_dist.severely_late_31_to_60
376 + late_dist.extremely_late_over_60;
377 if (late_sum - 1.0).abs() > 0.01 {
378 return Err(SynthError::validation(format!(
379 "p2p.payment_behavior.late_payment_days_distribution must sum to 1.0, got {}",
380 late_sum
381 )));
382 }
383
384 Ok(())
385}
386
387fn validate_o2c_payment_behavior(
389 config: &crate::schema::O2CPaymentBehaviorConfig,
390) -> SynthResult<()> {
391 let dunning = &config.dunning;
393 if dunning.enabled {
394 validate_rate(
395 "o2c.payment_behavior.dunning.dunning_block_rate",
396 dunning.dunning_block_rate,
397 )?;
398
399 if dunning.level_2_days_overdue <= dunning.level_1_days_overdue {
401 return Err(SynthError::validation(
402 "dunning.level_2_days_overdue must be greater than level_1_days_overdue",
403 ));
404 }
405 if dunning.level_3_days_overdue <= dunning.level_2_days_overdue {
406 return Err(SynthError::validation(
407 "dunning.level_3_days_overdue must be greater than level_2_days_overdue",
408 ));
409 }
410 if dunning.collection_days_overdue <= dunning.level_3_days_overdue {
411 return Err(SynthError::validation(
412 "dunning.collection_days_overdue must be greater than level_3_days_overdue",
413 ));
414 }
415
416 let rates = &dunning.payment_after_dunning_rates;
418 let rates_sum = rates.after_level_1
419 + rates.after_level_2
420 + rates.after_level_3
421 + rates.during_collection
422 + rates.never_pay;
423 if (rates_sum - 1.0).abs() > 0.01 {
424 return Err(SynthError::validation(format!(
425 "dunning.payment_after_dunning_rates must sum to 1.0, got {}",
426 rates_sum
427 )));
428 }
429 }
430
431 let partial = &config.partial_payments;
433 validate_rate("o2c.payment_behavior.partial_payments.rate", partial.rate)?;
434 let partial_dist = &partial.percentage_distribution;
435 let partial_sum = partial_dist.pay_25_percent
436 + partial_dist.pay_50_percent
437 + partial_dist.pay_75_percent
438 + partial_dist.pay_random_percent;
439 if (partial_sum - 1.0).abs() > 0.01 {
440 return Err(SynthError::validation(format!(
441 "partial_payments.percentage_distribution must sum to 1.0, got {}",
442 partial_sum
443 )));
444 }
445
446 let short = &config.short_payments;
448 validate_rate("o2c.payment_behavior.short_payments.rate", short.rate)?;
449 validate_rate(
450 "o2c.payment_behavior.short_payments.max_short_percent",
451 short.max_short_percent,
452 )?;
453 let short_dist = &short.reason_distribution;
454 let short_sum = short_dist.pricing_dispute
455 + short_dist.quality_issue
456 + short_dist.quantity_discrepancy
457 + short_dist.unauthorized_deduction
458 + short_dist.incorrect_discount;
459 if (short_sum - 1.0).abs() > 0.01 {
460 return Err(SynthError::validation(format!(
461 "short_payments.reason_distribution must sum to 1.0, got {}",
462 short_sum
463 )));
464 }
465
466 validate_rate(
468 "o2c.payment_behavior.on_account_payments.rate",
469 config.on_account_payments.rate,
470 )?;
471
472 let corrections = &config.payment_corrections;
474 validate_rate(
475 "o2c.payment_behavior.payment_corrections.rate",
476 corrections.rate,
477 )?;
478 let corr_dist = &corrections.type_distribution;
479 let corr_sum = corr_dist.nsf
480 + corr_dist.chargeback
481 + corr_dist.wrong_amount
482 + corr_dist.wrong_customer
483 + corr_dist.duplicate_payment;
484 if (corr_sum - 1.0).abs() > 0.01 {
485 return Err(SynthError::validation(format!(
486 "payment_corrections.type_distribution must sum to 1.0, got {}",
487 corr_sum
488 )));
489 }
490
491 Ok(())
492}
493
494fn validate_intercompany(config: &GeneratorConfig) -> SynthResult<()> {
496 if !config.intercompany.enabled {
497 return Ok(());
498 }
499
500 validate_rate(
501 "intercompany.ic_transaction_rate",
502 config.intercompany.ic_transaction_rate,
503 )?;
504
505 if config.intercompany.markup_percent < 0.0 {
506 return Err(SynthError::validation(
507 "intercompany.markup_percent must be non-negative",
508 ));
509 }
510
511 let dist = &config.intercompany.transaction_type_distribution;
513 let sum = dist.goods_sale
514 + dist.service_provided
515 + dist.loan
516 + dist.dividend
517 + dist.management_fee
518 + dist.royalty
519 + dist.cost_sharing;
520 if (sum - 1.0).abs() > 0.01 {
521 return Err(SynthError::validation(format!(
522 "intercompany.transaction_type_distribution must sum to 1.0, got {}",
523 sum
524 )));
525 }
526
527 Ok(())
528}
529
530fn validate_balance(config: &GeneratorConfig) -> SynthResult<()> {
532 let balance = &config.balance;
533
534 if balance.target_gross_margin < 0.0 || balance.target_gross_margin > 1.0 {
535 return Err(SynthError::validation(format!(
536 "target_gross_margin must be between 0.0 and 1.0, got {}",
537 balance.target_gross_margin
538 )));
539 }
540
541 if balance.target_current_ratio < 0.0 {
542 return Err(SynthError::validation(
543 "target_current_ratio must be non-negative",
544 ));
545 }
546
547 if balance.target_debt_to_equity < 0.0 {
548 return Err(SynthError::validation(
549 "target_debt_to_equity must be non-negative",
550 ));
551 }
552
553 Ok(())
554}
555
556fn validate_rate(field_name: &str, value: f64) -> SynthResult<()> {
558 if !(0.0..=1.0).contains(&value) {
559 return Err(SynthError::validation(format!(
560 "{} must be between 0.0 and 1.0, got {}",
561 field_name, value
562 )));
563 }
564 Ok(())
565}
566
567fn validate_accounting_standards(config: &GeneratorConfig) -> SynthResult<()> {
569 let standards = &config.accounting_standards;
570
571 if !standards.enabled {
572 return Ok(());
573 }
574
575 if standards.revenue_recognition.enabled {
577 let rev = &standards.revenue_recognition;
578
579 if rev.avg_obligations_per_contract < 1.0 {
580 return Err(SynthError::validation(
581 "avg_obligations_per_contract must be >= 1.0",
582 ));
583 }
584
585 validate_rate(
586 "revenue_recognition.variable_consideration_rate",
587 rev.variable_consideration_rate,
588 )?;
589
590 validate_rate(
591 "revenue_recognition.over_time_recognition_rate",
592 rev.over_time_recognition_rate,
593 )?;
594 }
595
596 if standards.leases.enabled {
598 let lease = &standards.leases;
599
600 if lease.avg_lease_term_months == 0 {
601 return Err(SynthError::validation(
602 "lease.avg_lease_term_months must be > 0",
603 ));
604 }
605
606 validate_rate("lease.finance_lease_percent", lease.finance_lease_percent)?;
607 validate_rate("lease.real_estate_percent", lease.real_estate_percent)?;
608 }
609
610 if standards.fair_value.enabled {
612 let fv = &standards.fair_value;
613
614 let level_sum = fv.level1_percent + fv.level2_percent + fv.level3_percent;
616 if (level_sum - 1.0).abs() > 0.01 {
617 return Err(SynthError::validation(format!(
618 "fair_value level percentages must sum to 1.0, got {}",
619 level_sum
620 )));
621 }
622
623 validate_rate("fair_value.level1_percent", fv.level1_percent)?;
624 validate_rate("fair_value.level2_percent", fv.level2_percent)?;
625 validate_rate("fair_value.level3_percent", fv.level3_percent)?;
626 }
627
628 if standards.impairment.enabled {
630 let imp = &standards.impairment;
631
632 validate_rate("impairment.impairment_rate", imp.impairment_rate)?;
633 }
634
635 Ok(())
636}
637
638fn validate_audit_standards(config: &GeneratorConfig) -> SynthResult<()> {
640 let standards = &config.audit_standards;
641
642 if !standards.enabled {
643 return Ok(());
644 }
645
646 if standards.isa_compliance.enabled {
648 let valid_levels = ["basic", "standard", "comprehensive"];
649 if !valid_levels.contains(&standards.isa_compliance.compliance_level.as_str()) {
650 return Err(SynthError::validation(format!(
651 "isa_compliance.compliance_level must be one of {:?}, got '{}'",
652 valid_levels, standards.isa_compliance.compliance_level
653 )));
654 }
655
656 let valid_frameworks = ["isa", "pcaob", "dual"];
657 if !valid_frameworks.contains(&standards.isa_compliance.framework.as_str()) {
658 return Err(SynthError::validation(format!(
659 "isa_compliance.framework must be one of {:?}, got '{}'",
660 valid_frameworks, standards.isa_compliance.framework
661 )));
662 }
663 }
664
665 if standards.analytical_procedures.enabled {
667 let ap = &standards.analytical_procedures;
668
669 if ap.procedures_per_account == 0 {
670 return Err(SynthError::validation(
671 "analytical_procedures.procedures_per_account must be > 0",
672 ));
673 }
674
675 validate_rate(
676 "analytical_procedures.variance_probability",
677 ap.variance_probability,
678 )?;
679 }
680
681 if standards.confirmations.enabled {
683 let conf = &standards.confirmations;
684
685 validate_rate(
686 "confirmations.positive_response_rate",
687 conf.positive_response_rate,
688 )?;
689
690 validate_rate("confirmations.exception_rate", conf.exception_rate)?;
691
692 let total_rate = conf.positive_response_rate + conf.exception_rate;
694 if total_rate > 1.0 {
695 return Err(SynthError::validation(
696 "confirmations: positive_response_rate + exception_rate cannot exceed 1.0",
697 ));
698 }
699 }
700
701 if standards.opinion.enabled {
703 let op = &standards.opinion;
704
705 if op.generate_kam && op.average_kam_count == 0 {
706 return Err(SynthError::validation(
707 "opinion.average_kam_count must be > 0 when generate_kam is true",
708 ));
709 }
710 }
711
712 if standards.sox.enabled {
714 let sox = &standards.sox;
715
716 if sox.materiality_threshold < 0.0 {
717 return Err(SynthError::validation(
718 "sox.materiality_threshold must be >= 0",
719 ));
720 }
721 }
722
723 if standards.pcaob.enabled {
725 if standards.isa_compliance.enabled
727 && standards.isa_compliance.framework != "pcaob"
728 && standards.isa_compliance.framework != "dual"
729 {
730 return Err(SynthError::validation(
731 "When PCAOB is enabled, ISA framework must be 'pcaob' or 'dual'",
732 ));
733 }
734 }
735
736 Ok(())
737}
738
739#[cfg(test)]
740mod tests {
741 use super::*;
742 use crate::presets::{create_preset, demo_preset, stress_test_preset};
743 use crate::schema::*;
744 use datasynth_core::models::{CoAComplexity, IndustrySector};
745
746 fn minimal_valid_config() -> GeneratorConfig {
748 GeneratorConfig {
749 global: GlobalConfig {
750 seed: Some(42),
751 industry: IndustrySector::Manufacturing,
752 start_date: "2024-01-01".to_string(),
753 period_months: 3,
754 group_currency: "USD".to_string(),
755 parallel: true,
756 worker_threads: 0,
757 memory_limit_mb: 0,
758 },
759 companies: vec![CompanyConfig {
760 code: "TEST".to_string(),
761 name: "Test Company".to_string(),
762 currency: "USD".to_string(),
763 country: "US".to_string(),
764 fiscal_year_variant: "K4".to_string(),
765 annual_transaction_volume: TransactionVolume::TenK,
766 volume_weight: 1.0,
767 }],
768 chart_of_accounts: ChartOfAccountsConfig {
769 complexity: CoAComplexity::Small,
770 industry_specific: true,
771 custom_accounts: None,
772 min_hierarchy_depth: 2,
773 max_hierarchy_depth: 5,
774 },
775 transactions: TransactionConfig::default(),
776 output: OutputConfig::default(),
777 fraud: FraudConfig::default(),
778 internal_controls: InternalControlsConfig::default(),
779 business_processes: BusinessProcessConfig::default(),
780 user_personas: UserPersonaConfig::default(),
781 templates: TemplateConfig::default(),
782 approval: ApprovalConfig::default(),
783 departments: DepartmentConfig::default(),
784 master_data: MasterDataConfig::default(),
785 document_flows: DocumentFlowConfig::default(),
786 intercompany: IntercompanyConfig::default(),
787 balance: BalanceConfig::default(),
788 ocpm: OcpmConfig::default(),
789 audit: AuditGenerationConfig::default(),
790 banking: datasynth_banking::BankingConfig::default(),
791 data_quality: DataQualitySchemaConfig::default(),
792 scenario: ScenarioConfig::default(),
793 temporal: TemporalDriftConfig::default(),
794 graph_export: GraphExportConfig::default(),
795 streaming: StreamingSchemaConfig::default(),
796 rate_limit: RateLimitSchemaConfig::default(),
797 temporal_attributes: TemporalAttributeSchemaConfig::default(),
798 relationships: RelationshipSchemaConfig::default(),
799 accounting_standards: AccountingStandardsConfig::default(),
800 audit_standards: AuditStandardsConfig::default(),
801 }
802 }
803
804 #[test]
809 fn test_valid_period_months() {
810 let config = minimal_valid_config();
811 assert!(validate_config(&config).is_ok());
812 }
813
814 #[test]
815 fn test_zero_period_months_rejected() {
816 let mut config = minimal_valid_config();
817 config.global.period_months = 0;
818 let result = validate_config(&config);
819 assert!(result.is_err());
820 assert!(result.unwrap_err().to_string().contains("period_months"));
821 }
822
823 #[test]
824 fn test_large_period_months_accepted() {
825 let mut config = minimal_valid_config();
826 config.global.period_months = 120; assert!(validate_config(&config).is_ok());
828 }
829
830 #[test]
831 fn test_period_months_exceeds_max_rejected() {
832 let mut config = minimal_valid_config();
833 config.global.period_months = 121; let result = validate_config(&config);
835 assert!(result.is_err());
836 assert!(result.unwrap_err().to_string().contains("period_months"));
837 }
838
839 #[test]
844 fn test_empty_companies_rejected() {
845 let mut config = minimal_valid_config();
846 config.companies.clear();
847 let result = validate_config(&config);
848 assert!(result.is_err());
849 assert!(result.unwrap_err().to_string().contains("company"));
850 }
851
852 #[test]
853 fn test_empty_company_code_rejected() {
854 let mut config = minimal_valid_config();
855 config.companies[0].code = "".to_string();
856 let result = validate_config(&config);
857 assert!(result.is_err());
858 assert!(result.unwrap_err().to_string().contains("Company code"));
859 }
860
861 #[test]
862 fn test_invalid_currency_code_rejected() {
863 let mut config = minimal_valid_config();
864 config.companies[0].currency = "US".to_string(); let result = validate_config(&config);
866 assert!(result.is_err());
867 assert!(result.unwrap_err().to_string().contains("currency"));
868 }
869
870 #[test]
871 fn test_long_currency_code_rejected() {
872 let mut config = minimal_valid_config();
873 config.companies[0].currency = "USDD".to_string(); let result = validate_config(&config);
875 assert!(result.is_err());
876 assert!(result.unwrap_err().to_string().contains("currency"));
877 }
878
879 #[test]
880 fn test_multiple_companies_validated() {
881 let mut config = minimal_valid_config();
882 config.companies.push(CompanyConfig {
883 code: "SUB1".to_string(),
884 name: "Subsidiary 1".to_string(),
885 currency: "EUR".to_string(),
886 country: "DE".to_string(),
887 fiscal_year_variant: "K4".to_string(),
888 annual_transaction_volume: TransactionVolume::TenK,
889 volume_weight: 0.5,
890 });
891 assert!(validate_config(&config).is_ok());
892 }
893
894 #[test]
895 fn test_second_company_invalid_currency_rejected() {
896 let mut config = minimal_valid_config();
897 config.companies.push(CompanyConfig {
898 code: "SUB1".to_string(),
899 name: "Subsidiary 1".to_string(),
900 currency: "EU".to_string(), country: "DE".to_string(),
902 fiscal_year_variant: "K4".to_string(),
903 annual_transaction_volume: TransactionVolume::TenK,
904 volume_weight: 0.5,
905 });
906 let result = validate_config(&config);
907 assert!(result.is_err());
908 }
909
910 #[test]
915 fn test_valid_source_distribution() {
916 let config = minimal_valid_config();
917 assert!(validate_config(&config).is_ok());
919 }
920
921 #[test]
922 fn test_source_distribution_not_summing_to_one_rejected() {
923 let mut config = minimal_valid_config();
924 config.transactions.source_distribution = SourceDistribution {
925 manual: 0.5,
926 automated: 0.5,
927 recurring: 0.5, adjustment: 0.1,
929 };
930 let result = validate_config(&config);
931 assert!(result.is_err());
932 assert!(result
933 .unwrap_err()
934 .to_string()
935 .contains("Source distribution"));
936 }
937
938 #[test]
939 fn test_source_distribution_slightly_off_accepted() {
940 let mut config = minimal_valid_config();
941 config.transactions.source_distribution = SourceDistribution {
943 manual: 0.20,
944 automated: 0.70,
945 recurring: 0.07,
946 adjustment: 0.025, };
948 assert!(validate_config(&config).is_ok());
949 }
950
951 #[test]
956 fn test_valid_business_process_weights() {
957 let config = minimal_valid_config();
958 assert!(validate_config(&config).is_ok());
960 }
961
962 #[test]
963 fn test_business_process_weights_not_summing_to_one_rejected() {
964 let mut config = minimal_valid_config();
965 config.business_processes = BusinessProcessConfig {
966 o2c_weight: 0.5,
967 p2p_weight: 0.5,
968 r2r_weight: 0.5, h2r_weight: 0.1,
970 a2r_weight: 0.1,
971 };
972 let result = validate_config(&config);
973 assert!(result.is_err());
974 assert!(result
975 .unwrap_err()
976 .to_string()
977 .contains("Business process weights"));
978 }
979
980 #[test]
985 fn test_fraud_disabled_invalid_rate_accepted() {
986 let mut config = minimal_valid_config();
987 config.fraud.enabled = false;
988 config.fraud.fraud_rate = 5.0; assert!(validate_config(&config).is_ok());
990 }
991
992 #[test]
993 fn test_fraud_enabled_valid_rate_accepted() {
994 let mut config = minimal_valid_config();
995 config.fraud.enabled = true;
996 config.fraud.fraud_rate = 0.05;
997 assert!(validate_config(&config).is_ok());
998 }
999
1000 #[test]
1001 fn test_fraud_enabled_negative_rate_rejected() {
1002 let mut config = minimal_valid_config();
1003 config.fraud.enabled = true;
1004 config.fraud.fraud_rate = -0.1;
1005 let result = validate_config(&config);
1006 assert!(result.is_err());
1007 assert!(result.unwrap_err().to_string().contains("fraud_rate"));
1008 }
1009
1010 #[test]
1011 fn test_fraud_enabled_rate_above_one_rejected() {
1012 let mut config = minimal_valid_config();
1013 config.fraud.enabled = true;
1014 config.fraud.fraud_rate = 1.5;
1015 let result = validate_config(&config);
1016 assert!(result.is_err());
1017 assert!(result.unwrap_err().to_string().contains("fraud_rate"));
1018 }
1019
1020 #[test]
1021 fn test_fraud_rate_zero_accepted() {
1022 let mut config = minimal_valid_config();
1023 config.fraud.enabled = true;
1024 config.fraud.fraud_rate = 0.0;
1025 assert!(validate_config(&config).is_ok());
1026 }
1027
1028 #[test]
1029 fn test_fraud_rate_one_accepted() {
1030 let mut config = minimal_valid_config();
1031 config.fraud.enabled = true;
1032 config.fraud.fraud_rate = 1.0;
1033 assert!(validate_config(&config).is_ok());
1034 }
1035
1036 #[test]
1041 fn test_demo_preset_valid() {
1042 let config = demo_preset();
1043 assert!(validate_config(&config).is_ok());
1044 }
1045
1046 #[test]
1047 fn test_stress_test_preset_valid() {
1048 let config = stress_test_preset();
1049 assert!(validate_config(&config).is_ok());
1050 }
1051
1052 #[test]
1053 fn test_manufacturing_preset_valid() {
1054 let config = create_preset(
1055 IndustrySector::Manufacturing,
1056 2,
1057 12,
1058 CoAComplexity::Medium,
1059 TransactionVolume::HundredK,
1060 );
1061 assert!(validate_config(&config).is_ok());
1062 }
1063
1064 #[test]
1065 fn test_retail_preset_valid() {
1066 let config = create_preset(
1067 IndustrySector::Retail,
1068 3,
1069 6,
1070 CoAComplexity::Large,
1071 TransactionVolume::OneM,
1072 );
1073 assert!(validate_config(&config).is_ok());
1074 }
1075
1076 #[test]
1077 fn test_financial_services_preset_valid() {
1078 let config = create_preset(
1079 IndustrySector::FinancialServices,
1080 2,
1081 12,
1082 CoAComplexity::Large,
1083 TransactionVolume::TenM,
1084 );
1085 assert!(validate_config(&config).is_ok());
1086 }
1087
1088 #[test]
1089 fn test_healthcare_preset_valid() {
1090 let config = create_preset(
1091 IndustrySector::Healthcare,
1092 2,
1093 6,
1094 CoAComplexity::Medium,
1095 TransactionVolume::HundredK,
1096 );
1097 assert!(validate_config(&config).is_ok());
1098 }
1099
1100 #[test]
1101 fn test_technology_preset_valid() {
1102 let config = create_preset(
1103 IndustrySector::Technology,
1104 3,
1105 12,
1106 CoAComplexity::Medium,
1107 TransactionVolume::HundredK,
1108 );
1109 assert!(validate_config(&config).is_ok());
1110 }
1111
1112 #[test]
1117 fn test_transaction_volume_counts() {
1118 assert_eq!(TransactionVolume::TenK.count(), 10_000);
1119 assert_eq!(TransactionVolume::HundredK.count(), 100_000);
1120 assert_eq!(TransactionVolume::OneM.count(), 1_000_000);
1121 assert_eq!(TransactionVolume::TenM.count(), 10_000_000);
1122 assert_eq!(TransactionVolume::HundredM.count(), 100_000_000);
1123 assert_eq!(TransactionVolume::Custom(50_000).count(), 50_000);
1124 }
1125
1126 #[test]
1131 fn test_source_distribution_default_sums_to_one() {
1132 let dist = SourceDistribution::default();
1133 let sum = dist.manual + dist.automated + dist.recurring + dist.adjustment;
1134 assert!((sum - 1.0).abs() < 0.001);
1135 }
1136
1137 #[test]
1138 fn test_business_process_default_sums_to_one() {
1139 let bp = BusinessProcessConfig::default();
1140 let sum = bp.o2c_weight + bp.p2p_weight + bp.r2r_weight + bp.h2r_weight + bp.a2r_weight;
1141 assert!((sum - 1.0).abs() < 0.001);
1142 }
1143
1144 #[test]
1145 fn test_fraud_type_distribution_default_sums_to_one() {
1146 let dist = FraudTypeDistribution::default();
1147 let sum = dist.suspense_account_abuse
1148 + dist.fictitious_transaction
1149 + dist.revenue_manipulation
1150 + dist.expense_capitalization
1151 + dist.split_transaction
1152 + dist.timing_anomaly
1153 + dist.unauthorized_access
1154 + dist.duplicate_payment;
1155 assert!((sum - 1.0).abs() < 0.001);
1156 }
1157
1158 #[test]
1159 fn test_persona_distribution_default_sums_to_one() {
1160 let dist = PersonaDistribution::default();
1161 let sum = dist.junior_accountant
1162 + dist.senior_accountant
1163 + dist.controller
1164 + dist.manager
1165 + dist.automated_system;
1166 assert!((sum - 1.0).abs() < 0.001);
1167 }
1168
1169 #[test]
1170 fn test_payment_terms_distribution_default_sums_to_one() {
1171 let dist = PaymentTermsDistribution::default();
1172 let sum = dist.net_30
1173 + dist.net_60
1174 + dist.net_90
1175 + dist.two_ten_net_30
1176 + dist.due_on_receipt
1177 + dist.end_of_month;
1178 assert!((sum - 1.0).abs() < 0.001);
1179 }
1180
1181 #[test]
1182 fn test_vendor_behavior_distribution_default_sums_to_one() {
1183 let dist = VendorBehaviorDistribution::default();
1184 let sum = dist.reliable
1185 + dist.sometimes_late
1186 + dist.inconsistent_quality
1187 + dist.premium
1188 + dist.budget;
1189 assert!((sum - 1.0).abs() < 0.001);
1190 }
1191
1192 #[test]
1193 fn test_credit_rating_distribution_default_sums_to_one() {
1194 let dist = CreditRatingDistribution::default();
1195 let sum = dist.aaa + dist.aa + dist.a + dist.bbb + dist.bb + dist.b + dist.below_b;
1196 assert!((sum - 1.0).abs() < 0.001);
1197 }
1198
1199 #[test]
1200 fn test_payment_behavior_distribution_default_sums_to_one() {
1201 let dist = PaymentBehaviorDistribution::default();
1202 let sum = dist.early_payer
1203 + dist.on_time
1204 + dist.occasional_late
1205 + dist.frequent_late
1206 + dist.discount_taker;
1207 assert!((sum - 1.0).abs() < 0.001);
1208 }
1209
1210 #[test]
1211 fn test_material_type_distribution_default_sums_to_one() {
1212 let dist = MaterialTypeDistribution::default();
1213 let sum = dist.raw_material
1214 + dist.semi_finished
1215 + dist.finished_good
1216 + dist.trading_good
1217 + dist.operating_supply
1218 + dist.service;
1219 assert!((sum - 1.0).abs() < 0.001);
1220 }
1221
1222 #[test]
1223 fn test_valuation_method_distribution_default_sums_to_one() {
1224 let dist = ValuationMethodDistribution::default();
1225 let sum = dist.standard_cost + dist.moving_average + dist.fifo + dist.lifo;
1226 assert!((sum - 1.0).abs() < 0.001);
1227 }
1228
1229 #[test]
1230 fn test_asset_class_distribution_default_sums_to_one() {
1231 let dist = AssetClassDistribution::default();
1232 let sum = dist.buildings
1233 + dist.machinery
1234 + dist.vehicles
1235 + dist.it_equipment
1236 + dist.furniture
1237 + dist.land
1238 + dist.leasehold;
1239 assert!((sum - 1.0).abs() < 0.001);
1240 }
1241
1242 #[test]
1243 fn test_depreciation_method_distribution_default_sums_to_one() {
1244 let dist = DepreciationMethodDistribution::default();
1245 let sum = dist.straight_line
1246 + dist.declining_balance
1247 + dist.double_declining
1248 + dist.sum_of_years
1249 + dist.units_of_production;
1250 assert!((sum - 1.0).abs() < 0.001);
1251 }
1252
1253 #[test]
1254 fn test_employee_department_distribution_default_sums_to_one() {
1255 let dist = EmployeeDepartmentDistribution::default();
1256 let sum = dist.finance
1257 + dist.procurement
1258 + dist.sales
1259 + dist.warehouse
1260 + dist.it
1261 + dist.hr
1262 + dist.operations
1263 + dist.executive;
1264 assert!((sum - 1.0).abs() < 0.001);
1265 }
1266
1267 #[test]
1268 fn test_ic_transaction_type_distribution_default_sums_to_one() {
1269 let dist = ICTransactionTypeDistribution::default();
1270 let sum = dist.goods_sale
1271 + dist.service_provided
1272 + dist.loan
1273 + dist.dividend
1274 + dist.management_fee
1275 + dist.royalty
1276 + dist.cost_sharing;
1277 assert!((sum - 1.0).abs() < 0.001);
1278 }
1279
1280 #[test]
1285 fn test_compression_level_valid() {
1286 let mut config = minimal_valid_config();
1287 config.output.compression.enabled = true;
1288 config.output.compression.level = 5;
1289 assert!(validate_config(&config).is_ok());
1290 }
1291
1292 #[test]
1293 fn test_compression_level_zero_rejected() {
1294 let mut config = minimal_valid_config();
1295 config.output.compression.enabled = true;
1296 config.output.compression.level = 0;
1297 let result = validate_config(&config);
1298 assert!(result.is_err());
1299 assert!(result
1300 .unwrap_err()
1301 .to_string()
1302 .contains("compression.level"));
1303 }
1304
1305 #[test]
1306 fn test_compression_level_ten_rejected() {
1307 let mut config = minimal_valid_config();
1308 config.output.compression.enabled = true;
1309 config.output.compression.level = 10;
1310 let result = validate_config(&config);
1311 assert!(result.is_err());
1312 assert!(result
1313 .unwrap_err()
1314 .to_string()
1315 .contains("compression.level"));
1316 }
1317
1318 #[test]
1319 fn test_compression_disabled_ignores_level() {
1320 let mut config = minimal_valid_config();
1321 config.output.compression.enabled = false;
1322 config.output.compression.level = 0; assert!(validate_config(&config).is_ok());
1324 }
1325
1326 #[test]
1331 fn test_approval_thresholds_ascending_accepted() {
1332 let mut config = minimal_valid_config();
1333 config.approval.enabled = true;
1334 config.approval.thresholds = vec![
1335 ApprovalThresholdConfig {
1336 amount: 1000.0,
1337 level: 1,
1338 roles: vec!["accountant".to_string()],
1339 },
1340 ApprovalThresholdConfig {
1341 amount: 5000.0,
1342 level: 2,
1343 roles: vec!["manager".to_string()],
1344 },
1345 ApprovalThresholdConfig {
1346 amount: 10000.0,
1347 level: 3,
1348 roles: vec!["director".to_string()],
1349 },
1350 ];
1351 assert!(validate_config(&config).is_ok());
1352 }
1353
1354 #[test]
1355 fn test_approval_thresholds_not_ascending_rejected() {
1356 let mut config = minimal_valid_config();
1357 config.approval.enabled = true;
1358 config.approval.thresholds = vec![
1359 ApprovalThresholdConfig {
1360 amount: 5000.0,
1361 level: 1,
1362 roles: vec!["accountant".to_string()],
1363 },
1364 ApprovalThresholdConfig {
1365 amount: 1000.0, level: 2,
1367 roles: vec!["manager".to_string()],
1368 },
1369 ];
1370 let result = validate_config(&config);
1371 assert!(result.is_err());
1372 assert!(result.unwrap_err().to_string().contains("ascending"));
1373 }
1374
1375 #[test]
1376 fn test_fraud_approval_thresholds_ascending_accepted() {
1377 let mut config = minimal_valid_config();
1378 config.fraud.enabled = true;
1379 config.fraud.fraud_rate = 0.05;
1380 config.fraud.approval_thresholds = vec![1000.0, 5000.0, 10000.0];
1381 assert!(validate_config(&config).is_ok());
1382 }
1383
1384 #[test]
1385 fn test_fraud_approval_thresholds_not_ascending_rejected() {
1386 let mut config = minimal_valid_config();
1387 config.fraud.enabled = true;
1388 config.fraud.fraud_rate = 0.05;
1389 config.fraud.approval_thresholds = vec![5000.0, 1000.0, 10000.0]; let result = validate_config(&config);
1391 assert!(result.is_err());
1392 assert!(result.unwrap_err().to_string().contains("ascending"));
1393 }
1394
1395 #[test]
1400 fn test_internal_controls_rates_valid() {
1401 let mut config = minimal_valid_config();
1402 config.internal_controls.enabled = true;
1403 config.internal_controls.exception_rate = 0.05;
1404 config.internal_controls.sod_violation_rate = 0.02;
1405 assert!(validate_config(&config).is_ok());
1406 }
1407
1408 #[test]
1409 fn test_internal_controls_exception_rate_invalid() {
1410 let mut config = minimal_valid_config();
1411 config.internal_controls.enabled = true;
1412 config.internal_controls.exception_rate = 1.5; let result = validate_config(&config);
1414 assert!(result.is_err());
1415 assert!(result.unwrap_err().to_string().contains("exception_rate"));
1416 }
1417
1418 #[test]
1419 fn test_approval_rejection_plus_revision_exceeds_one_rejected() {
1420 let mut config = minimal_valid_config();
1421 config.approval.enabled = true;
1422 config.approval.rejection_rate = 0.6;
1423 config.approval.revision_rate = 0.6; let result = validate_config(&config);
1425 assert!(result.is_err());
1426 assert!(result
1427 .unwrap_err()
1428 .to_string()
1429 .contains("must not exceed 1.0"));
1430 }
1431
1432 #[test]
1433 fn test_master_data_intercompany_percent_invalid() {
1434 let mut config = minimal_valid_config();
1435 config.master_data.vendors.intercompany_percent = 1.5; let result = validate_config(&config);
1437 assert!(result.is_err());
1438 assert!(result
1439 .unwrap_err()
1440 .to_string()
1441 .contains("intercompany_percent"));
1442 }
1443
1444 #[test]
1445 fn test_balance_gross_margin_invalid() {
1446 let mut config = minimal_valid_config();
1447 config.balance.target_gross_margin = 1.5; let result = validate_config(&config);
1449 assert!(result.is_err());
1450 assert!(result
1451 .unwrap_err()
1452 .to_string()
1453 .contains("target_gross_margin"));
1454 }
1455
1456 #[test]
1457 fn test_negative_volume_weight_rejected() {
1458 let mut config = minimal_valid_config();
1459 config.companies[0].volume_weight = -0.5;
1460 let result = validate_config(&config);
1461 assert!(result.is_err());
1462 assert!(result.unwrap_err().to_string().contains("volume_weight"));
1463 }
1464
1465 #[test]
1466 fn test_benford_tolerance_invalid() {
1467 let mut config = minimal_valid_config();
1468 config.transactions.benford.tolerance = 1.5; let result = validate_config(&config);
1470 assert!(result.is_err());
1471 assert!(result
1472 .unwrap_err()
1473 .to_string()
1474 .contains("benford.tolerance"));
1475 }
1476
1477 #[test]
1478 fn test_batch_size_zero_rejected() {
1479 let mut config = minimal_valid_config();
1480 config.output.batch_size = 0;
1481 let result = validate_config(&config);
1482 assert!(result.is_err());
1483 assert!(result.unwrap_err().to_string().contains("batch_size"));
1484 }
1485}