Skip to main content

datasynth_generators/master_data/
asset_generator.rs

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