use datasynth_core::models::{JournalEntry, JournalEntryLine};
use datasynth_generators::period_close::SegmentGenerator;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
fn make_je(
id: &str,
company: &str,
debit_account: &str,
credit_account: &str,
amount: Decimal,
) -> JournalEntry {
let date = chrono::NaiveDate::from_ymd_opt(2025, 3, 31).unwrap();
let mut je = JournalEntry::new_simple(id.into(), company.into(), date, id.into());
je.add_line(JournalEntryLine {
line_number: 1,
gl_account: debit_account.into(),
debit_amount: amount,
..Default::default()
});
je.add_line(JournalEntryLine {
line_number: 2,
gl_account: credit_account.into(),
credit_amount: amount,
..Default::default()
});
je
}
#[test]
fn test_segments_from_two_companies() {
let mut gen = SegmentGenerator::new(42);
let jes = vec![
make_je("JE-1", "CORP", "1010", "4000", dec!(100_000)),
make_je("JE-2", "CORP", "5000", "2000", dec!(60_000)),
make_je("JE-3", "SUB1", "1010", "4000", dec!(50_000)),
make_je("JE-4", "SUB1", "5000", "2000", dec!(30_000)),
];
let companies = vec![
("CORP".to_string(), "Corp HQ".to_string()),
("SUB1".to_string(), "Subsidiary 1".to_string()),
];
let (segments, recon) =
gen.generate_from_journal_entries(&jes, &companies, "2025-Q1", dec!(5_000));
assert_eq!(segments.len(), 2, "Should produce one segment per company");
let total_revenue: Decimal = segments.iter().map(|s| s.revenue_external).sum();
assert_eq!(total_revenue, dec!(150_000), "Total revenue mismatch");
let total_profit: Decimal = segments.iter().map(|s| s.operating_profit).sum();
assert_eq!(total_profit, dec!(60_000), "Total profit mismatch");
assert_eq!(recon.segment_revenue_total, dec!(150_000));
assert_eq!(recon.intersegment_eliminations, dec!(5_000));
assert_eq!(recon.consolidated_revenue, dec!(145_000));
}
#[test]
fn test_revenue_accumulates_across_multiple_jes() {
let mut gen = SegmentGenerator::new(1);
let jes = vec![
make_je("JE-A", "C001", "1010", "4000", dec!(20_000)),
make_je("JE-B", "C001", "1010", "4100", dec!(30_000)),
make_je("JE-C", "C001", "1010", "4200", dec!(50_000)),
];
let companies = vec![("C001".to_string(), "Company One".to_string())];
let (segments, _) =
gen.generate_from_journal_entries(&jes, &companies, "2025-Q1", Decimal::ZERO);
assert_eq!(segments.len(), 1);
assert_eq!(segments[0].revenue_external, dec!(100_000));
}
#[test]
fn test_opex_reduces_operating_profit() {
let mut gen = SegmentGenerator::new(2);
let jes = vec![
make_je("JE-1", "C001", "1010", "4000", dec!(100_000)),
make_je("JE-2", "C001", "5000", "2000", dec!(40_000)),
make_je("JE-3", "C001", "6000", "1010", dec!(15_000)),
make_je("JE-4", "C001", "7000", "1010", dec!(5_000)),
];
let companies = vec![("C001".to_string(), "One".to_string())];
let (segments, _) =
gen.generate_from_journal_entries(&jes, &companies, "2025-Q1", Decimal::ZERO);
assert_eq!(segments[0].operating_profit, dec!(40_000));
}
#[test]
fn test_asset_and_liability_aggregation() {
let mut gen = SegmentGenerator::new(3);
let jes = vec![
make_je("JE-1", "C001", "1010", "2000", dec!(200_000)),
];
let companies = vec![("C001".to_string(), "One".to_string())];
let (segments, _) =
gen.generate_from_journal_entries(&jes, &companies, "2025-Q1", Decimal::ZERO);
assert_eq!(segments.len(), 1);
assert_eq!(segments[0].total_assets, dec!(200_000));
assert_eq!(segments[0].total_liabilities, dec!(200_000));
}
#[test]
fn test_company_filtering_isolates_data() {
let mut gen = SegmentGenerator::new(4);
let jes = vec![
make_je("JE-1", "CORP", "1010", "4000", dec!(100_000)),
make_je("JE-2", "OTHER", "1010", "4000", dec!(999_999)), ];
let companies = vec![("CORP".to_string(), "Corp".to_string())];
let (segments, _) =
gen.generate_from_journal_entries(&jes, &companies, "2025-Q1", Decimal::ZERO);
assert_eq!(segments.len(), 1);
assert_eq!(
segments[0].revenue_external,
dec!(100_000),
"OTHER company data must not bleed into CORP segment"
);
}
#[test]
fn test_reconciliation_math() {
let mut gen = SegmentGenerator::new(5);
let jes = vec![
make_je("JE-1", "A", "1010", "4000", dec!(80_000)),
make_je("JE-2", "B", "1010", "4000", dec!(60_000)),
];
let companies = vec![
("A".to_string(), "Alpha".to_string()),
("B".to_string(), "Beta".to_string()),
];
let elimination = dec!(10_000);
let (segments, recon) =
gen.generate_from_journal_entries(&jes, &companies, "2025-Q1", elimination);
let expected_total: Decimal = segments.iter().map(|s| s.revenue_external).sum();
assert_eq!(recon.segment_revenue_total, expected_total);
assert_eq!(
recon.consolidated_revenue,
recon.segment_revenue_total - elimination
);
assert_eq!(
recon.consolidated_profit,
recon.segment_profit_total + recon.corporate_overhead
);
assert_eq!(
recon.consolidated_assets,
recon.segment_assets_total + recon.unallocated_assets
);
}
#[test]
fn test_empty_jes_produces_zero_segments() {
let mut gen = SegmentGenerator::new(6);
let companies = vec![("C001".to_string(), "One".to_string())];
let (segments, recon) =
gen.generate_from_journal_entries(&[], &companies, "2025-Q1", Decimal::ZERO);
assert_eq!(segments.len(), 1);
assert_eq!(segments[0].revenue_external, Decimal::ZERO);
assert_eq!(segments[0].operating_profit, Decimal::ZERO);
assert_eq!(recon.segment_revenue_total, Decimal::ZERO);
assert_eq!(recon.consolidated_revenue, Decimal::ZERO);
}
#[test]
fn test_period_and_segment_type_propagated() {
use datasynth_core::models::SegmentType;
let mut gen = SegmentGenerator::new(7);
let jes = vec![make_je("JE-1", "C001", "1010", "4000", dec!(1_000))];
let companies = vec![("C001".to_string(), "One".to_string())];
let (segments, recon) =
gen.generate_from_journal_entries(&jes, &companies, "2025-Q2", Decimal::ZERO);
assert_eq!(recon.period, "2025-Q2");
for seg in &segments {
assert_eq!(seg.period, "2025-Q2");
assert_eq!(seg.segment_type, SegmentType::LegalEntity);
}
}