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 template_provider: Option<datasynth_core::templates::SharedTemplateProvider>,
175}
176
177impl AssetGenerator {
178 pub fn new(seed: u64) -> Self {
180 Self::with_config(seed, AssetGeneratorConfig::default())
181 }
182
183 pub fn with_config(seed: u64, config: AssetGeneratorConfig) -> Self {
185 Self {
186 rng: seeded_rng(seed, 0),
187 seed,
188 config,
189 asset_counter: 0,
190 coa_framework: CoAFramework::UsGaap,
191 template_provider: None,
192 }
193 }
194
195 pub fn set_coa_framework(&mut self, framework: CoAFramework) {
197 self.coa_framework = framework;
198 }
199
200 pub fn set_template_provider(
203 &mut self,
204 provider: datasynth_core::templates::SharedTemplateProvider,
205 ) {
206 self.template_provider = Some(provider);
207 }
208
209 pub fn generate_asset(
211 &mut self,
212 company_code: &str,
213 acquisition_date: NaiveDate,
214 ) -> FixedAsset {
215 self.asset_counter += 1;
216
217 let asset_id = format!("FA-{}-{:06}", company_code, self.asset_counter);
218 let asset_class = self.select_asset_class();
219 let description = self.select_description(&asset_class);
220
221 let mut asset = FixedAsset::new(
222 asset_id,
223 description.to_string(),
224 asset_class,
225 company_code,
226 acquisition_date,
227 self.generate_acquisition_cost(),
228 );
229
230 let is_gwg = self.coa_framework == CoAFramework::GermanSkr04
232 && asset.acquisition_cost <= Decimal::from(800)
233 && asset_class.is_depreciable();
234
235 if is_gwg {
236 asset.is_gwg = Some(true);
237 asset.depreciation_method = DepreciationMethod::ImmediateExpense;
238 asset.useful_life_months = 1;
239 asset.salvage_value = Decimal::ZERO;
240 } else {
241 asset.depreciation_method = self.select_depreciation_method(&asset_class);
243 asset.useful_life_months = self.get_useful_life(&asset_class);
244 asset.salvage_value = (asset.acquisition_cost
245 * Decimal::from_f64_retain(self.config.salvage_value_percent)
246 .unwrap_or(Decimal::from_f64_retain(0.05).expect("valid decimal literal")))
247 .round_dp(2);
248 }
249
250 asset.account_determination = self.generate_account_determination(&asset_class);
252
253 asset.location = Some(format!("P{company_code}"));
255 asset.cost_center = Some(format!("CC-{company_code}-ADMIN"));
256
257 if matches!(
259 asset_class,
260 AssetClass::Machinery | AssetClass::Vehicles | AssetClass::ItEquipment
261 ) {
262 asset.serial_number = Some(self.generate_serial_number());
263 }
264
265 if self.rng.random::<f64>() < self.config.disposed_rate {
267 let disposal_date =
268 acquisition_date + chrono::Duration::days(self.rng.random_range(365..1825) as i64);
269 let (proceeds, _gain_loss) = self.generate_disposal_values(&asset);
270 asset.dispose(disposal_date, proceeds);
271 } else if self.rng.random::<f64>() < self.config.fully_depreciated_rate {
272 asset.accumulated_depreciation = asset.acquisition_cost - asset.salvage_value;
273 asset.net_book_value = asset.salvage_value;
274 }
275
276 asset
277 }
278
279 pub fn generate_asset_of_class(
281 &mut self,
282 asset_class: AssetClass,
283 company_code: &str,
284 acquisition_date: NaiveDate,
285 ) -> FixedAsset {
286 self.asset_counter += 1;
287
288 let asset_id = format!("FA-{}-{:06}", company_code, self.asset_counter);
289 let description = self.select_description(&asset_class);
290
291 let mut asset = FixedAsset::new(
292 asset_id,
293 description.to_string(),
294 asset_class,
295 company_code,
296 acquisition_date,
297 self.generate_acquisition_cost_for_class(&asset_class),
298 );
299
300 asset.depreciation_method = self.select_depreciation_method(&asset_class);
301 asset.useful_life_months = self.get_useful_life(&asset_class);
302 asset.salvage_value = (asset.acquisition_cost
303 * Decimal::from_f64_retain(self.config.salvage_value_percent)
304 .unwrap_or(Decimal::from_f64_retain(0.05).expect("valid decimal literal")))
305 .round_dp(2);
306
307 asset.account_determination = self.generate_account_determination(&asset_class);
308 asset.location = Some(format!("P{company_code}"));
309 asset.cost_center = Some(format!("CC-{company_code}-ADMIN"));
310
311 if matches!(
312 asset_class,
313 AssetClass::Machinery | AssetClass::Vehicles | AssetClass::ItEquipment
314 ) {
315 asset.serial_number = Some(self.generate_serial_number());
316 }
317
318 asset
319 }
320
321 pub fn generate_aged_asset(
323 &mut self,
324 company_code: &str,
325 acquisition_date: NaiveDate,
326 as_of_date: NaiveDate,
327 ) -> FixedAsset {
328 let mut asset = self.generate_asset(company_code, acquisition_date);
329
330 let months_elapsed = ((as_of_date - acquisition_date).num_days() / 30) as u32;
332
333 for month_offset in 0..months_elapsed {
335 if asset.status == AssetStatus::Active {
336 let dep_date =
338 acquisition_date + chrono::Duration::days((month_offset as i64 + 1) * 30);
339 let depreciation = asset.calculate_monthly_depreciation(dep_date);
340 asset.apply_depreciation(depreciation);
341 }
342 }
343
344 asset
345 }
346
347 pub fn generate_asset_pool(
349 &mut self,
350 count: usize,
351 company_code: &str,
352 date_range: (NaiveDate, NaiveDate),
353 ) -> FixedAssetPool {
354 debug!(count, company_code, "Generating fixed asset pool");
355 let mut pool = FixedAssetPool::new();
356
357 let (start_date, end_date) = date_range;
358 let days_range = (end_date - start_date).num_days() as u64;
359
360 for _ in 0..count {
361 let acquisition_date =
362 start_date + chrono::Duration::days(self.rng.random_range(0..=days_range) as i64);
363 let asset = self.generate_asset(company_code, acquisition_date);
364 pool.add_asset(asset);
365 }
366
367 pool
368 }
369
370 pub fn generate_aged_asset_pool(
372 &mut self,
373 count: usize,
374 company_code: &str,
375 acquisition_date_range: (NaiveDate, NaiveDate),
376 as_of_date: NaiveDate,
377 ) -> FixedAssetPool {
378 let mut pool = FixedAssetPool::new();
379
380 let (start_date, end_date) = acquisition_date_range;
381 let days_range = (end_date - start_date).num_days() as u64;
382
383 for _ in 0..count {
384 let acquisition_date =
385 start_date + chrono::Duration::days(self.rng.random_range(0..=days_range) as i64);
386 let asset = self.generate_aged_asset(company_code, acquisition_date, as_of_date);
387 pool.add_asset(asset);
388 }
389
390 pool
391 }
392
393 pub fn generate_diverse_pool(
395 &mut self,
396 count: usize,
397 company_code: &str,
398 date_range: (NaiveDate, NaiveDate),
399 ) -> FixedAssetPool {
400 let mut pool = FixedAssetPool::new();
401
402 let (start_date, end_date) = date_range;
403 let days_range = (end_date - start_date).num_days() as u64;
404
405 let class_counts = [
407 (AssetClass::Buildings, (count as f64 * 0.05) as usize),
408 (AssetClass::Machinery, (count as f64 * 0.25) as usize),
409 (AssetClass::Vehicles, (count as f64 * 0.15) as usize),
410 (AssetClass::Furniture, (count as f64 * 0.15) as usize),
411 (AssetClass::ItEquipment, (count as f64 * 0.25) as usize),
412 (AssetClass::Software, (count as f64 * 0.10) as usize),
413 (
414 AssetClass::LeaseholdImprovements,
415 (count as f64 * 0.05) as usize,
416 ),
417 ];
418
419 for (class, class_count) in class_counts {
420 for _ in 0..class_count {
421 let acquisition_date = start_date
422 + chrono::Duration::days(self.rng.random_range(0..=days_range) as i64);
423 let asset = self.generate_asset_of_class(class, company_code, acquisition_date);
424 pool.add_asset(asset);
425 }
426 }
427
428 while pool.assets.len() < count {
430 let acquisition_date =
431 start_date + chrono::Duration::days(self.rng.random_range(0..=days_range) as i64);
432 let asset = self.generate_asset(company_code, acquisition_date);
433 pool.add_asset(asset);
434 }
435
436 pool
437 }
438
439 fn select_asset_class(&mut self) -> AssetClass {
441 let roll: f64 = self.rng.random();
442 let mut cumulative = 0.0;
443
444 for (class, prob) in &self.config.asset_class_distribution {
445 cumulative += prob;
446 if roll < cumulative {
447 return *class;
448 }
449 }
450
451 AssetClass::ItEquipment
452 }
453
454 fn select_depreciation_method(&mut self, asset_class: &AssetClass) -> DepreciationMethod {
456 if matches!(
458 asset_class,
459 AssetClass::Land | AssetClass::ConstructionInProgress
460 ) {
461 return DepreciationMethod::StraightLine; }
463
464 if self.coa_framework == CoAFramework::GermanSkr04 {
466 let roll: f64 = self.rng.random();
467 return if roll < 0.75 {
468 DepreciationMethod::StraightLine
469 } else {
470 DepreciationMethod::Degressiv
471 };
472 }
473
474 let roll: f64 = self.rng.random();
475 let mut cumulative = 0.0;
476
477 for (method, prob) in &self.config.depreciation_method_distribution {
478 cumulative += prob;
479 if roll < cumulative {
480 return *method;
481 }
482 }
483
484 DepreciationMethod::StraightLine
485 }
486
487 fn get_useful_life(&self, asset_class: &AssetClass) -> u32 {
489 if self.coa_framework == CoAFramework::GermanSkr04 {
491 return self.get_useful_life_german(asset_class);
492 }
493
494 for (class, months) in &self.config.useful_life_by_class {
495 if class == asset_class {
496 return *months;
497 }
498 }
499 60 }
501
502 fn select_description(&mut self, asset_class: &AssetClass) -> String {
509 let class_key = Self::asset_class_to_key(asset_class);
510
511 if let Some(ref provider) = self.template_provider {
512 let candidate = provider.get_asset_description(class_key, &mut self.rng);
513 let generic_fallback = format!("{class_key} asset");
514 if candidate != generic_fallback {
515 return candidate;
516 }
517 }
518
519 for (class, descriptions) in ASSET_DESCRIPTIONS {
520 if class == asset_class {
521 let idx = self.rng.random_range(0..descriptions.len());
522 return descriptions[idx].to_string();
523 }
524 }
525 "Fixed Asset".to_string()
526 }
527
528 fn asset_class_to_key(asset_class: &AssetClass) -> &'static str {
530 match asset_class {
531 AssetClass::Buildings => "buildings",
532 AssetClass::BuildingImprovements => "building_improvements",
533 AssetClass::Machinery => "machinery",
534 AssetClass::Vehicles => "vehicles",
535 AssetClass::Furniture => "furniture",
536 AssetClass::ItEquipment => "it_equipment",
537 AssetClass::Software => "software",
538 AssetClass::LeaseholdImprovements => "leasehold_improvements",
539 AssetClass::Land => "land",
540 _ => "other",
541 }
542 }
543
544 fn generate_acquisition_cost(&mut self) -> Decimal {
546 if self.coa_framework == CoAFramework::GermanSkr04 && self.rng.random::<f64>() < 0.15 {
548 let gwg_cost = Decimal::from(100)
549 + Decimal::from_f64_retain(self.rng.random::<f64>() * 700.0)
550 .unwrap_or(Decimal::ZERO);
551 return gwg_cost.round_dp(2);
552 }
553
554 let min = self.config.acquisition_cost_range.0;
555 let max = self.config.acquisition_cost_range.1;
556 let range = (max - min).to_string().parse::<f64>().unwrap_or(0.0);
557 let offset =
558 Decimal::from_f64_retain(self.rng.random::<f64>() * range).unwrap_or(Decimal::ZERO);
559 (min + offset).round_dp(2)
560 }
561
562 fn generate_acquisition_cost_for_class(&mut self, asset_class: &AssetClass) -> Decimal {
564 let (min, max) = match asset_class {
565 AssetClass::Buildings => (Decimal::from(500_000), Decimal::from(10_000_000)),
566 AssetClass::BuildingImprovements => (Decimal::from(50_000), Decimal::from(500_000)),
567 AssetClass::Machinery | AssetClass::MachineryEquipment => {
568 (Decimal::from(50_000), Decimal::from(1_000_000))
569 }
570 AssetClass::Vehicles => (Decimal::from(20_000), Decimal::from(100_000)),
571 AssetClass::Furniture | AssetClass::FurnitureFixtures => {
572 (Decimal::from(1_000), Decimal::from(50_000))
573 }
574 AssetClass::ItEquipment | AssetClass::ComputerHardware => {
575 (Decimal::from(2_000), Decimal::from(200_000))
576 }
577 AssetClass::Software | AssetClass::Intangibles => {
578 (Decimal::from(5_000), Decimal::from(500_000))
579 }
580 AssetClass::LeaseholdImprovements => (Decimal::from(10_000), Decimal::from(300_000)),
581 AssetClass::Land => (Decimal::from(100_000), Decimal::from(5_000_000)),
582 AssetClass::ConstructionInProgress => {
583 (Decimal::from(100_000), Decimal::from(2_000_000))
584 }
585 AssetClass::LowValueAssets => (Decimal::from(100), Decimal::from(5_000)),
586 };
587
588 let range = (max - min).to_string().parse::<f64>().unwrap_or(0.0);
589 let offset =
590 Decimal::from_f64_retain(self.rng.random::<f64>() * range).unwrap_or(Decimal::ZERO);
591 (min + offset).round_dp(2)
592 }
593
594 fn generate_serial_number(&mut self) -> String {
596 format!(
597 "SN-{:04}-{:08}",
598 self.rng.random_range(1000..9999),
599 self.rng.random_range(10000000..99999999)
600 )
601 }
602
603 fn generate_disposal_values(&mut self, asset: &FixedAsset) -> (Decimal, Decimal) {
605 let proceeds_rate = self.rng.random::<f64>() * 0.5;
607 let proceeds = (asset.acquisition_cost
608 * Decimal::from_f64_retain(proceeds_rate).unwrap_or(Decimal::ZERO))
609 .round_dp(2);
610
611 let nbv = asset.net_book_value;
613 let gain_loss = proceeds - nbv;
614
615 (proceeds, gain_loss)
616 }
617
618 fn generate_account_determination(
620 &self,
621 asset_class: &AssetClass,
622 ) -> AssetAccountDetermination {
623 match asset_class {
624 AssetClass::Buildings | AssetClass::BuildingImprovements => AssetAccountDetermination {
625 asset_account: "160000".to_string(),
626 accumulated_depreciation_account: "165000".to_string(),
627 depreciation_expense_account: "680000".to_string(),
628 gain_loss_account: "790000".to_string(),
629 gain_on_disposal_account: "790010".to_string(),
630 loss_on_disposal_account: "790020".to_string(),
631 acquisition_clearing_account: "199100".to_string(),
632 },
633 AssetClass::Machinery | AssetClass::MachineryEquipment => AssetAccountDetermination {
634 asset_account: "161000".to_string(),
635 accumulated_depreciation_account: "166000".to_string(),
636 depreciation_expense_account: "681000".to_string(),
637 gain_loss_account: "791000".to_string(),
638 gain_on_disposal_account: "791010".to_string(),
639 loss_on_disposal_account: "791020".to_string(),
640 acquisition_clearing_account: "199110".to_string(),
641 },
642 AssetClass::Vehicles => AssetAccountDetermination {
643 asset_account: "162000".to_string(),
644 accumulated_depreciation_account: "167000".to_string(),
645 depreciation_expense_account: "682000".to_string(),
646 gain_loss_account: "792000".to_string(),
647 gain_on_disposal_account: "792010".to_string(),
648 loss_on_disposal_account: "792020".to_string(),
649 acquisition_clearing_account: "199120".to_string(),
650 },
651 AssetClass::Furniture | AssetClass::FurnitureFixtures => AssetAccountDetermination {
652 asset_account: "163000".to_string(),
653 accumulated_depreciation_account: "168000".to_string(),
654 depreciation_expense_account: "683000".to_string(),
655 gain_loss_account: "793000".to_string(),
656 gain_on_disposal_account: "793010".to_string(),
657 loss_on_disposal_account: "793020".to_string(),
658 acquisition_clearing_account: "199130".to_string(),
659 },
660 AssetClass::ItEquipment | AssetClass::ComputerHardware => AssetAccountDetermination {
661 asset_account: "164000".to_string(),
662 accumulated_depreciation_account: "169000".to_string(),
663 depreciation_expense_account: "684000".to_string(),
664 gain_loss_account: "794000".to_string(),
665 gain_on_disposal_account: "794010".to_string(),
666 loss_on_disposal_account: "794020".to_string(),
667 acquisition_clearing_account: "199140".to_string(),
668 },
669 AssetClass::Software | AssetClass::Intangibles => AssetAccountDetermination {
670 asset_account: "170000".to_string(),
671 accumulated_depreciation_account: "175000".to_string(),
672 depreciation_expense_account: "685000".to_string(),
673 gain_loss_account: "795000".to_string(),
674 gain_on_disposal_account: "795010".to_string(),
675 loss_on_disposal_account: "795020".to_string(),
676 acquisition_clearing_account: "199150".to_string(),
677 },
678 AssetClass::LeaseholdImprovements => AssetAccountDetermination {
679 asset_account: "171000".to_string(),
680 accumulated_depreciation_account: "176000".to_string(),
681 depreciation_expense_account: "686000".to_string(),
682 gain_loss_account: "796000".to_string(),
683 gain_on_disposal_account: "796010".to_string(),
684 loss_on_disposal_account: "796020".to_string(),
685 acquisition_clearing_account: "199160".to_string(),
686 },
687 AssetClass::Land => {
688 AssetAccountDetermination {
689 asset_account: "150000".to_string(),
690 accumulated_depreciation_account: "".to_string(), depreciation_expense_account: "".to_string(),
692 gain_loss_account: "790000".to_string(),
693 gain_on_disposal_account: "790010".to_string(),
694 loss_on_disposal_account: "790020".to_string(),
695 acquisition_clearing_account: "199000".to_string(),
696 }
697 }
698 AssetClass::ConstructionInProgress => AssetAccountDetermination {
699 asset_account: "159000".to_string(),
700 accumulated_depreciation_account: "".to_string(),
701 depreciation_expense_account: "".to_string(),
702 gain_loss_account: "".to_string(),
703 gain_on_disposal_account: "".to_string(),
704 loss_on_disposal_account: "".to_string(),
705 acquisition_clearing_account: "199090".to_string(),
706 },
707 AssetClass::LowValueAssets => AssetAccountDetermination {
708 asset_account: "172000".to_string(),
709 accumulated_depreciation_account: "177000".to_string(),
710 depreciation_expense_account: "687000".to_string(),
711 gain_loss_account: "797000".to_string(),
712 gain_on_disposal_account: "797010".to_string(),
713 loss_on_disposal_account: "797020".to_string(),
714 acquisition_clearing_account: "199170".to_string(),
715 },
716 }
717 }
718
719 fn get_useful_life_german(&self, asset_class: &AssetClass) -> u32 {
721 match asset_class {
722 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,
730 AssetClass::LowValueAssets => 12,
731 }
732 }
733
734 pub fn reset(&mut self) {
736 self.rng = seeded_rng(self.seed, 0);
737 self.asset_counter = 0;
738 }
739}
740
741#[cfg(test)]
742#[allow(clippy::unwrap_used)]
743mod tests {
744 use super::*;
745
746 #[test]
747 fn test_asset_generation() {
748 let mut gen = AssetGenerator::new(42);
749 let asset = gen.generate_asset("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
750
751 assert!(!asset.asset_id.is_empty());
752 assert!(!asset.description.is_empty());
753 assert!(asset.acquisition_cost > Decimal::ZERO);
754 assert!(
755 asset.useful_life_months > 0
756 || matches!(
757 asset.asset_class,
758 AssetClass::Land | AssetClass::ConstructionInProgress
759 )
760 );
761 }
762
763 #[test]
764 fn test_asset_pool_generation() {
765 let mut gen = AssetGenerator::new(42);
766 let pool = gen.generate_asset_pool(
767 50,
768 "1000",
769 (
770 NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
771 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
772 ),
773 );
774
775 assert_eq!(pool.assets.len(), 50);
776 }
777
778 #[test]
779 fn test_aged_asset() {
780 let mut gen = AssetGenerator::new(42);
781 let asset = gen.generate_aged_asset(
782 "1000",
783 NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
784 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
785 );
786
787 if asset.status == AssetStatus::Active && asset.useful_life_months > 0 {
789 assert!(asset.accumulated_depreciation > Decimal::ZERO);
790 assert!(asset.net_book_value < asset.acquisition_cost);
791 }
792 }
793
794 #[test]
795 fn test_diverse_pool() {
796 let mut gen = AssetGenerator::new(42);
797 let pool = gen.generate_diverse_pool(
798 100,
799 "1000",
800 (
801 NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
802 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
803 ),
804 );
805
806 let machinery_count = pool
808 .assets
809 .iter()
810 .filter(|a| a.asset_class == AssetClass::Machinery)
811 .count();
812 let it_count = pool
813 .assets
814 .iter()
815 .filter(|a| a.asset_class == AssetClass::ItEquipment)
816 .count();
817
818 assert!(machinery_count > 0);
819 assert!(it_count > 0);
820 }
821
822 #[test]
823 fn test_deterministic_generation() {
824 let mut gen1 = AssetGenerator::new(42);
825 let mut gen2 = AssetGenerator::new(42);
826
827 let asset1 = gen1.generate_asset("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
828 let asset2 = gen2.generate_asset("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
829
830 assert_eq!(asset1.asset_id, asset2.asset_id);
831 assert_eq!(asset1.description, asset2.description);
832 assert_eq!(asset1.acquisition_cost, asset2.acquisition_cost);
833 }
834
835 #[test]
836 fn test_depreciation_calculation() {
837 let mut gen = AssetGenerator::new(42);
838 let mut asset = gen.generate_asset_of_class(
839 AssetClass::ItEquipment,
840 "1000",
841 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
842 );
843
844 let initial_nbv = asset.net_book_value;
845
846 let depreciation =
848 asset.calculate_monthly_depreciation(NaiveDate::from_ymd_opt(2024, 2, 1).unwrap());
849 asset.apply_depreciation(depreciation);
850
851 assert!(asset.accumulated_depreciation > Decimal::ZERO);
852 assert!(asset.net_book_value < initial_nbv);
853 }
854
855 #[test]
856 fn test_german_gwg_assets() {
857 let mut gen = AssetGenerator::new(42);
858 gen.set_coa_framework(CoAFramework::GermanSkr04);
859
860 let mut gwg_count = 0;
861 for _ in 0..200 {
862 let asset = gen.generate_asset("DE01", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
863 if asset.is_gwg == Some(true) {
864 gwg_count += 1;
865 assert!(asset.acquisition_cost <= Decimal::from(800));
866 assert_eq!(
867 asset.depreciation_method,
868 DepreciationMethod::ImmediateExpense
869 );
870 assert_eq!(asset.useful_life_months, 1);
871 assert_eq!(asset.salvage_value, Decimal::ZERO);
872 }
873 }
874 assert!(gwg_count > 0, "Expected at least one GWG asset");
876 }
877
878 #[test]
879 fn test_german_depreciation_methods() {
880 let mut gen = AssetGenerator::new(42);
881 gen.set_coa_framework(CoAFramework::GermanSkr04);
882
883 let mut sl_count = 0;
884 let mut degressiv_count = 0;
885 let mut immediate_count = 0;
886 for _ in 0..200 {
887 let asset = gen.generate_asset("DE01", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
888 match asset.depreciation_method {
889 DepreciationMethod::StraightLine => sl_count += 1,
890 DepreciationMethod::Degressiv => degressiv_count += 1,
891 DepreciationMethod::ImmediateExpense => immediate_count += 1,
892 other => panic!("Unexpected German depreciation method: {:?}", other),
893 }
894 }
895 assert!(sl_count > 0, "Expected some straight-line assets");
897 assert!(degressiv_count > 0, "Expected some Degressiv assets");
898 assert!(
899 immediate_count > 0,
900 "Expected some GWG immediate expense assets"
901 );
902 }
904
905 #[test]
906 fn test_german_useful_life_afa() {
907 let mut gen = AssetGenerator::new(42);
908 gen.set_coa_framework(CoAFramework::GermanSkr04);
909
910 let vehicle = gen.generate_asset_of_class(
911 AssetClass::Vehicles,
912 "DE01",
913 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
914 );
915 assert_eq!(vehicle.useful_life_months, 72);
917
918 let building = gen.generate_asset_of_class(
919 AssetClass::Buildings,
920 "DE01",
921 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
922 );
923 assert_eq!(building.useful_life_months, 396);
925 }
926
927 #[test]
928 fn test_asset_class_cost_ranges() {
929 let mut gen = AssetGenerator::new(42);
930
931 let building = gen.generate_asset_of_class(
933 AssetClass::Buildings,
934 "1000",
935 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
936 );
937 let furniture = gen.generate_asset_of_class(
938 AssetClass::Furniture,
939 "1000",
940 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
941 );
942
943 assert!(building.acquisition_cost >= Decimal::from(500_000));
945 assert!(furniture.acquisition_cost <= Decimal::from(50_000));
946 }
947}