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)]
750mod tests {
751 use super::*;
752
753 #[test]
754 fn test_asset_generation() {
755 let mut gen = AssetGenerator::new(42);
756 let asset = gen.generate_asset("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
757
758 assert!(!asset.asset_id.is_empty());
759 assert!(!asset.description.is_empty());
760 assert!(asset.acquisition_cost > Decimal::ZERO);
761 assert!(
762 asset.useful_life_months > 0
763 || matches!(
764 asset.asset_class,
765 AssetClass::Land | AssetClass::ConstructionInProgress
766 )
767 );
768 }
769
770 #[test]
771 fn test_asset_pool_generation() {
772 let mut gen = AssetGenerator::new(42);
773 let pool = gen.generate_asset_pool(
774 50,
775 "1000",
776 (
777 NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
778 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
779 ),
780 );
781
782 assert_eq!(pool.assets.len(), 50);
783 }
784
785 #[test]
786 fn test_aged_asset() {
787 let mut gen = AssetGenerator::new(42);
788 let asset = gen.generate_aged_asset(
789 "1000",
790 NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
791 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
792 );
793
794 if asset.status == AssetStatus::Active && asset.useful_life_months > 0 {
796 assert!(asset.accumulated_depreciation > Decimal::ZERO);
797 assert!(asset.net_book_value < asset.acquisition_cost);
798 }
799 }
800
801 #[test]
802 fn test_diverse_pool() {
803 let mut gen = AssetGenerator::new(42);
804 let pool = gen.generate_diverse_pool(
805 100,
806 "1000",
807 (
808 NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
809 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
810 ),
811 );
812
813 let machinery_count = pool
815 .assets
816 .iter()
817 .filter(|a| a.asset_class == AssetClass::Machinery)
818 .count();
819 let it_count = pool
820 .assets
821 .iter()
822 .filter(|a| a.asset_class == AssetClass::ItEquipment)
823 .count();
824
825 assert!(machinery_count > 0);
826 assert!(it_count > 0);
827 }
828
829 #[test]
830 fn test_deterministic_generation() {
831 let mut gen1 = AssetGenerator::new(42);
832 let mut gen2 = AssetGenerator::new(42);
833
834 let asset1 = gen1.generate_asset("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
835 let asset2 = gen2.generate_asset("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
836
837 assert_eq!(asset1.asset_id, asset2.asset_id);
838 assert_eq!(asset1.description, asset2.description);
839 assert_eq!(asset1.acquisition_cost, asset2.acquisition_cost);
840 }
841
842 #[test]
843 fn test_depreciation_calculation() {
844 let mut gen = AssetGenerator::new(42);
845 let mut asset = gen.generate_asset_of_class(
846 AssetClass::ItEquipment,
847 "1000",
848 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
849 );
850
851 let initial_nbv = asset.net_book_value;
852
853 let depreciation =
855 asset.calculate_monthly_depreciation(NaiveDate::from_ymd_opt(2024, 2, 1).unwrap());
856 asset.apply_depreciation(depreciation);
857
858 assert!(asset.accumulated_depreciation > Decimal::ZERO);
859 assert!(asset.net_book_value < initial_nbv);
860 }
861
862 #[test]
863 fn test_german_gwg_assets() {
864 let mut gen = AssetGenerator::new(42);
865 gen.set_coa_framework(CoAFramework::GermanSkr04);
866
867 let mut gwg_count = 0;
868 for _ in 0..200 {
869 let asset = gen.generate_asset("DE01", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
870 if asset.is_gwg == Some(true) {
871 gwg_count += 1;
872 assert!(asset.acquisition_cost <= Decimal::from(800));
873 assert_eq!(
874 asset.depreciation_method,
875 DepreciationMethod::ImmediateExpense
876 );
877 assert_eq!(asset.useful_life_months, 1);
878 assert_eq!(asset.salvage_value, Decimal::ZERO);
879 }
880 }
881 assert!(gwg_count > 0, "Expected at least one GWG asset");
883 }
884
885 #[test]
886 fn test_german_depreciation_methods() {
887 let mut gen = AssetGenerator::new(42);
888 gen.set_coa_framework(CoAFramework::GermanSkr04);
889
890 let mut sl_count = 0;
891 let mut degressiv_count = 0;
892 let mut immediate_count = 0;
893 for _ in 0..200 {
894 let asset = gen.generate_asset("DE01", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
895 match asset.depreciation_method {
896 DepreciationMethod::StraightLine => sl_count += 1,
897 DepreciationMethod::Degressiv => degressiv_count += 1,
898 DepreciationMethod::ImmediateExpense => immediate_count += 1,
899 other => panic!("Unexpected German depreciation method: {:?}", other),
900 }
901 }
902 assert!(sl_count > 0, "Expected some straight-line assets");
904 assert!(degressiv_count > 0, "Expected some Degressiv assets");
905 assert!(
906 immediate_count > 0,
907 "Expected some GWG immediate expense assets"
908 );
909 }
911
912 #[test]
913 fn test_german_useful_life_afa() {
914 let mut gen = AssetGenerator::new(42);
915 gen.set_coa_framework(CoAFramework::GermanSkr04);
916
917 let vehicle = gen.generate_asset_of_class(
918 AssetClass::Vehicles,
919 "DE01",
920 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
921 );
922 assert_eq!(vehicle.useful_life_months, 72);
924
925 let building = gen.generate_asset_of_class(
926 AssetClass::Buildings,
927 "DE01",
928 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
929 );
930 assert_eq!(building.useful_life_months, 396);
932 }
933
934 #[test]
935 fn test_asset_class_cost_ranges() {
936 let mut gen = AssetGenerator::new(42);
937
938 let building = gen.generate_asset_of_class(
940 AssetClass::Buildings,
941 "1000",
942 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
943 );
944 let furniture = gen.generate_asset_of_class(
945 AssetClass::Furniture,
946 "1000",
947 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
948 );
949
950 assert!(building.acquisition_cost >= Decimal::from(500_000));
952 assert!(furniture.acquisition_cost <= Decimal::from(50_000));
953 }
954}