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)]
126mod tests {
127 use super::*;
128 use datasynth_core::models::subledger::fa::{
129 AssetClass, DepreciationArea, DepreciationAreaType, DepreciationMethod,
130 };
131 use rust_decimal::Decimal;
132 use rust_decimal_macros::dec;
133
134 fn make_asset(id: &str, company: &str, acquisition_cost: Decimal) -> FixedAssetRecord {
135 let mut asset = FixedAssetRecord::new(
136 id.to_string(),
137 company.to_string(),
138 AssetClass::MachineryEquipment,
139 format!("Machine {id}"),
140 NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(),
141 acquisition_cost,
142 "USD".to_string(),
143 );
144 let area = DepreciationArea::new(
145 DepreciationAreaType::Book,
146 DepreciationMethod::StraightLine,
147 60, acquisition_cost,
149 );
150 asset.add_depreciation_area(area);
151 asset
152 }
153
154 #[test]
155 fn test_straight_line_monthly_amount() {
156 let asset = make_asset("A001", "1000", dec!(60_000));
159 let cfg = FaDepreciationScheduleConfig {
160 fiscal_year: 2024,
161 start_period: 1,
162 end_period: 1,
163 seed_offset: 0,
164 };
165 let gen = FaDepreciationScheduleGenerator::new(cfg, 42);
166 let runs = gen.generate("1000", &[asset]);
167
168 assert_eq!(runs.len(), 1, "Expected exactly one run for period 1");
169 let run = &runs[0];
170 assert_eq!(run.fiscal_period, 1);
171 assert_eq!(run.asset_count, 1);
172 assert_eq!(
173 run.total_depreciation,
174 dec!(1_000),
175 "Straight-line monthly depreciation should be 1_000"
176 );
177 }
178
179 #[test]
180 fn test_accumulated_increases_each_period() {
181 let asset = make_asset("A002", "1000", dec!(120_000));
183 let cfg = FaDepreciationScheduleConfig {
184 fiscal_year: 2024,
185 start_period: 1,
186 end_period: 2,
187 seed_offset: 1,
188 };
189 let gen = FaDepreciationScheduleGenerator::new(cfg, 99);
190 let runs = gen.generate("1000", &[asset]);
191
192 assert_eq!(runs.len(), 2, "Expected two runs (one per period)");
193 for run in &runs {
195 assert!(
196 run.total_depreciation > Decimal::ZERO,
197 "Depreciation should be positive"
198 );
199 }
200 assert_eq!(
202 runs[0].total_depreciation, runs[1].total_depreciation,
203 "Straight-line gives equal depreciation each period"
204 );
205 }
206
207 #[test]
208 fn test_empty_fa_records_returns_empty() {
209 let cfg = FaDepreciationScheduleConfig::default();
210 let gen = FaDepreciationScheduleGenerator::new(cfg, 0);
211 let runs = gen.generate("1000", &[]);
212 assert!(runs.is_empty());
213 }
214
215 #[test]
216 fn test_twelve_periods_generated() {
217 let asset = make_asset("A003", "1000", dec!(36_000));
218 let cfg = FaDepreciationScheduleConfig {
219 fiscal_year: 2024,
220 start_period: 1,
221 end_period: 12,
222 seed_offset: 2,
223 };
224 let gen = FaDepreciationScheduleGenerator::new(cfg, 7);
225 let runs = gen.generate("1000", &[asset]);
226 assert_eq!(runs.len(), 12, "Should produce 12 monthly runs");
227 }
228}