use std::path::PathBuf;
use datasynth_audit_fsm::context::EngagementContext;
use datasynth_audit_fsm::engine::AuditFsmEngine;
use datasynth_audit_fsm::error::AuditFsmError;
use datasynth_audit_fsm::loader::*;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GroupAuditConfig {
pub group_entity: String,
pub components: Vec<ComponentConfig>,
pub group_blueprint: String,
pub overlay: String,
pub group_materiality: f64,
pub base_seed: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentConfig {
pub entity_id: String,
pub component_type: ComponentType,
pub blueprint: String,
pub overlay: String,
pub component_materiality: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ComponentType {
Significant,
NonSignificant,
NotInScope,
}
impl std::fmt::Display for ComponentType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ComponentType::Significant => write!(f, "significant"),
ComponentType::NonSignificant => write!(f, "non_significant"),
ComponentType::NotInScope => write!(f, "not_in_scope"),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct GroupAuditReport {
pub group_entity: String,
pub component_results: Vec<ComponentResult>,
pub aggregated_findings: usize,
pub aggregated_misstatements: Decimal,
pub group_coverage: f64,
pub components_with_findings: usize,
pub group_opinion_risk: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ComponentResult {
pub entity_id: String,
pub component_type: String,
pub events: usize,
pub artifacts: usize,
pub findings: usize,
pub completion_rate: f64,
}
fn resolve_blueprint(name: &str) -> Result<BlueprintWithPreconditions, AuditFsmError> {
match name {
"fsa" | "builtin:fsa" => BlueprintWithPreconditions::load_builtin_fsa(),
"ia" | "builtin:ia" => BlueprintWithPreconditions::load_builtin_ia(),
"kpmg" | "builtin:kpmg" => BlueprintWithPreconditions::load_builtin_kpmg(),
"pwc" | "builtin:pwc" => BlueprintWithPreconditions::load_builtin_pwc(),
"deloitte" | "builtin:deloitte" => BlueprintWithPreconditions::load_builtin_deloitte(),
"ey_gam_lite" | "builtin:ey_gam_lite" => {
BlueprintWithPreconditions::load_builtin_ey_gam_lite()
}
path => BlueprintWithPreconditions::load_from_file(PathBuf::from(path)),
}
}
fn resolve_overlay(
name: &str,
) -> Result<datasynth_audit_fsm::schema::GenerationOverlay, AuditFsmError> {
match name {
"default" | "builtin:default" => {
load_overlay(&OverlaySource::Builtin(BuiltinOverlay::Default))
}
"thorough" | "builtin:thorough" => {
load_overlay(&OverlaySource::Builtin(BuiltinOverlay::Thorough))
}
"rushed" | "builtin:rushed" => {
load_overlay(&OverlaySource::Builtin(BuiltinOverlay::Rushed))
}
"retail" | "builtin:retail" => {
load_overlay(&OverlaySource::Builtin(BuiltinOverlay::IndustryRetail))
}
"manufacturing" | "builtin:manufacturing" => load_overlay(&OverlaySource::Builtin(
BuiltinOverlay::IndustryManufacturing,
)),
"financial_services" | "builtin:financial_services" => load_overlay(
&OverlaySource::Builtin(BuiltinOverlay::IndustryFinancialServices),
),
path => load_overlay(&OverlaySource::Custom(PathBuf::from(path))),
}
}
pub fn run_group_audit(config: &GroupAuditConfig) -> Result<GroupAuditReport, AuditFsmError> {
let mut component_results = Vec::with_capacity(config.components.len());
let mut total_findings: usize = 0;
let mut total_misstatement = Decimal::ZERO;
let mut components_with_findings: usize = 0;
let mut significant_count: usize = 0;
let mut in_scope_count: usize = 0;
for (i, comp) in config.components.iter().enumerate() {
if comp.component_type == ComponentType::NotInScope {
component_results.push(ComponentResult {
entity_id: comp.entity_id.clone(),
component_type: comp.component_type.to_string(),
events: 0,
artifacts: 0,
findings: 0,
completion_rate: 0.0,
});
continue;
}
in_scope_count += 1;
if comp.component_type == ComponentType::Significant {
significant_count += 1;
}
let bwp = resolve_blueprint(&comp.blueprint)?;
let overlay = resolve_overlay(&comp.overlay)?;
let seed = config.base_seed.wrapping_add(i as u64 + 1);
let rng = ChaCha8Rng::seed_from_u64(seed);
let mut engine = AuditFsmEngine::new(bwp, overlay, rng);
let mut ctx = EngagementContext::demo();
ctx.company_code = comp.entity_id.clone();
let result = engine.run_engagement(&ctx)?;
let findings = result.artifacts.findings.len();
let total_procs = result.procedure_states.len();
let completed = result
.procedure_states
.values()
.filter(|s| s.as_str() == "completed" || s.as_str() == "closed")
.count();
if findings > 0 {
components_with_findings += 1;
}
total_findings += findings;
for finding in &result.artifacts.findings {
if let Some(amount) = finding.monetary_impact {
total_misstatement += amount.abs();
}
}
component_results.push(ComponentResult {
entity_id: comp.entity_id.clone(),
component_type: comp.component_type.to_string(),
events: result.event_log.len(),
artifacts: result.artifacts.total_artifacts(),
findings,
completion_rate: if total_procs > 0 {
completed as f64 / total_procs as f64
} else {
0.0
},
});
}
let group_coverage = if in_scope_count > 0 {
significant_count as f64 / in_scope_count as f64
} else {
0.0
};
let group_opinion_risk = assess_opinion_risk(
total_findings,
total_misstatement,
config.group_materiality,
group_coverage,
);
Ok(GroupAuditReport {
group_entity: config.group_entity.clone(),
component_results,
aggregated_findings: total_findings,
aggregated_misstatements: total_misstatement,
group_coverage,
components_with_findings,
group_opinion_risk,
})
}
fn assess_opinion_risk(
total_findings: usize,
total_misstatement: Decimal,
group_materiality: f64,
group_coverage: f64,
) -> String {
let mat_decimal =
Decimal::from_f64_retain(group_materiality).unwrap_or_else(|| Decimal::new(1_000_000, 0));
if total_misstatement > mat_decimal || total_findings > 10 {
"high".to_string()
} else if group_coverage < 0.5 || total_findings > 5 {
"medium".to_string()
} else {
"low".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_group_config() -> GroupAuditConfig {
GroupAuditConfig {
group_entity: "GROUP_PARENT".into(),
components: vec![
ComponentConfig {
entity_id: "COMP_A".into(),
component_type: ComponentType::Significant,
blueprint: "fsa".into(),
overlay: "default".into(),
component_materiality: 50_000.0,
},
ComponentConfig {
entity_id: "COMP_B".into(),
component_type: ComponentType::NonSignificant,
blueprint: "fsa".into(),
overlay: "default".into(),
component_materiality: 100_000.0,
},
ComponentConfig {
entity_id: "COMP_C".into(),
component_type: ComponentType::NotInScope,
blueprint: "fsa".into(),
overlay: "default".into(),
component_materiality: 200_000.0,
},
],
group_blueprint: "fsa".into(),
overlay: "default".into(),
group_materiality: 500_000.0,
base_seed: 42,
}
}
#[test]
fn test_group_with_three_components() {
let config = make_group_config();
let report = run_group_audit(&config).unwrap();
assert_eq!(report.group_entity, "GROUP_PARENT");
assert_eq!(report.component_results.len(), 3);
let in_scope: Vec<&ComponentResult> = report
.component_results
.iter()
.filter(|c| c.events > 0)
.collect();
assert_eq!(
in_scope.len(),
2,
"expected 2 in-scope component results with events"
);
}
#[test]
fn test_significant_component_coverage() {
let config = make_group_config();
let report = run_group_audit(&config).unwrap();
assert!(
(report.group_coverage - 0.5).abs() < 0.01,
"expected 50% coverage, got {}",
report.group_coverage
);
}
#[test]
fn test_findings_aggregation() {
let config = make_group_config();
let report = run_group_audit(&config).unwrap();
let sum: usize = report.component_results.iter().map(|c| c.findings).sum();
assert_eq!(
report.aggregated_findings, sum,
"aggregated findings should equal sum of component findings"
);
}
#[test]
fn test_not_in_scope_skipped() {
let config = make_group_config();
let report = run_group_audit(&config).unwrap();
let comp_c = report
.component_results
.iter()
.find(|c| c.entity_id == "COMP_C")
.expect("COMP_C should be in results");
assert_eq!(
comp_c.events, 0,
"not-in-scope component should have 0 events"
);
assert_eq!(
comp_c.artifacts, 0,
"not-in-scope component should have 0 artifacts"
);
assert_eq!(
comp_c.findings, 0,
"not-in-scope component should have 0 findings"
);
assert_eq!(
comp_c.component_type, "not_in_scope",
"component type should be not_in_scope"
);
}
}