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 datasynth_core::utils::seeded_rng;
9use rand::prelude::*;
10use rand_chacha::ChaCha8Rng;
11use rust_decimal::Decimal;
12use tracing::debug;
13
14/// Configuration for asset generation.
15#[derive(Debug, Clone)]
16pub struct AssetGeneratorConfig {
17    /// Distribution of asset classes (class, probability)
18    pub asset_class_distribution: Vec<(AssetClass, f64)>,
19    /// Distribution of depreciation methods (method, probability)
20    pub depreciation_method_distribution: Vec<(DepreciationMethod, f64)>,
21    /// Default useful life by asset class (class, months)
22    pub useful_life_by_class: Vec<(AssetClass, u32)>,
23    /// Acquisition cost range (min, max)
24    pub acquisition_cost_range: (Decimal, Decimal),
25    /// Salvage value percentage (typically 5-10%)
26    pub salvage_value_percent: f64,
27    /// Probability of asset being fully depreciated
28    pub fully_depreciated_rate: f64,
29    /// Probability of asset being disposed
30    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),             // 40 years
53                (AssetClass::BuildingImprovements, 180),  // 15 years
54                (AssetClass::Machinery, 84),              // 7 years
55                (AssetClass::Vehicles, 60),               // 5 years
56                (AssetClass::Furniture, 84),              // 7 years
57                (AssetClass::ItEquipment, 36),            // 3 years
58                (AssetClass::Software, 36),               // 3 years
59                (AssetClass::LeaseholdImprovements, 120), // 10 years
60                (AssetClass::Land, 0),                    // Not depreciated
61                (AssetClass::ConstructionInProgress, 0),  // Not depreciated
62            ],
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
71/// Asset description templates by class.
72const 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
164/// Generator for fixed asset master data.
165pub struct AssetGenerator {
166    rng: ChaCha8Rng,
167    seed: u64,
168    config: AssetGeneratorConfig,
169    asset_counter: usize,
170}
171
172impl AssetGenerator {
173    /// Create a new asset generator.
174    pub fn new(seed: u64) -> Self {
175        Self::with_config(seed, AssetGeneratorConfig::default())
176    }
177
178    /// Create a new asset generator with custom configuration.
179    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    /// Generate a single fixed asset.
189    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        // Set depreciation parameters
210        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        // Set account determination
218        asset.account_determination = self.generate_account_determination(&asset_class);
219
220        // Set location info
221        asset.location = Some(format!("P{}", company_code));
222        asset.cost_center = Some(format!("CC-{}-ADMIN", company_code));
223
224        // Generate serial number for equipment
225        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        // Handle fully depreciated or disposed assets
233        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    /// Generate an asset with specific class.
247    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    /// Generate an asset with depreciation already applied.
289    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        // Calculate months elapsed
298        let months_elapsed = ((as_of_date - acquisition_date).num_days() / 30) as u32;
299
300        // Apply depreciation for each month
301        for month_offset in 0..months_elapsed {
302            if asset.status == AssetStatus::Active {
303                // Calculate the depreciation date for this month
304                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    /// Generate an asset pool with specified count.
315    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    /// Generate an asset pool with aged assets (depreciation applied).
338    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    /// Generate a diverse asset pool with various classes.
361    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        // Define class distribution
373        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        // Fill remaining slots
396        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    /// Select asset class based on distribution.
407    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    /// Select depreciation method based on distribution and asset class.
422    fn select_depreciation_method(&mut self, asset_class: &AssetClass) -> DepreciationMethod {
423        // Land and CIP are not depreciated
424        if matches!(
425            asset_class,
426            AssetClass::Land | AssetClass::ConstructionInProgress
427        ) {
428            return DepreciationMethod::StraightLine; // Won't be used but needs a value
429        }
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    /// Get useful life for asset class.
445    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 // Default 5 years
452    }
453
454    /// Select description for asset class.
455    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    /// Generate acquisition cost.
466    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    /// Generate acquisition cost for specific asset class.
476    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    /// Generate serial number.
508    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    /// Generate disposal values.
517    fn generate_disposal_values(&mut self, asset: &FixedAsset) -> (Decimal, Decimal) {
518        // Disposal proceeds typically 0-50% of acquisition cost
519        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        // Gain/loss = proceeds - NBV (can be negative)
525        let nbv = asset.net_book_value;
526        let gain_loss = proceeds - nbv;
527
528        (proceeds, gain_loss)
529    }
530
531    /// Generate account determination for asset class.
532    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(), // Land not depreciated
604                    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    /// Reset the generator.
633    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        // Should have accumulated depreciation
686        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        // Should have various asset classes
705        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        // Apply one month of depreciation
745        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        // Buildings should be more expensive than furniture
758        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        // Buildings min is 500k, furniture max is 50k
770        assert!(building.acquisition_cost >= Decimal::from(500_000));
771        assert!(furniture.acquisition_cost <= Decimal::from(50_000));
772    }
773}