use std::collections::{BTreeMap, HashMap};
use datasynth_core::models::{ChartOfAccounts, CoAComplexity, IndustrySector};
use datasynth_core::pcg_loader;
use datasynth_core::skr_loader;
use datasynth_generators::ChartOfAccountsGenerator;
use serde::{Deserialize, Serialize};
use crate::errors::{GroupError, GroupResult};
use crate::manifest::expansion::ExpandedEntity;
fn canonicalize_framework(raw: &str) -> String {
match raw.to_lowercase().replace('-', "_").as_str() {
"ifrs" => "ifrs".to_string(),
"us_gaap" | "usgaap" | "usgaap2" => "us_gaap".to_string(),
"hgb" | "german_gaap" | "germangaap" => "hgb".to_string(),
"pcg" | "french_gaap" | "frenchgaap" => "pcg".to_string(),
"swiss_or" | "swissor" => "swiss_or".to_string(),
other => other.to_string(),
}
}
fn defaults_accounting_framework(defaults: &serde_yaml::Value) -> Option<String> {
defaults
.get("accounting_framework")
.and_then(|v| v.as_str())
.map(canonicalize_framework)
}
fn defaults_complexity(defaults: &serde_yaml::Value) -> CoAComplexity {
defaults
.get("complexity")
.and_then(|v| v.as_str())
.map(|s| match s.to_lowercase().as_str() {
"small" => CoAComplexity::Small,
"large" => CoAComplexity::Large,
_ => CoAComplexity::Medium,
})
.unwrap_or(CoAComplexity::Medium)
}
fn defaults_industry(defaults: &serde_yaml::Value) -> IndustrySector {
defaults
.get("industry")
.and_then(|v| v.as_str())
.map(|s| match s.to_lowercase().as_str() {
"retail" => IndustrySector::Retail,
"financial_services" | "financialservices" => IndustrySector::FinancialServices,
"healthcare" => IndustrySector::Healthcare,
"technology" => IndustrySector::Technology,
"professional_services" | "professionalservices" => {
IndustrySector::ProfessionalServices
}
"energy" => IndustrySector::Energy,
"transportation" => IndustrySector::Transportation,
"real_estate" | "realestate" => IndustrySector::RealEstate,
"telecommunications" => IndustrySector::Telecommunications,
_ => IndustrySector::Manufacturing,
})
.unwrap_or(IndustrySector::Manufacturing)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChartOfAccountsMaster {
pub primary_framework: String,
pub frameworks: BTreeMap<String, ChartOfAccounts>,
pub coa_id: String,
}
pub fn build_coa_master(
expanded_entities: &[ExpandedEntity],
defaults: &serde_yaml::Value,
group_id: &str,
) -> GroupResult<ChartOfAccountsMaster> {
let fallback_framework =
defaults_accounting_framework(defaults).unwrap_or_else(|| "ifrs".to_string());
let complexity = defaults_complexity(defaults);
let industry = defaults_industry(defaults);
let mut framework_country: HashMap<String, Vec<String>> = HashMap::new();
for entity in expanded_entities {
let fw = entity
.accounting_framework
.as_deref()
.map(canonicalize_framework)
.unwrap_or_else(|| fallback_framework.clone());
framework_country
.entry(fw)
.or_default()
.push(entity.country.clone());
}
if framework_country.is_empty() {
framework_country.insert(fallback_framework.clone(), vec![]);
}
let mut frameworks: BTreeMap<String, ChartOfAccounts> = BTreeMap::new();
for (fw, countries) in &framework_country {
let country = most_common_country(countries).unwrap_or("XX");
let coa = build_coa_for_framework(fw, group_id, country, industry, complexity)?;
frameworks.insert(fw.clone(), coa);
}
let primary_framework = if let Some(d) = defaults_accounting_framework(defaults) {
d
} else {
frameworks
.keys()
.next()
.cloned()
.unwrap_or_else(|| fallback_framework.clone())
};
let sorted_keys: Vec<&str> = frameworks.keys().map(String::as_str).collect();
let hash_input = format!("{}|{complexity:?}", sorted_keys.join(","));
let digest = blake3::hash(hash_input.as_bytes());
let coa_id = format!("GROUP_CoA_{}", hex::encode(&digest.as_bytes()[..8]));
Ok(ChartOfAccountsMaster {
primary_framework,
frameworks,
coa_id,
})
}
fn most_common_country(countries: &[String]) -> Option<&str> {
if countries.is_empty() {
return None;
}
let mut counts: HashMap<&str, usize> = HashMap::new();
for c in countries {
*counts.entry(c.as_str()).or_default() += 1;
}
counts
.into_iter()
.max_by_key(|&(_, cnt)| cnt)
.map(|(c, _)| c)
}
fn build_coa_for_framework(
framework: &str,
group_id: &str,
country: &str,
industry: IndustrySector,
complexity: CoAComplexity,
) -> GroupResult<ChartOfAccounts> {
match framework {
"hgb" => {
let mut coa = skr_loader::build_chart_of_accounts_from_skr04(complexity, industry)
.map_err(|e| GroupError::Manifest(format!("SKR04 load failed: {e}")))?;
coa.coa_id = format!("{group_id}_{framework}_coa");
coa.name = format!("{group_id} {framework} chart of accounts");
coa.accounting_framework = Some(framework.to_string());
Ok(coa)
}
"pcg" => {
let mut coa = pcg_loader::build_chart_of_accounts_from_pcg_2024(complexity, industry)
.map_err(|e| GroupError::Manifest(format!("PCG load failed: {e}")))?;
coa.coa_id = format!("{group_id}_{framework}_coa");
coa.name = format!("{group_id} {framework} chart of accounts");
coa.accounting_framework = Some(framework.to_string());
Ok(coa)
}
_ => {
let mut gen = ChartOfAccountsGenerator::new(complexity, industry, 0);
let mut coa = gen.generate();
coa.coa_id = format!("{group_id}_{framework}_coa");
coa.name = format!("{group_id} {framework} chart of accounts");
coa.country = country.to_string();
coa.accounting_framework = Some(framework.to_string());
Ok(coa)
}
}
}