1use std::str::FromStr;
7
8use chrono::{Datelike, NaiveDate};
9use rust_decimal::Decimal;
10use rust_decimal_macros::dec;
11use serde::{Deserialize, Serialize};
12
13const MACRS_GDS_3_YEAR: &[&str] = &["33.33", "44.45", "14.81", "7.41"];
16const MACRS_GDS_5_YEAR: &[&str] = &["20.00", "32.00", "19.20", "11.52", "11.52", "5.76"];
17const MACRS_GDS_7_YEAR: &[&str] = &[
18 "14.29", "24.49", "17.49", "12.49", "8.93", "8.92", "8.93", "4.46",
19];
20const MACRS_GDS_10_YEAR: &[&str] = &[
21 "10.00", "18.00", "14.40", "11.52", "9.22", "7.37", "6.55", "6.55", "6.56", "6.55", "3.28",
22];
23const MACRS_GDS_15_YEAR: &[&str] = &[
24 "5.00", "9.50", "8.55", "7.70", "6.93", "6.23", "5.90", "5.90", "5.91", "5.90", "5.91", "5.90",
25 "5.91", "5.90", "5.91", "2.95",
26];
27const MACRS_GDS_20_YEAR: &[&str] = &[
28 "3.750", "7.219", "6.677", "6.177", "5.713", "5.285", "4.888", "4.522", "4.462", "4.461",
29 "4.462", "4.461", "4.462", "4.461", "4.462", "4.461", "4.462", "4.461", "4.462", "4.461",
30 "2.231",
31];
32
33fn macrs_table_for_life(useful_life_years: u32) -> Option<&'static [&'static str]> {
35 match useful_life_years {
36 1..=3 => Some(MACRS_GDS_3_YEAR),
37 4..=5 => Some(MACRS_GDS_5_YEAR),
38 6..=7 => Some(MACRS_GDS_7_YEAR),
39 8..=10 => Some(MACRS_GDS_10_YEAR),
40 11..=15 => Some(MACRS_GDS_15_YEAR),
41 16..=20 => Some(MACRS_GDS_20_YEAR),
42 _ => None,
43 }
44}
45
46fn macrs_pct(s: &str) -> Decimal {
48 Decimal::from_str(s).unwrap_or(Decimal::ZERO)
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
53#[serde(rename_all = "snake_case")]
54pub enum AssetClass {
55 Buildings,
57 BuildingImprovements,
59 Land,
61 #[default]
63 MachineryEquipment,
64 Machinery,
66 ComputerHardware,
68 ItEquipment,
70 FurnitureFixtures,
72 Furniture,
74 Vehicles,
76 LeaseholdImprovements,
78 Intangibles,
80 Software,
82 ConstructionInProgress,
84 LowValueAssets,
86}
87
88impl AssetClass {
89 pub fn default_useful_life_months(&self) -> u32 {
91 match self {
92 Self::Buildings | Self::BuildingImprovements => 480, Self::Land => 0, Self::MachineryEquipment | Self::Machinery => 120, Self::ComputerHardware | Self::ItEquipment => 36, Self::FurnitureFixtures | Self::Furniture => 84, Self::Vehicles => 60, Self::LeaseholdImprovements => 120, Self::Intangibles | Self::Software => 60, Self::ConstructionInProgress => 0, Self::LowValueAssets => 12, }
103 }
104
105 pub fn is_depreciable(&self) -> bool {
107 !matches!(self, Self::Land | Self::ConstructionInProgress)
108 }
109
110 pub fn default_depreciation_method(&self) -> DepreciationMethod {
112 match self {
113 Self::Buildings | Self::BuildingImprovements | Self::LeaseholdImprovements => {
114 DepreciationMethod::StraightLine
115 }
116 Self::MachineryEquipment | Self::Machinery => DepreciationMethod::StraightLine,
117 Self::ComputerHardware | Self::ItEquipment => {
118 DepreciationMethod::DoubleDecliningBalance
119 }
120 Self::FurnitureFixtures | Self::Furniture => DepreciationMethod::StraightLine,
121 Self::Vehicles => DepreciationMethod::DoubleDecliningBalance,
122 Self::Intangibles | Self::Software => DepreciationMethod::StraightLine,
123 Self::LowValueAssets => DepreciationMethod::ImmediateExpense,
124 Self::Land | Self::ConstructionInProgress => DepreciationMethod::None,
125 }
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
131#[serde(rename_all = "snake_case")]
132pub enum DepreciationMethod {
133 #[default]
135 StraightLine,
136 DoubleDecliningBalance,
138 SumOfYearsDigits,
140 UnitsOfProduction,
142 Macrs,
144 ImmediateExpense,
146 Degressiv,
150 None,
152}
153
154impl DepreciationMethod {
155 pub fn calculate_monthly_depreciation(
157 &self,
158 acquisition_cost: Decimal,
159 salvage_value: Decimal,
160 useful_life_months: u32,
161 months_elapsed: u32,
162 accumulated_depreciation: Decimal,
163 ) -> Decimal {
164 if useful_life_months == 0 {
165 return Decimal::ZERO;
166 }
167
168 let depreciable_base = acquisition_cost - salvage_value;
169 let net_book_value = acquisition_cost - accumulated_depreciation;
170
171 if net_book_value <= salvage_value {
173 return Decimal::ZERO;
174 }
175
176 match self {
177 Self::StraightLine => {
178 let monthly_amount = depreciable_base / Decimal::from(useful_life_months);
179 monthly_amount.min(net_book_value - salvage_value)
181 }
182
183 Self::DoubleDecliningBalance => {
184 let annual_rate = Decimal::from(2) / Decimal::from(useful_life_months) * dec!(12);
186 let monthly_rate = annual_rate / dec!(12);
187 let depreciation = net_book_value * monthly_rate;
188 depreciation.min(net_book_value - salvage_value)
190 }
191
192 Self::SumOfYearsDigits => {
193 let years_total = useful_life_months / 12;
194 let sum_of_years: u32 = (1..=years_total).sum();
195 let current_year = (months_elapsed / 12) + 1;
196 let remaining_years = years_total.saturating_sub(current_year) + 1;
197
198 if sum_of_years == 0 || remaining_years == 0 {
199 return Decimal::ZERO;
200 }
201
202 let year_fraction = Decimal::from(remaining_years) / Decimal::from(sum_of_years);
203 let annual_depreciation = depreciable_base * year_fraction;
204 let monthly_amount = annual_depreciation / dec!(12);
205 monthly_amount.min(net_book_value - salvage_value)
206 }
207
208 Self::UnitsOfProduction => {
209 let monthly_amount = depreciable_base / Decimal::from(useful_life_months);
212 monthly_amount.min(net_book_value - salvage_value)
213 }
214
215 Self::Macrs => {
216 let useful_life_years = useful_life_months / 12;
219 let current_year = (months_elapsed / 12) as usize;
220
221 if let Some(table) = macrs_table_for_life(useful_life_years) {
222 if current_year < table.len() {
223 let pct = macrs_pct(table[current_year]);
224 let annual_depreciation = acquisition_cost * pct / dec!(100);
225 let monthly_amount = annual_depreciation / dec!(12);
226 monthly_amount.min(net_book_value)
228 } else {
229 Decimal::ZERO
230 }
231 } else {
232 let annual_rate =
234 Decimal::from(2) / Decimal::from(useful_life_months) * dec!(12);
235 let monthly_rate = annual_rate / dec!(12);
236 let depreciation = net_book_value * monthly_rate;
237 depreciation.min(net_book_value - salvage_value)
238 }
239 }
240
241 Self::ImmediateExpense => {
242 if months_elapsed == 0 {
244 depreciable_base
245 } else {
246 Decimal::ZERO
247 }
248 }
249
250 Self::Degressiv => {
251 let useful_life_years = useful_life_months / 12;
254 if useful_life_years == 0 {
255 return Decimal::ZERO;
256 }
257
258 let sl_annual_rate = Decimal::ONE / Decimal::from(useful_life_years);
259 let degressiv_rate = (sl_annual_rate * Decimal::from(3)).min(dec!(0.30));
261 let degressiv_monthly = net_book_value * degressiv_rate / dec!(12);
262
263 let remaining_months = useful_life_months.saturating_sub(months_elapsed);
265 let sl_monthly = if remaining_months > 0 {
266 (net_book_value - salvage_value) / Decimal::from(remaining_months)
267 } else {
268 Decimal::ZERO
269 };
270
271 let monthly_amount = degressiv_monthly.max(sl_monthly);
273 monthly_amount
274 .min(net_book_value - salvage_value)
275 .max(Decimal::ZERO)
276 }
277
278 Self::None => Decimal::ZERO,
279 }
280 }
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct AssetAccountDetermination {
286 pub asset_account: String,
288 pub accumulated_depreciation_account: String,
290 pub depreciation_expense_account: String,
292 pub gain_on_disposal_account: String,
294 pub loss_on_disposal_account: String,
296 pub acquisition_clearing_account: String,
298 pub gain_loss_account: String,
300}
301
302impl Default for AssetAccountDetermination {
303 fn default() -> Self {
304 Self {
305 asset_account: "160000".to_string(),
306 accumulated_depreciation_account: "169000".to_string(),
307 depreciation_expense_account: "640000".to_string(),
308 gain_on_disposal_account: "810000".to_string(),
309 loss_on_disposal_account: "840000".to_string(),
310 acquisition_clearing_account: "299000".to_string(),
311 gain_loss_account: "810000".to_string(),
312 }
313 }
314}
315
316#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
318#[serde(rename_all = "snake_case")]
319pub enum AcquisitionType {
320 #[default]
322 Purchase,
323 SelfConstructed,
325 Transfer,
327 BusinessCombination,
329 FinanceLease,
331 Donation,
333}
334
335#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
337#[serde(rename_all = "snake_case")]
338pub enum AssetStatus {
339 UnderConstruction,
341 #[default]
343 Active,
344 Inactive,
346 FullyDepreciated,
348 PendingDisposal,
350 Disposed,
352}
353
354#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct FixedAsset {
357 pub asset_id: String,
359
360 pub sub_number: u16,
362
363 pub description: String,
365
366 pub asset_class: AssetClass,
368
369 pub company_code: String,
371
372 pub cost_center: Option<String>,
374
375 pub location: Option<String>,
377
378 pub acquisition_date: NaiveDate,
380
381 pub acquisition_type: AcquisitionType,
383
384 pub acquisition_cost: Decimal,
386
387 pub capitalized_date: Option<NaiveDate>,
389
390 pub depreciation_method: DepreciationMethod,
392
393 pub useful_life_months: u32,
395
396 pub salvage_value: Decimal,
398
399 pub accumulated_depreciation: Decimal,
401
402 pub net_book_value: Decimal,
404
405 pub account_determination: AssetAccountDetermination,
407
408 pub status: AssetStatus,
410
411 pub disposal_date: Option<NaiveDate>,
413
414 pub disposal_proceeds: Option<Decimal>,
416
417 pub serial_number: Option<String>,
419
420 pub manufacturer: Option<String>,
422
423 pub model: Option<String>,
425
426 pub warranty_expiration: Option<NaiveDate>,
428
429 pub insurance_policy: Option<String>,
431
432 pub purchase_order: Option<String>,
434
435 pub vendor_id: Option<String>,
437
438 pub invoice_reference: Option<String>,
440
441 #[serde(default, skip_serializing_if = "Option::is_none")]
445 pub is_gwg: Option<bool>,
446}
447
448impl FixedAsset {
449 pub fn new(
451 asset_id: impl Into<String>,
452 description: impl Into<String>,
453 asset_class: AssetClass,
454 company_code: impl Into<String>,
455 acquisition_date: NaiveDate,
456 acquisition_cost: Decimal,
457 ) -> Self {
458 let useful_life_months = asset_class.default_useful_life_months();
459 let depreciation_method = asset_class.default_depreciation_method();
460
461 Self {
462 asset_id: asset_id.into(),
463 sub_number: 0,
464 description: description.into(),
465 asset_class,
466 company_code: company_code.into(),
467 cost_center: None,
468 location: None,
469 acquisition_date,
470 acquisition_type: AcquisitionType::Purchase,
471 acquisition_cost,
472 capitalized_date: Some(acquisition_date),
473 depreciation_method,
474 useful_life_months,
475 salvage_value: Decimal::ZERO,
476 accumulated_depreciation: Decimal::ZERO,
477 net_book_value: acquisition_cost,
478 account_determination: AssetAccountDetermination::default(),
479 status: AssetStatus::Active,
480 disposal_date: None,
481 disposal_proceeds: None,
482 serial_number: None,
483 manufacturer: None,
484 model: None,
485 warranty_expiration: None,
486 insurance_policy: None,
487 purchase_order: None,
488 vendor_id: None,
489 invoice_reference: None,
490 is_gwg: None,
491 }
492 }
493
494 pub fn with_cost_center(mut self, cost_center: impl Into<String>) -> Self {
496 self.cost_center = Some(cost_center.into());
497 self
498 }
499
500 pub fn with_location(mut self, location: impl Into<String>) -> Self {
502 self.location = Some(location.into());
503 self
504 }
505
506 pub fn with_salvage_value(mut self, salvage_value: Decimal) -> Self {
508 self.salvage_value = salvage_value;
509 self
510 }
511
512 pub fn with_depreciation_method(mut self, method: DepreciationMethod) -> Self {
514 self.depreciation_method = method;
515 self
516 }
517
518 pub fn with_useful_life_months(mut self, months: u32) -> Self {
520 self.useful_life_months = months;
521 self
522 }
523
524 pub fn with_vendor(mut self, vendor_id: impl Into<String>) -> Self {
526 self.vendor_id = Some(vendor_id.into());
527 self
528 }
529
530 pub fn months_since_capitalization(&self, as_of_date: NaiveDate) -> u32 {
532 let cap_date = self.capitalized_date.unwrap_or(self.acquisition_date);
533 if as_of_date < cap_date {
534 return 0;
535 }
536
537 let years = as_of_date.year() - cap_date.year();
538 let months = as_of_date.month() as i32 - cap_date.month() as i32;
539 ((years * 12) + months).max(0) as u32
540 }
541
542 pub fn calculate_monthly_depreciation(&self, as_of_date: NaiveDate) -> Decimal {
544 if !self.asset_class.is_depreciable() {
545 return Decimal::ZERO;
546 }
547
548 if self.status == AssetStatus::Disposed {
549 return Decimal::ZERO;
550 }
551
552 let months_elapsed = self.months_since_capitalization(as_of_date);
553
554 self.depreciation_method.calculate_monthly_depreciation(
555 self.acquisition_cost,
556 self.salvage_value,
557 self.useful_life_months,
558 months_elapsed,
559 self.accumulated_depreciation,
560 )
561 }
562
563 pub fn apply_depreciation(&mut self, depreciation_amount: Decimal) {
565 self.accumulated_depreciation += depreciation_amount;
566 self.net_book_value = self.acquisition_cost - self.accumulated_depreciation;
567
568 if self.net_book_value <= self.salvage_value && self.status == AssetStatus::Active {
570 self.status = AssetStatus::FullyDepreciated;
571 }
572 }
573
574 pub fn calculate_disposal_gain_loss(&self, proceeds: Decimal) -> Decimal {
576 proceeds - self.net_book_value
577 }
578
579 pub fn dispose(&mut self, disposal_date: NaiveDate, proceeds: Decimal) {
581 self.disposal_date = Some(disposal_date);
582 self.disposal_proceeds = Some(proceeds);
583 self.status = AssetStatus::Disposed;
584 }
585
586 pub fn is_fully_depreciated(&self) -> bool {
588 self.net_book_value <= self.salvage_value
589 }
590
591 pub fn remaining_useful_life_months(&self, as_of_date: NaiveDate) -> u32 {
593 let months_elapsed = self.months_since_capitalization(as_of_date);
594 self.useful_life_months.saturating_sub(months_elapsed)
595 }
596
597 pub fn annual_depreciation_rate(&self) -> Decimal {
599 if self.useful_life_months == 0 {
600 return Decimal::ZERO;
601 }
602
603 match self.depreciation_method {
604 DepreciationMethod::StraightLine => {
605 Decimal::from(12) / Decimal::from(self.useful_life_months) * dec!(100)
606 }
607 DepreciationMethod::DoubleDecliningBalance => {
608 Decimal::from(24) / Decimal::from(self.useful_life_months) * dec!(100)
609 }
610 _ => Decimal::from(12) / Decimal::from(self.useful_life_months) * dec!(100),
611 }
612 }
613
614 pub fn macrs_depreciation(&self, year: u32) -> Decimal {
620 if year == 0 {
621 return Decimal::ZERO;
622 }
623
624 let useful_life_years = self.useful_life_months / 12;
625 let table_index = (year - 1) as usize;
626
627 match macrs_table_for_life(useful_life_years) {
628 Some(table) if table_index < table.len() => {
629 let pct = macrs_pct(table[table_index]);
630 self.acquisition_cost * pct / dec!(100)
631 }
632 _ => Decimal::ZERO,
633 }
634 }
635
636 pub fn ddb_depreciation(&self) -> Decimal {
642 if self.useful_life_months == 0 {
643 return Decimal::ZERO;
644 }
645
646 let net_book_value = self.acquisition_cost - self.accumulated_depreciation;
647 if net_book_value <= self.salvage_value {
648 return Decimal::ZERO;
649 }
650
651 let annual_rate = Decimal::from(2) / Decimal::from(self.useful_life_months) * dec!(12);
652 let monthly_rate = annual_rate / dec!(12);
653 let depreciation = (net_book_value * monthly_rate).round_dp(2);
654 depreciation.min(net_book_value - self.salvage_value)
655 }
656}
657
658#[derive(Debug, Clone, Default, Serialize, Deserialize)]
660pub struct FixedAssetPool {
661 pub assets: Vec<FixedAsset>,
663 #[serde(skip)]
665 class_index: std::collections::HashMap<AssetClass, Vec<usize>>,
666 #[serde(skip)]
668 company_index: std::collections::HashMap<String, Vec<usize>>,
669}
670
671impl FixedAssetPool {
672 pub fn new() -> Self {
674 Self::default()
675 }
676
677 pub fn add_asset(&mut self, asset: FixedAsset) {
679 let idx = self.assets.len();
680 let asset_class = asset.asset_class;
681 let company_code = asset.company_code.clone();
682
683 self.assets.push(asset);
684
685 self.class_index.entry(asset_class).or_default().push(idx);
686 self.company_index
687 .entry(company_code)
688 .or_default()
689 .push(idx);
690 }
691
692 pub fn get_depreciable_assets(&self) -> Vec<&FixedAsset> {
694 self.assets
695 .iter()
696 .filter(|a| {
697 a.asset_class.is_depreciable()
698 && a.status == AssetStatus::Active
699 && !a.is_fully_depreciated()
700 })
701 .collect()
702 }
703
704 pub fn get_depreciable_assets_mut(&mut self) -> Vec<&mut FixedAsset> {
706 self.assets
707 .iter_mut()
708 .filter(|a| {
709 a.asset_class.is_depreciable()
710 && a.status == AssetStatus::Active
711 && !a.is_fully_depreciated()
712 })
713 .collect()
714 }
715
716 pub fn get_by_company(&self, company_code: &str) -> Vec<&FixedAsset> {
718 self.company_index
719 .get(company_code)
720 .map(|indices| indices.iter().map(|&i| &self.assets[i]).collect())
721 .unwrap_or_default()
722 }
723
724 pub fn get_by_class(&self, asset_class: AssetClass) -> Vec<&FixedAsset> {
726 self.class_index
727 .get(&asset_class)
728 .map(|indices| indices.iter().map(|&i| &self.assets[i]).collect())
729 .unwrap_or_default()
730 }
731
732 pub fn get_by_id(&self, asset_id: &str) -> Option<&FixedAsset> {
734 self.assets.iter().find(|a| a.asset_id == asset_id)
735 }
736
737 pub fn get_by_id_mut(&mut self, asset_id: &str) -> Option<&mut FixedAsset> {
739 self.assets.iter_mut().find(|a| a.asset_id == asset_id)
740 }
741
742 pub fn calculate_period_depreciation(&self, as_of_date: NaiveDate) -> Decimal {
744 self.get_depreciable_assets()
745 .iter()
746 .map(|a| a.calculate_monthly_depreciation(as_of_date))
747 .sum()
748 }
749
750 pub fn total_net_book_value(&self) -> Decimal {
752 self.assets
753 .iter()
754 .filter(|a| a.status != AssetStatus::Disposed)
755 .map(|a| a.net_book_value)
756 .sum()
757 }
758
759 pub fn len(&self) -> usize {
761 self.assets.len()
762 }
763
764 pub fn is_empty(&self) -> bool {
766 self.assets.is_empty()
767 }
768
769 pub fn rebuild_indices(&mut self) {
771 self.class_index.clear();
772 self.company_index.clear();
773
774 for (idx, asset) in self.assets.iter().enumerate() {
775 self.class_index
776 .entry(asset.asset_class)
777 .or_default()
778 .push(idx);
779 self.company_index
780 .entry(asset.company_code.clone())
781 .or_default()
782 .push(idx);
783 }
784 }
785}
786
787#[cfg(test)]
788#[allow(clippy::unwrap_used)]
789mod tests {
790 use super::*;
791
792 fn test_date(year: i32, month: u32, day: u32) -> NaiveDate {
793 NaiveDate::from_ymd_opt(year, month, day).unwrap()
794 }
795
796 #[test]
797 fn test_asset_creation() {
798 let asset = FixedAsset::new(
799 "FA-001",
800 "Office Computer",
801 AssetClass::ComputerHardware,
802 "1000",
803 test_date(2024, 1, 1),
804 Decimal::from(2000),
805 );
806
807 assert_eq!(asset.asset_id, "FA-001");
808 assert_eq!(asset.acquisition_cost, Decimal::from(2000));
809 assert_eq!(asset.useful_life_months, 36); }
811
812 #[test]
813 fn test_straight_line_depreciation() {
814 let asset = FixedAsset::new(
815 "FA-001",
816 "Office Equipment",
817 AssetClass::FurnitureFixtures,
818 "1000",
819 test_date(2024, 1, 1),
820 Decimal::from(8400),
821 )
822 .with_useful_life_months(84) .with_depreciation_method(DepreciationMethod::StraightLine);
824
825 let monthly_dep = asset.calculate_monthly_depreciation(test_date(2024, 2, 1));
826 assert_eq!(monthly_dep, Decimal::from(100)); }
828
829 #[test]
830 fn test_salvage_value_limit() {
831 let mut asset = FixedAsset::new(
832 "FA-001",
833 "Test Asset",
834 AssetClass::MachineryEquipment,
835 "1000",
836 test_date(2024, 1, 1),
837 Decimal::from(1200),
838 )
839 .with_useful_life_months(12)
840 .with_salvage_value(Decimal::from(200));
841
842 for _ in 0..11 {
844 let dep = Decimal::from(83);
845 asset.apply_depreciation(dep);
846 }
847
848 let final_dep = asset.calculate_monthly_depreciation(test_date(2024, 12, 1));
851
852 asset.apply_depreciation(final_dep);
854 assert!(asset.net_book_value >= asset.salvage_value);
855 }
856
857 #[test]
858 fn test_disposal() {
859 let mut asset = FixedAsset::new(
860 "FA-001",
861 "Old Equipment",
862 AssetClass::MachineryEquipment,
863 "1000",
864 test_date(2020, 1, 1),
865 Decimal::from(10000),
866 );
867
868 asset.apply_depreciation(Decimal::from(5000));
870
871 let gain_loss = asset.calculate_disposal_gain_loss(Decimal::from(6000));
873 assert_eq!(gain_loss, Decimal::from(1000)); asset.dispose(test_date(2024, 1, 1), Decimal::from(6000));
877 assert_eq!(asset.status, AssetStatus::Disposed);
878 }
879
880 #[test]
881 fn test_land_not_depreciable() {
882 let asset = FixedAsset::new(
883 "FA-001",
884 "Land Parcel",
885 AssetClass::Land,
886 "1000",
887 test_date(2024, 1, 1),
888 Decimal::from(500000),
889 );
890
891 let dep = asset.calculate_monthly_depreciation(test_date(2024, 6, 1));
892 assert_eq!(dep, Decimal::ZERO);
893 }
894
895 #[test]
896 fn test_asset_pool() {
897 let mut pool = FixedAssetPool::new();
898
899 pool.add_asset(FixedAsset::new(
900 "FA-001",
901 "Computer 1",
902 AssetClass::ComputerHardware,
903 "1000",
904 test_date(2024, 1, 1),
905 Decimal::from(2000),
906 ));
907
908 pool.add_asset(FixedAsset::new(
909 "FA-002",
910 "Desk",
911 AssetClass::FurnitureFixtures,
912 "1000",
913 test_date(2024, 1, 1),
914 Decimal::from(500),
915 ));
916
917 assert_eq!(pool.len(), 2);
918 assert_eq!(pool.get_by_class(AssetClass::ComputerHardware).len(), 1);
919 assert_eq!(pool.get_by_company("1000").len(), 2);
920 }
921
922 #[test]
923 fn test_months_since_capitalization() {
924 let asset = FixedAsset::new(
925 "FA-001",
926 "Test",
927 AssetClass::MachineryEquipment,
928 "1000",
929 test_date(2024, 3, 15),
930 Decimal::from(10000),
931 );
932
933 assert_eq!(asset.months_since_capitalization(test_date(2024, 3, 1)), 0);
934 assert_eq!(asset.months_since_capitalization(test_date(2024, 6, 1)), 3);
935 assert_eq!(asset.months_since_capitalization(test_date(2025, 3, 1)), 12);
936 }
937
938 #[test]
941 fn test_macrs_tables_sum_to_100() {
942 let tables: &[(&str, &[&str])] = &[
943 ("3-year", MACRS_GDS_3_YEAR),
944 ("5-year", MACRS_GDS_5_YEAR),
945 ("7-year", MACRS_GDS_7_YEAR),
946 ("10-year", MACRS_GDS_10_YEAR),
947 ("15-year", MACRS_GDS_15_YEAR),
948 ("20-year", MACRS_GDS_20_YEAR),
949 ];
950
951 let tolerance = dec!(0.02);
952 let hundred = dec!(100);
953
954 for (label, table) in tables {
955 let sum: Decimal = table.iter().map(|s| macrs_pct(s)).sum();
956 let diff = (sum - hundred).abs();
957 assert!(
958 diff < tolerance,
959 "MACRS GDS {label} table sums to {sum}, expected ~100.0"
960 );
961 }
962 }
963
964 #[test]
965 fn test_macrs_table_for_life_mapping() {
966 assert_eq!(macrs_table_for_life(1).unwrap().len(), 4);
968 assert_eq!(macrs_table_for_life(3).unwrap().len(), 4);
969
970 assert_eq!(macrs_table_for_life(4).unwrap().len(), 6);
972 assert_eq!(macrs_table_for_life(5).unwrap().len(), 6);
973
974 assert_eq!(macrs_table_for_life(6).unwrap().len(), 8);
976 assert_eq!(macrs_table_for_life(7).unwrap().len(), 8);
977
978 assert_eq!(macrs_table_for_life(8).unwrap().len(), 11);
980 assert_eq!(macrs_table_for_life(10).unwrap().len(), 11);
981
982 assert_eq!(macrs_table_for_life(11).unwrap().len(), 16);
984 assert_eq!(macrs_table_for_life(15).unwrap().len(), 16);
985
986 assert_eq!(macrs_table_for_life(16).unwrap().len(), 21);
988 assert_eq!(macrs_table_for_life(20).unwrap().len(), 21);
989
990 assert!(macrs_table_for_life(0).is_none());
992 assert!(macrs_table_for_life(21).is_none());
993 assert!(macrs_table_for_life(100).is_none());
994 }
995
996 #[test]
997 fn test_macrs_depreciation_5_year_asset() {
998 let asset = FixedAsset::new(
999 "FA-MACRS",
1000 "Vehicle",
1001 AssetClass::Vehicles,
1002 "1000",
1003 test_date(2024, 1, 1),
1004 Decimal::from(10000),
1005 )
1006 .with_useful_life_months(60) .with_depreciation_method(DepreciationMethod::Macrs);
1008
1009 assert_eq!(asset.macrs_depreciation(1), Decimal::from(2000));
1011 assert_eq!(asset.macrs_depreciation(2), Decimal::from(3200));
1013 assert_eq!(asset.macrs_depreciation(3), Decimal::from(1920));
1015 assert_eq!(asset.macrs_depreciation(4), Decimal::from(1152));
1017 assert_eq!(asset.macrs_depreciation(5), Decimal::from(1152));
1019 assert_eq!(asset.macrs_depreciation(6), Decimal::from(576));
1021 assert_eq!(asset.macrs_depreciation(7), Decimal::ZERO);
1023 assert_eq!(asset.macrs_depreciation(0), Decimal::ZERO);
1025 }
1026
1027 #[test]
1028 fn test_macrs_calculate_monthly_depreciation_uses_tables() {
1029 let asset = FixedAsset::new(
1030 "FA-MACRS-M",
1031 "Vehicle",
1032 AssetClass::Vehicles,
1033 "1000",
1034 test_date(2024, 1, 1),
1035 Decimal::from(12000),
1036 )
1037 .with_useful_life_months(60) .with_depreciation_method(DepreciationMethod::Macrs);
1039
1040 let monthly_year1 = asset.calculate_monthly_depreciation(test_date(2024, 2, 1));
1042 assert_eq!(monthly_year1, Decimal::from(200));
1043
1044 let monthly_year2 = asset.calculate_monthly_depreciation(test_date(2025, 2, 1));
1046 assert_eq!(monthly_year2, Decimal::from(320));
1047 }
1048
1049 #[test]
1050 fn test_ddb_depreciation() {
1051 let asset = FixedAsset::new(
1052 "FA-DDB",
1053 "Server",
1054 AssetClass::ComputerHardware,
1055 "1000",
1056 test_date(2024, 1, 1),
1057 Decimal::from(3600),
1058 )
1059 .with_useful_life_months(36) .with_depreciation_method(DepreciationMethod::DoubleDecliningBalance);
1061
1062 let monthly = asset.ddb_depreciation();
1066 assert_eq!(monthly, Decimal::from(200));
1067 }
1068
1069 #[test]
1070 fn test_ddb_depreciation_with_accumulated() {
1071 let mut asset = FixedAsset::new(
1072 "FA-DDB2",
1073 "Laptop",
1074 AssetClass::ComputerHardware,
1075 "1000",
1076 test_date(2024, 1, 1),
1077 Decimal::from(1800),
1078 )
1079 .with_useful_life_months(36);
1080
1081 asset.apply_depreciation(Decimal::from(900));
1083
1084 let monthly = asset.ddb_depreciation();
1086 assert_eq!(monthly, Decimal::from(50));
1087 }
1088
1089 #[test]
1090 fn test_ddb_depreciation_respects_salvage() {
1091 let mut asset = FixedAsset::new(
1092 "FA-DDB3",
1093 "Printer",
1094 AssetClass::ComputerHardware,
1095 "1000",
1096 test_date(2024, 1, 1),
1097 Decimal::from(1800),
1098 )
1099 .with_useful_life_months(36)
1100 .with_salvage_value(Decimal::from(200));
1101
1102 asset.apply_depreciation(Decimal::from(1590));
1105
1106 let monthly = asset.ddb_depreciation();
1108 assert_eq!(monthly, Decimal::from(10));
1109 }
1110
1111 #[test]
1114 fn test_degressiv_depreciation_initial() {
1115 let asset = FixedAsset::new(
1119 "FA-DEG",
1120 "Maschine",
1121 AssetClass::MachineryEquipment,
1122 "DE01",
1123 test_date(2024, 1, 1),
1124 Decimal::from(120000),
1125 )
1126 .with_useful_life_months(120)
1127 .with_depreciation_method(DepreciationMethod::Degressiv);
1128
1129 let monthly = asset.calculate_monthly_depreciation(test_date(2024, 2, 1));
1130 assert_eq!(monthly, Decimal::from(3000));
1131 }
1132
1133 #[test]
1134 fn test_degressiv_rate_capped_at_30_percent() {
1135 let asset = FixedAsset::new(
1140 "FA-DEG2",
1141 "Fahrzeug",
1142 AssetClass::Vehicles,
1143 "DE01",
1144 test_date(2024, 1, 1),
1145 Decimal::from(6000),
1146 )
1147 .with_useful_life_months(60)
1148 .with_depreciation_method(DepreciationMethod::Degressiv);
1149
1150 let monthly = asset.calculate_monthly_depreciation(test_date(2024, 2, 1));
1151 assert_eq!(monthly, Decimal::from(150));
1152
1153 let short_asset = FixedAsset::new(
1156 "FA-DEG2S",
1157 "Server",
1158 AssetClass::ComputerHardware,
1159 "DE01",
1160 test_date(2024, 1, 1),
1161 Decimal::from(3600),
1162 )
1163 .with_useful_life_months(36)
1164 .with_depreciation_method(DepreciationMethod::Degressiv);
1165
1166 let monthly_short = short_asset.calculate_monthly_depreciation(test_date(2024, 2, 1));
1168 assert!(
1169 monthly_short > Decimal::from(100),
1170 "SL should win for 3-year asset"
1171 );
1172 }
1173
1174 #[test]
1175 fn test_degressiv_switches_to_straight_line() {
1176 let mut asset = FixedAsset::new(
1181 "FA-DEG3",
1182 "Fahrzeug",
1183 AssetClass::Vehicles,
1184 "DE01",
1185 test_date(2024, 1, 1),
1186 Decimal::from(10000),
1187 )
1188 .with_useful_life_months(120)
1189 .with_depreciation_method(DepreciationMethod::Degressiv);
1190
1191 asset.apply_depreciation(Decimal::from(9000));
1193 let dep = asset.calculate_monthly_depreciation(test_date(2032, 1, 1));
1198 assert!(
1203 dep > Decimal::from(25),
1204 "Should switch to SL when it exceeds Degressiv"
1205 );
1206 assert!(dep < Decimal::from(42), "SL should be ~41.67");
1207 }
1208
1209 #[test]
1210 fn test_gwg_field_default() {
1211 let asset = FixedAsset::new(
1212 "FA-GWG",
1213 "Keyboard",
1214 AssetClass::ComputerHardware,
1215 "DE01",
1216 test_date(2024, 1, 1),
1217 Decimal::from(200),
1218 );
1219 assert_eq!(asset.is_gwg, None, "is_gwg should default to None");
1220 }
1221}