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
14#[derive(Debug, Clone)]
16pub struct AssetGeneratorConfig {
17 pub asset_class_distribution: Vec<(AssetClass, f64)>,
19 pub depreciation_method_distribution: Vec<(DepreciationMethod, f64)>,
21 pub useful_life_by_class: Vec<(AssetClass, u32)>,
23 pub acquisition_cost_range: (Decimal, Decimal),
25 pub salvage_value_percent: f64,
27 pub fully_depreciated_rate: f64,
29 pub disposed_rate: f64,
31}
32
33impl Default for AssetGeneratorConfig {
34 fn default() -> Self {
35 Self {
36 asset_class_distribution: vec![
37 (AssetClass::Buildings, 0.05),
38 (AssetClass::Machinery, 0.25),
39 (AssetClass::Vehicles, 0.15),
40 (AssetClass::Furniture, 0.15),
41 (AssetClass::ItEquipment, 0.25),
42 (AssetClass::Software, 0.10),
43 (AssetClass::LeaseholdImprovements, 0.05),
44 ],
45 depreciation_method_distribution: vec![
46 (DepreciationMethod::StraightLine, 0.70),
47 (DepreciationMethod::DoubleDecliningBalance, 0.15),
48 (DepreciationMethod::Macrs, 0.10),
49 (DepreciationMethod::SumOfYearsDigits, 0.05),
50 ],
51 useful_life_by_class: vec![
52 (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), ],
63 acquisition_cost_range: (Decimal::from(1_000), Decimal::from(500_000)),
64 salvage_value_percent: 0.05,
65 fully_depreciated_rate: 0.10,
66 disposed_rate: 0.02,
67 }
68 }
69}
70
71const ASSET_DESCRIPTIONS: &[(AssetClass, &[&str])] = &[
73 (
74 AssetClass::Buildings,
75 &[
76 "Corporate Office Building",
77 "Manufacturing Facility",
78 "Warehouse Complex",
79 "Distribution Center",
80 "Research Laboratory",
81 "Administrative Building",
82 ],
83 ),
84 (
85 AssetClass::Machinery,
86 &[
87 "Production Line Equipment",
88 "CNC Machining Center",
89 "Assembly Robot System",
90 "Industrial Press Machine",
91 "Packaging Equipment",
92 "Testing Equipment",
93 "Quality Control System",
94 "Material Handling System",
95 ],
96 ),
97 (
98 AssetClass::Vehicles,
99 &[
100 "Delivery Truck",
101 "Company Car",
102 "Forklift",
103 "Van Fleet Unit",
104 "Executive Vehicle",
105 "Service Vehicle",
106 "Cargo Truck",
107 "Utility Vehicle",
108 ],
109 ),
110 (
111 AssetClass::Furniture,
112 &[
113 "Office Workstation Set",
114 "Conference Room Furniture",
115 "Executive Desk Set",
116 "Reception Area Furniture",
117 "Cubicle System",
118 "Storage Cabinet Set",
119 "Meeting Room Table",
120 "Ergonomic Chair Set",
121 ],
122 ),
123 (
124 AssetClass::ItEquipment,
125 &[
126 "Server Rack System",
127 "Network Switch Array",
128 "Desktop Computer Set",
129 "Laptop Fleet",
130 "Storage Array",
131 "Backup System",
132 "Security System",
133 "Communication System",
134 ],
135 ),
136 (
137 AssetClass::Software,
138 &[
139 "ERP System License",
140 "CAD Software Suite",
141 "Database License",
142 "Office Suite License",
143 "Security Software",
144 "Development Tools",
145 "Analytics Platform",
146 "CRM System",
147 ],
148 ),
149 (
150 AssetClass::LeaseholdImprovements,
151 &[
152 "Office Build-out",
153 "HVAC Improvements",
154 "Electrical Upgrades",
155 "Floor Renovations",
156 "Lighting System",
157 "Security Improvements",
158 "Accessibility Upgrades",
159 "IT Infrastructure",
160 ],
161 ),
162];
163
164pub struct AssetGenerator {
166 rng: ChaCha8Rng,
167 seed: u64,
168 config: AssetGeneratorConfig,
169 asset_counter: usize,
170}
171
172impl AssetGenerator {
173 pub fn new(seed: u64) -> Self {
175 Self::with_config(seed, AssetGeneratorConfig::default())
176 }
177
178 pub fn with_config(seed: u64, config: AssetGeneratorConfig) -> Self {
180 Self {
181 rng: seeded_rng(seed, 0),
182 seed,
183 config,
184 asset_counter: 0,
185 }
186 }
187
188 pub fn generate_asset(
190 &mut self,
191 company_code: &str,
192 acquisition_date: NaiveDate,
193 ) -> FixedAsset {
194 self.asset_counter += 1;
195
196 let asset_id = format!("FA-{}-{:06}", company_code, self.asset_counter);
197 let asset_class = self.select_asset_class();
198 let description = self.select_description(&asset_class);
199
200 let mut asset = FixedAsset::new(
201 asset_id,
202 description.to_string(),
203 asset_class,
204 company_code,
205 acquisition_date,
206 self.generate_acquisition_cost(),
207 );
208
209 asset.depreciation_method = self.select_depreciation_method(&asset_class);
211 asset.useful_life_months = self.get_useful_life(&asset_class);
212 asset.salvage_value = (asset.acquisition_cost
213 * Decimal::from_f64_retain(self.config.salvage_value_percent)
214 .unwrap_or(Decimal::from_f64_retain(0.05).expect("valid decimal literal")))
215 .round_dp(2);
216
217 asset.account_determination = self.generate_account_determination(&asset_class);
219
220 asset.location = Some(format!("P{}", company_code));
222 asset.cost_center = Some(format!("CC-{}-ADMIN", company_code));
223
224 if matches!(
226 asset_class,
227 AssetClass::Machinery | AssetClass::Vehicles | AssetClass::ItEquipment
228 ) {
229 asset.serial_number = Some(self.generate_serial_number());
230 }
231
232 if self.rng.random::<f64>() < self.config.disposed_rate {
234 let disposal_date =
235 acquisition_date + chrono::Duration::days(self.rng.random_range(365..1825) as i64);
236 let (proceeds, _gain_loss) = self.generate_disposal_values(&asset);
237 asset.dispose(disposal_date, proceeds);
238 } else if self.rng.random::<f64>() < self.config.fully_depreciated_rate {
239 asset.accumulated_depreciation = asset.acquisition_cost - asset.salvage_value;
240 asset.net_book_value = asset.salvage_value;
241 }
242
243 asset
244 }
245
246 pub fn generate_asset_of_class(
248 &mut self,
249 asset_class: AssetClass,
250 company_code: &str,
251 acquisition_date: NaiveDate,
252 ) -> FixedAsset {
253 self.asset_counter += 1;
254
255 let asset_id = format!("FA-{}-{:06}", company_code, self.asset_counter);
256 let description = self.select_description(&asset_class);
257
258 let mut asset = FixedAsset::new(
259 asset_id,
260 description.to_string(),
261 asset_class,
262 company_code,
263 acquisition_date,
264 self.generate_acquisition_cost_for_class(&asset_class),
265 );
266
267 asset.depreciation_method = self.select_depreciation_method(&asset_class);
268 asset.useful_life_months = self.get_useful_life(&asset_class);
269 asset.salvage_value = (asset.acquisition_cost
270 * Decimal::from_f64_retain(self.config.salvage_value_percent)
271 .unwrap_or(Decimal::from_f64_retain(0.05).expect("valid decimal literal")))
272 .round_dp(2);
273
274 asset.account_determination = self.generate_account_determination(&asset_class);
275 asset.location = Some(format!("P{}", company_code));
276 asset.cost_center = Some(format!("CC-{}-ADMIN", company_code));
277
278 if matches!(
279 asset_class,
280 AssetClass::Machinery | AssetClass::Vehicles | AssetClass::ItEquipment
281 ) {
282 asset.serial_number = Some(self.generate_serial_number());
283 }
284
285 asset
286 }
287
288 pub fn generate_aged_asset(
290 &mut self,
291 company_code: &str,
292 acquisition_date: NaiveDate,
293 as_of_date: NaiveDate,
294 ) -> FixedAsset {
295 let mut asset = self.generate_asset(company_code, acquisition_date);
296
297 let months_elapsed = ((as_of_date - acquisition_date).num_days() / 30) as u32;
299
300 for month_offset in 0..months_elapsed {
302 if asset.status == AssetStatus::Active {
303 let dep_date =
305 acquisition_date + chrono::Duration::days((month_offset as i64 + 1) * 30);
306 let depreciation = asset.calculate_monthly_depreciation(dep_date);
307 asset.apply_depreciation(depreciation);
308 }
309 }
310
311 asset
312 }
313
314 pub fn generate_asset_pool(
316 &mut self,
317 count: usize,
318 company_code: &str,
319 date_range: (NaiveDate, NaiveDate),
320 ) -> FixedAssetPool {
321 debug!(count, company_code, "Generating fixed asset pool");
322 let mut pool = FixedAssetPool::new();
323
324 let (start_date, end_date) = date_range;
325 let days_range = (end_date - start_date).num_days() as u64;
326
327 for _ in 0..count {
328 let acquisition_date =
329 start_date + chrono::Duration::days(self.rng.random_range(0..=days_range) as i64);
330 let asset = self.generate_asset(company_code, acquisition_date);
331 pool.add_asset(asset);
332 }
333
334 pool
335 }
336
337 pub fn generate_aged_asset_pool(
339 &mut self,
340 count: usize,
341 company_code: &str,
342 acquisition_date_range: (NaiveDate, NaiveDate),
343 as_of_date: NaiveDate,
344 ) -> FixedAssetPool {
345 let mut pool = FixedAssetPool::new();
346
347 let (start_date, end_date) = acquisition_date_range;
348 let days_range = (end_date - start_date).num_days() as u64;
349
350 for _ in 0..count {
351 let acquisition_date =
352 start_date + chrono::Duration::days(self.rng.random_range(0..=days_range) as i64);
353 let asset = self.generate_aged_asset(company_code, acquisition_date, as_of_date);
354 pool.add_asset(asset);
355 }
356
357 pool
358 }
359
360 pub fn generate_diverse_pool(
362 &mut self,
363 count: usize,
364 company_code: &str,
365 date_range: (NaiveDate, NaiveDate),
366 ) -> FixedAssetPool {
367 let mut pool = FixedAssetPool::new();
368
369 let (start_date, end_date) = date_range;
370 let days_range = (end_date - start_date).num_days() as u64;
371
372 let class_counts = [
374 (AssetClass::Buildings, (count as f64 * 0.05) as usize),
375 (AssetClass::Machinery, (count as f64 * 0.25) as usize),
376 (AssetClass::Vehicles, (count as f64 * 0.15) as usize),
377 (AssetClass::Furniture, (count as f64 * 0.15) as usize),
378 (AssetClass::ItEquipment, (count as f64 * 0.25) as usize),
379 (AssetClass::Software, (count as f64 * 0.10) as usize),
380 (
381 AssetClass::LeaseholdImprovements,
382 (count as f64 * 0.05) as usize,
383 ),
384 ];
385
386 for (class, class_count) in class_counts {
387 for _ in 0..class_count {
388 let acquisition_date = start_date
389 + chrono::Duration::days(self.rng.random_range(0..=days_range) as i64);
390 let asset = self.generate_asset_of_class(class, company_code, acquisition_date);
391 pool.add_asset(asset);
392 }
393 }
394
395 while pool.assets.len() < count {
397 let acquisition_date =
398 start_date + chrono::Duration::days(self.rng.random_range(0..=days_range) as i64);
399 let asset = self.generate_asset(company_code, acquisition_date);
400 pool.add_asset(asset);
401 }
402
403 pool
404 }
405
406 fn select_asset_class(&mut self) -> AssetClass {
408 let roll: f64 = self.rng.random();
409 let mut cumulative = 0.0;
410
411 for (class, prob) in &self.config.asset_class_distribution {
412 cumulative += prob;
413 if roll < cumulative {
414 return *class;
415 }
416 }
417
418 AssetClass::ItEquipment
419 }
420
421 fn select_depreciation_method(&mut self, asset_class: &AssetClass) -> DepreciationMethod {
423 if matches!(
425 asset_class,
426 AssetClass::Land | AssetClass::ConstructionInProgress
427 ) {
428 return DepreciationMethod::StraightLine; }
430
431 let roll: f64 = self.rng.random();
432 let mut cumulative = 0.0;
433
434 for (method, prob) in &self.config.depreciation_method_distribution {
435 cumulative += prob;
436 if roll < cumulative {
437 return *method;
438 }
439 }
440
441 DepreciationMethod::StraightLine
442 }
443
444 fn get_useful_life(&self, asset_class: &AssetClass) -> u32 {
446 for (class, months) in &self.config.useful_life_by_class {
447 if class == asset_class {
448 return *months;
449 }
450 }
451 60 }
453
454 fn select_description(&mut self, asset_class: &AssetClass) -> &'static str {
456 for (class, descriptions) in ASSET_DESCRIPTIONS {
457 if class == asset_class {
458 let idx = self.rng.random_range(0..descriptions.len());
459 return descriptions[idx];
460 }
461 }
462 "Fixed Asset"
463 }
464
465 fn generate_acquisition_cost(&mut self) -> Decimal {
467 let min = self.config.acquisition_cost_range.0;
468 let max = self.config.acquisition_cost_range.1;
469 let range = (max - min).to_string().parse::<f64>().unwrap_or(0.0);
470 let offset =
471 Decimal::from_f64_retain(self.rng.random::<f64>() * range).unwrap_or(Decimal::ZERO);
472 (min + offset).round_dp(2)
473 }
474
475 fn generate_acquisition_cost_for_class(&mut self, asset_class: &AssetClass) -> Decimal {
477 let (min, max) = match asset_class {
478 AssetClass::Buildings => (Decimal::from(500_000), Decimal::from(10_000_000)),
479 AssetClass::BuildingImprovements => (Decimal::from(50_000), Decimal::from(500_000)),
480 AssetClass::Machinery | AssetClass::MachineryEquipment => {
481 (Decimal::from(50_000), Decimal::from(1_000_000))
482 }
483 AssetClass::Vehicles => (Decimal::from(20_000), Decimal::from(100_000)),
484 AssetClass::Furniture | AssetClass::FurnitureFixtures => {
485 (Decimal::from(1_000), Decimal::from(50_000))
486 }
487 AssetClass::ItEquipment | AssetClass::ComputerHardware => {
488 (Decimal::from(2_000), Decimal::from(200_000))
489 }
490 AssetClass::Software | AssetClass::Intangibles => {
491 (Decimal::from(5_000), Decimal::from(500_000))
492 }
493 AssetClass::LeaseholdImprovements => (Decimal::from(10_000), Decimal::from(300_000)),
494 AssetClass::Land => (Decimal::from(100_000), Decimal::from(5_000_000)),
495 AssetClass::ConstructionInProgress => {
496 (Decimal::from(100_000), Decimal::from(2_000_000))
497 }
498 AssetClass::LowValueAssets => (Decimal::from(100), Decimal::from(5_000)),
499 };
500
501 let range = (max - min).to_string().parse::<f64>().unwrap_or(0.0);
502 let offset =
503 Decimal::from_f64_retain(self.rng.random::<f64>() * range).unwrap_or(Decimal::ZERO);
504 (min + offset).round_dp(2)
505 }
506
507 fn generate_serial_number(&mut self) -> String {
509 format!(
510 "SN-{:04}-{:08}",
511 self.rng.random_range(1000..9999),
512 self.rng.random_range(10000000..99999999)
513 )
514 }
515
516 fn generate_disposal_values(&mut self, asset: &FixedAsset) -> (Decimal, Decimal) {
518 let proceeds_rate = self.rng.random::<f64>() * 0.5;
520 let proceeds = (asset.acquisition_cost
521 * Decimal::from_f64_retain(proceeds_rate).unwrap_or(Decimal::ZERO))
522 .round_dp(2);
523
524 let nbv = asset.net_book_value;
526 let gain_loss = proceeds - nbv;
527
528 (proceeds, gain_loss)
529 }
530
531 fn generate_account_determination(
533 &self,
534 asset_class: &AssetClass,
535 ) -> AssetAccountDetermination {
536 match asset_class {
537 AssetClass::Buildings | AssetClass::BuildingImprovements => AssetAccountDetermination {
538 asset_account: "160000".to_string(),
539 accumulated_depreciation_account: "165000".to_string(),
540 depreciation_expense_account: "680000".to_string(),
541 gain_loss_account: "790000".to_string(),
542 gain_on_disposal_account: "790010".to_string(),
543 loss_on_disposal_account: "790020".to_string(),
544 acquisition_clearing_account: "199100".to_string(),
545 },
546 AssetClass::Machinery | AssetClass::MachineryEquipment => AssetAccountDetermination {
547 asset_account: "161000".to_string(),
548 accumulated_depreciation_account: "166000".to_string(),
549 depreciation_expense_account: "681000".to_string(),
550 gain_loss_account: "791000".to_string(),
551 gain_on_disposal_account: "791010".to_string(),
552 loss_on_disposal_account: "791020".to_string(),
553 acquisition_clearing_account: "199110".to_string(),
554 },
555 AssetClass::Vehicles => AssetAccountDetermination {
556 asset_account: "162000".to_string(),
557 accumulated_depreciation_account: "167000".to_string(),
558 depreciation_expense_account: "682000".to_string(),
559 gain_loss_account: "792000".to_string(),
560 gain_on_disposal_account: "792010".to_string(),
561 loss_on_disposal_account: "792020".to_string(),
562 acquisition_clearing_account: "199120".to_string(),
563 },
564 AssetClass::Furniture | AssetClass::FurnitureFixtures => AssetAccountDetermination {
565 asset_account: "163000".to_string(),
566 accumulated_depreciation_account: "168000".to_string(),
567 depreciation_expense_account: "683000".to_string(),
568 gain_loss_account: "793000".to_string(),
569 gain_on_disposal_account: "793010".to_string(),
570 loss_on_disposal_account: "793020".to_string(),
571 acquisition_clearing_account: "199130".to_string(),
572 },
573 AssetClass::ItEquipment | AssetClass::ComputerHardware => AssetAccountDetermination {
574 asset_account: "164000".to_string(),
575 accumulated_depreciation_account: "169000".to_string(),
576 depreciation_expense_account: "684000".to_string(),
577 gain_loss_account: "794000".to_string(),
578 gain_on_disposal_account: "794010".to_string(),
579 loss_on_disposal_account: "794020".to_string(),
580 acquisition_clearing_account: "199140".to_string(),
581 },
582 AssetClass::Software | AssetClass::Intangibles => AssetAccountDetermination {
583 asset_account: "170000".to_string(),
584 accumulated_depreciation_account: "175000".to_string(),
585 depreciation_expense_account: "685000".to_string(),
586 gain_loss_account: "795000".to_string(),
587 gain_on_disposal_account: "795010".to_string(),
588 loss_on_disposal_account: "795020".to_string(),
589 acquisition_clearing_account: "199150".to_string(),
590 },
591 AssetClass::LeaseholdImprovements => AssetAccountDetermination {
592 asset_account: "171000".to_string(),
593 accumulated_depreciation_account: "176000".to_string(),
594 depreciation_expense_account: "686000".to_string(),
595 gain_loss_account: "796000".to_string(),
596 gain_on_disposal_account: "796010".to_string(),
597 loss_on_disposal_account: "796020".to_string(),
598 acquisition_clearing_account: "199160".to_string(),
599 },
600 AssetClass::Land => {
601 AssetAccountDetermination {
602 asset_account: "150000".to_string(),
603 accumulated_depreciation_account: "".to_string(), depreciation_expense_account: "".to_string(),
605 gain_loss_account: "790000".to_string(),
606 gain_on_disposal_account: "790010".to_string(),
607 loss_on_disposal_account: "790020".to_string(),
608 acquisition_clearing_account: "199000".to_string(),
609 }
610 }
611 AssetClass::ConstructionInProgress => AssetAccountDetermination {
612 asset_account: "159000".to_string(),
613 accumulated_depreciation_account: "".to_string(),
614 depreciation_expense_account: "".to_string(),
615 gain_loss_account: "".to_string(),
616 gain_on_disposal_account: "".to_string(),
617 loss_on_disposal_account: "".to_string(),
618 acquisition_clearing_account: "199090".to_string(),
619 },
620 AssetClass::LowValueAssets => AssetAccountDetermination {
621 asset_account: "172000".to_string(),
622 accumulated_depreciation_account: "177000".to_string(),
623 depreciation_expense_account: "687000".to_string(),
624 gain_loss_account: "797000".to_string(),
625 gain_on_disposal_account: "797010".to_string(),
626 loss_on_disposal_account: "797020".to_string(),
627 acquisition_clearing_account: "199170".to_string(),
628 },
629 }
630 }
631
632 pub fn reset(&mut self) {
634 self.rng = seeded_rng(self.seed, 0);
635 self.asset_counter = 0;
636 }
637}
638
639#[cfg(test)]
640#[allow(clippy::unwrap_used)]
641mod tests {
642 use super::*;
643
644 #[test]
645 fn test_asset_generation() {
646 let mut gen = AssetGenerator::new(42);
647 let asset = gen.generate_asset("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
648
649 assert!(!asset.asset_id.is_empty());
650 assert!(!asset.description.is_empty());
651 assert!(asset.acquisition_cost > Decimal::ZERO);
652 assert!(
653 asset.useful_life_months > 0
654 || matches!(
655 asset.asset_class,
656 AssetClass::Land | AssetClass::ConstructionInProgress
657 )
658 );
659 }
660
661 #[test]
662 fn test_asset_pool_generation() {
663 let mut gen = AssetGenerator::new(42);
664 let pool = gen.generate_asset_pool(
665 50,
666 "1000",
667 (
668 NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
669 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
670 ),
671 );
672
673 assert_eq!(pool.assets.len(), 50);
674 }
675
676 #[test]
677 fn test_aged_asset() {
678 let mut gen = AssetGenerator::new(42);
679 let asset = gen.generate_aged_asset(
680 "1000",
681 NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
682 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
683 );
684
685 if asset.status == AssetStatus::Active && asset.useful_life_months > 0 {
687 assert!(asset.accumulated_depreciation > Decimal::ZERO);
688 assert!(asset.net_book_value < asset.acquisition_cost);
689 }
690 }
691
692 #[test]
693 fn test_diverse_pool() {
694 let mut gen = AssetGenerator::new(42);
695 let pool = gen.generate_diverse_pool(
696 100,
697 "1000",
698 (
699 NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
700 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
701 ),
702 );
703
704 let machinery_count = pool
706 .assets
707 .iter()
708 .filter(|a| a.asset_class == AssetClass::Machinery)
709 .count();
710 let it_count = pool
711 .assets
712 .iter()
713 .filter(|a| a.asset_class == AssetClass::ItEquipment)
714 .count();
715
716 assert!(machinery_count > 0);
717 assert!(it_count > 0);
718 }
719
720 #[test]
721 fn test_deterministic_generation() {
722 let mut gen1 = AssetGenerator::new(42);
723 let mut gen2 = AssetGenerator::new(42);
724
725 let asset1 = gen1.generate_asset("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
726 let asset2 = gen2.generate_asset("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
727
728 assert_eq!(asset1.asset_id, asset2.asset_id);
729 assert_eq!(asset1.description, asset2.description);
730 assert_eq!(asset1.acquisition_cost, asset2.acquisition_cost);
731 }
732
733 #[test]
734 fn test_depreciation_calculation() {
735 let mut gen = AssetGenerator::new(42);
736 let mut asset = gen.generate_asset_of_class(
737 AssetClass::ItEquipment,
738 "1000",
739 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
740 );
741
742 let initial_nbv = asset.net_book_value;
743
744 let depreciation =
746 asset.calculate_monthly_depreciation(NaiveDate::from_ymd_opt(2024, 2, 1).unwrap());
747 asset.apply_depreciation(depreciation);
748
749 assert!(asset.accumulated_depreciation > Decimal::ZERO);
750 assert!(asset.net_book_value < initial_nbv);
751 }
752
753 #[test]
754 fn test_asset_class_cost_ranges() {
755 let mut gen = AssetGenerator::new(42);
756
757 let building = gen.generate_asset_of_class(
759 AssetClass::Buildings,
760 "1000",
761 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
762 );
763 let furniture = gen.generate_asset_of_class(
764 AssetClass::Furniture,
765 "1000",
766 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
767 );
768
769 assert!(building.acquisition_cost >= Decimal::from(500_000));
771 assert!(furniture.acquisition_cost <= Decimal::from(50_000));
772 }
773}