Skip to main content

koprogo_api/domain/services/
expense_calculator.rs

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