1use crate::application::ports::UnitOwnerRepository;
7use crate::domain::entities::{ChargeDistribution, LotHolder, OwnershipType, UnitOwner};
8use async_trait::async_trait;
9use rust_decimal::Decimal;
10use sqlx::{PgPool, Row};
11use std::str::FromStr;
12use uuid::Uuid;
13
14pub struct PostgresUnitOwnerRepository {
15 pool: PgPool,
16}
17
18impl PostgresUnitOwnerRepository {
19 pub fn new(pool: PgPool) -> Self {
20 Self { pool }
21 }
22}
23
24#[async_trait]
25impl UnitOwnerRepository for PostgresUnitOwnerRepository {
26 async fn create(&self, unit_owner: &UnitOwner) -> Result<UnitOwner, String> {
27 let result = sqlx::query!(
28 r#"
29 INSERT INTO unit_owners (
30 id, unit_id, owner_id, ownership_percentage,
31 start_date, end_date, is_primary_contact,
32 created_at, updated_at
33 )
34 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
35 RETURNING *
36 "#,
37 unit_owner.id,
38 unit_owner.unit_id,
39 unit_owner.owner_id,
40 unit_owner.ownership_percentage,
41 unit_owner.start_date,
42 unit_owner.end_date,
43 unit_owner.is_primary_contact,
44 unit_owner.created_at,
45 unit_owner.updated_at
46 )
47 .fetch_one(&self.pool)
48 .await
49 .map_err(|e| format!("Failed to create unit_owner: {}", e))?;
50
51 Ok(UnitOwner {
52 id: result.id,
53 unit_id: result.unit_id,
54 owner_id: result.owner_id,
55 ownership_percentage: result.ownership_percentage,
56 start_date: result.start_date,
57 end_date: result.end_date,
58 is_primary_contact: result.is_primary_contact,
59 created_at: result.created_at,
60 updated_at: result.updated_at,
61 })
62 }
63
64 async fn find_by_id(&self, id: Uuid) -> Result<Option<UnitOwner>, String> {
65 let result = sqlx::query!(
66 r#"
67 SELECT * FROM unit_owners WHERE id = $1
68 "#,
69 id
70 )
71 .fetch_optional(&self.pool)
72 .await
73 .map_err(|e| format!("Failed to find unit_owner: {}", e))?;
74
75 Ok(result.map(|row| UnitOwner {
76 id: row.id,
77 unit_id: row.unit_id,
78 owner_id: row.owner_id,
79 ownership_percentage: row.ownership_percentage,
80 start_date: row.start_date,
81 end_date: row.end_date,
82 is_primary_contact: row.is_primary_contact,
83 created_at: row.created_at,
84 updated_at: row.updated_at,
85 }))
86 }
87
88 async fn find_current_owners_by_unit(&self, unit_id: Uuid) -> Result<Vec<UnitOwner>, String> {
89 let results = sqlx::query!(
90 r#"
91 SELECT * FROM unit_owners
92 WHERE unit_id = $1 AND end_date IS NULL
93 ORDER BY is_primary_contact DESC, created_at ASC
94 "#,
95 unit_id
96 )
97 .fetch_all(&self.pool)
98 .await
99 .map_err(|e| format!("Failed to find owners by unit: {}", e))?;
100
101 Ok(results
102 .into_iter()
103 .map(|row| UnitOwner {
104 id: row.id,
105 unit_id: row.unit_id,
106 owner_id: row.owner_id,
107 ownership_percentage: row.ownership_percentage,
108 start_date: row.start_date,
109 end_date: row.end_date,
110 is_primary_contact: row.is_primary_contact,
111 created_at: row.created_at,
112 updated_at: row.updated_at,
113 })
114 .collect())
115 }
116
117 async fn find_current_units_by_owner(&self, owner_id: Uuid) -> Result<Vec<UnitOwner>, String> {
118 let results = sqlx::query!(
119 r#"
120 SELECT * FROM unit_owners
121 WHERE owner_id = $1 AND end_date IS NULL
122 ORDER BY created_at ASC
123 "#,
124 owner_id
125 )
126 .fetch_all(&self.pool)
127 .await
128 .map_err(|e| format!("Failed to find units by owner: {}", e))?;
129
130 Ok(results
131 .into_iter()
132 .map(|row| UnitOwner {
133 id: row.id,
134 unit_id: row.unit_id,
135 owner_id: row.owner_id,
136 ownership_percentage: row.ownership_percentage,
137 start_date: row.start_date,
138 end_date: row.end_date,
139 is_primary_contact: row.is_primary_contact,
140 created_at: row.created_at,
141 updated_at: row.updated_at,
142 })
143 .collect())
144 }
145
146 async fn find_all_owners_by_unit(&self, unit_id: Uuid) -> Result<Vec<UnitOwner>, String> {
147 let results = sqlx::query!(
148 r#"
149 SELECT * FROM unit_owners
150 WHERE unit_id = $1
151 ORDER BY start_date DESC
152 "#,
153 unit_id
154 )
155 .fetch_all(&self.pool)
156 .await
157 .map_err(|e| format!("Failed to find all owners by unit: {}", e))?;
158
159 Ok(results
160 .into_iter()
161 .map(|row| UnitOwner {
162 id: row.id,
163 unit_id: row.unit_id,
164 owner_id: row.owner_id,
165 ownership_percentage: row.ownership_percentage,
166 start_date: row.start_date,
167 end_date: row.end_date,
168 is_primary_contact: row.is_primary_contact,
169 created_at: row.created_at,
170 updated_at: row.updated_at,
171 })
172 .collect())
173 }
174
175 async fn find_all_units_by_owner(&self, owner_id: Uuid) -> Result<Vec<UnitOwner>, String> {
176 let results = sqlx::query!(
177 r#"
178 SELECT * FROM unit_owners
179 WHERE owner_id = $1
180 ORDER BY start_date DESC
181 "#,
182 owner_id
183 )
184 .fetch_all(&self.pool)
185 .await
186 .map_err(|e| format!("Failed to find all units by owner: {}", e))?;
187
188 Ok(results
189 .into_iter()
190 .map(|row| UnitOwner {
191 id: row.id,
192 unit_id: row.unit_id,
193 owner_id: row.owner_id,
194 ownership_percentage: row.ownership_percentage,
195 start_date: row.start_date,
196 end_date: row.end_date,
197 is_primary_contact: row.is_primary_contact,
198 created_at: row.created_at,
199 updated_at: row.updated_at,
200 })
201 .collect())
202 }
203
204 async fn update(&self, unit_owner: &UnitOwner) -> Result<UnitOwner, String> {
205 let result = sqlx::query!(
206 r#"
207 UPDATE unit_owners
208 SET ownership_percentage = $2,
209 end_date = $3,
210 is_primary_contact = $4,
211 updated_at = $5
212 WHERE id = $1
213 RETURNING *
214 "#,
215 unit_owner.id,
216 unit_owner.ownership_percentage,
217 unit_owner.end_date,
218 unit_owner.is_primary_contact,
219 unit_owner.updated_at
220 )
221 .fetch_one(&self.pool)
222 .await
223 .map_err(|e| format!("Failed to update unit_owner: {}", e))?;
224
225 Ok(UnitOwner {
226 id: result.id,
227 unit_id: result.unit_id,
228 owner_id: result.owner_id,
229 ownership_percentage: result.ownership_percentage,
230 start_date: result.start_date,
231 end_date: result.end_date,
232 is_primary_contact: result.is_primary_contact,
233 created_at: result.created_at,
234 updated_at: result.updated_at,
235 })
236 }
237
238 async fn delete(&self, id: Uuid) -> Result<(), String> {
239 sqlx::query!(
240 r#"
241 DELETE FROM unit_owners WHERE id = $1
242 "#,
243 id
244 )
245 .execute(&self.pool)
246 .await
247 .map_err(|e| format!("Failed to delete unit_owner: {}", e))?;
248
249 Ok(())
250 }
251
252 async fn has_active_owners(&self, unit_id: Uuid) -> Result<bool, String> {
253 let result = sqlx::query!(
254 r#"
255 SELECT EXISTS(SELECT 1 FROM unit_owners WHERE unit_id = $1 AND end_date IS NULL) as "exists!"
256 "#,
257 unit_id
258 )
259 .fetch_one(&self.pool)
260 .await
261 .map_err(|e| format!("Failed to check active owners: {}", e))?;
262
263 Ok(result.exists)
264 }
265
266 async fn get_total_ownership_percentage(&self, unit_id: Uuid) -> Result<Decimal, String> {
267 let result = sqlx::query!(
268 r#"
269 SELECT COALESCE(SUM(ownership_percentage), 0) as "total!"
270 FROM unit_owners
271 WHERE unit_id = $1 AND end_date IS NULL
272 "#,
273 unit_id
274 )
275 .fetch_one(&self.pool)
276 .await
277 .map_err(|e| format!("Failed to get total ownership percentage: {}", e))?;
278
279 Ok(result.total)
280 }
281
282 async fn find_active_by_unit_and_owner(
283 &self,
284 unit_id: Uuid,
285 owner_id: Uuid,
286 ) -> Result<Option<UnitOwner>, String> {
287 let result = sqlx::query!(
288 r#"
289 SELECT * FROM unit_owners
290 WHERE unit_id = $1 AND owner_id = $2 AND end_date IS NULL
291 "#,
292 unit_id,
293 owner_id
294 )
295 .fetch_optional(&self.pool)
296 .await
297 .map_err(|e| format!("Failed to find active unit_owner: {}", e))?;
298
299 Ok(result.map(|row| UnitOwner {
300 id: row.id,
301 unit_id: row.unit_id,
302 owner_id: row.owner_id,
303 ownership_percentage: row.ownership_percentage,
304 start_date: row.start_date,
305 end_date: row.end_date,
306 is_primary_contact: row.is_primary_contact,
307 created_at: row.created_at,
308 updated_at: row.updated_at,
309 }))
310 }
311
312 async fn find_active_by_building(
313 &self,
314 building_id: Uuid,
315 ) -> Result<Vec<(Uuid, Uuid, Decimal)>, String> {
316 let results = sqlx::query!(
317 r#"
318 SELECT uo.unit_id, uo.owner_id, uo.ownership_percentage
319 FROM unit_owners uo
320 JOIN units u ON uo.unit_id = u.id
321 WHERE u.building_id = $1 AND uo.end_date IS NULL
322 "#,
323 building_id
324 )
325 .fetch_all(&self.pool)
326 .await
327 .map_err(|e| format!("Failed to find active unit_owners by building: {}", e))?;
328
329 Ok(results
330 .into_iter()
331 .map(|row| (row.unit_id, row.owner_id, row.ownership_percentage))
332 .collect())
333 }
334
335 async fn find_active_quota_shares_by_building(
336 &self,
337 building_id: Uuid,
338 ) -> Result<Vec<(Uuid, Uuid, Decimal)>, String> {
339 let rows = sqlx::query!(
345 r#"
346 SELECT uo.unit_id, uo.owner_id, uo.ownership_percentage,
347 u.quota AS unit_quota,
348 b.total_tantiemes
349 FROM unit_owners uo
350 JOIN units u ON uo.unit_id = u.id
351 JOIN buildings b ON u.building_id = b.id
352 WHERE u.building_id = $1 AND uo.end_date IS NULL
353 "#,
354 building_id
355 )
356 .fetch_all(&self.pool)
357 .await
358 .map_err(|e| format!("Failed to find active quota shares by building: {}", e))?;
359
360 let mut parts = Vec::with_capacity(rows.len());
361 for row in rows {
362 let part = ChargeDistribution::resolve_owner_quota(
363 row.unit_quota,
364 Decimal::from(row.total_tantiemes),
365 row.ownership_percentage,
366 )
367 .map_err(|e| format!("Invalid quota share for unit {}: {}", row.unit_id, e))?;
368 parts.push((row.unit_id, row.owner_id, part));
369 }
370
371 Ok(parts)
372 }
373
374 async fn find_voting_holders_by_unit(&self, unit_id: Uuid) -> Result<Vec<LotHolder>, String> {
375 let rows = sqlx::query(
379 r#"
380 SELECT ownership_type, is_voting_representative
381 FROM unit_owners
382 WHERE unit_id = $1 AND end_date IS NULL
383 "#,
384 )
385 .bind(unit_id)
386 .fetch_all(&self.pool)
387 .await
388 .map_err(|e| format!("Failed to find voting holders by unit: {}", e))?;
389
390 rows.into_iter()
391 .map(|row| {
392 let raw: String = row
393 .try_get("ownership_type")
394 .map_err(|e| format!("ownership_type read error: {}", e))?;
395 let ownership_type = OwnershipType::from_str(&raw).map_err(|e| e.to_string())?;
398 let is_voting_representative: bool = row
399 .try_get("is_voting_representative")
400 .map_err(|e| format!("is_voting_representative read error: {}", e))?;
401 Ok(LotHolder::new(ownership_type, is_voting_representative))
402 })
403 .collect()
404 }
405
406 async fn is_voting_representative(&self, unit_owner_id: Uuid) -> Result<bool, String> {
407 let row = sqlx::query(r#"SELECT is_voting_representative FROM unit_owners WHERE id = $1"#)
411 .bind(unit_owner_id)
412 .fetch_optional(&self.pool)
413 .await
414 .map_err(|e| format!("Failed to read is_voting_representative: {}", e))?;
415
416 let Some(row) = row else {
417 return Err(format!("unit_owner {} not found", unit_owner_id));
418 };
419 row.try_get("is_voting_representative")
420 .map_err(|e| format!("is_voting_representative read error: {}", e))
421 }
422
423 async fn set_voting_representative(&self, unit_owner_id: Uuid) -> Result<(), String> {
424 let result = sqlx::query(
425 r#"UPDATE unit_owners SET is_voting_representative = true, updated_at = now()
426 WHERE id = $1"#,
427 )
428 .bind(unit_owner_id)
429 .execute(&self.pool)
430 .await
431 .map_err(|e| format!("Failed to set voting representative: {}", e))?;
432
433 if result.rows_affected() == 0 {
434 return Err(format!("unit_owner {} not found", unit_owner_id));
435 }
436 Ok(())
437 }
438}