Skip to main content

datasynth_core/models/
fixed_asset.rs

1//! Fixed asset master data model.
2//!
3//! Provides fixed asset master data including depreciation schedules
4//! for realistic fixed asset accounting simulation.
5
6use chrono::{Datelike, NaiveDate};
7use rust_decimal::Decimal;
8use rust_decimal_macros::dec;
9use serde::{Deserialize, Serialize};
10
11/// Asset class for categorization and default depreciation rules.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
13#[serde(rename_all = "snake_case")]
14pub enum AssetClass {
15    /// Buildings and structures
16    Buildings,
17    /// Building improvements
18    BuildingImprovements,
19    /// Land (typically non-depreciable)
20    Land,
21    /// Machinery and equipment
22    #[default]
23    MachineryEquipment,
24    /// Machinery (alias for MachineryEquipment)
25    Machinery,
26    /// Computer hardware
27    ComputerHardware,
28    /// IT Equipment (alias for ComputerHardware)
29    ItEquipment,
30    /// Office furniture and fixtures
31    FurnitureFixtures,
32    /// Furniture (alias for FurnitureFixtures)
33    Furniture,
34    /// Vehicles
35    Vehicles,
36    /// Leasehold improvements
37    LeaseholdImprovements,
38    /// Intangible assets (software, patents)
39    Intangibles,
40    /// Software
41    Software,
42    /// Construction in progress (not yet depreciating)
43    ConstructionInProgress,
44    /// Low-value assets
45    LowValueAssets,
46}
47
48impl AssetClass {
49    /// Get default useful life in months for this asset class.
50    pub fn default_useful_life_months(&self) -> u32 {
51        match self {
52            Self::Buildings | Self::BuildingImprovements => 480, // 40 years
53            Self::Land => 0,                                     // Not depreciated
54            Self::MachineryEquipment | Self::Machinery => 120,   // 10 years
55            Self::ComputerHardware | Self::ItEquipment => 36,    // 3 years
56            Self::FurnitureFixtures | Self::Furniture => 84,     // 7 years
57            Self::Vehicles => 60,                                // 5 years
58            Self::LeaseholdImprovements => 120,                  // 10 years (or lease term)
59            Self::Intangibles | Self::Software => 60,            // 5 years
60            Self::ConstructionInProgress => 0,                   // Not depreciated until complete
61            Self::LowValueAssets => 12,                          // 1 year
62        }
63    }
64
65    /// Check if this asset class is depreciable.
66    pub fn is_depreciable(&self) -> bool {
67        !matches!(self, Self::Land | Self::ConstructionInProgress)
68    }
69
70    /// Get default depreciation method for this asset class.
71    pub fn default_depreciation_method(&self) -> DepreciationMethod {
72        match self {
73            Self::Buildings | Self::BuildingImprovements | Self::LeaseholdImprovements => {
74                DepreciationMethod::StraightLine
75            }
76            Self::MachineryEquipment | Self::Machinery => DepreciationMethod::StraightLine,
77            Self::ComputerHardware | Self::ItEquipment => {
78                DepreciationMethod::DoubleDecliningBalance
79            }
80            Self::FurnitureFixtures | Self::Furniture => DepreciationMethod::StraightLine,
81            Self::Vehicles => DepreciationMethod::DoubleDecliningBalance,
82            Self::Intangibles | Self::Software => DepreciationMethod::StraightLine,
83            Self::LowValueAssets => DepreciationMethod::ImmediateExpense,
84            Self::Land | Self::ConstructionInProgress => DepreciationMethod::None,
85        }
86    }
87}
88
89/// Depreciation calculation method.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
91#[serde(rename_all = "snake_case")]
92pub enum DepreciationMethod {
93    /// Straight-line depreciation
94    #[default]
95    StraightLine,
96    /// Double declining balance
97    DoubleDecliningBalance,
98    /// Sum of years' digits
99    SumOfYearsDigits,
100    /// Units of production
101    UnitsOfProduction,
102    /// MACRS (Modified Accelerated Cost Recovery System)
103    Macrs,
104    /// Immediate expense (low-value assets)
105    ImmediateExpense,
106    /// No depreciation (land, CIP)
107    None,
108}
109
110impl DepreciationMethod {
111    /// Calculate monthly depreciation amount.
112    pub fn calculate_monthly_depreciation(
113        &self,
114        acquisition_cost: Decimal,
115        salvage_value: Decimal,
116        useful_life_months: u32,
117        months_elapsed: u32,
118        accumulated_depreciation: Decimal,
119    ) -> Decimal {
120        if useful_life_months == 0 {
121            return Decimal::ZERO;
122        }
123
124        let depreciable_base = acquisition_cost - salvage_value;
125        let net_book_value = acquisition_cost - accumulated_depreciation;
126
127        // Don't depreciate below salvage value
128        if net_book_value <= salvage_value {
129            return Decimal::ZERO;
130        }
131
132        match self {
133            Self::StraightLine => {
134                let monthly_amount = depreciable_base / Decimal::from(useful_life_months);
135                // Cap at remaining book value above salvage
136                monthly_amount.min(net_book_value - salvage_value)
137            }
138
139            Self::DoubleDecliningBalance => {
140                // Double the straight-line rate applied to NBV
141                let annual_rate = Decimal::from(2) / Decimal::from(useful_life_months) * dec!(12);
142                let monthly_rate = annual_rate / dec!(12);
143                let depreciation = net_book_value * monthly_rate;
144                // Cap at remaining book value above salvage
145                depreciation.min(net_book_value - salvage_value)
146            }
147
148            Self::SumOfYearsDigits => {
149                let years_total = useful_life_months / 12;
150                let sum_of_years: u32 = (1..=years_total).sum();
151                let current_year = (months_elapsed / 12) + 1;
152                let remaining_years = years_total.saturating_sub(current_year) + 1;
153
154                if sum_of_years == 0 || remaining_years == 0 {
155                    return Decimal::ZERO;
156                }
157
158                let year_fraction = Decimal::from(remaining_years) / Decimal::from(sum_of_years);
159                let annual_depreciation = depreciable_base * year_fraction;
160                let monthly_amount = annual_depreciation / dec!(12);
161                monthly_amount.min(net_book_value - salvage_value)
162            }
163
164            Self::UnitsOfProduction => {
165                // For units of production, caller should use specific production-based calculation
166                // This is a fallback that uses straight-line
167                let monthly_amount = depreciable_base / Decimal::from(useful_life_months);
168                monthly_amount.min(net_book_value - salvage_value)
169            }
170
171            Self::Macrs => {
172                // Simplified MACRS using half-year convention
173                // Full implementation would need MACRS tables
174                let annual_rate = Decimal::from(2) / Decimal::from(useful_life_months) * dec!(12);
175                let monthly_rate = annual_rate / dec!(12);
176                let depreciation = net_book_value * monthly_rate;
177                depreciation.min(net_book_value - salvage_value)
178            }
179
180            Self::ImmediateExpense => {
181                // Full expense in first month
182                if months_elapsed == 0 {
183                    depreciable_base
184                } else {
185                    Decimal::ZERO
186                }
187            }
188
189            Self::None => Decimal::ZERO,
190        }
191    }
192}
193
194/// Account determination rules for fixed asset transactions.
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct AssetAccountDetermination {
197    /// Asset balance sheet account
198    pub asset_account: String,
199    /// Accumulated depreciation account
200    pub accumulated_depreciation_account: String,
201    /// Depreciation expense account
202    pub depreciation_expense_account: String,
203    /// Gain on disposal account
204    pub gain_on_disposal_account: String,
205    /// Loss on disposal account
206    pub loss_on_disposal_account: String,
207    /// Clearing account for acquisitions
208    pub acquisition_clearing_account: String,
209    /// Gain/loss account (combined, for backward compatibility).
210    pub gain_loss_account: String,
211}
212
213impl Default for AssetAccountDetermination {
214    fn default() -> Self {
215        Self {
216            asset_account: "160000".to_string(),
217            accumulated_depreciation_account: "169000".to_string(),
218            depreciation_expense_account: "640000".to_string(),
219            gain_on_disposal_account: "810000".to_string(),
220            loss_on_disposal_account: "840000".to_string(),
221            acquisition_clearing_account: "299000".to_string(),
222            gain_loss_account: "810000".to_string(),
223        }
224    }
225}
226
227/// Asset acquisition type.
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
229#[serde(rename_all = "snake_case")]
230pub enum AcquisitionType {
231    /// External purchase
232    #[default]
233    Purchase,
234    /// Self-constructed
235    SelfConstructed,
236    /// Transfer from another entity
237    Transfer,
238    /// Acquired in business combination
239    BusinessCombination,
240    /// Leased asset (finance lease)
241    FinanceLease,
242    /// Donation received
243    Donation,
244}
245
246/// Status of a fixed asset.
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
248#[serde(rename_all = "snake_case")]
249pub enum AssetStatus {
250    /// Under construction (CIP)
251    UnderConstruction,
252    /// Active and in use
253    #[default]
254    Active,
255    /// Temporarily not in use
256    Inactive,
257    /// Fully depreciated but still in use
258    FullyDepreciated,
259    /// Scheduled for disposal
260    PendingDisposal,
261    /// Disposed/retired
262    Disposed,
263}
264
265/// Fixed asset master data.
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct FixedAsset {
268    /// Asset ID (e.g., "FA-001234")
269    pub asset_id: String,
270
271    /// Asset sub-number (for component accounting)
272    pub sub_number: u16,
273
274    /// Asset description
275    pub description: String,
276
277    /// Asset class
278    pub asset_class: AssetClass,
279
280    /// Company code
281    pub company_code: String,
282
283    /// Cost center responsible for the asset
284    pub cost_center: Option<String>,
285
286    /// Location/plant
287    pub location: Option<String>,
288
289    /// Acquisition date
290    pub acquisition_date: NaiveDate,
291
292    /// Acquisition type
293    pub acquisition_type: AcquisitionType,
294
295    /// Original acquisition cost
296    pub acquisition_cost: Decimal,
297
298    /// Capitalized date (when depreciation starts)
299    pub capitalized_date: Option<NaiveDate>,
300
301    /// Depreciation method
302    pub depreciation_method: DepreciationMethod,
303
304    /// Useful life in months
305    pub useful_life_months: u32,
306
307    /// Salvage/residual value
308    pub salvage_value: Decimal,
309
310    /// Accumulated depreciation as of current period
311    pub accumulated_depreciation: Decimal,
312
313    /// Net book value (acquisition_cost - accumulated_depreciation)
314    pub net_book_value: Decimal,
315
316    /// Account determination rules
317    pub account_determination: AssetAccountDetermination,
318
319    /// Current status
320    pub status: AssetStatus,
321
322    /// Disposal date (if disposed)
323    pub disposal_date: Option<NaiveDate>,
324
325    /// Disposal proceeds (if disposed)
326    pub disposal_proceeds: Option<Decimal>,
327
328    /// Serial number (for tracking)
329    pub serial_number: Option<String>,
330
331    /// Manufacturer
332    pub manufacturer: Option<String>,
333
334    /// Model
335    pub model: Option<String>,
336
337    /// Warranty expiration date
338    pub warranty_expiration: Option<NaiveDate>,
339
340    /// Insurance policy number
341    pub insurance_policy: Option<String>,
342
343    /// Original PO number
344    pub purchase_order: Option<String>,
345
346    /// Vendor ID (who supplied the asset)
347    pub vendor_id: Option<String>,
348
349    /// Invoice reference
350    pub invoice_reference: Option<String>,
351}
352
353impl FixedAsset {
354    /// Create a new fixed asset.
355    pub fn new(
356        asset_id: impl Into<String>,
357        description: impl Into<String>,
358        asset_class: AssetClass,
359        company_code: impl Into<String>,
360        acquisition_date: NaiveDate,
361        acquisition_cost: Decimal,
362    ) -> Self {
363        let useful_life_months = asset_class.default_useful_life_months();
364        let depreciation_method = asset_class.default_depreciation_method();
365
366        Self {
367            asset_id: asset_id.into(),
368            sub_number: 0,
369            description: description.into(),
370            asset_class,
371            company_code: company_code.into(),
372            cost_center: None,
373            location: None,
374            acquisition_date,
375            acquisition_type: AcquisitionType::Purchase,
376            acquisition_cost,
377            capitalized_date: Some(acquisition_date),
378            depreciation_method,
379            useful_life_months,
380            salvage_value: Decimal::ZERO,
381            accumulated_depreciation: Decimal::ZERO,
382            net_book_value: acquisition_cost,
383            account_determination: AssetAccountDetermination::default(),
384            status: AssetStatus::Active,
385            disposal_date: None,
386            disposal_proceeds: None,
387            serial_number: None,
388            manufacturer: None,
389            model: None,
390            warranty_expiration: None,
391            insurance_policy: None,
392            purchase_order: None,
393            vendor_id: None,
394            invoice_reference: None,
395        }
396    }
397
398    /// Set cost center.
399    pub fn with_cost_center(mut self, cost_center: impl Into<String>) -> Self {
400        self.cost_center = Some(cost_center.into());
401        self
402    }
403
404    /// Set location.
405    pub fn with_location(mut self, location: impl Into<String>) -> Self {
406        self.location = Some(location.into());
407        self
408    }
409
410    /// Set salvage value.
411    pub fn with_salvage_value(mut self, salvage_value: Decimal) -> Self {
412        self.salvage_value = salvage_value;
413        self
414    }
415
416    /// Set depreciation method.
417    pub fn with_depreciation_method(mut self, method: DepreciationMethod) -> Self {
418        self.depreciation_method = method;
419        self
420    }
421
422    /// Set useful life.
423    pub fn with_useful_life_months(mut self, months: u32) -> Self {
424        self.useful_life_months = months;
425        self
426    }
427
428    /// Set vendor ID.
429    pub fn with_vendor(mut self, vendor_id: impl Into<String>) -> Self {
430        self.vendor_id = Some(vendor_id.into());
431        self
432    }
433
434    /// Calculate months since capitalization.
435    pub fn months_since_capitalization(&self, as_of_date: NaiveDate) -> u32 {
436        let cap_date = self.capitalized_date.unwrap_or(self.acquisition_date);
437        if as_of_date < cap_date {
438            return 0;
439        }
440
441        let years = as_of_date.year() - cap_date.year();
442        let months = as_of_date.month() as i32 - cap_date.month() as i32;
443        ((years * 12) + months).max(0) as u32
444    }
445
446    /// Calculate depreciation for a specific month.
447    pub fn calculate_monthly_depreciation(&self, as_of_date: NaiveDate) -> Decimal {
448        if !self.asset_class.is_depreciable() {
449            return Decimal::ZERO;
450        }
451
452        if self.status == AssetStatus::Disposed {
453            return Decimal::ZERO;
454        }
455
456        let months_elapsed = self.months_since_capitalization(as_of_date);
457
458        self.depreciation_method.calculate_monthly_depreciation(
459            self.acquisition_cost,
460            self.salvage_value,
461            self.useful_life_months,
462            months_elapsed,
463            self.accumulated_depreciation,
464        )
465    }
466
467    /// Apply depreciation and update balances.
468    pub fn apply_depreciation(&mut self, depreciation_amount: Decimal) {
469        self.accumulated_depreciation += depreciation_amount;
470        self.net_book_value = self.acquisition_cost - self.accumulated_depreciation;
471
472        // Update status if fully depreciated
473        if self.net_book_value <= self.salvage_value && self.status == AssetStatus::Active {
474            self.status = AssetStatus::FullyDepreciated;
475        }
476    }
477
478    /// Calculate gain/loss on disposal.
479    pub fn calculate_disposal_gain_loss(&self, proceeds: Decimal) -> Decimal {
480        proceeds - self.net_book_value
481    }
482
483    /// Record disposal.
484    pub fn dispose(&mut self, disposal_date: NaiveDate, proceeds: Decimal) {
485        self.disposal_date = Some(disposal_date);
486        self.disposal_proceeds = Some(proceeds);
487        self.status = AssetStatus::Disposed;
488    }
489
490    /// Check if asset is fully depreciated.
491    pub fn is_fully_depreciated(&self) -> bool {
492        self.net_book_value <= self.salvage_value
493    }
494
495    /// Calculate remaining useful life in months.
496    pub fn remaining_useful_life_months(&self, as_of_date: NaiveDate) -> u32 {
497        let months_elapsed = self.months_since_capitalization(as_of_date);
498        self.useful_life_months.saturating_sub(months_elapsed)
499    }
500
501    /// Calculate depreciation rate (annual percentage).
502    pub fn annual_depreciation_rate(&self) -> Decimal {
503        if self.useful_life_months == 0 {
504            return Decimal::ZERO;
505        }
506
507        match self.depreciation_method {
508            DepreciationMethod::StraightLine => {
509                Decimal::from(12) / Decimal::from(self.useful_life_months) * dec!(100)
510            }
511            DepreciationMethod::DoubleDecliningBalance => {
512                Decimal::from(24) / Decimal::from(self.useful_life_months) * dec!(100)
513            }
514            _ => Decimal::from(12) / Decimal::from(self.useful_life_months) * dec!(100),
515        }
516    }
517}
518
519/// Pool of fixed assets for transaction generation.
520#[derive(Debug, Clone, Default, Serialize, Deserialize)]
521pub struct FixedAssetPool {
522    /// All fixed assets
523    pub assets: Vec<FixedAsset>,
524    /// Index by asset class
525    #[serde(skip)]
526    class_index: std::collections::HashMap<AssetClass, Vec<usize>>,
527    /// Index by company code
528    #[serde(skip)]
529    company_index: std::collections::HashMap<String, Vec<usize>>,
530}
531
532impl FixedAssetPool {
533    /// Create a new empty asset pool.
534    pub fn new() -> Self {
535        Self::default()
536    }
537
538    /// Add an asset to the pool.
539    pub fn add_asset(&mut self, asset: FixedAsset) {
540        let idx = self.assets.len();
541        let asset_class = asset.asset_class;
542        let company_code = asset.company_code.clone();
543
544        self.assets.push(asset);
545
546        self.class_index.entry(asset_class).or_default().push(idx);
547        self.company_index
548            .entry(company_code)
549            .or_default()
550            .push(idx);
551    }
552
553    /// Get all assets requiring depreciation for a given month.
554    pub fn get_depreciable_assets(&self) -> Vec<&FixedAsset> {
555        self.assets
556            .iter()
557            .filter(|a| {
558                a.asset_class.is_depreciable()
559                    && a.status == AssetStatus::Active
560                    && !a.is_fully_depreciated()
561            })
562            .collect()
563    }
564
565    /// Get mutable references to depreciable assets.
566    pub fn get_depreciable_assets_mut(&mut self) -> Vec<&mut FixedAsset> {
567        self.assets
568            .iter_mut()
569            .filter(|a| {
570                a.asset_class.is_depreciable()
571                    && a.status == AssetStatus::Active
572                    && !a.is_fully_depreciated()
573            })
574            .collect()
575    }
576
577    /// Get assets by company code.
578    pub fn get_by_company(&self, company_code: &str) -> Vec<&FixedAsset> {
579        self.company_index
580            .get(company_code)
581            .map(|indices| indices.iter().map(|&i| &self.assets[i]).collect())
582            .unwrap_or_default()
583    }
584
585    /// Get assets by class.
586    pub fn get_by_class(&self, asset_class: AssetClass) -> Vec<&FixedAsset> {
587        self.class_index
588            .get(&asset_class)
589            .map(|indices| indices.iter().map(|&i| &self.assets[i]).collect())
590            .unwrap_or_default()
591    }
592
593    /// Get asset by ID.
594    pub fn get_by_id(&self, asset_id: &str) -> Option<&FixedAsset> {
595        self.assets.iter().find(|a| a.asset_id == asset_id)
596    }
597
598    /// Get mutable asset by ID.
599    pub fn get_by_id_mut(&mut self, asset_id: &str) -> Option<&mut FixedAsset> {
600        self.assets.iter_mut().find(|a| a.asset_id == asset_id)
601    }
602
603    /// Calculate total depreciation for all assets in a period.
604    pub fn calculate_period_depreciation(&self, as_of_date: NaiveDate) -> Decimal {
605        self.get_depreciable_assets()
606            .iter()
607            .map(|a| a.calculate_monthly_depreciation(as_of_date))
608            .sum()
609    }
610
611    /// Get total net book value.
612    pub fn total_net_book_value(&self) -> Decimal {
613        self.assets
614            .iter()
615            .filter(|a| a.status != AssetStatus::Disposed)
616            .map(|a| a.net_book_value)
617            .sum()
618    }
619
620    /// Get count of assets.
621    pub fn len(&self) -> usize {
622        self.assets.len()
623    }
624
625    /// Check if pool is empty.
626    pub fn is_empty(&self) -> bool {
627        self.assets.is_empty()
628    }
629
630    /// Rebuild indices after deserialization.
631    pub fn rebuild_indices(&mut self) {
632        self.class_index.clear();
633        self.company_index.clear();
634
635        for (idx, asset) in self.assets.iter().enumerate() {
636            self.class_index
637                .entry(asset.asset_class)
638                .or_default()
639                .push(idx);
640            self.company_index
641                .entry(asset.company_code.clone())
642                .or_default()
643                .push(idx);
644        }
645    }
646}
647
648#[cfg(test)]
649mod tests {
650    use super::*;
651
652    fn test_date(year: i32, month: u32, day: u32) -> NaiveDate {
653        NaiveDate::from_ymd_opt(year, month, day).unwrap()
654    }
655
656    #[test]
657    fn test_asset_creation() {
658        let asset = FixedAsset::new(
659            "FA-001",
660            "Office Computer",
661            AssetClass::ComputerHardware,
662            "1000",
663            test_date(2024, 1, 1),
664            Decimal::from(2000),
665        );
666
667        assert_eq!(asset.asset_id, "FA-001");
668        assert_eq!(asset.acquisition_cost, Decimal::from(2000));
669        assert_eq!(asset.useful_life_months, 36); // 3 years for computers
670    }
671
672    #[test]
673    fn test_straight_line_depreciation() {
674        let asset = FixedAsset::new(
675            "FA-001",
676            "Office Equipment",
677            AssetClass::FurnitureFixtures,
678            "1000",
679            test_date(2024, 1, 1),
680            Decimal::from(8400),
681        )
682        .with_useful_life_months(84) // 7 years
683        .with_depreciation_method(DepreciationMethod::StraightLine);
684
685        let monthly_dep = asset.calculate_monthly_depreciation(test_date(2024, 2, 1));
686        assert_eq!(monthly_dep, Decimal::from(100)); // 8400 / 84 months
687    }
688
689    #[test]
690    fn test_salvage_value_limit() {
691        let mut asset = FixedAsset::new(
692            "FA-001",
693            "Test Asset",
694            AssetClass::MachineryEquipment,
695            "1000",
696            test_date(2024, 1, 1),
697            Decimal::from(1200),
698        )
699        .with_useful_life_months(12)
700        .with_salvage_value(Decimal::from(200));
701
702        // Apply 11 months of depreciation (1000/12 ~= 83.33 each)
703        for _ in 0..11 {
704            let dep = Decimal::from(83);
705            asset.apply_depreciation(dep);
706        }
707
708        // At this point, NBV should be around 287, which is above salvage (200)
709        // Next depreciation should be limited to not go below salvage
710        let final_dep = asset.calculate_monthly_depreciation(test_date(2024, 12, 1));
711
712        // Verify we don't depreciate below salvage
713        asset.apply_depreciation(final_dep);
714        assert!(asset.net_book_value >= asset.salvage_value);
715    }
716
717    #[test]
718    fn test_disposal() {
719        let mut asset = FixedAsset::new(
720            "FA-001",
721            "Old Equipment",
722            AssetClass::MachineryEquipment,
723            "1000",
724            test_date(2020, 1, 1),
725            Decimal::from(10000),
726        );
727
728        // Simulate some depreciation
729        asset.apply_depreciation(Decimal::from(5000));
730
731        // Calculate gain/loss
732        let gain_loss = asset.calculate_disposal_gain_loss(Decimal::from(6000));
733        assert_eq!(gain_loss, Decimal::from(1000)); // Gain of 1000
734
735        // Record disposal
736        asset.dispose(test_date(2024, 1, 1), Decimal::from(6000));
737        assert_eq!(asset.status, AssetStatus::Disposed);
738    }
739
740    #[test]
741    fn test_land_not_depreciable() {
742        let asset = FixedAsset::new(
743            "FA-001",
744            "Land Parcel",
745            AssetClass::Land,
746            "1000",
747            test_date(2024, 1, 1),
748            Decimal::from(500000),
749        );
750
751        let dep = asset.calculate_monthly_depreciation(test_date(2024, 6, 1));
752        assert_eq!(dep, Decimal::ZERO);
753    }
754
755    #[test]
756    fn test_asset_pool() {
757        let mut pool = FixedAssetPool::new();
758
759        pool.add_asset(FixedAsset::new(
760            "FA-001",
761            "Computer 1",
762            AssetClass::ComputerHardware,
763            "1000",
764            test_date(2024, 1, 1),
765            Decimal::from(2000),
766        ));
767
768        pool.add_asset(FixedAsset::new(
769            "FA-002",
770            "Desk",
771            AssetClass::FurnitureFixtures,
772            "1000",
773            test_date(2024, 1, 1),
774            Decimal::from(500),
775        ));
776
777        assert_eq!(pool.len(), 2);
778        assert_eq!(pool.get_by_class(AssetClass::ComputerHardware).len(), 1);
779        assert_eq!(pool.get_by_company("1000").len(), 2);
780    }
781
782    #[test]
783    fn test_months_since_capitalization() {
784        let asset = FixedAsset::new(
785            "FA-001",
786            "Test",
787            AssetClass::MachineryEquipment,
788            "1000",
789            test_date(2024, 3, 15),
790            Decimal::from(10000),
791        );
792
793        assert_eq!(asset.months_since_capitalization(test_date(2024, 3, 1)), 0);
794        assert_eq!(asset.months_since_capitalization(test_date(2024, 6, 1)), 3);
795        assert_eq!(asset.months_since_capitalization(test_date(2025, 3, 1)), 12);
796    }
797}