use std::collections::{BTreeMap, BTreeSet};
use chrono::NaiveDate;
use datasynth_core::models::balance::TrialBalance;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use crate::aggregate::pre_elim::AggregatedTb;
use crate::errors::GroupResult;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ConsolidationSchedule {
pub group_id: String,
pub as_of_date: NaiveDate,
pub currency: String,
pub lines: Vec<ScheduleLine>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ScheduleLine {
pub account_category: String,
pub account_code: String,
pub entity_amounts: BTreeMap<String, Decimal>,
pub pre_elimination_total: Decimal,
pub elimination_adjustments: Decimal,
pub post_elimination_total: Decimal,
}
pub fn build_consolidation_schedule(
pre_elim_tb: &AggregatedTb,
post_elim_tb: &AggregatedTb,
entity_tbs: &[(String, TrialBalance)],
group_id: &str,
as_of_date: NaiveDate,
) -> GroupResult<ConsolidationSchedule> {
let mut entity_contributions: BTreeMap<String, BTreeMap<String, Decimal>> = BTreeMap::new();
for (entity_code, tb) in entity_tbs {
for line in &tb.lines {
let net = line.debit_balance - line.credit_balance;
entity_contributions
.entry(line.account_code.clone())
.or_default()
.insert(entity_code.clone(), net);
}
}
build_consolidation_schedule_with_contributions(
pre_elim_tb,
post_elim_tb,
&entity_contributions,
group_id,
as_of_date,
)
}
pub fn build_consolidation_schedule_with_contributions(
pre_elim_tb: &AggregatedTb,
post_elim_tb: &AggregatedTb,
entity_contributions: &BTreeMap<String, BTreeMap<String, Decimal>>,
group_id: &str,
as_of_date: NaiveDate,
) -> GroupResult<ConsolidationSchedule> {
let mut codes: BTreeSet<String> = BTreeSet::new();
codes.extend(pre_elim_tb.account_totals.keys().cloned());
codes.extend(post_elim_tb.account_totals.keys().cloned());
let mut lines: Vec<ScheduleLine> = Vec::with_capacity(codes.len());
for code in &codes {
let pre = pre_elim_tb
.account_totals
.get(code)
.map(|a| a.net_balance)
.unwrap_or(Decimal::ZERO);
let post = post_elim_tb
.account_totals
.get(code)
.map(|a| a.net_balance)
.unwrap_or(Decimal::ZERO);
let adj = post - pre;
let entity_amounts = entity_contributions.get(code).cloned().unwrap_or_default();
lines.push(ScheduleLine {
account_category: account_category_label(code),
account_code: code.clone(),
entity_amounts,
pre_elimination_total: pre,
elimination_adjustments: adj,
post_elimination_total: post,
});
}
lines.sort_by(|a, b| a.account_code.cmp(&b.account_code));
Ok(ConsolidationSchedule {
group_id: group_id.to_string(),
as_of_date,
currency: pre_elim_tb.currency.clone(),
lines,
})
}
fn account_category_label(code: &str) -> String {
let n: u32 = match code.parse() {
Ok(n) => n,
Err(_) => return "Other".to_string(),
};
match n {
1000..=1999 => "Asset".to_string(),
2000..=2999 => "Liability".to_string(),
3000..=3999 => "Equity".to_string(),
4000..=4999 => "Revenue".to_string(),
5000..=6999 => "Expense".to_string(),
7000..=7999 => "Other Income/Expense".to_string(),
8000..=8999 => "Tax".to_string(),
9000..=9999 => "Suspense".to_string(),
_ => "Other".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn category_label_ranges() {
assert_eq!(account_category_label("1000"), "Asset");
assert_eq!(account_category_label("2000"), "Liability");
assert_eq!(account_category_label("3500"), "Equity");
assert_eq!(account_category_label("4000"), "Revenue");
assert_eq!(account_category_label("5000"), "Expense");
assert_eq!(account_category_label("7100"), "Other Income/Expense");
assert_eq!(account_category_label("8000"), "Tax");
assert_eq!(account_category_label("9000"), "Suspense");
assert_eq!(account_category_label("abc"), "Other");
}
}