use std::collections::BTreeMap;
use datasynth_core::models::balance::TrialBalance;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use crate::config::ConsolidationMethod;
use crate::errors::{GroupError, GroupResult};
use crate::manifest::builder::GroupManifest;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AggregatedTb {
pub group_id: String,
pub currency: String,
pub as_of_date: chrono::NaiveDate,
pub account_totals: BTreeMap<String, AggregatedAccount>,
pub contributing_entities: Vec<String>,
pub deferred_entities: Vec<DeferredEntity>,
pub total_debits: Decimal,
pub total_credits: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AggregatedAccount {
pub account_code: String,
pub debit_total: Decimal,
pub credit_total: Decimal,
pub net_balance: Decimal,
pub contributing_entities: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeferredEntity {
pub entity_code: String,
pub method: ConsolidationMethod,
}
pub fn aggregate_pre_elimination(
manifest: &GroupManifest,
entity_tbs: &[(String, TrialBalance)],
) -> GroupResult<AggregatedTb> {
let mut agg = AggregatedTb {
group_id: manifest.group_id.clone(),
currency: manifest.presentation_currency.clone(),
as_of_date: manifest.period.end,
account_totals: BTreeMap::new(),
contributing_entities: Vec::new(),
deferred_entities: Vec::new(),
total_debits: Decimal::ZERO,
total_credits: Decimal::ZERO,
};
for (entity_code, tb) in entity_tbs {
let method = lookup_consolidation_method(manifest, entity_code)?;
match method {
ConsolidationMethod::Parent | ConsolidationMethod::Full => {
ensure_currency_matches(manifest, entity_code, tb)?;
accumulate_tb(&mut agg, entity_code, tb);
}
ConsolidationMethod::EquityMethod
| ConsolidationMethod::Proportional
| ConsolidationMethod::FairValue => {
agg.deferred_entities.push(DeferredEntity {
entity_code: entity_code.clone(),
method,
});
}
}
}
agg.contributing_entities.sort();
agg.deferred_entities
.sort_by(|a, b| a.entity_code.cmp(&b.entity_code));
Ok(agg)
}
fn lookup_consolidation_method(
manifest: &GroupManifest,
entity_code: &str,
) -> GroupResult<ConsolidationMethod> {
manifest
.ownership_graph
.entities
.iter()
.find(|e| e.code == entity_code)
.map(|e| e.consolidation_method)
.ok_or_else(|| {
GroupError::Aggregate(format!(
"aggregate_pre_elimination: entity `{entity_code}` not in manifest"
))
})
}
fn ensure_currency_matches(
manifest: &GroupManifest,
entity_code: &str,
tb: &TrialBalance,
) -> GroupResult<()> {
if tb.currency != manifest.presentation_currency {
tracing::debug!(
entity = entity_code,
tb_currency = %tb.currency,
presentation_currency = %manifest.presentation_currency,
"TB currency differs from presentation currency — translation worksheet emitted separately (Chunk 6)",
);
}
Ok(())
}
fn accumulate_tb(agg: &mut AggregatedTb, entity_code: &str, tb: &TrialBalance) {
let mut accounts_touched_by_this_entity: std::collections::BTreeSet<String> =
std::collections::BTreeSet::new();
for line in &tb.lines {
let entry = agg
.account_totals
.entry(line.account_code.clone())
.or_insert_with(|| AggregatedAccount {
account_code: line.account_code.clone(),
debit_total: Decimal::ZERO,
credit_total: Decimal::ZERO,
net_balance: Decimal::ZERO,
contributing_entities: 0,
});
entry.debit_total += line.debit_balance;
entry.credit_total += line.credit_balance;
entry.net_balance = entry.debit_total - entry.credit_total;
if accounts_touched_by_this_entity.insert(line.account_code.clone()) {
entry.contributing_entities += 1;
}
}
agg.total_debits += tb.total_debits;
agg.total_credits += tb.total_credits;
agg.contributing_entities.push(entity_code.to_string());
}