use datasynth_config::schema::{
AdvancedDistributionConfig, CopulaSchemaType, CorrelatedFieldConfig, CorrelationSchemaConfig,
};
use datasynth_runtime::{EnhancedOrchestrator, PhaseConfig};
use datasynth_test_utils::fixtures::minimal_config;
use rust_decimal::prelude::ToPrimitive;
fn build_runtime(
cfg_tweak: impl FnOnce(&mut datasynth_config::GeneratorConfig),
) -> EnhancedOrchestrator {
let mut config = minimal_config();
config.global.seed = Some(3540);
config.global.period_months = 3;
config.fraud.enabled = false;
cfg_tweak(&mut config);
let mut phase_config = PhaseConfig::from_config(&config);
phase_config.generate_document_flows = false;
phase_config.inject_anomalies = false;
phase_config.generate_banking = false;
phase_config.generate_graph_export = false;
phase_config.generate_ocpm_events = false;
phase_config.generate_period_close = false;
phase_config.generate_evolution_events = false;
phase_config.generate_sourcing = false;
phase_config.generate_intercompany = false;
phase_config.generate_financial_statements = false;
phase_config.generate_bank_reconciliation = false;
phase_config.generate_accounting_standards = false;
phase_config.generate_manufacturing = false;
phase_config.generate_sales_kpi_budgets = false;
phase_config.generate_tax = false;
phase_config.generate_esg = false;
phase_config.generate_hr = false;
phase_config.generate_treasury = false;
phase_config.generate_project_accounting = false;
phase_config.generate_compliance_regulations = false;
phase_config.inject_data_quality = false;
phase_config.validate_balances = false;
phase_config.show_progress = false;
phase_config.generate_audit = false;
phase_config.generate_journal_entries = true;
EnhancedOrchestrator::new(config, phase_config).expect("build orchestrator")
}
fn amounts(orch: &mut EnhancedOrchestrator) -> Vec<f64> {
let result = orch.generate().expect("generate");
result
.journal_entries
.iter()
.flat_map(|je| {
je.lines
.iter()
.map(|l| (l.debit_amount + l.credit_amount).to_f64().unwrap_or(0.0))
})
.filter(|a| *a > 0.0)
.collect()
}
fn mean(v: &[f64]) -> f64 {
if v.is_empty() {
0.0
} else {
v.iter().sum::<f64>() / v.len() as f64
}
}
#[test]
fn correlations_disabled_is_no_op() {
let mut orch = build_runtime(|c| {
c.distributions = AdvancedDistributionConfig {
enabled: true,
correlations: CorrelationSchemaConfig {
enabled: false,
..Default::default()
},
..Default::default()
};
});
let result = orch.generate().expect("generate");
assert!(!result.journal_entries.is_empty());
}
#[test]
fn gaussian_copula_produces_amount_nudge_with_full_matrix() {
let mut orch = build_runtime(|c| {
c.distributions = AdvancedDistributionConfig {
enabled: true,
correlations: CorrelationSchemaConfig {
enabled: true,
copula_type: CopulaSchemaType::Gaussian,
fields: vec![
CorrelatedFieldConfig {
name: "amount".to_string(),
distribution: Default::default(),
},
CorrelatedFieldConfig {
name: "line_count".to_string(),
distribution: Default::default(),
},
],
matrix: vec![1.0, 0.8, 0.8, 1.0],
..Default::default()
},
..Default::default()
};
});
let xs = amounts(&mut orch);
assert!(!xs.is_empty());
assert!(mean(&xs) > 0.0);
}
#[test]
fn gaussian_copula_upper_triangular_matrix_works() {
let mut orch = build_runtime(|c| {
c.distributions = AdvancedDistributionConfig {
enabled: true,
correlations: CorrelationSchemaConfig {
enabled: true,
copula_type: CopulaSchemaType::Gaussian,
fields: vec![
CorrelatedFieldConfig {
name: "amount".to_string(),
distribution: Default::default(),
},
CorrelatedFieldConfig {
name: "line_count".to_string(),
distribution: Default::default(),
},
],
matrix: vec![0.5],
..Default::default()
},
..Default::default()
};
});
let xs = amounts(&mut orch);
assert!(!xs.is_empty());
}
#[test]
fn non_gaussian_copulas_are_recognized_but_inert() {
let mut orch = build_runtime(|c| {
c.distributions = AdvancedDistributionConfig {
enabled: true,
correlations: CorrelationSchemaConfig {
enabled: true,
copula_type: CopulaSchemaType::Clayton,
fields: vec![
CorrelatedFieldConfig {
name: "amount".to_string(),
distribution: Default::default(),
},
CorrelatedFieldConfig {
name: "line_count".to_string(),
distribution: Default::default(),
},
],
matrix: vec![0.5],
..Default::default()
},
..Default::default()
};
});
let result = orch.generate().expect("generate");
assert!(!result.journal_entries.is_empty());
}