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