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
14use crate::coa_generator::CoAFramework;
15
16/// Configuration for asset generation.
17#[derive(Debug, Clone)]
18pub struct AssetGeneratorConfig {
19    /// Distribution of asset classes (class, probability)
20    pub asset_class_distribution: Vec<(AssetClass, f64)>,
21    /// Distribution of depreciation methods (method, probability)
22    pub depreciation_method_distribution: Vec<(DepreciationMethod, f64)>,
23    /// Default useful life by asset class (class, months)
24    pub useful_life_by_class: Vec<(AssetClass, u32)>,
25    /// Acquisition cost range (min, max)
26    pub acquisition_cost_range: (Decimal, Decimal),
27    /// Salvage value percentage (typically 5-10%)
28    pub salvage_value_percent: f64,
29    /// Probability of asset being fully depreciated
30    pub fully_depreciated_rate: f64,
31    /// Probability of asset being disposed
32    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),             // 40 years
55                (AssetClass::BuildingImprovements, 180),  // 15 years
56                (AssetClass::Machinery, 84),              // 7 years
57                (AssetClass::Vehicles, 60),               // 5 years
58                (AssetClass::Furniture, 84),              // 7 years
59                (AssetClass::ItEquipment, 36),            // 3 years
60                (AssetClass::Software, 36),               // 3 years
61                (AssetClass::LeaseholdImprovements, 120), // 10 years
62                (AssetClass::Land, 0),                    // Not depreciated
63                (AssetClass::ConstructionInProgress, 0),  // Not depreciated
64            ],
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
73/// Asset description templates by class.
74const 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
166/// Generator for fixed asset master data.
167pub struct AssetGenerator {
168    rng: ChaCha8Rng,
169    seed: u64,
170    config: AssetGeneratorConfig,
171    asset_counter: usize,
172    coa_framework: CoAFramework,
173}
174
175impl AssetGenerator {
176    /// Create a new asset generator.
177    pub fn new(seed: u64) -> Self {
178        Self::with_config(seed, AssetGeneratorConfig::default())
179    }
180
181    /// Create a new asset generator with custom configuration.
182    pub fn with_config(seed: u64, config: AssetGeneratorConfig) -> Self {
183        Self {
184            rng: seeded_rng(seed, 0),
185            seed,
186            config,
187            asset_counter: 0,
188            coa_framework: CoAFramework::UsGaap,
189        }
190    }
191
192    /// Set the accounting framework for framework-aware asset generation.
193    pub fn set_coa_framework(&mut self, framework: CoAFramework) {
194        self.coa_framework = framework;
195    }
196
197    /// Generate a single fixed asset.
198    pub fn generate_asset(
199        &mut self,
200        company_code: &str,
201        acquisition_date: NaiveDate,
202    ) -> FixedAsset {
203        self.asset_counter += 1;
204
205        let asset_id = format!("FA-{}-{:06}", company_code, self.asset_counter);
206        let asset_class = self.select_asset_class();
207        let description = self.select_description(&asset_class);
208
209        let mut asset = FixedAsset::new(
210            asset_id,
211            description.to_string(),
212            asset_class,
213            company_code,
214            acquisition_date,
215            self.generate_acquisition_cost(),
216        );
217
218        // GWG check: German GAAP immediate expensing for assets ≤ 800 EUR
219        let is_gwg = self.coa_framework == CoAFramework::GermanSkr04
220            && asset.acquisition_cost <= Decimal::from(800)
221            && asset_class.is_depreciable();
222
223        if is_gwg {
224            asset.is_gwg = Some(true);
225            asset.depreciation_method = DepreciationMethod::ImmediateExpense;
226            asset.useful_life_months = 1;
227            asset.salvage_value = Decimal::ZERO;
228        } else {
229            // Set depreciation parameters
230            asset.depreciation_method = self.select_depreciation_method(&asset_class);
231            asset.useful_life_months = self.get_useful_life(&asset_class);
232            asset.salvage_value = (asset.acquisition_cost
233                * Decimal::from_f64_retain(self.config.salvage_value_percent)
234                    .unwrap_or(Decimal::from_f64_retain(0.05).expect("valid decimal literal")))
235            .round_dp(2);
236        }
237
238        // Set account determination
239        asset.account_determination = self.generate_account_determination(&asset_class);
240
241        // Set location info
242        asset.location = Some(format!("P{}", company_code));
243        asset.cost_center = Some(format!("CC-{}-ADMIN", company_code));
244
245        // Generate serial number for equipment
246        if matches!(
247            asset_class,
248            AssetClass::Machinery | AssetClass::Vehicles | AssetClass::ItEquipment
249        ) {
250            asset.serial_number = Some(self.generate_serial_number());
251        }
252
253        // Handle fully depreciated or disposed assets
254        if self.rng.random::<f64>() < self.config.disposed_rate {
255            let disposal_date =
256                acquisition_date + chrono::Duration::days(self.rng.random_range(365..1825) as i64);
257            let (proceeds, _gain_loss) = self.generate_disposal_values(&asset);
258            asset.dispose(disposal_date, proceeds);
259        } else if self.rng.random::<f64>() < self.config.fully_depreciated_rate {
260            asset.accumulated_depreciation = asset.acquisition_cost - asset.salvage_value;
261            asset.net_book_value = asset.salvage_value;
262        }
263
264        asset
265    }
266
267    /// Generate an asset with specific class.
268    pub fn generate_asset_of_class(
269        &mut self,
270        asset_class: AssetClass,
271        company_code: &str,
272        acquisition_date: NaiveDate,
273    ) -> FixedAsset {
274        self.asset_counter += 1;
275
276        let asset_id = format!("FA-{}-{:06}", company_code, self.asset_counter);
277        let description = self.select_description(&asset_class);
278
279        let mut asset = FixedAsset::new(
280            asset_id,
281            description.to_string(),
282            asset_class,
283            company_code,
284            acquisition_date,
285            self.generate_acquisition_cost_for_class(&asset_class),
286        );
287
288        asset.depreciation_method = self.select_depreciation_method(&asset_class);
289        asset.useful_life_months = self.get_useful_life(&asset_class);
290        asset.salvage_value = (asset.acquisition_cost
291            * Decimal::from_f64_retain(self.config.salvage_value_percent)
292                .unwrap_or(Decimal::from_f64_retain(0.05).expect("valid decimal literal")))
293        .round_dp(2);
294
295        asset.account_determination = self.generate_account_determination(&asset_class);
296        asset.location = Some(format!("P{}", company_code));
297        asset.cost_center = Some(format!("CC-{}-ADMIN", company_code));
298
299        if matches!(
300            asset_class,
301            AssetClass::Machinery | AssetClass::Vehicles | AssetClass::ItEquipment
302        ) {
303            asset.serial_number = Some(self.generate_serial_number());
304        }
305
306        asset
307    }
308
309    /// Generate an asset with depreciation already applied.
310    pub fn generate_aged_asset(
311        &mut self,
312        company_code: &str,
313        acquisition_date: NaiveDate,
314        as_of_date: NaiveDate,
315    ) -> FixedAsset {
316        let mut asset = self.generate_asset(company_code, acquisition_date);
317
318        // Calculate months elapsed
319        let months_elapsed = ((as_of_date - acquisition_date).num_days() / 30) as u32;
320
321        // Apply depreciation for each month
322        for month_offset in 0..months_elapsed {
323            if asset.status == AssetStatus::Active {
324                // Calculate the depreciation date for this month
325                let dep_date =
326                    acquisition_date + chrono::Duration::days((month_offset as i64 + 1) * 30);
327                let depreciation = asset.calculate_monthly_depreciation(dep_date);
328                asset.apply_depreciation(depreciation);
329            }
330        }
331
332        asset
333    }
334
335    /// Generate an asset pool with specified count.
336    pub fn generate_asset_pool(
337        &mut self,
338        count: usize,
339        company_code: &str,
340        date_range: (NaiveDate, NaiveDate),
341    ) -> FixedAssetPool {
342        debug!(count, company_code, "Generating fixed asset pool");
343        let mut pool = FixedAssetPool::new();
344
345        let (start_date, end_date) = date_range;
346        let days_range = (end_date - start_date).num_days() as u64;
347
348        for _ in 0..count {
349            let acquisition_date =
350                start_date + chrono::Duration::days(self.rng.random_range(0..=days_range) as i64);
351            let asset = self.generate_asset(company_code, acquisition_date);
352            pool.add_asset(asset);
353        }
354
355        pool
356    }
357
358    /// Generate an asset pool with aged assets (depreciation applied).
359    pub fn generate_aged_asset_pool(
360        &mut self,
361        count: usize,
362        company_code: &str,
363        acquisition_date_range: (NaiveDate, NaiveDate),
364        as_of_date: NaiveDate,
365    ) -> FixedAssetPool {
366        let mut pool = FixedAssetPool::new();
367
368        let (start_date, end_date) = acquisition_date_range;
369        let days_range = (end_date - start_date).num_days() as u64;
370
371        for _ in 0..count {
372            let acquisition_date =
373                start_date + chrono::Duration::days(self.rng.random_range(0..=days_range) as i64);
374            let asset = self.generate_aged_asset(company_code, acquisition_date, as_of_date);
375            pool.add_asset(asset);
376        }
377
378        pool
379    }
380
381    /// Generate a diverse asset pool with various classes.
382    pub fn generate_diverse_pool(
383        &mut self,
384        count: usize,
385        company_code: &str,
386        date_range: (NaiveDate, NaiveDate),
387    ) -> FixedAssetPool {
388        let mut pool = FixedAssetPool::new();
389
390        let (start_date, end_date) = date_range;
391        let days_range = (end_date - start_date).num_days() as u64;
392
393        // Define class distribution
394        let class_counts = [
395            (AssetClass::Buildings, (count as f64 * 0.05) as usize),
396            (AssetClass::Machinery, (count as f64 * 0.25) as usize),
397            (AssetClass::Vehicles, (count as f64 * 0.15) as usize),
398            (AssetClass::Furniture, (count as f64 * 0.15) as usize),
399            (AssetClass::ItEquipment, (count as f64 * 0.25) as usize),
400            (AssetClass::Software, (count as f64 * 0.10) as usize),
401            (
402                AssetClass::LeaseholdImprovements,
403                (count as f64 * 0.05) as usize,
404            ),
405        ];
406
407        for (class, class_count) in class_counts {
408            for _ in 0..class_count {
409                let acquisition_date = start_date
410                    + chrono::Duration::days(self.rng.random_range(0..=days_range) as i64);
411                let asset = self.generate_asset_of_class(class, company_code, acquisition_date);
412                pool.add_asset(asset);
413            }
414        }
415
416        // Fill remaining slots
417        while pool.assets.len() < count {
418            let acquisition_date =
419                start_date + chrono::Duration::days(self.rng.random_range(0..=days_range) as i64);
420            let asset = self.generate_asset(company_code, acquisition_date);
421            pool.add_asset(asset);
422        }
423
424        pool
425    }
426
427    /// Select asset class based on distribution.
428    fn select_asset_class(&mut self) -> AssetClass {
429        let roll: f64 = self.rng.random();
430        let mut cumulative = 0.0;
431
432        for (class, prob) in &self.config.asset_class_distribution {
433            cumulative += prob;
434            if roll < cumulative {
435                return *class;
436            }
437        }
438
439        AssetClass::ItEquipment
440    }
441
442    /// Select depreciation method based on distribution and asset class.
443    fn select_depreciation_method(&mut self, asset_class: &AssetClass) -> DepreciationMethod {
444        // Land and CIP are not depreciated
445        if matches!(
446            asset_class,
447            AssetClass::Land | AssetClass::ConstructionInProgress
448        ) {
449            return DepreciationMethod::StraightLine; // Won't be used but needs a value
450        }
451
452        // German GAAP: use Degressiv instead of DDB/MACRS, with higher SL proportion
453        if self.coa_framework == CoAFramework::GermanSkr04 {
454            let roll: f64 = self.rng.random();
455            return if roll < 0.75 {
456                DepreciationMethod::StraightLine
457            } else {
458                DepreciationMethod::Degressiv
459            };
460        }
461
462        let roll: f64 = self.rng.random();
463        let mut cumulative = 0.0;
464
465        for (method, prob) in &self.config.depreciation_method_distribution {
466            cumulative += prob;
467            if roll < cumulative {
468                return *method;
469            }
470        }
471
472        DepreciationMethod::StraightLine
473    }
474
475    /// Get useful life for asset class.
476    fn get_useful_life(&self, asset_class: &AssetClass) -> u32 {
477        // German GAAP: use AfA-Tabellen defaults
478        if self.coa_framework == CoAFramework::GermanSkr04 {
479            return self.get_useful_life_german(asset_class);
480        }
481
482        for (class, months) in &self.config.useful_life_by_class {
483            if class == asset_class {
484                return *months;
485            }
486        }
487        60 // Default 5 years
488    }
489
490    /// Select description for asset class.
491    fn select_description(&mut self, asset_class: &AssetClass) -> &'static str {
492        for (class, descriptions) in ASSET_DESCRIPTIONS {
493            if class == asset_class {
494                let idx = self.rng.random_range(0..descriptions.len());
495                return descriptions[idx];
496            }
497        }
498        "Fixed Asset"
499    }
500
501    /// Generate acquisition cost.
502    fn generate_acquisition_cost(&mut self) -> Decimal {
503        // German GAAP: ~15% of assets are GWG-eligible (≤ 800 EUR)
504        if self.coa_framework == CoAFramework::GermanSkr04 && self.rng.random::<f64>() < 0.15 {
505            let gwg_cost = Decimal::from(100)
506                + Decimal::from_f64_retain(self.rng.random::<f64>() * 700.0)
507                    .unwrap_or(Decimal::ZERO);
508            return gwg_cost.round_dp(2);
509        }
510
511        let min = self.config.acquisition_cost_range.0;
512        let max = self.config.acquisition_cost_range.1;
513        let range = (max - min).to_string().parse::<f64>().unwrap_or(0.0);
514        let offset =
515            Decimal::from_f64_retain(self.rng.random::<f64>() * range).unwrap_or(Decimal::ZERO);
516        (min + offset).round_dp(2)
517    }
518
519    /// Generate acquisition cost for specific asset class.
520    fn generate_acquisition_cost_for_class(&mut self, asset_class: &AssetClass) -> Decimal {
521        let (min, max) = match asset_class {
522            AssetClass::Buildings => (Decimal::from(500_000), Decimal::from(10_000_000)),
523            AssetClass::BuildingImprovements => (Decimal::from(50_000), Decimal::from(500_000)),
524            AssetClass::Machinery | AssetClass::MachineryEquipment => {
525                (Decimal::from(50_000), Decimal::from(1_000_000))
526            }
527            AssetClass::Vehicles => (Decimal::from(20_000), Decimal::from(100_000)),
528            AssetClass::Furniture | AssetClass::FurnitureFixtures => {
529                (Decimal::from(1_000), Decimal::from(50_000))
530            }
531            AssetClass::ItEquipment | AssetClass::ComputerHardware => {
532                (Decimal::from(2_000), Decimal::from(200_000))
533            }
534            AssetClass::Software | AssetClass::Intangibles => {
535                (Decimal::from(5_000), Decimal::from(500_000))
536            }
537            AssetClass::LeaseholdImprovements => (Decimal::from(10_000), Decimal::from(300_000)),
538            AssetClass::Land => (Decimal::from(100_000), Decimal::from(5_000_000)),
539            AssetClass::ConstructionInProgress => {
540                (Decimal::from(100_000), Decimal::from(2_000_000))
541            }
542            AssetClass::LowValueAssets => (Decimal::from(100), Decimal::from(5_000)),
543        };
544
545        let range = (max - min).to_string().parse::<f64>().unwrap_or(0.0);
546        let offset =
547            Decimal::from_f64_retain(self.rng.random::<f64>() * range).unwrap_or(Decimal::ZERO);
548        (min + offset).round_dp(2)
549    }
550
551    /// Generate serial number.
552    fn generate_serial_number(&mut self) -> String {
553        format!(
554            "SN-{:04}-{:08}",
555            self.rng.random_range(1000..9999),
556            self.rng.random_range(10000000..99999999)
557        )
558    }
559
560    /// Generate disposal values.
561    fn generate_disposal_values(&mut self, asset: &FixedAsset) -> (Decimal, Decimal) {
562        // Disposal proceeds typically 0-50% of acquisition cost
563        let proceeds_rate = self.rng.random::<f64>() * 0.5;
564        let proceeds = (asset.acquisition_cost
565            * Decimal::from_f64_retain(proceeds_rate).unwrap_or(Decimal::ZERO))
566        .round_dp(2);
567
568        // Gain/loss = proceeds - NBV (can be negative)
569        let nbv = asset.net_book_value;
570        let gain_loss = proceeds - nbv;
571
572        (proceeds, gain_loss)
573    }
574
575    /// Generate account determination for asset class.
576    fn generate_account_determination(
577        &self,
578        asset_class: &AssetClass,
579    ) -> AssetAccountDetermination {
580        match asset_class {
581            AssetClass::Buildings | AssetClass::BuildingImprovements => AssetAccountDetermination {
582                asset_account: "160000".to_string(),
583                accumulated_depreciation_account: "165000".to_string(),
584                depreciation_expense_account: "680000".to_string(),
585                gain_loss_account: "790000".to_string(),
586                gain_on_disposal_account: "790010".to_string(),
587                loss_on_disposal_account: "790020".to_string(),
588                acquisition_clearing_account: "199100".to_string(),
589            },
590            AssetClass::Machinery | AssetClass::MachineryEquipment => AssetAccountDetermination {
591                asset_account: "161000".to_string(),
592                accumulated_depreciation_account: "166000".to_string(),
593                depreciation_expense_account: "681000".to_string(),
594                gain_loss_account: "791000".to_string(),
595                gain_on_disposal_account: "791010".to_string(),
596                loss_on_disposal_account: "791020".to_string(),
597                acquisition_clearing_account: "199110".to_string(),
598            },
599            AssetClass::Vehicles => AssetAccountDetermination {
600                asset_account: "162000".to_string(),
601                accumulated_depreciation_account: "167000".to_string(),
602                depreciation_expense_account: "682000".to_string(),
603                gain_loss_account: "792000".to_string(),
604                gain_on_disposal_account: "792010".to_string(),
605                loss_on_disposal_account: "792020".to_string(),
606                acquisition_clearing_account: "199120".to_string(),
607            },
608            AssetClass::Furniture | AssetClass::FurnitureFixtures => AssetAccountDetermination {
609                asset_account: "163000".to_string(),
610                accumulated_depreciation_account: "168000".to_string(),
611                depreciation_expense_account: "683000".to_string(),
612                gain_loss_account: "793000".to_string(),
613                gain_on_disposal_account: "793010".to_string(),
614                loss_on_disposal_account: "793020".to_string(),
615                acquisition_clearing_account: "199130".to_string(),
616            },
617            AssetClass::ItEquipment | AssetClass::ComputerHardware => AssetAccountDetermination {
618                asset_account: "164000".to_string(),
619                accumulated_depreciation_account: "169000".to_string(),
620                depreciation_expense_account: "684000".to_string(),
621                gain_loss_account: "794000".to_string(),
622                gain_on_disposal_account: "794010".to_string(),
623                loss_on_disposal_account: "794020".to_string(),
624                acquisition_clearing_account: "199140".to_string(),
625            },
626            AssetClass::Software | AssetClass::Intangibles => AssetAccountDetermination {
627                asset_account: "170000".to_string(),
628                accumulated_depreciation_account: "175000".to_string(),
629                depreciation_expense_account: "685000".to_string(),
630                gain_loss_account: "795000".to_string(),
631                gain_on_disposal_account: "795010".to_string(),
632                loss_on_disposal_account: "795020".to_string(),
633                acquisition_clearing_account: "199150".to_string(),
634            },
635            AssetClass::LeaseholdImprovements => AssetAccountDetermination {
636                asset_account: "171000".to_string(),
637                accumulated_depreciation_account: "176000".to_string(),
638                depreciation_expense_account: "686000".to_string(),
639                gain_loss_account: "796000".to_string(),
640                gain_on_disposal_account: "796010".to_string(),
641                loss_on_disposal_account: "796020".to_string(),
642                acquisition_clearing_account: "199160".to_string(),
643            },
644            AssetClass::Land => {
645                AssetAccountDetermination {
646                    asset_account: "150000".to_string(),
647                    accumulated_depreciation_account: "".to_string(), // Land not depreciated
648                    depreciation_expense_account: "".to_string(),
649                    gain_loss_account: "790000".to_string(),
650                    gain_on_disposal_account: "790010".to_string(),
651                    loss_on_disposal_account: "790020".to_string(),
652                    acquisition_clearing_account: "199000".to_string(),
653                }
654            }
655            AssetClass::ConstructionInProgress => AssetAccountDetermination {
656                asset_account: "159000".to_string(),
657                accumulated_depreciation_account: "".to_string(),
658                depreciation_expense_account: "".to_string(),
659                gain_loss_account: "".to_string(),
660                gain_on_disposal_account: "".to_string(),
661                loss_on_disposal_account: "".to_string(),
662                acquisition_clearing_account: "199090".to_string(),
663            },
664            AssetClass::LowValueAssets => AssetAccountDetermination {
665                asset_account: "172000".to_string(),
666                accumulated_depreciation_account: "177000".to_string(),
667                depreciation_expense_account: "687000".to_string(),
668                gain_loss_account: "797000".to_string(),
669                gain_on_disposal_account: "797010".to_string(),
670                loss_on_disposal_account: "797020".to_string(),
671                acquisition_clearing_account: "199170".to_string(),
672            },
673        }
674    }
675
676    /// Generate German useful life based on AfA-Tabellen defaults.
677    fn get_useful_life_german(&self, asset_class: &AssetClass) -> u32 {
678        match asset_class {
679            AssetClass::Buildings | AssetClass::BuildingImprovements => 396, // 33 years (AfA §7(4))
680            AssetClass::Machinery | AssetClass::MachineryEquipment => 120,   // 10 years
681            AssetClass::Vehicles => 72,                                      // 6 years
682            AssetClass::Furniture | AssetClass::FurnitureFixtures => 156,    // 13 years
683            AssetClass::ItEquipment | AssetClass::ComputerHardware => 36,    // 3 years (digital)
684            AssetClass::Software | AssetClass::Intangibles => 36,            // 3 years (digital)
685            AssetClass::LeaseholdImprovements => 120,                        // 10 years
686            AssetClass::Land | AssetClass::ConstructionInProgress => 0,
687            AssetClass::LowValueAssets => 12,
688        }
689    }
690
691    /// Reset the generator.
692    pub fn reset(&mut self) {
693        self.rng = seeded_rng(self.seed, 0);
694        self.asset_counter = 0;
695    }
696}
697
698#[cfg(test)]
699#[allow(clippy::unwrap_used)]
700mod tests {
701    use super::*;
702
703    #[test]
704    fn test_asset_generation() {
705        let mut gen = AssetGenerator::new(42);
706        let asset = gen.generate_asset("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
707
708        assert!(!asset.asset_id.is_empty());
709        assert!(!asset.description.is_empty());
710        assert!(asset.acquisition_cost > Decimal::ZERO);
711        assert!(
712            asset.useful_life_months > 0
713                || matches!(
714                    asset.asset_class,
715                    AssetClass::Land | AssetClass::ConstructionInProgress
716                )
717        );
718    }
719
720    #[test]
721    fn test_asset_pool_generation() {
722        let mut gen = AssetGenerator::new(42);
723        let pool = gen.generate_asset_pool(
724            50,
725            "1000",
726            (
727                NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
728                NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
729            ),
730        );
731
732        assert_eq!(pool.assets.len(), 50);
733    }
734
735    #[test]
736    fn test_aged_asset() {
737        let mut gen = AssetGenerator::new(42);
738        let asset = gen.generate_aged_asset(
739            "1000",
740            NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
741            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
742        );
743
744        // Should have accumulated depreciation
745        if asset.status == AssetStatus::Active && asset.useful_life_months > 0 {
746            assert!(asset.accumulated_depreciation > Decimal::ZERO);
747            assert!(asset.net_book_value < asset.acquisition_cost);
748        }
749    }
750
751    #[test]
752    fn test_diverse_pool() {
753        let mut gen = AssetGenerator::new(42);
754        let pool = gen.generate_diverse_pool(
755            100,
756            "1000",
757            (
758                NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
759                NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
760            ),
761        );
762
763        // Should have various asset classes
764        let machinery_count = pool
765            .assets
766            .iter()
767            .filter(|a| a.asset_class == AssetClass::Machinery)
768            .count();
769        let it_count = pool
770            .assets
771            .iter()
772            .filter(|a| a.asset_class == AssetClass::ItEquipment)
773            .count();
774
775        assert!(machinery_count > 0);
776        assert!(it_count > 0);
777    }
778
779    #[test]
780    fn test_deterministic_generation() {
781        let mut gen1 = AssetGenerator::new(42);
782        let mut gen2 = AssetGenerator::new(42);
783
784        let asset1 = gen1.generate_asset("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
785        let asset2 = gen2.generate_asset("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
786
787        assert_eq!(asset1.asset_id, asset2.asset_id);
788        assert_eq!(asset1.description, asset2.description);
789        assert_eq!(asset1.acquisition_cost, asset2.acquisition_cost);
790    }
791
792    #[test]
793    fn test_depreciation_calculation() {
794        let mut gen = AssetGenerator::new(42);
795        let mut asset = gen.generate_asset_of_class(
796            AssetClass::ItEquipment,
797            "1000",
798            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
799        );
800
801        let initial_nbv = asset.net_book_value;
802
803        // Apply one month of depreciation
804        let depreciation =
805            asset.calculate_monthly_depreciation(NaiveDate::from_ymd_opt(2024, 2, 1).unwrap());
806        asset.apply_depreciation(depreciation);
807
808        assert!(asset.accumulated_depreciation > Decimal::ZERO);
809        assert!(asset.net_book_value < initial_nbv);
810    }
811
812    #[test]
813    fn test_german_gwg_assets() {
814        let mut gen = AssetGenerator::new(42);
815        gen.set_coa_framework(CoAFramework::GermanSkr04);
816
817        let mut gwg_count = 0;
818        for _ in 0..200 {
819            let asset = gen.generate_asset("DE01", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
820            if asset.is_gwg == Some(true) {
821                gwg_count += 1;
822                assert!(asset.acquisition_cost <= Decimal::from(800));
823                assert_eq!(
824                    asset.depreciation_method,
825                    DepreciationMethod::ImmediateExpense
826                );
827                assert_eq!(asset.useful_life_months, 1);
828                assert_eq!(asset.salvage_value, Decimal::ZERO);
829            }
830        }
831        // Should have some GWG assets (~15% of the ~15% that get low cost)
832        assert!(gwg_count > 0, "Expected at least one GWG asset");
833    }
834
835    #[test]
836    fn test_german_depreciation_methods() {
837        let mut gen = AssetGenerator::new(42);
838        gen.set_coa_framework(CoAFramework::GermanSkr04);
839
840        let mut sl_count = 0;
841        let mut degressiv_count = 0;
842        let mut immediate_count = 0;
843        for _ in 0..200 {
844            let asset = gen.generate_asset("DE01", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
845            match asset.depreciation_method {
846                DepreciationMethod::StraightLine => sl_count += 1,
847                DepreciationMethod::Degressiv => degressiv_count += 1,
848                DepreciationMethod::ImmediateExpense => immediate_count += 1,
849                other => panic!("Unexpected German depreciation method: {:?}", other),
850            }
851        }
852        // Should have a mix: mostly SL, some Degressiv, some ImmediateExpense (GWG)
853        assert!(sl_count > 0, "Expected some straight-line assets");
854        assert!(degressiv_count > 0, "Expected some Degressiv assets");
855        assert!(
856            immediate_count > 0,
857            "Expected some GWG immediate expense assets"
858        );
859        // No MACRS or DDB under German GAAP
860    }
861
862    #[test]
863    fn test_german_useful_life_afa() {
864        let mut gen = AssetGenerator::new(42);
865        gen.set_coa_framework(CoAFramework::GermanSkr04);
866
867        let vehicle = gen.generate_asset_of_class(
868            AssetClass::Vehicles,
869            "DE01",
870            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
871        );
872        // German AfA: vehicles = 72 months (6 years)
873        assert_eq!(vehicle.useful_life_months, 72);
874
875        let building = gen.generate_asset_of_class(
876            AssetClass::Buildings,
877            "DE01",
878            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
879        );
880        // German AfA: buildings = 396 months (33 years)
881        assert_eq!(building.useful_life_months, 396);
882    }
883
884    #[test]
885    fn test_asset_class_cost_ranges() {
886        let mut gen = AssetGenerator::new(42);
887
888        // Buildings should be more expensive than furniture
889        let building = gen.generate_asset_of_class(
890            AssetClass::Buildings,
891            "1000",
892            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
893        );
894        let furniture = gen.generate_asset_of_class(
895            AssetClass::Furniture,
896            "1000",
897            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
898        );
899
900        // Buildings min is 500k, furniture max is 50k
901        assert!(building.acquisition_cost >= Decimal::from(500_000));
902        assert!(furniture.acquisition_cost <= Decimal::from(50_000));
903    }
904}