koprogo_api/domain/entities/
expense.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5/// Catégorie de charges
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub enum ExpenseCategory {
8    Maintenance,    // Entretien
9    Repairs,        // Réparations
10    Insurance,      // Assurance
11    Utilities,      // Charges courantes (eau, électricité)
12    Cleaning,       // Nettoyage
13    Administration, // Administration
14    Works,          // Travaux
15    Other,
16}
17
18/// Statut de paiement
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
20pub enum PaymentStatus {
21    Pending,
22    Paid,
23    Overdue,
24    Cancelled,
25}
26
27/// Représente une charge de copropriété
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
29pub struct Expense {
30    pub id: Uuid,
31    pub organization_id: Uuid,
32    pub building_id: Uuid,
33    pub category: ExpenseCategory,
34    pub description: String,
35    pub amount: f64, // en euros
36    pub expense_date: DateTime<Utc>,
37    pub payment_status: PaymentStatus,
38    pub supplier: Option<String>,
39    pub invoice_number: Option<String>,
40    pub created_at: DateTime<Utc>,
41    pub updated_at: DateTime<Utc>,
42}
43
44impl Expense {
45    #[allow(clippy::too_many_arguments)]
46    pub fn new(
47        organization_id: Uuid,
48        building_id: Uuid,
49        category: ExpenseCategory,
50        description: String,
51        amount: f64,
52        expense_date: DateTime<Utc>,
53        supplier: Option<String>,
54        invoice_number: Option<String>,
55    ) -> Result<Self, String> {
56        if description.is_empty() {
57            return Err("Description cannot be empty".to_string());
58        }
59        if amount <= 0.0 {
60            return Err("Amount must be greater than 0".to_string());
61        }
62
63        let now = Utc::now();
64        Ok(Self {
65            id: Uuid::new_v4(),
66            organization_id,
67            building_id,
68            category,
69            description,
70            amount,
71            expense_date,
72            payment_status: PaymentStatus::Pending,
73            supplier,
74            invoice_number,
75            created_at: now,
76            updated_at: now,
77        })
78    }
79
80    pub fn mark_as_paid(&mut self) {
81        self.payment_status = PaymentStatus::Paid;
82        self.updated_at = Utc::now();
83    }
84
85    pub fn mark_as_overdue(&mut self) {
86        self.payment_status = PaymentStatus::Overdue;
87        self.updated_at = Utc::now();
88    }
89
90    pub fn cancel(&mut self) {
91        self.payment_status = PaymentStatus::Cancelled;
92        self.updated_at = Utc::now();
93    }
94
95    pub fn is_paid(&self) -> bool {
96        self.payment_status == PaymentStatus::Paid
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_create_expense_success() {
106        let org_id = Uuid::new_v4();
107        let building_id = Uuid::new_v4();
108        let expense = Expense::new(
109            org_id,
110            building_id,
111            ExpenseCategory::Maintenance,
112            "Entretien ascenseur".to_string(),
113            500.0,
114            Utc::now(),
115            Some("ACME Elevators".to_string()),
116            Some("INV-2024-001".to_string()),
117        );
118
119        assert!(expense.is_ok());
120        let expense = expense.unwrap();
121        assert_eq!(expense.organization_id, org_id);
122        assert_eq!(expense.amount, 500.0);
123        assert_eq!(expense.payment_status, PaymentStatus::Pending);
124    }
125
126    #[test]
127    fn test_create_expense_negative_amount_fails() {
128        let org_id = Uuid::new_v4();
129        let building_id = Uuid::new_v4();
130        let expense = Expense::new(
131            org_id,
132            building_id,
133            ExpenseCategory::Maintenance,
134            "Test".to_string(),
135            -100.0,
136            Utc::now(),
137            None,
138            None,
139        );
140
141        assert!(expense.is_err());
142    }
143
144    #[test]
145    fn test_mark_expense_as_paid() {
146        let org_id = Uuid::new_v4();
147        let building_id = Uuid::new_v4();
148        let mut expense = Expense::new(
149            org_id,
150            building_id,
151            ExpenseCategory::Maintenance,
152            "Test".to_string(),
153            100.0,
154            Utc::now(),
155            None,
156            None,
157        )
158        .unwrap();
159
160        assert!(!expense.is_paid());
161        expense.mark_as_paid();
162        assert!(expense.is_paid());
163    }
164}