use chrono::{Datelike, NaiveDate};
use rand::SeedableRng;
use datasynth_core::models::subledger::fa::{DepreciationRun, FixedAssetRecord};
use crate::FAGenerator;
use crate::FAGeneratorConfig;
#[derive(Debug, Clone)]
pub struct FaDepreciationScheduleConfig {
pub fiscal_year: i32,
pub start_period: u32,
pub end_period: u32,
pub seed_offset: u64,
}
impl Default for FaDepreciationScheduleConfig {
fn default() -> Self {
let year = chrono::Utc::now().date_naive().year();
Self {
fiscal_year: year,
start_period: 1,
end_period: 12,
seed_offset: 800,
}
}
}
pub struct FaDepreciationScheduleGenerator {
config: FaDepreciationScheduleConfig,
seed: u64,
}
impl FaDepreciationScheduleGenerator {
pub fn new(config: FaDepreciationScheduleConfig, seed: u64) -> Self {
Self { config, seed }
}
pub fn generate(
&self,
company_code: &str,
fa_records: &[FixedAssetRecord],
) -> Vec<DepreciationRun> {
if fa_records.is_empty() {
return Vec::new();
}
let mut fa_gen = FAGenerator::new(
FAGeneratorConfig::default(),
rand_chacha::ChaCha8Rng::seed_from_u64(self.seed + self.config.seed_offset),
);
let asset_refs: Vec<&FixedAssetRecord> = fa_records.iter().collect();
let mut runs = Vec::new();
for period in self.config.start_period..=self.config.end_period {
let (year, month) = if period > 12 {
(self.config.fiscal_year, 12u32)
} else {
(self.config.fiscal_year, period)
};
let period_date = last_day_of_month(year, month);
let (run, _jes) = fa_gen.run_depreciation(
company_code,
&asset_refs,
period_date,
self.config.fiscal_year,
period,
);
if run.asset_count > 0 {
runs.push(run);
}
}
runs
}
}
fn last_day_of_month(year: i32, month: u32) -> NaiveDate {
let next_month = if month == 12 { 1 } else { month + 1 };
let next_year = if month == 12 { year + 1 } else { year };
NaiveDate::from_ymd_opt(next_year, next_month, 1)
.expect("valid next-month date")
.pred_opt()
.expect("valid last-day date")
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use datasynth_core::models::subledger::fa::{
AssetClass, DepreciationArea, DepreciationAreaType, DepreciationMethod,
};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
fn make_asset(id: &str, company: &str, acquisition_cost: Decimal) -> FixedAssetRecord {
let mut asset = FixedAssetRecord::new(
id.to_string(),
company.to_string(),
AssetClass::MachineryEquipment,
format!("Machine {id}"),
NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(),
acquisition_cost,
"USD".to_string(),
);
let area = DepreciationArea::new(
DepreciationAreaType::Book,
DepreciationMethod::StraightLine,
60, acquisition_cost,
);
asset.add_depreciation_area(area);
asset
}
#[test]
fn test_straight_line_monthly_amount() {
let asset = make_asset("A001", "1000", dec!(60_000));
let cfg = FaDepreciationScheduleConfig {
fiscal_year: 2024,
start_period: 1,
end_period: 1,
seed_offset: 0,
};
let gen = FaDepreciationScheduleGenerator::new(cfg, 42);
let runs = gen.generate("1000", &[asset]);
assert_eq!(runs.len(), 1, "Expected exactly one run for period 1");
let run = &runs[0];
assert_eq!(run.fiscal_period, 1);
assert_eq!(run.asset_count, 1);
assert_eq!(
run.total_depreciation,
dec!(1_000),
"Straight-line monthly depreciation should be 1_000"
);
}
#[test]
fn test_accumulated_increases_each_period() {
let asset = make_asset("A002", "1000", dec!(120_000));
let cfg = FaDepreciationScheduleConfig {
fiscal_year: 2024,
start_period: 1,
end_period: 2,
seed_offset: 1,
};
let gen = FaDepreciationScheduleGenerator::new(cfg, 99);
let runs = gen.generate("1000", &[asset]);
assert_eq!(runs.len(), 2, "Expected two runs (one per period)");
for run in &runs {
assert!(
run.total_depreciation > Decimal::ZERO,
"Depreciation should be positive"
);
}
assert_eq!(
runs[0].total_depreciation, runs[1].total_depreciation,
"Straight-line gives equal depreciation each period"
);
}
#[test]
fn test_empty_fa_records_returns_empty() {
let cfg = FaDepreciationScheduleConfig::default();
let gen = FaDepreciationScheduleGenerator::new(cfg, 0);
let runs = gen.generate("1000", &[]);
assert!(runs.is_empty());
}
#[test]
fn test_twelve_periods_generated() {
let asset = make_asset("A003", "1000", dec!(36_000));
let cfg = FaDepreciationScheduleConfig {
fiscal_year: 2024,
start_period: 1,
end_period: 12,
seed_offset: 2,
};
let gen = FaDepreciationScheduleGenerator::new(cfg, 7);
let runs = gen.generate("1000", &[asset]);
assert_eq!(runs.len(), 12, "Should produce 12 monthly runs");
}
}