koprogo_api/domain/entities/
building.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub struct Building {
8 pub id: Uuid,
9 pub organization_id: Uuid,
10 pub name: String,
11 pub address: String,
12 pub city: String,
13 pub postal_code: String,
14 pub country: String,
15 pub total_units: i32,
16 pub total_tantiemes: i32,
17 pub construction_year: Option<i32>,
18
19 pub syndic_name: Option<String>,
21 pub syndic_email: Option<String>,
22 pub syndic_phone: Option<String>,
23 pub syndic_address: Option<String>,
24 pub syndic_office_hours: Option<String>,
25 pub syndic_emergency_contact: Option<String>,
26 pub slug: Option<String>,
27
28 pub created_at: DateTime<Utc>,
29 pub updated_at: DateTime<Utc>,
30}
31
32impl Building {
33 #[allow(clippy::too_many_arguments)]
34 pub fn new(
35 organization_id: Uuid,
36 name: String,
37 address: String,
38 city: String,
39 postal_code: String,
40 country: String,
41 total_units: i32,
42 total_tantiemes: i32,
43 construction_year: Option<i32>,
44 ) -> Result<Self, String> {
45 if name.is_empty() {
46 return Err("Building name cannot be empty".to_string());
47 }
48 if total_units <= 0 {
49 return Err("Total units must be greater than 0".to_string());
50 }
51 if total_tantiemes <= 0 {
52 return Err("Total tantiemes must be greater than 0".to_string());
53 }
54
55 let now = Utc::now();
56 let slug = Self::generate_slug(&name, &address, &city);
57
58 Ok(Self {
59 id: Uuid::new_v4(),
60 organization_id,
61 name,
62 address,
63 city,
64 postal_code,
65 country,
66 total_units,
67 total_tantiemes,
68 construction_year,
69 syndic_name: None,
70 syndic_email: None,
71 syndic_phone: None,
72 syndic_address: None,
73 syndic_office_hours: None,
74 syndic_emergency_contact: None,
75 slug: Some(slug),
76 created_at: now,
77 updated_at: now,
78 })
79 }
80
81 #[allow(clippy::too_many_arguments)]
82 pub fn update_info(
83 &mut self,
84 name: String,
85 address: String,
86 city: String,
87 postal_code: String,
88 country: String,
89 total_units: i32,
90 total_tantiemes: i32,
91 construction_year: Option<i32>,
92 ) {
93 self.name = name.clone();
94 self.address = address.clone();
95 self.city = city.clone();
96 self.postal_code = postal_code;
97 self.country = country;
98 self.total_units = total_units;
99 self.total_tantiemes = total_tantiemes;
100 self.construction_year = construction_year;
101
102 self.slug = Some(Self::generate_slug(&name, &address, &city));
104
105 self.updated_at = Utc::now();
106 }
107
108 #[allow(clippy::too_many_arguments)]
110 pub fn update_syndic_info(
111 &mut self,
112 syndic_name: Option<String>,
113 syndic_email: Option<String>,
114 syndic_phone: Option<String>,
115 syndic_address: Option<String>,
116 syndic_office_hours: Option<String>,
117 syndic_emergency_contact: Option<String>,
118 ) {
119 self.syndic_name = syndic_name;
120 self.syndic_email = syndic_email;
121 self.syndic_phone = syndic_phone;
122 self.syndic_address = syndic_address;
123 self.syndic_office_hours = syndic_office_hours;
124 self.syndic_emergency_contact = syndic_emergency_contact;
125 self.updated_at = Utc::now();
126 }
127
128 fn generate_slug(name: &str, _address: &str, city: &str) -> String {
131 let combined = format!("{} {}", name, city);
132
133 combined
134 .chars()
135 .map(|c| {
136 match c {
138 'À' | 'Á' | 'Â' | 'Ã' | 'Ä' | 'à' | 'á' | 'â' | 'ã' | 'ä' => 'a',
139 'È' | 'É' | 'Ê' | 'Ë' | 'è' | 'é' | 'ê' | 'ë' => 'e',
140 'Ì' | 'Í' | 'Î' | 'Ï' | 'ì' | 'í' | 'î' | 'ï' => 'i',
141 'Ò' | 'Ó' | 'Ô' | 'Õ' | 'Ö' | 'ò' | 'ó' | 'ô' | 'õ' | 'ö' => 'o',
142 'Ù' | 'Ú' | 'Û' | 'Ü' | 'ù' | 'ú' | 'û' | 'ü' => 'u',
143 'Ç' | 'ç' => 'c',
144 'Ñ' | 'ñ' => 'n',
145 _ if c.is_alphanumeric() => c.to_ascii_lowercase(),
146 _ if c.is_whitespace() || c == '-' => '-',
147 _ => '-',
148 }
149 })
150 .collect::<String>()
151 .split('-')
152 .filter(|s| !s.is_empty())
153 .collect::<Vec<&str>>()
154 .join("-")
155 }
156
157 pub fn has_public_syndic_info(&self) -> bool {
159 self.syndic_name.is_some() || self.syndic_email.is_some() || self.syndic_phone.is_some()
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn test_create_building_success() {
169 let org_id = Uuid::new_v4();
170 let building = Building::new(
171 org_id,
172 "Résidence Les Jardins".to_string(),
173 "123 Rue de la Paix".to_string(),
174 "Paris".to_string(),
175 "75001".to_string(),
176 "France".to_string(),
177 50,
178 1000,
179 Some(1985),
180 );
181
182 assert!(building.is_ok());
183 let building = building.unwrap();
184 assert_eq!(building.organization_id, org_id);
185 assert_eq!(building.name, "Résidence Les Jardins");
186 assert_eq!(building.total_units, 50);
187 assert_eq!(building.total_tantiemes, 1000);
188 }
189
190 #[test]
191 fn test_create_building_empty_name_fails() {
192 let org_id = Uuid::new_v4();
193 let building = Building::new(
194 org_id,
195 "".to_string(),
196 "123 Rue de la Paix".to_string(),
197 "Paris".to_string(),
198 "75001".to_string(),
199 "France".to_string(),
200 50,
201 1000,
202 Some(1985),
203 );
204
205 assert!(building.is_err());
206 assert_eq!(building.unwrap_err(), "Building name cannot be empty");
207 }
208
209 #[test]
210 fn test_create_building_zero_units_fails() {
211 let org_id = Uuid::new_v4();
212 let building = Building::new(
213 org_id,
214 "Résidence Les Jardins".to_string(),
215 "123 Rue de la Paix".to_string(),
216 "Paris".to_string(),
217 "75001".to_string(),
218 "France".to_string(),
219 0,
220 1000,
221 Some(1985),
222 );
223
224 assert!(building.is_err());
225 assert_eq!(building.unwrap_err(), "Total units must be greater than 0");
226 }
227
228 #[test]
229 fn test_update_building_info() {
230 let org_id = Uuid::new_v4();
231 let mut building = Building::new(
232 org_id,
233 "Old Name".to_string(),
234 "Old Address".to_string(),
235 "Old City".to_string(),
236 "00000".to_string(),
237 "France".to_string(),
238 10,
239 1000,
240 None,
241 )
242 .unwrap();
243
244 let old_updated_at = building.updated_at;
245
246 building.update_info(
247 "New Name".to_string(),
248 "New Address".to_string(),
249 "New City".to_string(),
250 "11111".to_string(),
251 "France".to_string(),
252 10,
253 1500,
254 None,
255 );
256
257 assert_eq!(building.name, "New Name");
258 assert_eq!(building.address, "New Address");
259 assert_eq!(building.total_tantiemes, 1500);
260 assert!(building.updated_at > old_updated_at);
261 }
262}