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