use std::collections::BTreeMap;
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use crate::config::{GroupConfig, OutputLayoutConfig, PeriodLength};
use crate::errors::{GroupError, GroupResult};
use crate::manifest::audit_plan::{build_audit_engagement_plan, AuditEngagementPlan};
use crate::manifest::cgu_plan::{build_cgu_plan, CguPlan};
use crate::manifest::coa_master::{build_coa_master, ChartOfAccountsMaster};
use crate::manifest::expansion::{expand_ownership, ExpandedEntity};
use crate::manifest::fx_master::{build_fx_master, FxRateMaster};
use crate::manifest::ic_expansion::{expand_ic_relationships, ResolvedIcRelationship};
use crate::manifest::seeds::{derive_aggregate_seed, derive_entity_seed, derive_manifest_seed};
use crate::manifest::shard_plan::{build_shard_plan, ShardPlan};
use crate::manifest::tax_plan::{build_tax_group_plan, TaxGroupPlan};
pub const MANIFEST_SCHEMA_VERSION: &str = "1.0";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GroupManifest {
pub schema_version: String,
pub group_id: String,
pub group_seed: u64,
pub presentation_currency: String,
pub period: ManifestPeriod,
pub manifest_seed: String,
pub aggregate_seed: String,
pub ownership_graph: OwnershipGraphSection,
pub scoping_profiles: BTreeMap<String, serde_yaml::Value>,
pub chart_of_accounts_master: ChartOfAccountsMaster,
pub fx_rate_master: FxRateMaster,
pub ic_relationships: Vec<ResolvedIcRelationship>,
pub audit_engagement_plan: AuditEngagementPlan,
pub tax_group_plan: TaxGroupPlan,
#[serde(default)]
pub cgu_plan: CguPlan,
pub shard_plan: ShardPlan,
pub output: OutputLayoutConfig,
#[serde(default)]
pub matching: crate::config::IcMatchingConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ManifestPeriod {
pub start: NaiveDate,
pub end: NaiveDate,
pub length: PeriodLength,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OwnershipGraphSection {
pub parent_entity_code: String,
pub entities: Vec<ManifestEntity>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ManifestEntity {
pub code: String,
pub name: Option<String>,
pub country: String,
pub functional_currency: String,
pub scoping_profile: String,
pub consolidation_method: crate::config::ConsolidationMethod,
pub ownership_percent: Option<rust_decimal::Decimal>,
pub parent_code: Option<String>,
pub accounting_framework: Option<String>,
pub industry: Option<String>,
#[serde(default)]
pub hyperinflation_status: datasynth_core::models::HyperinflationStatus,
#[serde(default)]
pub ownership_changes: Vec<datasynth_core::models::intercompany::OwnershipChangeEvent>,
pub entity_seed: String,
pub shard_id: String,
}
pub fn build_manifest(cfg: &GroupConfig) -> GroupResult<GroupManifest> {
let period = compute_period(&cfg.period)?;
let manifest_seed = derive_manifest_seed(cfg.seed, period.start);
let aggregate_seed = derive_aggregate_seed(cfg.seed, period.start);
let expanded: Vec<ExpandedEntity> = expand_ownership(&cfg.ownership, cfg.seed, period.start)?;
let shard_plan = build_shard_plan(&expanded, &cfg.scoping_profiles)?;
let shard_by_code = shard_plan.shard_by_code();
let coa_master = build_coa_master(&expanded, &cfg.defaults, &cfg.id)?;
let fx_master = build_fx_master(
&cfg.fx,
&cfg.presentation_currency,
period.start,
period.end,
&expanded,
)?;
let ic_relationships = expand_ic_relationships(&cfg.intercompany, &expanded, cfg.seed)?;
let audit_engagement_plan =
build_audit_engagement_plan(&cfg.audit, &expanded, &cfg.id, &aggregate_seed)?;
let tax_group_plan = build_tax_group_plan(&cfg.tax, &expanded)?;
let cgu_plan = build_cgu_plan(&cfg.cgu, &expanded)?;
let entities: Vec<ManifestEntity> = expanded
.iter()
.map(|e| {
for ev in &e.ownership_changes {
if ev.effective_date < period.start || ev.effective_date > period.end {
return Err(GroupError::Config(format!(
"entity {}: ownership_change effective_date {} is outside the \
engagement period [{}, {}]",
e.code, ev.effective_date, period.start, period.end,
)));
}
if ev.ownership_percent_before < rust_decimal::Decimal::ZERO
|| ev.ownership_percent_before > rust_decimal::Decimal::ONE
{
return Err(GroupError::Config(format!(
"entity {}: ownership_percent_before {} is not in [0, 1]",
e.code, ev.ownership_percent_before,
)));
}
if ev.ownership_percent_after < rust_decimal::Decimal::ZERO
|| ev.ownership_percent_after > rust_decimal::Decimal::ONE
{
return Err(GroupError::Config(format!(
"entity {}: ownership_percent_after {} is not in [0, 1]",
e.code, ev.ownership_percent_after,
)));
}
}
Ok(ManifestEntity {
code: e.code.clone(),
name: e.name.clone(),
country: e.country.clone(),
functional_currency: e.functional_currency.clone(),
scoping_profile: e.scoping_profile.clone(),
consolidation_method: e.consolidation_method,
ownership_percent: e.ownership_percent,
parent_code: e.parent_code.clone(),
accounting_framework: e.accounting_framework.clone(),
industry: e.industry.clone(),
hyperinflation_status: e.hyperinflation_status,
ownership_changes: e.ownership_changes.clone(),
entity_seed: hex::encode(derive_entity_seed(cfg.seed, &e.code)),
shard_id: shard_by_code
.get(e.code.as_str())
.cloned()
.unwrap_or_default(),
})
})
.collect::<GroupResult<Vec<_>>>()?;
Ok(GroupManifest {
schema_version: MANIFEST_SCHEMA_VERSION.to_string(),
group_id: cfg.id.clone(),
group_seed: cfg.seed,
presentation_currency: cfg.presentation_currency.clone(),
period,
manifest_seed: hex::encode(manifest_seed),
aggregate_seed: hex::encode(aggregate_seed),
ownership_graph: OwnershipGraphSection {
parent_entity_code: cfg.ownership.parent_entity_code.clone(),
entities,
},
scoping_profiles: cfg.scoping_profiles.clone(),
chart_of_accounts_master: coa_master,
fx_rate_master: fx_master,
ic_relationships,
audit_engagement_plan,
tax_group_plan,
cgu_plan,
shard_plan,
output: cfg.output.clone(),
matching: cfg.intercompany.matching.clone(),
})
}
fn compute_period(cfg: &crate::config::PeriodConfig) -> GroupResult<ManifestPeriod> {
compute_period_pub(cfg)
}
pub fn compute_period_pub(cfg: &crate::config::PeriodConfig) -> GroupResult<ManifestPeriod> {
let start = cfg.start_date;
let end = match cfg.length {
PeriodLength::Monthly => start.checked_add_months(chrono::Months::new(1)),
PeriodLength::Quarterly => start.checked_add_months(chrono::Months::new(3)),
PeriodLength::SemiAnnual => start.checked_add_months(chrono::Months::new(6)),
PeriodLength::Annual => start.checked_add_months(chrono::Months::new(12)),
}
.and_then(|d| d.pred_opt())
.ok_or_else(|| {
GroupError::Config(format!(
"invalid period start_date '{}' — cannot compute period end",
start
))
})?;
Ok(ManifestPeriod {
start,
end,
length: cfg.length,
})
}