datasynth_generators/subledger/
depreciation_run_generator.rs1use chrono::{Datelike, NaiveDate};
17use rand::SeedableRng;
18
19use datasynth_core::models::subledger::fa::{DepreciationRun, FixedAssetRecord};
20
21use crate::FAGenerator;
22use crate::FAGeneratorConfig;
23
24#[derive(Debug, Clone)]
26pub struct FaDepreciationScheduleConfig {
27 pub fiscal_year: i32,
29 pub start_period: u32,
31 pub end_period: u32,
33 pub seed_offset: u64,
36}
37
38impl Default for FaDepreciationScheduleConfig {
39 fn default() -> Self {
40 let year = chrono::Utc::now().date_naive().year();
41 Self {
42 fiscal_year: year,
43 start_period: 1,
44 end_period: 12,
45 seed_offset: 800,
46 }
47 }
48}
49
50pub struct FaDepreciationScheduleGenerator {
53 config: FaDepreciationScheduleConfig,
54 seed: u64,
55}
56
57impl FaDepreciationScheduleGenerator {
58 pub fn new(config: FaDepreciationScheduleConfig, seed: u64) -> Self {
60 Self { config, seed }
61 }
62
63 pub fn generate(
69 &self,
70 company_code: &str,
71 fa_records: &[FixedAssetRecord],
72 ) -> Vec<DepreciationRun> {
73 if fa_records.is_empty() {
74 return Vec::new();
75 }
76
77 let mut fa_gen = FAGenerator::new(
78 FAGeneratorConfig::default(),
79 rand_chacha::ChaCha8Rng::seed_from_u64(self.seed + self.config.seed_offset),
80 );
81
82 let asset_refs: Vec<&FixedAssetRecord> = fa_records.iter().collect();
83
84 let mut runs = Vec::new();
85
86 for period in self.config.start_period..=self.config.end_period {
87 let (year, month) = if period > 12 {
90 (self.config.fiscal_year, 12u32)
92 } else {
93 (self.config.fiscal_year, period)
94 };
95
96 let period_date = last_day_of_month(year, month);
97
98 let (run, _jes) = fa_gen.run_depreciation(
99 company_code,
100 &asset_refs,
101 period_date,
102 self.config.fiscal_year,
103 period,
104 );
105
106 if run.asset_count > 0 {
107 runs.push(run);
108 }
109 }
110
111 runs
112 }
113}
114
115fn last_day_of_month(year: i32, month: u32) -> NaiveDate {
117 let next_month = if month == 12 { 1 } else { month + 1 };
118 let next_year = if month == 12 { year + 1 } else { year };
119 NaiveDate::from_ymd_opt(next_year, next_month, 1)
120 .expect("valid next-month date")
121 .pred_opt()
122 .expect("valid last-day date")
123}
124
125#[cfg(test)]
126#[allow(clippy::unwrap_used)]
127mod tests {
128 use super::*;
129 use datasynth_core::models::subledger::fa::{
130 AssetClass, DepreciationArea, DepreciationAreaType, DepreciationMethod,
131 };
132 use rust_decimal::Decimal;
133 use rust_decimal_macros::dec;
134
135 fn make_asset(id: &str, company: &str, acquisition_cost: Decimal) -> FixedAssetRecord {
136 let mut asset = FixedAssetRecord::new(
137 id.to_string(),
138 company.to_string(),
139 AssetClass::MachineryEquipment,
140 format!("Machine {id}"),
141 NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(),
142 acquisition_cost,
143 "USD".to_string(),
144 );
145 let area = DepreciationArea::new(
146 DepreciationAreaType::Book,
147 DepreciationMethod::StraightLine,
148 60, acquisition_cost,
150 );
151 asset.add_depreciation_area(area);
152 asset
153 }
154
155 #[test]
156 fn test_straight_line_monthly_amount() {
157 let asset = make_asset("A001", "1000", dec!(60_000));
160 let cfg = FaDepreciationScheduleConfig {
161 fiscal_year: 2024,
162 start_period: 1,
163 end_period: 1,
164 seed_offset: 0,
165 };
166 let gen = FaDepreciationScheduleGenerator::new(cfg, 42);
167 let runs = gen.generate("1000", &[asset]);
168
169 assert_eq!(runs.len(), 1, "Expected exactly one run for period 1");
170 let run = &runs[0];
171 assert_eq!(run.fiscal_period, 1);
172 assert_eq!(run.asset_count, 1);
173 assert_eq!(
174 run.total_depreciation,
175 dec!(1_000),
176 "Straight-line monthly depreciation should be 1_000"
177 );
178 }
179
180 #[test]
181 fn test_accumulated_increases_each_period() {
182 let asset = make_asset("A002", "1000", dec!(120_000));
184 let cfg = FaDepreciationScheduleConfig {
185 fiscal_year: 2024,
186 start_period: 1,
187 end_period: 2,
188 seed_offset: 1,
189 };
190 let gen = FaDepreciationScheduleGenerator::new(cfg, 99);
191 let runs = gen.generate("1000", &[asset]);
192
193 assert_eq!(runs.len(), 2, "Expected two runs (one per period)");
194 for run in &runs {
196 assert!(
197 run.total_depreciation > Decimal::ZERO,
198 "Depreciation should be positive"
199 );
200 }
201 assert_eq!(
203 runs[0].total_depreciation, runs[1].total_depreciation,
204 "Straight-line gives equal depreciation each period"
205 );
206 }
207
208 #[test]
209 fn test_empty_fa_records_returns_empty() {
210 let cfg = FaDepreciationScheduleConfig::default();
211 let gen = FaDepreciationScheduleGenerator::new(cfg, 0);
212 let runs = gen.generate("1000", &[]);
213 assert!(runs.is_empty());
214 }
215
216 #[test]
217 fn test_twelve_periods_generated() {
218 let asset = make_asset("A003", "1000", dec!(36_000));
219 let cfg = FaDepreciationScheduleConfig {
220 fiscal_year: 2024,
221 start_period: 1,
222 end_period: 12,
223 seed_offset: 2,
224 };
225 let gen = FaDepreciationScheduleGenerator::new(cfg, 7);
226 let runs = gen.generate("1000", &[asset]);
227 assert_eq!(runs.len(), 12, "Should produce 12 monthly runs");
228 }
229}