1use chrono::NaiveDate;
4use datasynth_core::models::{
5 AssetAccountDetermination, AssetClass, AssetStatus, DepreciationMethod, FixedAsset,
6 FixedAssetPool,
7};
8use datasynth_core::utils::seeded_rng;
9use rand::prelude::*;
10use rand_chacha::ChaCha8Rng;
11use rust_decimal::Decimal;
12use tracing::debug;
13
14use crate::coa_generator::CoAFramework;
15
16#[derive(Debug, Clone)]
18pub struct AssetGeneratorConfig {
19 pub asset_class_distribution: Vec<(AssetClass, f64)>,
21 pub depreciation_method_distribution: Vec<(DepreciationMethod, f64)>,
23 pub useful_life_by_class: Vec<(AssetClass, u32)>,
25 pub acquisition_cost_range: (Decimal, Decimal),
27 pub salvage_value_percent: f64,
29 pub fully_depreciated_rate: f64,
31 pub disposed_rate: f64,
33}
34
35impl Default for AssetGeneratorConfig {
36 fn default() -> Self {
37 Self {
38 asset_class_distribution: vec![
39 (AssetClass::Buildings, 0.05),
40 (AssetClass::Machinery, 0.25),
41 (AssetClass::Vehicles, 0.15),
42 (AssetClass::Furniture, 0.15),
43 (AssetClass::ItEquipment, 0.25),
44 (AssetClass::Software, 0.10),
45 (AssetClass::LeaseholdImprovements, 0.05),
46 ],
47 depreciation_method_distribution: vec![
48 (DepreciationMethod::StraightLine, 0.70),
49 (DepreciationMethod::DoubleDecliningBalance, 0.15),
50 (DepreciationMethod::Macrs, 0.10),
51 (DepreciationMethod::SumOfYearsDigits, 0.05),
52 ],
53 useful_life_by_class: vec![
54 (AssetClass::Buildings, 480), (AssetClass::BuildingImprovements, 180), (AssetClass::Machinery, 84), (AssetClass::Vehicles, 60), (AssetClass::Furniture, 84), (AssetClass::ItEquipment, 36), (AssetClass::Software, 36), (AssetClass::LeaseholdImprovements, 120), (AssetClass::Land, 0), (AssetClass::ConstructionInProgress, 0), ],
65 acquisition_cost_range: (Decimal::from(1_000), Decimal::from(500_000)),
66 salvage_value_percent: 0.05,
67 fully_depreciated_rate: 0.10,
68 disposed_rate: 0.02,
69 }
70 }
71}
72
73const ASSET_DESCRIPTIONS: &[(AssetClass, &[&str])] = &[
75 (
76 AssetClass::Buildings,
77 &[
78 "Corporate Office Building",
79 "Manufacturing Facility",
80 "Warehouse Complex",
81 "Distribution Center",
82 "Research Laboratory",
83 "Administrative Building",
84 ],
85 ),
86 (
87 AssetClass::Machinery,
88 &[
89 "Production Line Equipment",
90 "CNC Machining Center",
91 "Assembly Robot System",
92 "Industrial Press Machine",
93 "Packaging Equipment",
94 "Testing Equipment",
95 "Quality Control System",
96 "Material Handling System",
97 ],
98 ),
99 (
100 AssetClass::Vehicles,
101 &[
102 "Delivery Truck",
103 "Company Car",
104 "Forklift",
105 "Van Fleet Unit",
106 "Executive Vehicle",
107 "Service Vehicle",
108 "Cargo Truck",
109 "Utility Vehicle",
110 ],
111 ),
112 (
113 AssetClass::Furniture,
114 &[
115 "Office Workstation Set",
116 "Conference Room Furniture",
117 "Executive Desk Set",
118 "Reception Area Furniture",
119 "Cubicle System",
120 "Storage Cabinet Set",
121 "Meeting Room Table",
122 "Ergonomic Chair Set",
123 ],
124 ),
125 (
126 AssetClass::ItEquipment,
127 &[
128 "Server Rack System",
129 "Network Switch Array",
130 "Desktop Computer Set",
131 "Laptop Fleet",
132 "Storage Array",
133 "Backup System",
134 "Security System",
135 "Communication System",
136 ],
137 ),
138 (
139 AssetClass::Software,
140 &[
141 "ERP System License",
142 "CAD Software Suite",
143 "Database License",
144 "Office Suite License",
145 "Security Software",
146 "Development Tools",
147 "Analytics Platform",
148 "CRM System",
149 ],
150 ),
151 (
152 AssetClass::LeaseholdImprovements,
153 &[
154 "Office Build-out",
155 "HVAC Improvements",
156 "Electrical Upgrades",
157 "Floor Renovations",
158 "Lighting System",
159 "Security Improvements",
160 "Accessibility Upgrades",
161 "IT Infrastructure",
162 ],
163 ),
164];
165
166pub struct AssetGenerator {
168 rng: ChaCha8Rng,
169 seed: u64,
170 config: AssetGeneratorConfig,
171 asset_counter: usize,
172 coa_framework: CoAFramework,
173}
174
175impl AssetGenerator {
176 pub fn new(seed: u64) -> Self {
178 Self::with_config(seed, AssetGeneratorConfig::default())
179 }
180
181 pub fn with_config(seed: u64, config: AssetGeneratorConfig) -> Self {
183 Self {
184 rng: seeded_rng(seed, 0),
185 seed,
186 config,
187 asset_counter: 0,
188 coa_framework: CoAFramework::UsGaap,
189 }
190 }
191
192 pub fn set_coa_framework(&mut self, framework: CoAFramework) {
194 self.coa_framework = framework;
195 }
196
197 pub fn generate_asset(
199 &mut self,
200 company_code: &str,
201 acquisition_date: NaiveDate,
202 ) -> FixedAsset {
203 self.asset_counter += 1;
204
205 let asset_id = format!("FA-{}-{:06}", company_code, self.asset_counter);
206 let asset_class = self.select_asset_class();
207 let description = self.select_description(&asset_class);
208
209 let mut asset = FixedAsset::new(
210 asset_id,
211 description.to_string(),
212 asset_class,
213 company_code,
214 acquisition_date,
215 self.generate_acquisition_cost(),
216 );
217
218 let is_gwg = self.coa_framework == CoAFramework::GermanSkr04
220 && asset.acquisition_cost <= Decimal::from(800)
221 && asset_class.is_depreciable();
222
223 if is_gwg {
224 asset.is_gwg = Some(true);
225 asset.depreciation_method = DepreciationMethod::ImmediateExpense;
226 asset.useful_life_months = 1;
227 asset.salvage_value = Decimal::ZERO;
228 } else {
229 asset.depreciation_method = self.select_depreciation_method(&asset_class);
231 asset.useful_life_months = self.get_useful_life(&asset_class);
232 asset.salvage_value = (asset.acquisition_cost
233 * Decimal::from_f64_retain(self.config.salvage_value_percent)
234 .unwrap_or(Decimal::from_f64_retain(0.05).expect("valid decimal literal")))
235 .round_dp(2);
236 }
237
238 asset.account_determination = self.generate_account_determination(&asset_class);
240
241 asset.location = Some(format!("P{}", company_code));
243 asset.cost_center = Some(format!("CC-{}-ADMIN", company_code));
244
245 if matches!(
247 asset_class,
248 AssetClass::Machinery | AssetClass::Vehicles | AssetClass::ItEquipment
249 ) {
250 asset.serial_number = Some(self.generate_serial_number());
251 }
252
253 if self.rng.random::<f64>() < self.config.disposed_rate {
255 let disposal_date =
256 acquisition_date + chrono::Duration::days(self.rng.random_range(365..1825) as i64);
257 let (proceeds, _gain_loss) = self.generate_disposal_values(&asset);
258 asset.dispose(disposal_date, proceeds);
259 } else if self.rng.random::<f64>() < self.config.fully_depreciated_rate {
260 asset.accumulated_depreciation = asset.acquisition_cost - asset.salvage_value;
261 asset.net_book_value = asset.salvage_value;
262 }
263
264 asset
265 }
266
267 pub fn generate_asset_of_class(
269 &mut self,
270 asset_class: AssetClass,
271 company_code: &str,
272 acquisition_date: NaiveDate,
273 ) -> FixedAsset {
274 self.asset_counter += 1;
275
276 let asset_id = format!("FA-{}-{:06}", company_code, self.asset_counter);
277 let description = self.select_description(&asset_class);
278
279 let mut asset = FixedAsset::new(
280 asset_id,
281 description.to_string(),
282 asset_class,
283 company_code,
284 acquisition_date,
285 self.generate_acquisition_cost_for_class(&asset_class),
286 );
287
288 asset.depreciation_method = self.select_depreciation_method(&asset_class);
289 asset.useful_life_months = self.get_useful_life(&asset_class);
290 asset.salvage_value = (asset.acquisition_cost
291 * Decimal::from_f64_retain(self.config.salvage_value_percent)
292 .unwrap_or(Decimal::from_f64_retain(0.05).expect("valid decimal literal")))
293 .round_dp(2);
294
295 asset.account_determination = self.generate_account_determination(&asset_class);
296 asset.location = Some(format!("P{}", company_code));
297 asset.cost_center = Some(format!("CC-{}-ADMIN", company_code));
298
299 if matches!(
300 asset_class,
301 AssetClass::Machinery | AssetClass::Vehicles | AssetClass::ItEquipment
302 ) {
303 asset.serial_number = Some(self.generate_serial_number());
304 }
305
306 asset
307 }
308
309 pub fn generate_aged_asset(
311 &mut self,
312 company_code: &str,
313 acquisition_date: NaiveDate,
314 as_of_date: NaiveDate,
315 ) -> FixedAsset {
316 let mut asset = self.generate_asset(company_code, acquisition_date);
317
318 let months_elapsed = ((as_of_date - acquisition_date).num_days() / 30) as u32;
320
321 for month_offset in 0..months_elapsed {
323 if asset.status == AssetStatus::Active {
324 let dep_date =
326 acquisition_date + chrono::Duration::days((month_offset as i64 + 1) * 30);
327 let depreciation = asset.calculate_monthly_depreciation(dep_date);
328 asset.apply_depreciation(depreciation);
329 }
330 }
331
332 asset
333 }
334
335 pub fn generate_asset_pool(
337 &mut self,
338 count: usize,
339 company_code: &str,
340 date_range: (NaiveDate, NaiveDate),
341 ) -> FixedAssetPool {
342 debug!(count, company_code, "Generating fixed asset pool");
343 let mut pool = FixedAssetPool::new();
344
345 let (start_date, end_date) = date_range;
346 let days_range = (end_date - start_date).num_days() as u64;
347
348 for _ in 0..count {
349 let acquisition_date =
350 start_date + chrono::Duration::days(self.rng.random_range(0..=days_range) as i64);
351 let asset = self.generate_asset(company_code, acquisition_date);
352 pool.add_asset(asset);
353 }
354
355 pool
356 }
357
358 pub fn generate_aged_asset_pool(
360 &mut self,
361 count: usize,
362 company_code: &str,
363 acquisition_date_range: (NaiveDate, NaiveDate),
364 as_of_date: NaiveDate,
365 ) -> FixedAssetPool {
366 let mut pool = FixedAssetPool::new();
367
368 let (start_date, end_date) = acquisition_date_range;
369 let days_range = (end_date - start_date).num_days() as u64;
370
371 for _ in 0..count {
372 let acquisition_date =
373 start_date + chrono::Duration::days(self.rng.random_range(0..=days_range) as i64);
374 let asset = self.generate_aged_asset(company_code, acquisition_date, as_of_date);
375 pool.add_asset(asset);
376 }
377
378 pool
379 }
380
381 pub fn generate_diverse_pool(
383 &mut self,
384 count: usize,
385 company_code: &str,
386 date_range: (NaiveDate, NaiveDate),
387 ) -> FixedAssetPool {
388 let mut pool = FixedAssetPool::new();
389
390 let (start_date, end_date) = date_range;
391 let days_range = (end_date - start_date).num_days() as u64;
392
393 let class_counts = [
395 (AssetClass::Buildings, (count as f64 * 0.05) as usize),
396 (AssetClass::Machinery, (count as f64 * 0.25) as usize),
397 (AssetClass::Vehicles, (count as f64 * 0.15) as usize),
398 (AssetClass::Furniture, (count as f64 * 0.15) as usize),
399 (AssetClass::ItEquipment, (count as f64 * 0.25) as usize),
400 (AssetClass::Software, (count as f64 * 0.10) as usize),
401 (
402 AssetClass::LeaseholdImprovements,
403 (count as f64 * 0.05) as usize,
404 ),
405 ];
406
407 for (class, class_count) in class_counts {
408 for _ in 0..class_count {
409 let acquisition_date = start_date
410 + chrono::Duration::days(self.rng.random_range(0..=days_range) as i64);
411 let asset = self.generate_asset_of_class(class, company_code, acquisition_date);
412 pool.add_asset(asset);
413 }
414 }
415
416 while pool.assets.len() < count {
418 let acquisition_date =
419 start_date + chrono::Duration::days(self.rng.random_range(0..=days_range) as i64);
420 let asset = self.generate_asset(company_code, acquisition_date);
421 pool.add_asset(asset);
422 }
423
424 pool
425 }
426
427 fn select_asset_class(&mut self) -> AssetClass {
429 let roll: f64 = self.rng.random();
430 let mut cumulative = 0.0;
431
432 for (class, prob) in &self.config.asset_class_distribution {
433 cumulative += prob;
434 if roll < cumulative {
435 return *class;
436 }
437 }
438
439 AssetClass::ItEquipment
440 }
441
442 fn select_depreciation_method(&mut self, asset_class: &AssetClass) -> DepreciationMethod {
444 if matches!(
446 asset_class,
447 AssetClass::Land | AssetClass::ConstructionInProgress
448 ) {
449 return DepreciationMethod::StraightLine; }
451
452 if self.coa_framework == CoAFramework::GermanSkr04 {
454 let roll: f64 = self.rng.random();
455 return if roll < 0.75 {
456 DepreciationMethod::StraightLine
457 } else {
458 DepreciationMethod::Degressiv
459 };
460 }
461
462 let roll: f64 = self.rng.random();
463 let mut cumulative = 0.0;
464
465 for (method, prob) in &self.config.depreciation_method_distribution {
466 cumulative += prob;
467 if roll < cumulative {
468 return *method;
469 }
470 }
471
472 DepreciationMethod::StraightLine
473 }
474
475 fn get_useful_life(&self, asset_class: &AssetClass) -> u32 {
477 if self.coa_framework == CoAFramework::GermanSkr04 {
479 return self.get_useful_life_german(asset_class);
480 }
481
482 for (class, months) in &self.config.useful_life_by_class {
483 if class == asset_class {
484 return *months;
485 }
486 }
487 60 }
489
490 fn select_description(&mut self, asset_class: &AssetClass) -> &'static str {
492 for (class, descriptions) in ASSET_DESCRIPTIONS {
493 if class == asset_class {
494 let idx = self.rng.random_range(0..descriptions.len());
495 return descriptions[idx];
496 }
497 }
498 "Fixed Asset"
499 }
500
501 fn generate_acquisition_cost(&mut self) -> Decimal {
503 if self.coa_framework == CoAFramework::GermanSkr04 && self.rng.random::<f64>() < 0.15 {
505 let gwg_cost = Decimal::from(100)
506 + Decimal::from_f64_retain(self.rng.random::<f64>() * 700.0)
507 .unwrap_or(Decimal::ZERO);
508 return gwg_cost.round_dp(2);
509 }
510
511 let min = self.config.acquisition_cost_range.0;
512 let max = self.config.acquisition_cost_range.1;
513 let range = (max - min).to_string().parse::<f64>().unwrap_or(0.0);
514 let offset =
515 Decimal::from_f64_retain(self.rng.random::<f64>() * range).unwrap_or(Decimal::ZERO);
516 (min + offset).round_dp(2)
517 }
518
519 fn generate_acquisition_cost_for_class(&mut self, asset_class: &AssetClass) -> Decimal {
521 let (min, max) = match asset_class {
522 AssetClass::Buildings => (Decimal::from(500_000), Decimal::from(10_000_000)),
523 AssetClass::BuildingImprovements => (Decimal::from(50_000), Decimal::from(500_000)),
524 AssetClass::Machinery | AssetClass::MachineryEquipment => {
525 (Decimal::from(50_000), Decimal::from(1_000_000))
526 }
527 AssetClass::Vehicles => (Decimal::from(20_000), Decimal::from(100_000)),
528 AssetClass::Furniture | AssetClass::FurnitureFixtures => {
529 (Decimal::from(1_000), Decimal::from(50_000))
530 }
531 AssetClass::ItEquipment | AssetClass::ComputerHardware => {
532 (Decimal::from(2_000), Decimal::from(200_000))
533 }
534 AssetClass::Software | AssetClass::Intangibles => {
535 (Decimal::from(5_000), Decimal::from(500_000))
536 }
537 AssetClass::LeaseholdImprovements => (Decimal::from(10_000), Decimal::from(300_000)),
538 AssetClass::Land => (Decimal::from(100_000), Decimal::from(5_000_000)),
539 AssetClass::ConstructionInProgress => {
540 (Decimal::from(100_000), Decimal::from(2_000_000))
541 }
542 AssetClass::LowValueAssets => (Decimal::from(100), Decimal::from(5_000)),
543 };
544
545 let range = (max - min).to_string().parse::<f64>().unwrap_or(0.0);
546 let offset =
547 Decimal::from_f64_retain(self.rng.random::<f64>() * range).unwrap_or(Decimal::ZERO);
548 (min + offset).round_dp(2)
549 }
550
551 fn generate_serial_number(&mut self) -> String {
553 format!(
554 "SN-{:04}-{:08}",
555 self.rng.random_range(1000..9999),
556 self.rng.random_range(10000000..99999999)
557 )
558 }
559
560 fn generate_disposal_values(&mut self, asset: &FixedAsset) -> (Decimal, Decimal) {
562 let proceeds_rate = self.rng.random::<f64>() * 0.5;
564 let proceeds = (asset.acquisition_cost
565 * Decimal::from_f64_retain(proceeds_rate).unwrap_or(Decimal::ZERO))
566 .round_dp(2);
567
568 let nbv = asset.net_book_value;
570 let gain_loss = proceeds - nbv;
571
572 (proceeds, gain_loss)
573 }
574
575 fn generate_account_determination(
577 &self,
578 asset_class: &AssetClass,
579 ) -> AssetAccountDetermination {
580 match asset_class {
581 AssetClass::Buildings | AssetClass::BuildingImprovements => AssetAccountDetermination {
582 asset_account: "160000".to_string(),
583 accumulated_depreciation_account: "165000".to_string(),
584 depreciation_expense_account: "680000".to_string(),
585 gain_loss_account: "790000".to_string(),
586 gain_on_disposal_account: "790010".to_string(),
587 loss_on_disposal_account: "790020".to_string(),
588 acquisition_clearing_account: "199100".to_string(),
589 },
590 AssetClass::Machinery | AssetClass::MachineryEquipment => AssetAccountDetermination {
591 asset_account: "161000".to_string(),
592 accumulated_depreciation_account: "166000".to_string(),
593 depreciation_expense_account: "681000".to_string(),
594 gain_loss_account: "791000".to_string(),
595 gain_on_disposal_account: "791010".to_string(),
596 loss_on_disposal_account: "791020".to_string(),
597 acquisition_clearing_account: "199110".to_string(),
598 },
599 AssetClass::Vehicles => AssetAccountDetermination {
600 asset_account: "162000".to_string(),
601 accumulated_depreciation_account: "167000".to_string(),
602 depreciation_expense_account: "682000".to_string(),
603 gain_loss_account: "792000".to_string(),
604 gain_on_disposal_account: "792010".to_string(),
605 loss_on_disposal_account: "792020".to_string(),
606 acquisition_clearing_account: "199120".to_string(),
607 },
608 AssetClass::Furniture | AssetClass::FurnitureFixtures => AssetAccountDetermination {
609 asset_account: "163000".to_string(),
610 accumulated_depreciation_account: "168000".to_string(),
611 depreciation_expense_account: "683000".to_string(),
612 gain_loss_account: "793000".to_string(),
613 gain_on_disposal_account: "793010".to_string(),
614 loss_on_disposal_account: "793020".to_string(),
615 acquisition_clearing_account: "199130".to_string(),
616 },
617 AssetClass::ItEquipment | AssetClass::ComputerHardware => AssetAccountDetermination {
618 asset_account: "164000".to_string(),
619 accumulated_depreciation_account: "169000".to_string(),
620 depreciation_expense_account: "684000".to_string(),
621 gain_loss_account: "794000".to_string(),
622 gain_on_disposal_account: "794010".to_string(),
623 loss_on_disposal_account: "794020".to_string(),
624 acquisition_clearing_account: "199140".to_string(),
625 },
626 AssetClass::Software | AssetClass::Intangibles => AssetAccountDetermination {
627 asset_account: "170000".to_string(),
628 accumulated_depreciation_account: "175000".to_string(),
629 depreciation_expense_account: "685000".to_string(),
630 gain_loss_account: "795000".to_string(),
631 gain_on_disposal_account: "795010".to_string(),
632 loss_on_disposal_account: "795020".to_string(),
633 acquisition_clearing_account: "199150".to_string(),
634 },
635 AssetClass::LeaseholdImprovements => AssetAccountDetermination {
636 asset_account: "171000".to_string(),
637 accumulated_depreciation_account: "176000".to_string(),
638 depreciation_expense_account: "686000".to_string(),
639 gain_loss_account: "796000".to_string(),
640 gain_on_disposal_account: "796010".to_string(),
641 loss_on_disposal_account: "796020".to_string(),
642 acquisition_clearing_account: "199160".to_string(),
643 },
644 AssetClass::Land => {
645 AssetAccountDetermination {
646 asset_account: "150000".to_string(),
647 accumulated_depreciation_account: "".to_string(), depreciation_expense_account: "".to_string(),
649 gain_loss_account: "790000".to_string(),
650 gain_on_disposal_account: "790010".to_string(),
651 loss_on_disposal_account: "790020".to_string(),
652 acquisition_clearing_account: "199000".to_string(),
653 }
654 }
655 AssetClass::ConstructionInProgress => AssetAccountDetermination {
656 asset_account: "159000".to_string(),
657 accumulated_depreciation_account: "".to_string(),
658 depreciation_expense_account: "".to_string(),
659 gain_loss_account: "".to_string(),
660 gain_on_disposal_account: "".to_string(),
661 loss_on_disposal_account: "".to_string(),
662 acquisition_clearing_account: "199090".to_string(),
663 },
664 AssetClass::LowValueAssets => AssetAccountDetermination {
665 asset_account: "172000".to_string(),
666 accumulated_depreciation_account: "177000".to_string(),
667 depreciation_expense_account: "687000".to_string(),
668 gain_loss_account: "797000".to_string(),
669 gain_on_disposal_account: "797010".to_string(),
670 loss_on_disposal_account: "797020".to_string(),
671 acquisition_clearing_account: "199170".to_string(),
672 },
673 }
674 }
675
676 fn get_useful_life_german(&self, asset_class: &AssetClass) -> u32 {
678 match asset_class {
679 AssetClass::Buildings | AssetClass::BuildingImprovements => 396, AssetClass::Machinery | AssetClass::MachineryEquipment => 120, AssetClass::Vehicles => 72, AssetClass::Furniture | AssetClass::FurnitureFixtures => 156, AssetClass::ItEquipment | AssetClass::ComputerHardware => 36, AssetClass::Software | AssetClass::Intangibles => 36, AssetClass::LeaseholdImprovements => 120, AssetClass::Land | AssetClass::ConstructionInProgress => 0,
687 AssetClass::LowValueAssets => 12,
688 }
689 }
690
691 pub fn reset(&mut self) {
693 self.rng = seeded_rng(self.seed, 0);
694 self.asset_counter = 0;
695 }
696}
697
698#[cfg(test)]
699#[allow(clippy::unwrap_used)]
700mod tests {
701 use super::*;
702
703 #[test]
704 fn test_asset_generation() {
705 let mut gen = AssetGenerator::new(42);
706 let asset = gen.generate_asset("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
707
708 assert!(!asset.asset_id.is_empty());
709 assert!(!asset.description.is_empty());
710 assert!(asset.acquisition_cost > Decimal::ZERO);
711 assert!(
712 asset.useful_life_months > 0
713 || matches!(
714 asset.asset_class,
715 AssetClass::Land | AssetClass::ConstructionInProgress
716 )
717 );
718 }
719
720 #[test]
721 fn test_asset_pool_generation() {
722 let mut gen = AssetGenerator::new(42);
723 let pool = gen.generate_asset_pool(
724 50,
725 "1000",
726 (
727 NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
728 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
729 ),
730 );
731
732 assert_eq!(pool.assets.len(), 50);
733 }
734
735 #[test]
736 fn test_aged_asset() {
737 let mut gen = AssetGenerator::new(42);
738 let asset = gen.generate_aged_asset(
739 "1000",
740 NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
741 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
742 );
743
744 if asset.status == AssetStatus::Active && asset.useful_life_months > 0 {
746 assert!(asset.accumulated_depreciation > Decimal::ZERO);
747 assert!(asset.net_book_value < asset.acquisition_cost);
748 }
749 }
750
751 #[test]
752 fn test_diverse_pool() {
753 let mut gen = AssetGenerator::new(42);
754 let pool = gen.generate_diverse_pool(
755 100,
756 "1000",
757 (
758 NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
759 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
760 ),
761 );
762
763 let machinery_count = pool
765 .assets
766 .iter()
767 .filter(|a| a.asset_class == AssetClass::Machinery)
768 .count();
769 let it_count = pool
770 .assets
771 .iter()
772 .filter(|a| a.asset_class == AssetClass::ItEquipment)
773 .count();
774
775 assert!(machinery_count > 0);
776 assert!(it_count > 0);
777 }
778
779 #[test]
780 fn test_deterministic_generation() {
781 let mut gen1 = AssetGenerator::new(42);
782 let mut gen2 = AssetGenerator::new(42);
783
784 let asset1 = gen1.generate_asset("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
785 let asset2 = gen2.generate_asset("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
786
787 assert_eq!(asset1.asset_id, asset2.asset_id);
788 assert_eq!(asset1.description, asset2.description);
789 assert_eq!(asset1.acquisition_cost, asset2.acquisition_cost);
790 }
791
792 #[test]
793 fn test_depreciation_calculation() {
794 let mut gen = AssetGenerator::new(42);
795 let mut asset = gen.generate_asset_of_class(
796 AssetClass::ItEquipment,
797 "1000",
798 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
799 );
800
801 let initial_nbv = asset.net_book_value;
802
803 let depreciation =
805 asset.calculate_monthly_depreciation(NaiveDate::from_ymd_opt(2024, 2, 1).unwrap());
806 asset.apply_depreciation(depreciation);
807
808 assert!(asset.accumulated_depreciation > Decimal::ZERO);
809 assert!(asset.net_book_value < initial_nbv);
810 }
811
812 #[test]
813 fn test_german_gwg_assets() {
814 let mut gen = AssetGenerator::new(42);
815 gen.set_coa_framework(CoAFramework::GermanSkr04);
816
817 let mut gwg_count = 0;
818 for _ in 0..200 {
819 let asset = gen.generate_asset("DE01", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
820 if asset.is_gwg == Some(true) {
821 gwg_count += 1;
822 assert!(asset.acquisition_cost <= Decimal::from(800));
823 assert_eq!(
824 asset.depreciation_method,
825 DepreciationMethod::ImmediateExpense
826 );
827 assert_eq!(asset.useful_life_months, 1);
828 assert_eq!(asset.salvage_value, Decimal::ZERO);
829 }
830 }
831 assert!(gwg_count > 0, "Expected at least one GWG asset");
833 }
834
835 #[test]
836 fn test_german_depreciation_methods() {
837 let mut gen = AssetGenerator::new(42);
838 gen.set_coa_framework(CoAFramework::GermanSkr04);
839
840 let mut sl_count = 0;
841 let mut degressiv_count = 0;
842 let mut immediate_count = 0;
843 for _ in 0..200 {
844 let asset = gen.generate_asset("DE01", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
845 match asset.depreciation_method {
846 DepreciationMethod::StraightLine => sl_count += 1,
847 DepreciationMethod::Degressiv => degressiv_count += 1,
848 DepreciationMethod::ImmediateExpense => immediate_count += 1,
849 other => panic!("Unexpected German depreciation method: {:?}", other),
850 }
851 }
852 assert!(sl_count > 0, "Expected some straight-line assets");
854 assert!(degressiv_count > 0, "Expected some Degressiv assets");
855 assert!(
856 immediate_count > 0,
857 "Expected some GWG immediate expense assets"
858 );
859 }
861
862 #[test]
863 fn test_german_useful_life_afa() {
864 let mut gen = AssetGenerator::new(42);
865 gen.set_coa_framework(CoAFramework::GermanSkr04);
866
867 let vehicle = gen.generate_asset_of_class(
868 AssetClass::Vehicles,
869 "DE01",
870 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
871 );
872 assert_eq!(vehicle.useful_life_months, 72);
874
875 let building = gen.generate_asset_of_class(
876 AssetClass::Buildings,
877 "DE01",
878 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
879 );
880 assert_eq!(building.useful_life_months, 396);
882 }
883
884 #[test]
885 fn test_asset_class_cost_ranges() {
886 let mut gen = AssetGenerator::new(42);
887
888 let building = gen.generate_asset_of_class(
890 AssetClass::Buildings,
891 "1000",
892 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
893 );
894 let furniture = gen.generate_asset_of_class(
895 AssetClass::Furniture,
896 "1000",
897 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
898 );
899
900 assert!(building.acquisition_cost >= Decimal::from(500_000));
902 assert!(furniture.acquisition_cost <= Decimal::from(50_000));
903 }
904}