koprogo_api/domain/entities/
expense.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub enum ExpenseCategory {
8 Maintenance, Repairs, Insurance, Utilities, Cleaning, Administration, Works, Other,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
20pub enum PaymentStatus {
21 Pending,
22 Paid,
23 Overdue,
24 Cancelled,
25}
26
27#[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, 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}