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)]
788mod tests {
789 use super::*;
790
791 fn test_date(year: i32, month: u32, day: u32) -> NaiveDate {
792 NaiveDate::from_ymd_opt(year, month, day).unwrap()
793 }
794
795 #[test]
796 fn test_asset_creation() {
797 let asset = FixedAsset::new(
798 "FA-001",
799 "Office Computer",
800 AssetClass::ComputerHardware,
801 "1000",
802 test_date(2024, 1, 1),
803 Decimal::from(2000),
804 );
805
806 assert_eq!(asset.asset_id, "FA-001");
807 assert_eq!(asset.acquisition_cost, Decimal::from(2000));
808 assert_eq!(asset.useful_life_months, 36); }
810
811 #[test]
812 fn test_straight_line_depreciation() {
813 let asset = FixedAsset::new(
814 "FA-001",
815 "Office Equipment",
816 AssetClass::FurnitureFixtures,
817 "1000",
818 test_date(2024, 1, 1),
819 Decimal::from(8400),
820 )
821 .with_useful_life_months(84) .with_depreciation_method(DepreciationMethod::StraightLine);
823
824 let monthly_dep = asset.calculate_monthly_depreciation(test_date(2024, 2, 1));
825 assert_eq!(monthly_dep, Decimal::from(100)); }
827
828 #[test]
829 fn test_salvage_value_limit() {
830 let mut asset = FixedAsset::new(
831 "FA-001",
832 "Test Asset",
833 AssetClass::MachineryEquipment,
834 "1000",
835 test_date(2024, 1, 1),
836 Decimal::from(1200),
837 )
838 .with_useful_life_months(12)
839 .with_salvage_value(Decimal::from(200));
840
841 for _ in 0..11 {
843 let dep = Decimal::from(83);
844 asset.apply_depreciation(dep);
845 }
846
847 let final_dep = asset.calculate_monthly_depreciation(test_date(2024, 12, 1));
850
851 asset.apply_depreciation(final_dep);
853 assert!(asset.net_book_value >= asset.salvage_value);
854 }
855
856 #[test]
857 fn test_disposal() {
858 let mut asset = FixedAsset::new(
859 "FA-001",
860 "Old Equipment",
861 AssetClass::MachineryEquipment,
862 "1000",
863 test_date(2020, 1, 1),
864 Decimal::from(10000),
865 );
866
867 asset.apply_depreciation(Decimal::from(5000));
869
870 let gain_loss = asset.calculate_disposal_gain_loss(Decimal::from(6000));
872 assert_eq!(gain_loss, Decimal::from(1000)); asset.dispose(test_date(2024, 1, 1), Decimal::from(6000));
876 assert_eq!(asset.status, AssetStatus::Disposed);
877 }
878
879 #[test]
880 fn test_land_not_depreciable() {
881 let asset = FixedAsset::new(
882 "FA-001",
883 "Land Parcel",
884 AssetClass::Land,
885 "1000",
886 test_date(2024, 1, 1),
887 Decimal::from(500000),
888 );
889
890 let dep = asset.calculate_monthly_depreciation(test_date(2024, 6, 1));
891 assert_eq!(dep, Decimal::ZERO);
892 }
893
894 #[test]
895 fn test_asset_pool() {
896 let mut pool = FixedAssetPool::new();
897
898 pool.add_asset(FixedAsset::new(
899 "FA-001",
900 "Computer 1",
901 AssetClass::ComputerHardware,
902 "1000",
903 test_date(2024, 1, 1),
904 Decimal::from(2000),
905 ));
906
907 pool.add_asset(FixedAsset::new(
908 "FA-002",
909 "Desk",
910 AssetClass::FurnitureFixtures,
911 "1000",
912 test_date(2024, 1, 1),
913 Decimal::from(500),
914 ));
915
916 assert_eq!(pool.len(), 2);
917 assert_eq!(pool.get_by_class(AssetClass::ComputerHardware).len(), 1);
918 assert_eq!(pool.get_by_company("1000").len(), 2);
919 }
920
921 #[test]
922 fn test_months_since_capitalization() {
923 let asset = FixedAsset::new(
924 "FA-001",
925 "Test",
926 AssetClass::MachineryEquipment,
927 "1000",
928 test_date(2024, 3, 15),
929 Decimal::from(10000),
930 );
931
932 assert_eq!(asset.months_since_capitalization(test_date(2024, 3, 1)), 0);
933 assert_eq!(asset.months_since_capitalization(test_date(2024, 6, 1)), 3);
934 assert_eq!(asset.months_since_capitalization(test_date(2025, 3, 1)), 12);
935 }
936
937 #[test]
940 fn test_macrs_tables_sum_to_100() {
941 let tables: &[(&str, &[&str])] = &[
942 ("3-year", MACRS_GDS_3_YEAR),
943 ("5-year", MACRS_GDS_5_YEAR),
944 ("7-year", MACRS_GDS_7_YEAR),
945 ("10-year", MACRS_GDS_10_YEAR),
946 ("15-year", MACRS_GDS_15_YEAR),
947 ("20-year", MACRS_GDS_20_YEAR),
948 ];
949
950 let tolerance = dec!(0.02);
951 let hundred = dec!(100);
952
953 for (label, table) in tables {
954 let sum: Decimal = table.iter().map(|s| macrs_pct(s)).sum();
955 let diff = (sum - hundred).abs();
956 assert!(
957 diff < tolerance,
958 "MACRS GDS {label} table sums to {sum}, expected ~100.0"
959 );
960 }
961 }
962
963 #[test]
964 fn test_macrs_table_for_life_mapping() {
965 assert_eq!(macrs_table_for_life(1).unwrap().len(), 4);
967 assert_eq!(macrs_table_for_life(3).unwrap().len(), 4);
968
969 assert_eq!(macrs_table_for_life(4).unwrap().len(), 6);
971 assert_eq!(macrs_table_for_life(5).unwrap().len(), 6);
972
973 assert_eq!(macrs_table_for_life(6).unwrap().len(), 8);
975 assert_eq!(macrs_table_for_life(7).unwrap().len(), 8);
976
977 assert_eq!(macrs_table_for_life(8).unwrap().len(), 11);
979 assert_eq!(macrs_table_for_life(10).unwrap().len(), 11);
980
981 assert_eq!(macrs_table_for_life(11).unwrap().len(), 16);
983 assert_eq!(macrs_table_for_life(15).unwrap().len(), 16);
984
985 assert_eq!(macrs_table_for_life(16).unwrap().len(), 21);
987 assert_eq!(macrs_table_for_life(20).unwrap().len(), 21);
988
989 assert!(macrs_table_for_life(0).is_none());
991 assert!(macrs_table_for_life(21).is_none());
992 assert!(macrs_table_for_life(100).is_none());
993 }
994
995 #[test]
996 fn test_macrs_depreciation_5_year_asset() {
997 let asset = FixedAsset::new(
998 "FA-MACRS",
999 "Vehicle",
1000 AssetClass::Vehicles,
1001 "1000",
1002 test_date(2024, 1, 1),
1003 Decimal::from(10000),
1004 )
1005 .with_useful_life_months(60) .with_depreciation_method(DepreciationMethod::Macrs);
1007
1008 assert_eq!(asset.macrs_depreciation(1), Decimal::from(2000));
1010 assert_eq!(asset.macrs_depreciation(2), Decimal::from(3200));
1012 assert_eq!(asset.macrs_depreciation(3), Decimal::from(1920));
1014 assert_eq!(asset.macrs_depreciation(4), Decimal::from(1152));
1016 assert_eq!(asset.macrs_depreciation(5), Decimal::from(1152));
1018 assert_eq!(asset.macrs_depreciation(6), Decimal::from(576));
1020 assert_eq!(asset.macrs_depreciation(7), Decimal::ZERO);
1022 assert_eq!(asset.macrs_depreciation(0), Decimal::ZERO);
1024 }
1025
1026 #[test]
1027 fn test_macrs_calculate_monthly_depreciation_uses_tables() {
1028 let asset = FixedAsset::new(
1029 "FA-MACRS-M",
1030 "Vehicle",
1031 AssetClass::Vehicles,
1032 "1000",
1033 test_date(2024, 1, 1),
1034 Decimal::from(12000),
1035 )
1036 .with_useful_life_months(60) .with_depreciation_method(DepreciationMethod::Macrs);
1038
1039 let monthly_year1 = asset.calculate_monthly_depreciation(test_date(2024, 2, 1));
1041 assert_eq!(monthly_year1, Decimal::from(200));
1042
1043 let monthly_year2 = asset.calculate_monthly_depreciation(test_date(2025, 2, 1));
1045 assert_eq!(monthly_year2, Decimal::from(320));
1046 }
1047
1048 #[test]
1049 fn test_ddb_depreciation() {
1050 let asset = FixedAsset::new(
1051 "FA-DDB",
1052 "Server",
1053 AssetClass::ComputerHardware,
1054 "1000",
1055 test_date(2024, 1, 1),
1056 Decimal::from(3600),
1057 )
1058 .with_useful_life_months(36) .with_depreciation_method(DepreciationMethod::DoubleDecliningBalance);
1060
1061 let monthly = asset.ddb_depreciation();
1065 assert_eq!(monthly, Decimal::from(200));
1066 }
1067
1068 #[test]
1069 fn test_ddb_depreciation_with_accumulated() {
1070 let mut asset = FixedAsset::new(
1071 "FA-DDB2",
1072 "Laptop",
1073 AssetClass::ComputerHardware,
1074 "1000",
1075 test_date(2024, 1, 1),
1076 Decimal::from(1800),
1077 )
1078 .with_useful_life_months(36);
1079
1080 asset.apply_depreciation(Decimal::from(900));
1082
1083 let monthly = asset.ddb_depreciation();
1085 assert_eq!(monthly, Decimal::from(50));
1086 }
1087
1088 #[test]
1089 fn test_ddb_depreciation_respects_salvage() {
1090 let mut asset = FixedAsset::new(
1091 "FA-DDB3",
1092 "Printer",
1093 AssetClass::ComputerHardware,
1094 "1000",
1095 test_date(2024, 1, 1),
1096 Decimal::from(1800),
1097 )
1098 .with_useful_life_months(36)
1099 .with_salvage_value(Decimal::from(200));
1100
1101 asset.apply_depreciation(Decimal::from(1590));
1104
1105 let monthly = asset.ddb_depreciation();
1107 assert_eq!(monthly, Decimal::from(10));
1108 }
1109
1110 #[test]
1113 fn test_degressiv_depreciation_initial() {
1114 let asset = FixedAsset::new(
1118 "FA-DEG",
1119 "Maschine",
1120 AssetClass::MachineryEquipment,
1121 "DE01",
1122 test_date(2024, 1, 1),
1123 Decimal::from(120000),
1124 )
1125 .with_useful_life_months(120)
1126 .with_depreciation_method(DepreciationMethod::Degressiv);
1127
1128 let monthly = asset.calculate_monthly_depreciation(test_date(2024, 2, 1));
1129 assert_eq!(monthly, Decimal::from(3000));
1130 }
1131
1132 #[test]
1133 fn test_degressiv_rate_capped_at_30_percent() {
1134 let asset = FixedAsset::new(
1139 "FA-DEG2",
1140 "Fahrzeug",
1141 AssetClass::Vehicles,
1142 "DE01",
1143 test_date(2024, 1, 1),
1144 Decimal::from(6000),
1145 )
1146 .with_useful_life_months(60)
1147 .with_depreciation_method(DepreciationMethod::Degressiv);
1148
1149 let monthly = asset.calculate_monthly_depreciation(test_date(2024, 2, 1));
1150 assert_eq!(monthly, Decimal::from(150));
1151
1152 let short_asset = FixedAsset::new(
1155 "FA-DEG2S",
1156 "Server",
1157 AssetClass::ComputerHardware,
1158 "DE01",
1159 test_date(2024, 1, 1),
1160 Decimal::from(3600),
1161 )
1162 .with_useful_life_months(36)
1163 .with_depreciation_method(DepreciationMethod::Degressiv);
1164
1165 let monthly_short = short_asset.calculate_monthly_depreciation(test_date(2024, 2, 1));
1167 assert!(
1168 monthly_short > Decimal::from(100),
1169 "SL should win for 3-year asset"
1170 );
1171 }
1172
1173 #[test]
1174 fn test_degressiv_switches_to_straight_line() {
1175 let mut asset = FixedAsset::new(
1180 "FA-DEG3",
1181 "Fahrzeug",
1182 AssetClass::Vehicles,
1183 "DE01",
1184 test_date(2024, 1, 1),
1185 Decimal::from(10000),
1186 )
1187 .with_useful_life_months(120)
1188 .with_depreciation_method(DepreciationMethod::Degressiv);
1189
1190 asset.apply_depreciation(Decimal::from(9000));
1192 let dep = asset.calculate_monthly_depreciation(test_date(2032, 1, 1));
1197 assert!(
1202 dep > Decimal::from(25),
1203 "Should switch to SL when it exceeds Degressiv"
1204 );
1205 assert!(dep < Decimal::from(42), "SL should be ~41.67");
1206 }
1207
1208 #[test]
1209 fn test_gwg_field_default() {
1210 let asset = FixedAsset::new(
1211 "FA-GWG",
1212 "Keyboard",
1213 AssetClass::ComputerHardware,
1214 "DE01",
1215 test_date(2024, 1, 1),
1216 Decimal::from(200),
1217 );
1218 assert_eq!(asset.is_gwg, None, "is_gwg should default to None");
1219 }
1220}