koprogo_api/domain/services/
expense_calculator.rs

1use crate::domain::entities::{Expense, Unit};
2
3/// Service de domaine pour calculer la répartition des charges
4pub struct ExpenseCalculator;
5
6impl ExpenseCalculator {
7    /// Calcule le montant dû par un lot selon sa quote-part
8    pub fn calculate_unit_share(expense: &Expense, unit: &Unit) -> f64 {
9        expense.amount * (unit.quota / 1000.0)
10    }
11
12    /// Calcule le total des charges pour un ensemble de dépenses
13    pub fn calculate_total_expenses(expenses: &[Expense]) -> f64 {
14        expenses.iter().map(|e| e.amount).sum()
15    }
16
17    /// Calcule le montant total payé
18    pub fn calculate_paid_expenses(expenses: &[Expense]) -> f64 {
19        expenses
20            .iter()
21            .filter(|e| e.is_paid())
22            .map(|e| e.amount)
23            .sum()
24    }
25
26    /// Calcule le montant total impayé
27    pub fn calculate_unpaid_expenses(expenses: &[Expense]) -> f64 {
28        expenses
29            .iter()
30            .filter(|e| !e.is_paid())
31            .map(|e| e.amount)
32            .sum()
33    }
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39    use crate::domain::entities::{ExpenseCategory, UnitType};
40    use chrono::Utc;
41    use uuid::Uuid;
42
43    #[test]
44    fn test_calculate_unit_share() {
45        let org_id = Uuid::new_v4();
46        let building_id = Uuid::new_v4();
47
48        let expense = Expense::new(
49            org_id,
50            building_id,
51            ExpenseCategory::Maintenance,
52            "Test".to_string(),
53            1000.0,
54            Utc::now(),
55            None,
56            None,
57            None, // account_code
58        )
59        .unwrap();
60
61        let unit = Unit::new(
62            org_id,
63            building_id,
64            "A101".to_string(),
65            UnitType::Apartment,
66            Some(1),
67            75.0,
68            50.0, // 50/1000 = 5%
69        )
70        .unwrap();
71
72        let share = ExpenseCalculator::calculate_unit_share(&expense, &unit);
73        assert_eq!(share, 50.0); // 5% de 1000€ = 50€
74    }
75
76    #[test]
77    fn test_calculate_total_expenses() {
78        let org_id = Uuid::new_v4();
79        let building_id = Uuid::new_v4();
80
81        let expenses = vec![
82            Expense::new(
83                org_id,
84                building_id,
85                ExpenseCategory::Maintenance,
86                "Test 1".to_string(),
87                100.0,
88                Utc::now(),
89                None,
90                None,
91                None, // account_code
92            )
93            .unwrap(),
94            Expense::new(
95                org_id,
96                building_id,
97                ExpenseCategory::Repairs,
98                "Test 2".to_string(),
99                200.0,
100                Utc::now(),
101                None,
102                None,
103                None, // account_code
104            )
105            .unwrap(),
106        ];
107
108        let total = ExpenseCalculator::calculate_total_expenses(&expenses);
109        assert_eq!(total, 300.0);
110    }
111
112    #[test]
113    fn test_calculate_paid_and_unpaid() {
114        let org_id = Uuid::new_v4();
115        let building_id = Uuid::new_v4();
116        let syndic_id = Uuid::new_v4();
117
118        let mut expense1 = Expense::new(
119            org_id,
120            building_id,
121            ExpenseCategory::Maintenance,
122            "Test 1".to_string(),
123            100.0,
124            Utc::now(),
125            None,
126            None,
127            None, // account_code
128        )
129        .unwrap();
130        // Follow approval workflow before payment
131        expense1.submit_for_approval().unwrap();
132        expense1.approve(syndic_id).unwrap();
133        let _ = expense1.mark_as_paid();
134
135        let expense2 = Expense::new(
136            org_id,
137            building_id,
138            ExpenseCategory::Repairs,
139            "Test 2".to_string(),
140            200.0,
141            Utc::now(),
142            None,
143            None,
144            None, // account_code
145        )
146        .unwrap();
147
148        let expenses = vec![expense1, expense2];
149
150        let paid = ExpenseCalculator::calculate_paid_expenses(&expenses);
151        let unpaid = ExpenseCalculator::calculate_unpaid_expenses(&expenses);
152
153        assert_eq!(paid, 100.0);
154        assert_eq!(unpaid, 200.0);
155    }
156}