use crate::config::{GroupConfig, IcRelationshipConfig};
use crate::errors::{GroupError, GroupResult};
const ENTITY_CONFIG_FIELDS: &[&str] = &[
"code",
"name",
"country",
"functional_currency",
"scoping_profile",
"consolidation_method",
"ownership_percent",
"parent_code",
"acquisition_date",
"accounting_framework",
"industry",
"rows",
];
const TYPO_DISTANCE_THRESHOLD: usize = 2;
pub fn validate(cfg: &GroupConfig) -> GroupResult<()> {
let mut errors: Vec<String> = Vec::new();
let entity_codes: std::collections::BTreeSet<&str> = cfg
.ownership
.entities
.iter()
.map(|e| e.code.as_str())
.collect();
let parent = cfg.ownership.parent_entity_code.as_str();
if !entity_codes.contains(parent) {
errors.push(format!(
"parent_entity_code {parent} is not present in ownership.entities"
));
}
for entity in &cfg.ownership.entities {
let code = entity.code.as_str();
if !cfg.scoping_profiles.contains_key(&entity.scoping_profile) {
errors.push(format!(
"entity {code} references unknown scoping_profile {}",
entity.scoping_profile
));
}
if let Some(ref pc) = entity.parent_code {
if !entity_codes.contains(pc.as_str()) {
errors.push(format!(
"entity {code} has parent_code {pc} which is not in ownership.entities"
));
}
}
if let Some(pct) = entity.ownership_percent {
if pct < rust_decimal::Decimal::ZERO || pct > rust_decimal::Decimal::ONE {
errors.push(format!(
"entity {code} has ownership_percent {pct} outside [0.0, 1.0]"
));
}
}
for key in entity.overrides.keys() {
if let Some(suggestion) = find_closest_field(key) {
errors.push(format!(
"entity {code}: possible typo in field '{key}' — did you mean '{suggestion}'?"
));
}
}
}
for rel in &cfg.intercompany.relationships {
match rel {
IcRelationshipConfig::Explicit(ex) => {
if !entity_codes.contains(ex.seller.as_str()) {
errors.push(format!(
"IC relationship references unknown entity {}",
ex.seller
));
}
if !entity_codes.contains(ex.buyer.as_str()) {
errors.push(format!(
"IC relationship references unknown entity {}",
ex.buyer
));
}
if ex.seller == ex.buyer {
errors.push(format!(
"IC relationship has seller == buyer ({})",
ex.seller
));
}
}
IcRelationshipConfig::Pattern(pat) => {
let p = &pat.pattern;
if let Some(ref seller) = p.seller {
if !entity_codes.contains(seller.as_str()) {
errors.push(format!(
"IC pattern references unknown entity/profile {seller}"
));
}
}
if let Some(ref buyer) = p.buyer {
if !entity_codes.contains(buyer.as_str()) {
errors.push(format!(
"IC pattern references unknown entity/profile {buyer}"
));
}
}
if let Some(ref sp) = p.seller_scoping_profile {
if sp != "any" && !cfg.scoping_profiles.contains_key(sp) {
errors.push(format!("IC pattern references unknown entity/profile {sp}"));
}
}
if let Some(ref sp) = p.buyer_scoping_profile {
if sp != "any" && !cfg.scoping_profiles.contains_key(sp) {
errors.push(format!("IC pattern references unknown entity/profile {sp}"));
}
}
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(GroupError::Config(errors.join("\n")))
}
}
fn find_closest_field(key: &str) -> Option<&'static str> {
let mut best_dist = usize::MAX;
let mut best_field = None;
for &field in ENTITY_CONFIG_FIELDS {
let d = strsim::levenshtein(key, field);
if d < best_dist {
best_dist = d;
best_field = Some(field);
}
}
if best_dist <= TYPO_DISTANCE_THRESHOLD && best_dist > 0 {
best_field
} else {
None
}
}