1use crate::application::dto::{
2 AddOwnerToUnitDto, TransferOwnershipDto, UnitOwnerResponseDto, UpdateOwnershipDto,
3};
4use crate::domain::entities::UnitOwner;
5use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
6use crate::infrastructure::web::{AppState, AuthenticatedUser};
7use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
8use chrono::{DateTime, Utc};
9use uuid::Uuid;
10use validator::Validate;
11
12fn check_unit_ownership_permission(user: &AuthenticatedUser) -> Option<HttpResponse> {
15 if user.role == "owner" || user.role == "accountant" {
16 Some(HttpResponse::Forbidden().json(serde_json::json!({
17 "error": "Only SuperAdmin and Syndic can modify unit ownership"
18 })))
19 } else {
20 None
21 }
22}
23
24#[post("/units/{unit_id}/owners")]
26pub async fn add_owner_to_unit(
27 state: web::Data<AppState>,
28 user: AuthenticatedUser,
29 unit_id: web::Path<String>,
30 dto: web::Json<AddOwnerToUnitDto>,
31) -> impl Responder {
32 if let Some(response) = check_unit_ownership_permission(&user) {
33 return response;
34 }
35
36 if let Err(errors) = dto.validate() {
38 return HttpResponse::BadRequest().json(serde_json::json!({
39 "error": "Validation failed",
40 "details": errors.to_string()
41 }));
42 }
43
44 let unit_id = match Uuid::parse_str(&unit_id) {
46 Ok(id) => id,
47 Err(_) => {
48 return HttpResponse::BadRequest().json(serde_json::json!({
49 "error": "Invalid unit_id format"
50 }))
51 }
52 };
53
54 let owner_id = match Uuid::parse_str(&dto.owner_id) {
55 Ok(id) => id,
56 Err(_) => {
57 return HttpResponse::BadRequest().json(serde_json::json!({
58 "error": "Invalid owner_id format"
59 }))
60 }
61 };
62
63 match state
65 .unit_owner_use_cases
66 .add_owner_to_unit(
67 unit_id,
68 owner_id,
69 dto.ownership_percentage,
70 dto.is_primary_contact,
71 )
72 .await
73 {
74 Ok(unit_owner) => {
75 if let Some(org_id) = user.organization_id {
77 AuditLogEntry::new(
78 AuditEventType::UnitOwnerCreated,
79 Some(user.user_id),
80 Some(org_id),
81 )
82 .with_resource("UnitOwner", unit_owner.id)
83 .log();
84 }
85
86 HttpResponse::Created().json(to_response_dto(&unit_owner))
87 }
88 Err(err) => {
89 if let Some(org_id) = user.organization_id {
90 AuditLogEntry::new(
91 AuditEventType::UnitOwnerCreated,
92 Some(user.user_id),
93 Some(org_id),
94 )
95 .with_error(err.clone())
96 .log();
97 }
98
99 HttpResponse::BadRequest().json(serde_json::json!({
100 "error": err
101 }))
102 }
103 }
104}
105
106#[delete("/units/{unit_id}/owners/{owner_id}")]
108pub async fn remove_owner_from_unit(
109 state: web::Data<AppState>,
110 user: AuthenticatedUser,
111 path: web::Path<(String, String)>,
112) -> impl Responder {
113 if let Some(response) = check_unit_ownership_permission(&user) {
114 return response;
115 }
116
117 let (unit_id_str, owner_id_str) = path.into_inner();
118
119 let unit_id = match Uuid::parse_str(&unit_id_str) {
121 Ok(id) => id,
122 Err(_) => {
123 return HttpResponse::BadRequest().json(serde_json::json!({
124 "error": "Invalid unit_id format"
125 }))
126 }
127 };
128
129 let owner_id = match Uuid::parse_str(&owner_id_str) {
130 Ok(id) => id,
131 Err(_) => {
132 return HttpResponse::BadRequest().json(serde_json::json!({
133 "error": "Invalid owner_id format"
134 }))
135 }
136 };
137
138 match state
140 .unit_owner_use_cases
141 .remove_owner_from_unit(unit_id, owner_id)
142 .await
143 {
144 Ok(unit_owner) => {
145 if let Some(org_id) = user.organization_id {
147 AuditLogEntry::new(
148 AuditEventType::UnitOwnerDeleted,
149 Some(user.user_id),
150 Some(org_id),
151 )
152 .with_resource("UnitOwner", unit_owner.id)
153 .log();
154 }
155
156 HttpResponse::Ok().json(to_response_dto(&unit_owner))
157 }
158 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
159 "error": err
160 })),
161 }
162}
163
164#[put("/unit-owners/{id}")]
166pub async fn update_unit_owner(
167 state: web::Data<AppState>,
168 user: AuthenticatedUser,
169 id: web::Path<String>,
170 dto: web::Json<UpdateOwnershipDto>,
171) -> impl Responder {
172 if let Some(response) = check_unit_ownership_permission(&user) {
173 return response;
174 }
175
176 if let Err(errors) = dto.validate() {
178 return HttpResponse::BadRequest().json(serde_json::json!({
179 "error": "Validation failed",
180 "details": errors.to_string()
181 }));
182 }
183
184 let unit_owner_id = match Uuid::parse_str(&id) {
186 Ok(id) => id,
187 Err(_) => {
188 return HttpResponse::BadRequest().json(serde_json::json!({
189 "error": "Invalid unit_owner_id format"
190 }))
191 }
192 };
193
194 let result = if let Some(percentage) = dto.ownership_percentage {
196 state
197 .unit_owner_use_cases
198 .update_ownership_percentage(unit_owner_id, percentage)
199 .await
200 } else if let Some(is_primary) = dto.is_primary_contact {
201 if is_primary {
202 state
203 .unit_owner_use_cases
204 .set_primary_contact(unit_owner_id)
205 .await
206 } else {
207 match state
209 .unit_owner_use_cases
210 .get_unit_owner(unit_owner_id)
211 .await
212 {
213 Ok(Some(mut unit_owner)) => {
214 unit_owner.set_primary_contact(false);
215 return HttpResponse::BadRequest().json(serde_json::json!({
217 "error": "Cannot unset primary contact directly. Set another owner as primary instead."
218 }));
219 }
220 Ok(None) => {
221 return HttpResponse::NotFound().json(serde_json::json!({
222 "error": "Unit-owner relationship not found"
223 }))
224 }
225 Err(err) => {
226 return HttpResponse::InternalServerError().json(serde_json::json!({
227 "error": err
228 }))
229 }
230 }
231 }
232 } else {
233 return HttpResponse::BadRequest().json(serde_json::json!({
234 "error": "Must provide either ownership_percentage or is_primary_contact"
235 }));
236 };
237
238 match result {
239 Ok(unit_owner) => {
240 if let Some(org_id) = user.organization_id {
242 AuditLogEntry::new(
243 AuditEventType::UnitOwnerUpdated,
244 Some(user.user_id),
245 Some(org_id),
246 )
247 .with_resource("UnitOwner", unit_owner.id)
248 .log();
249 }
250
251 HttpResponse::Ok().json(to_response_dto(&unit_owner))
252 }
253 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
254 "error": err
255 })),
256 }
257}
258
259#[get("/units/{unit_id}/owners")]
261pub async fn get_unit_owners(
262 state: web::Data<AppState>,
263 _user: AuthenticatedUser,
264 unit_id: web::Path<String>,
265) -> impl Responder {
266 let unit_id = match Uuid::parse_str(&unit_id) {
268 Ok(id) => id,
269 Err(_) => {
270 return HttpResponse::BadRequest().json(serde_json::json!({
271 "error": "Invalid unit_id format"
272 }))
273 }
274 };
275
276 match state.unit_owner_use_cases.get_unit_owners(unit_id).await {
278 Ok(unit_owners) => {
279 let dtos: Vec<UnitOwnerResponseDto> = unit_owners.iter().map(to_response_dto).collect();
280 HttpResponse::Ok().json(dtos)
281 }
282 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
283 "error": err
284 })),
285 }
286}
287
288#[get("/owners/{owner_id}/units")]
290pub async fn get_owner_units(
291 state: web::Data<AppState>,
292 _user: AuthenticatedUser,
293 owner_id: web::Path<String>,
294) -> impl Responder {
295 let owner_id = match Uuid::parse_str(&owner_id) {
297 Ok(id) => id,
298 Err(_) => {
299 return HttpResponse::BadRequest().json(serde_json::json!({
300 "error": "Invalid owner_id format"
301 }))
302 }
303 };
304
305 match state.unit_owner_use_cases.get_owner_units(owner_id).await {
307 Ok(unit_owners) => {
308 let dtos: Vec<UnitOwnerResponseDto> = unit_owners.iter().map(to_response_dto).collect();
309 HttpResponse::Ok().json(dtos)
310 }
311 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
312 "error": err
313 })),
314 }
315}
316
317#[get("/units/{unit_id}/owners/history")]
319pub async fn get_unit_ownership_history(
320 state: web::Data<AppState>,
321 _user: AuthenticatedUser,
322 unit_id: web::Path<String>,
323) -> impl Responder {
324 let unit_id = match Uuid::parse_str(&unit_id) {
326 Ok(id) => id,
327 Err(_) => {
328 return HttpResponse::BadRequest().json(serde_json::json!({
329 "error": "Invalid unit_id format"
330 }))
331 }
332 };
333
334 match state
336 .unit_owner_use_cases
337 .get_unit_ownership_history(unit_id)
338 .await
339 {
340 Ok(unit_owners) => {
341 let dtos: Vec<UnitOwnerResponseDto> = unit_owners.iter().map(to_response_dto).collect();
342 HttpResponse::Ok().json(dtos)
343 }
344 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
345 "error": err
346 })),
347 }
348}
349
350#[get("/owners/{owner_id}/units/history")]
352pub async fn get_owner_ownership_history(
353 state: web::Data<AppState>,
354 _user: AuthenticatedUser,
355 owner_id: web::Path<String>,
356) -> impl Responder {
357 let owner_id = match Uuid::parse_str(&owner_id) {
359 Ok(id) => id,
360 Err(_) => {
361 return HttpResponse::BadRequest().json(serde_json::json!({
362 "error": "Invalid owner_id format"
363 }))
364 }
365 };
366
367 match state
369 .unit_owner_use_cases
370 .get_owner_ownership_history(owner_id)
371 .await
372 {
373 Ok(unit_owners) => {
374 let dtos: Vec<UnitOwnerResponseDto> = unit_owners.iter().map(to_response_dto).collect();
375 HttpResponse::Ok().json(dtos)
376 }
377 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
378 "error": err
379 })),
380 }
381}
382
383#[post("/units/{unit_id}/owners/transfer")]
385pub async fn transfer_ownership(
386 state: web::Data<AppState>,
387 user: AuthenticatedUser,
388 unit_id: web::Path<String>,
389 dto: web::Json<TransferOwnershipDto>,
390) -> impl Responder {
391 if let Some(response) = check_unit_ownership_permission(&user) {
392 return response;
393 }
394
395 if let Err(errors) = dto.validate() {
397 return HttpResponse::BadRequest().json(serde_json::json!({
398 "error": "Validation failed",
399 "details": errors.to_string()
400 }));
401 }
402
403 let unit_id = match Uuid::parse_str(&unit_id) {
405 Ok(id) => id,
406 Err(_) => {
407 return HttpResponse::BadRequest().json(serde_json::json!({
408 "error": "Invalid unit_id format"
409 }))
410 }
411 };
412
413 let from_owner_id = match Uuid::parse_str(&dto.from_owner_id) {
414 Ok(id) => id,
415 Err(_) => {
416 return HttpResponse::BadRequest().json(serde_json::json!({
417 "error": "Invalid from_owner_id format"
418 }))
419 }
420 };
421
422 let to_owner_id = match Uuid::parse_str(&dto.to_owner_id) {
423 Ok(id) => id,
424 Err(_) => {
425 return HttpResponse::BadRequest().json(serde_json::json!({
426 "error": "Invalid to_owner_id format"
427 }))
428 }
429 };
430
431 match state
433 .unit_owner_use_cases
434 .transfer_ownership(from_owner_id, to_owner_id, unit_id)
435 .await
436 {
437 Ok((ended, created)) => {
438 if let Some(org_id) = user.organization_id {
440 AuditLogEntry::new(
441 AuditEventType::UnitOwnerUpdated,
442 Some(user.user_id),
443 Some(org_id),
444 )
445 .with_resource("UnitOwner", ended.id)
446 .with_metadata(serde_json::json!({
447 "transferred_to": created.id.to_string(),
448 "new_unit_owner_id": created.id.to_string()
449 }))
450 .log();
451 }
452
453 HttpResponse::Ok().json(serde_json::json!({
454 "ended_relationship": to_response_dto(&ended),
455 "new_relationship": to_response_dto(&created)
456 }))
457 }
458 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
459 "error": err
460 })),
461 }
462}
463
464#[get("/units/{unit_id}/owners/total-percentage")]
466pub async fn get_total_ownership_percentage(
467 state: web::Data<AppState>,
468 _user: AuthenticatedUser,
469 unit_id: web::Path<String>,
470) -> impl Responder {
471 let unit_id = match Uuid::parse_str(&unit_id) {
473 Ok(id) => id,
474 Err(_) => {
475 return HttpResponse::BadRequest().json(serde_json::json!({
476 "error": "Invalid unit_id format"
477 }))
478 }
479 };
480
481 match state
483 .unit_owner_use_cases
484 .get_total_ownership_percentage(unit_id)
485 .await
486 {
487 Ok(total) => HttpResponse::Ok().json(serde_json::json!({
488 "unit_id": unit_id.to_string(),
489 "total_ownership_percentage": total,
490 "percentage_display": format!("{:.2}%", total * 100.0)
491 })),
492 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
493 "error": err
494 })),
495 }
496}
497
498fn to_response_dto(unit_owner: &UnitOwner) -> UnitOwnerResponseDto {
500 UnitOwnerResponseDto {
501 id: unit_owner.id.to_string(),
502 unit_id: unit_owner.unit_id.to_string(),
503 owner_id: unit_owner.owner_id.to_string(),
504 ownership_percentage: unit_owner.ownership_percentage,
505 start_date: unit_owner.start_date,
506 end_date: unit_owner.end_date,
507 is_primary_contact: unit_owner.is_primary_contact,
508 is_active: unit_owner.is_active(),
509 created_at: unit_owner.created_at,
510 updated_at: unit_owner.updated_at,
511 }
512}
513
514#[get("/unit-owners/{id}/export-contract-pdf")]
520pub async fn export_ownership_contract_pdf(
521 state: web::Data<AppState>,
522 user: AuthenticatedUser,
523 id: web::Path<Uuid>,
524) -> impl Responder {
525 use crate::domain::entities::{Building, Owner, Unit};
526 use crate::domain::services::OwnershipContractExporter;
527
528 let organization_id = match user.require_organization() {
529 Ok(org_id) => org_id,
530 Err(e) => {
531 return HttpResponse::Unauthorized().json(serde_json::json!({
532 "error": e.to_string()
533 }))
534 }
535 };
536
537 let relationship_id = *id;
538
539 let unit_owner = match state
541 .unit_owner_use_cases
542 .get_unit_owner(relationship_id)
543 .await
544 {
545 Ok(Some(uo)) => uo,
546 Ok(None) => {
547 return HttpResponse::NotFound().json(serde_json::json!({
548 "error": "Unit ownership relationship not found"
549 }))
550 }
551 Err(err) => {
552 return HttpResponse::InternalServerError().json(serde_json::json!({
553 "error": format!("Failed to get unit ownership: {}", err)
554 }))
555 }
556 };
557
558 let unit_dto = match state.unit_use_cases.get_unit(unit_owner.unit_id).await {
560 Ok(Some(dto)) => dto,
561 Ok(None) => {
562 return HttpResponse::NotFound().json(serde_json::json!({
563 "error": "Unit not found"
564 }))
565 }
566 Err(err) => {
567 return HttpResponse::InternalServerError().json(serde_json::json!({
568 "error": err
569 }))
570 }
571 };
572
573 let owner_dto = match state.owner_use_cases.get_owner(unit_owner.owner_id).await {
575 Ok(Some(dto)) => dto,
576 Ok(None) => {
577 return HttpResponse::NotFound().json(serde_json::json!({
578 "error": "Owner not found"
579 }))
580 }
581 Err(err) => {
582 return HttpResponse::InternalServerError().json(serde_json::json!({
583 "error": err
584 }))
585 }
586 };
587
588 let building_uuid = match Uuid::parse_str(&unit_dto.building_id) {
590 Ok(uuid) => uuid,
591 Err(_) => {
592 return HttpResponse::BadRequest().json(serde_json::json!({
593 "error": "Invalid building ID format"
594 }))
595 }
596 };
597 let building_dto = match state.building_use_cases.get_building(building_uuid).await {
598 Ok(Some(dto)) => dto,
599 Ok(None) => {
600 return HttpResponse::NotFound().json(serde_json::json!({
601 "error": "Building not found"
602 }))
603 }
604 Err(err) => {
605 return HttpResponse::InternalServerError().json(serde_json::json!({
606 "error": err
607 }))
608 }
609 };
610
611 let building_org_id = Uuid::parse_str(&building_dto.organization_id).unwrap_or(organization_id);
613
614 let building_created_at = DateTime::parse_from_rfc3339(&building_dto.created_at)
615 .map(|dt| dt.with_timezone(&Utc))
616 .unwrap_or_else(|_| Utc::now());
617
618 let building_updated_at = DateTime::parse_from_rfc3339(&building_dto.updated_at)
619 .map(|dt| dt.with_timezone(&Utc))
620 .unwrap_or_else(|_| Utc::now());
621
622 let building_entity = Building {
623 id: Uuid::parse_str(&building_dto.id).unwrap_or(building_uuid),
624 name: building_dto.name.clone(),
625 address: building_dto.address,
626 city: building_dto.city,
627 postal_code: building_dto.postal_code,
628 country: building_dto.country,
629 total_units: building_dto.total_units,
630 total_tantiemes: building_dto.total_tantiemes,
631 construction_year: building_dto.construction_year,
632 syndic_name: None,
633 syndic_email: None,
634 syndic_phone: None,
635 syndic_address: None,
636 syndic_office_hours: None,
637 syndic_emergency_contact: None,
638 slug: None,
639 organization_id: building_org_id,
640 created_at: building_created_at,
641 updated_at: building_updated_at,
642 };
643
644 let unit_entity = Unit {
645 id: Uuid::parse_str(&unit_dto.id).unwrap_or(unit_owner.unit_id),
646 organization_id,
647 building_id: building_uuid,
648 unit_number: unit_dto.unit_number,
649 floor: unit_dto.floor,
650 unit_type: unit_dto.unit_type,
651 surface_area: unit_dto.surface_area,
652 quota: unit_dto.quota,
653 owner_id: unit_dto.owner_id.and_then(|s| Uuid::parse_str(&s).ok()),
654 created_at: Utc::now(), updated_at: Utc::now(),
656 };
657
658 let owner_entity = Owner {
659 id: Uuid::parse_str(&owner_dto.id).unwrap_or(unit_owner.owner_id),
660 organization_id: Uuid::parse_str(&owner_dto.organization_id).unwrap_or(organization_id),
661 first_name: owner_dto.first_name.clone(),
662 last_name: owner_dto.last_name.clone(),
663 email: owner_dto.email,
664 phone: owner_dto.phone,
665 address: owner_dto.address,
666 city: owner_dto.city,
667 postal_code: owner_dto.postal_code,
668 country: owner_dto.country,
669 user_id: owner_dto.user_id.and_then(|s| Uuid::parse_str(&s).ok()),
670 created_at: Utc::now(), updated_at: Utc::now(),
672 };
673
674 match OwnershipContractExporter::export_to_pdf(
676 &building_entity,
677 &unit_entity,
678 &owner_entity,
679 unit_owner.ownership_percentage,
680 unit_owner.start_date,
681 ) {
682 Ok(pdf_bytes) => {
683 AuditLogEntry::new(
685 AuditEventType::ReportGenerated,
686 Some(user.user_id),
687 Some(organization_id),
688 )
689 .with_resource("UnitOwner", relationship_id)
690 .with_metadata(serde_json::json!({
691 "report_type": "ownership_contract_pdf",
692 "building_name": building_entity.name,
693 "unit_number": unit_entity.unit_number,
694 "owner_name": format!("{} {}", owner_entity.first_name, owner_entity.last_name)
695 }))
696 .log();
697
698 HttpResponse::Ok()
699 .content_type("application/pdf")
700 .insert_header((
701 "Content-Disposition",
702 format!(
703 "attachment; filename=\"Contrat_Copropriete_{}_{}_{}.pdf\"",
704 building_entity.name.replace(' ', "_"),
705 unit_entity.unit_number.replace(' ', "_"),
706 owner_entity.last_name.replace(' ', "_")
707 ),
708 ))
709 .body(pdf_bytes)
710 }
711 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
712 "error": format!("Failed to generate PDF: {}", err)
713 })),
714 }
715}