koprogo_api/infrastructure/web/handlers/
unit_owner_handlers.rs

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
12/// Helper function to check if user role can modify unit ownership
13/// Only SuperAdmin and Syndic can modify unit ownership (who owns what)
14fn 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/// Add an owner to a unit
25#[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    // Validate DTO
37    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    // Parse UUIDs
45    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    // Call use case
64    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            // Audit log
76            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/// Remove an owner from a unit
107#[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    // Parse UUIDs
120    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    // Call use case
139    match state
140        .unit_owner_use_cases
141        .remove_owner_from_unit(unit_id, owner_id)
142        .await
143    {
144        Ok(unit_owner) => {
145            // Audit log
146            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/// Update a unit-owner relationship (ownership percentage or primary contact)
165#[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    // Validate DTO
177    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    // Parse UUID
185    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    // Update ownership percentage if provided
195    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            // If unsetting primary, just get the current one and update it
208            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                    // We'd need an update method in the repository, but for now return error
216                    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            // Audit log
241            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 all current owners of a unit
260#[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    // Parse UUID
267    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    // Call use case
277    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 all current units owned by an owner
289#[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    // Parse UUID
296    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    // Call use case
306    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 ownership history for a unit
318#[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    // Parse UUID
325    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    // Call use case
335    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 ownership history for an owner
351#[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    // Parse UUID
358    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    // Call use case
368    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/// Transfer ownership from one owner to another
384#[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    // Validate DTO
396    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    // Parse UUIDs
404    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    // Call use case
432    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            // Audit log
439            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 the total ownership percentage for a unit
465#[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    // Parse UUID
472    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    // Call use case
482    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
498// Helper function to convert entity to response DTO
499fn 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/// Export Ownership Contract to PDF
515///
516/// GET /unit-owners/{relationship_id}/export-contract-pdf
517///
518/// Generates a "Contrat de Copropriété" PDF for a unit ownership relationship.
519#[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    // 1. Get the UnitOwner relationship
540    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    // 2. Get unit
559    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    // 3. Get owner
574    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    // 4. Get building
589    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    // Convert DTOs to domain entities
612    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(), // DTOs don't have timestamps, use current time
655        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(), // DTOs don't have timestamps, use current time
671        updated_at: Utc::now(),
672    };
673
674    // 5. Generate PDF
675    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            // Audit log
684            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}