koprogo_api/domain/entities/
call_for_funds.rs1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use super::ContributionType;
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14#[serde(rename_all = "lowercase")]
15pub enum CallForFundsStatus {
16 Draft,
18 Sent,
20 Partial,
22 Completed,
24 Cancelled,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
33pub struct CallForFunds {
34 pub id: Uuid,
35 pub organization_id: Uuid,
36 pub building_id: Uuid,
37
38 pub title: String,
40 pub description: String,
41
42 pub total_amount: f64, pub contribution_type: ContributionType,
47
48 pub call_date: DateTime<Utc>, pub due_date: DateTime<Utc>, pub sent_date: Option<DateTime<Utc>>, pub status: CallForFundsStatus,
55
56 pub account_code: Option<String>, pub notes: Option<String>,
61 pub created_at: DateTime<Utc>,
62 pub updated_at: DateTime<Utc>,
63 pub created_by: Option<Uuid>,
64}
65
66impl CallForFunds {
67 #[allow(clippy::too_many_arguments)]
68 pub fn new(
69 organization_id: Uuid,
70 building_id: Uuid,
71 title: String,
72 description: String,
73 total_amount: f64,
74 contribution_type: ContributionType,
75 call_date: DateTime<Utc>,
76 due_date: DateTime<Utc>,
77 account_code: Option<String>,
78 ) -> Result<Self, String> {
79 if total_amount <= 0.0 {
81 return Err("Total amount must be positive".to_string());
82 }
83
84 if title.trim().is_empty() {
86 return Err("Title cannot be empty".to_string());
87 }
88
89 if description.trim().is_empty() {
91 return Err("Description cannot be empty".to_string());
92 }
93
94 if due_date <= call_date {
96 return Err("Due date must be after call date".to_string());
97 }
98
99 Ok(Self {
100 id: Uuid::new_v4(),
101 organization_id,
102 building_id,
103 title,
104 description,
105 total_amount,
106 contribution_type,
107 call_date,
108 due_date,
109 sent_date: None,
110 status: CallForFundsStatus::Draft,
111 account_code,
112 notes: None,
113 created_at: Utc::now(),
114 updated_at: Utc::now(),
115 created_by: None,
116 })
117 }
118
119 pub fn mark_as_sent(&mut self) {
121 self.sent_date = Some(Utc::now());
122 self.status = CallForFundsStatus::Sent;
123 self.updated_at = Utc::now();
124 }
125
126 pub fn mark_as_completed(&mut self) {
128 self.status = CallForFundsStatus::Completed;
129 self.updated_at = Utc::now();
130 }
131
132 pub fn cancel(&mut self) {
134 self.status = CallForFundsStatus::Cancelled;
135 self.updated_at = Utc::now();
136 }
137
138 pub fn is_overdue(&self) -> bool {
140 self.status != CallForFundsStatus::Completed
141 && self.status != CallForFundsStatus::Cancelled
142 && Utc::now() > self.due_date
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use crate::domain::entities::ContributionType;
150
151 #[test]
152 fn test_create_call_for_funds_success() {
153 let call_date = Utc::now();
154 let due_date = call_date + chrono::Duration::days(30);
155
156 let call = CallForFunds::new(
157 Uuid::new_v4(),
158 Uuid::new_v4(),
159 "Appel de fonds Q1 2025".to_string(),
160 "Charges courantes trimestrielles".to_string(),
161 5000.0,
162 ContributionType::Regular,
163 call_date,
164 due_date,
165 Some("7000".to_string()),
166 );
167
168 assert!(call.is_ok());
169 let call = call.unwrap();
170 assert_eq!(call.total_amount, 5000.0);
171 assert_eq!(call.status, CallForFundsStatus::Draft);
172 }
173
174 #[test]
175 fn test_create_call_negative_amount() {
176 let call_date = Utc::now();
177 let due_date = call_date + chrono::Duration::days(30);
178
179 let call = CallForFunds::new(
180 Uuid::new_v4(),
181 Uuid::new_v4(),
182 "Test".to_string(),
183 "Test".to_string(),
184 -100.0,
185 ContributionType::Regular,
186 call_date,
187 due_date,
188 None,
189 );
190
191 assert!(call.is_err());
192 assert!(call.unwrap_err().contains("must be positive"));
193 }
194
195 #[test]
196 fn test_create_call_invalid_dates() {
197 let call_date = Utc::now();
198 let due_date = call_date - chrono::Duration::days(1); let call = CallForFunds::new(
201 Uuid::new_v4(),
202 Uuid::new_v4(),
203 "Test".to_string(),
204 "Test".to_string(),
205 100.0,
206 ContributionType::Regular,
207 call_date,
208 due_date,
209 None,
210 );
211
212 assert!(call.is_err());
213 assert!(call.unwrap_err().contains("Due date must be after"));
214 }
215
216 #[test]
217 fn test_mark_as_sent() {
218 let call_date = Utc::now();
219 let due_date = call_date + chrono::Duration::days(30);
220
221 let mut call = CallForFunds::new(
222 Uuid::new_v4(),
223 Uuid::new_v4(),
224 "Test".to_string(),
225 "Test".to_string(),
226 100.0,
227 ContributionType::Regular,
228 call_date,
229 due_date,
230 None,
231 )
232 .unwrap();
233
234 assert_eq!(call.status, CallForFundsStatus::Draft);
235 assert!(call.sent_date.is_none());
236
237 call.mark_as_sent();
238
239 assert_eq!(call.status, CallForFundsStatus::Sent);
240 assert!(call.sent_date.is_some());
241 }
242
243 #[test]
244 fn test_is_overdue() {
245 let call_date = Utc::now() - chrono::Duration::days(60);
246 let due_date = Utc::now() - chrono::Duration::days(30); let call = CallForFunds::new(
249 Uuid::new_v4(),
250 Uuid::new_v4(),
251 "Overdue call".to_string(),
252 "Test".to_string(),
253 100.0,
254 ContributionType::Regular,
255 call_date,
256 due_date,
257 None,
258 )
259 .unwrap();
260
261 assert!(call.is_overdue());
262 }
263}