1use crate::application::dto::{
2 AddOwnerToUnitDto, DesignateVotingRepresentativeDto, TransferOwnershipDto,
3 UnitOwnerResponseDto, UpdateOwnershipDto, VotingRepresentativeResponseDto,
4};
5use crate::domain::entities::UnitOwner;
6use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
7use crate::infrastructure::web::middleware::scope_guard::{
8 verify_owner_org_access, verify_unit_org_access,
9};
10use crate::infrastructure::web::{AppState, AuthenticatedUser};
11use actix_web::{delete, get, post, put, web, HttpResponse, Responder, ResponseError};
12use chrono::{DateTime, Utc};
13use uuid::Uuid;
14use validator::Validate;
15
16fn verify_unit_ownership_permission(user: &AuthenticatedUser) -> Option<HttpResponse> {
19 if user.role == "owner" || user.role == "accountant" {
20 Some(HttpResponse::Forbidden().json(serde_json::json!({
21 "error": "Only SuperAdmin and Syndic can modify unit ownership"
22 })))
23 } else {
24 None
25 }
26}
27
28#[post("/units/{unit_id}/owners")]
30pub async fn add_owner_to_unit(
31 state: web::Data<AppState>,
32 user: AuthenticatedUser,
33 unit_id: web::Path<String>,
34 dto: web::Json<AddOwnerToUnitDto>,
35) -> impl Responder {
36 if let Some(response) = verify_unit_ownership_permission(&user) {
37 return response;
38 }
39
40 if let Err(errors) = dto.validate() {
42 return HttpResponse::BadRequest().json(serde_json::json!({
43 "error": "Validation failed",
44 "details": errors.to_string()
45 }));
46 }
47
48 let unit_id = match Uuid::parse_str(&unit_id) {
50 Ok(id) => id,
51 Err(_) => {
52 return HttpResponse::BadRequest().json(serde_json::json!({
53 "error": "Invalid unit_id format"
54 }))
55 }
56 };
57
58 let owner_id = match Uuid::parse_str(&dto.owner_id) {
59 Ok(id) => id,
60 Err(_) => {
61 return HttpResponse::BadRequest().json(serde_json::json!({
62 "error": "Invalid owner_id format"
63 }))
64 }
65 };
66
67 match state
69 .unit_owner_use_cases
70 .add_owner_to_unit(
71 unit_id,
72 owner_id,
73 dto.ownership_percentage,
74 dto.is_primary_contact,
75 )
76 .await
77 {
78 Ok(unit_owner) => {
79 if let Some(org_id) = user.organization_id {
81 AuditLogEntry::new(
82 AuditEventType::UnitOwnerCreated,
83 Some(user.user_id),
84 Some(org_id),
85 )
86 .with_resource("UnitOwner", unit_owner.id)
87 .log();
88 }
89
90 HttpResponse::Created().json(to_response_dto(&unit_owner))
91 }
92 Err(err) => {
93 if let Some(org_id) = user.organization_id {
94 AuditLogEntry::new(
95 AuditEventType::UnitOwnerCreated,
96 Some(user.user_id),
97 Some(org_id),
98 )
99 .with_error(err.clone())
100 .log();
101 }
102
103 HttpResponse::BadRequest().json(serde_json::json!({
104 "error": err
105 }))
106 }
107 }
108}
109
110#[delete("/units/{unit_id}/owners/{owner_id}")]
112pub async fn remove_owner_from_unit(
113 state: web::Data<AppState>,
114 user: AuthenticatedUser,
115 path: web::Path<(String, String)>,
116) -> impl Responder {
117 if let Some(response) = verify_unit_ownership_permission(&user) {
118 return response;
119 }
120
121 let (unit_id_str, owner_id_str) = path.into_inner();
122
123 let unit_id = match Uuid::parse_str(&unit_id_str) {
125 Ok(id) => id,
126 Err(_) => {
127 return HttpResponse::BadRequest().json(serde_json::json!({
128 "error": "Invalid unit_id format"
129 }))
130 }
131 };
132
133 let owner_id = match Uuid::parse_str(&owner_id_str) {
134 Ok(id) => id,
135 Err(_) => {
136 return HttpResponse::BadRequest().json(serde_json::json!({
137 "error": "Invalid owner_id format"
138 }))
139 }
140 };
141
142 if let Err(err) = verify_unit_org_access(
153 &user,
154 unit_id,
155 &state.unit_use_cases,
156 &state.building_use_cases,
157 &state.acp_use_cases,
158 )
159 .await
160 {
161 return err.error_response();
162 }
163
164 match state
166 .unit_owner_use_cases
167 .remove_owner_from_unit(unit_id, owner_id)
168 .await
169 {
170 Ok(unit_owner) => {
171 if let Some(org_id) = user.organization_id {
173 AuditLogEntry::new(
174 AuditEventType::UnitOwnerDeleted,
175 Some(user.user_id),
176 Some(org_id),
177 )
178 .with_resource("UnitOwner", unit_owner.id)
179 .log();
180 }
181
182 HttpResponse::Ok().json(to_response_dto(&unit_owner))
183 }
184 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
185 "error": err
186 })),
187 }
188}
189
190#[put("/unit-owners/{id}")]
192pub async fn update_unit_owner(
193 state: web::Data<AppState>,
194 user: AuthenticatedUser,
195 id: web::Path<String>,
196 dto: web::Json<UpdateOwnershipDto>,
197) -> impl Responder {
198 if let Some(response) = verify_unit_ownership_permission(&user) {
199 return response;
200 }
201
202 if let Err(errors) = dto.validate() {
204 return HttpResponse::BadRequest().json(serde_json::json!({
205 "error": "Validation failed",
206 "details": errors.to_string()
207 }));
208 }
209
210 let unit_owner_id = match Uuid::parse_str(&id) {
212 Ok(id) => id,
213 Err(_) => {
214 return HttpResponse::BadRequest().json(serde_json::json!({
215 "error": "Invalid unit_owner_id format"
216 }))
217 }
218 };
219
220 let result = if let Some(percentage) = dto.ownership_percentage {
222 state
223 .unit_owner_use_cases
224 .update_ownership_percentage(unit_owner_id, percentage)
225 .await
226 } else if let Some(is_primary) = dto.is_primary_contact {
227 if is_primary {
228 state
229 .unit_owner_use_cases
230 .set_primary_contact(unit_owner_id)
231 .await
232 } else {
233 match state
235 .unit_owner_use_cases
236 .get_unit_owner(unit_owner_id)
237 .await
238 {
239 Ok(Some(mut unit_owner)) => {
240 unit_owner.set_primary_contact(false);
241 return HttpResponse::BadRequest().json(serde_json::json!({
243 "error": "Cannot unset primary contact directly. Set another owner as primary instead."
244 }));
245 }
246 Ok(None) => {
247 return HttpResponse::NotFound().json(serde_json::json!({
248 "error": "Unit-owner relationship not found"
249 }))
250 }
251 Err(err) => {
252 return HttpResponse::InternalServerError().json(serde_json::json!({
253 "error": err
254 }))
255 }
256 }
257 }
258 } else {
259 return HttpResponse::BadRequest().json(serde_json::json!({
260 "error": "Must provide either ownership_percentage or is_primary_contact"
261 }));
262 };
263
264 match result {
265 Ok(unit_owner) => {
266 if let Some(org_id) = user.organization_id {
268 AuditLogEntry::new(
269 AuditEventType::UnitOwnerUpdated,
270 Some(user.user_id),
271 Some(org_id),
272 )
273 .with_resource("UnitOwner", unit_owner.id)
274 .log();
275 }
276
277 HttpResponse::Ok().json(to_response_dto(&unit_owner))
278 }
279 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
280 "error": err
281 })),
282 }
283}
284
285#[get("/units/{unit_id}/owners")]
287pub async fn get_unit_owners(
288 state: web::Data<AppState>,
289 user: AuthenticatedUser,
290 unit_id: web::Path<String>,
291) -> impl Responder {
292 let unit_id = match Uuid::parse_str(&unit_id) {
294 Ok(id) => id,
295 Err(_) => {
296 return HttpResponse::BadRequest().json(serde_json::json!({
297 "error": "Invalid unit_id format"
298 }))
299 }
300 };
301
302 if let Err(err) = verify_unit_org_access(
308 &user,
309 unit_id,
310 &state.unit_use_cases,
311 &state.building_use_cases,
312 &state.acp_use_cases,
313 )
314 .await
315 {
316 return err.error_response();
317 }
318
319 match state.unit_owner_use_cases.get_unit_owners(unit_id).await {
321 Ok(unit_owners) => {
322 let dtos: Vec<UnitOwnerResponseDto> = unit_owners.iter().map(to_response_dto).collect();
323 HttpResponse::Ok().json(dtos)
324 }
325 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
326 "error": err
327 })),
328 }
329}
330
331#[get("/owners/{owner_id}/units")]
333pub async fn get_owner_units(
334 state: web::Data<AppState>,
335 user: AuthenticatedUser,
336 owner_id: web::Path<String>,
337) -> impl Responder {
338 let owner_id = match Uuid::parse_str(&owner_id) {
340 Ok(id) => id,
341 Err(_) => {
342 return HttpResponse::BadRequest().json(serde_json::json!({
343 "error": "Invalid owner_id format"
344 }))
345 }
346 };
347
348 if let Err(err) = verify_owner_org_access(&user, owner_id, &state.owner_use_cases).await {
352 return err.error_response();
353 }
354
355 match state.unit_owner_use_cases.get_owner_units(owner_id).await {
357 Ok(unit_owners) => {
358 let dtos: Vec<UnitOwnerResponseDto> = unit_owners.iter().map(to_response_dto).collect();
359 HttpResponse::Ok().json(dtos)
360 }
361 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
362 "error": err
363 })),
364 }
365}
366
367#[get("/units/{unit_id}/owners/history")]
369pub async fn get_unit_ownership_history(
370 state: web::Data<AppState>,
371 user: AuthenticatedUser,
372 unit_id: web::Path<String>,
373) -> impl Responder {
374 let unit_id = match Uuid::parse_str(&unit_id) {
376 Ok(id) => id,
377 Err(_) => {
378 return HttpResponse::BadRequest().json(serde_json::json!({
379 "error": "Invalid unit_id format"
380 }))
381 }
382 };
383
384 if let Err(err) = verify_unit_org_access(
390 &user,
391 unit_id,
392 &state.unit_use_cases,
393 &state.building_use_cases,
394 &state.acp_use_cases,
395 )
396 .await
397 {
398 return err.error_response();
399 }
400
401 match state
403 .unit_owner_use_cases
404 .get_unit_ownership_history(unit_id)
405 .await
406 {
407 Ok(unit_owners) => {
408 let dtos: Vec<UnitOwnerResponseDto> = unit_owners.iter().map(to_response_dto).collect();
409 HttpResponse::Ok().json(dtos)
410 }
411 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
412 "error": err
413 })),
414 }
415}
416
417#[get("/owners/{owner_id}/units/history")]
419pub async fn get_owner_ownership_history(
420 state: web::Data<AppState>,
421 user: AuthenticatedUser,
422 owner_id: web::Path<String>,
423) -> impl Responder {
424 let owner_id = match Uuid::parse_str(&owner_id) {
426 Ok(id) => id,
427 Err(_) => {
428 return HttpResponse::BadRequest().json(serde_json::json!({
429 "error": "Invalid owner_id format"
430 }))
431 }
432 };
433
434 if let Err(err) = verify_owner_org_access(&user, owner_id, &state.owner_use_cases).await {
438 return err.error_response();
439 }
440
441 match state
443 .unit_owner_use_cases
444 .get_owner_ownership_history(owner_id)
445 .await
446 {
447 Ok(unit_owners) => {
448 let dtos: Vec<UnitOwnerResponseDto> = unit_owners.iter().map(to_response_dto).collect();
449 HttpResponse::Ok().json(dtos)
450 }
451 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
452 "error": err
453 })),
454 }
455}
456
457#[post("/units/{unit_id}/owners/transfer")]
459pub async fn transfer_ownership(
460 state: web::Data<AppState>,
461 user: AuthenticatedUser,
462 unit_id: web::Path<String>,
463 dto: web::Json<TransferOwnershipDto>,
464) -> impl Responder {
465 if let Some(response) = verify_unit_ownership_permission(&user) {
466 return response;
467 }
468
469 if let Err(errors) = dto.validate() {
471 return HttpResponse::BadRequest().json(serde_json::json!({
472 "error": "Validation failed",
473 "details": errors.to_string()
474 }));
475 }
476
477 let unit_id = match Uuid::parse_str(&unit_id) {
479 Ok(id) => id,
480 Err(_) => {
481 return HttpResponse::BadRequest().json(serde_json::json!({
482 "error": "Invalid unit_id format"
483 }))
484 }
485 };
486
487 let from_owner_id = match Uuid::parse_str(&dto.from_owner_id) {
488 Ok(id) => id,
489 Err(_) => {
490 return HttpResponse::BadRequest().json(serde_json::json!({
491 "error": "Invalid from_owner_id format"
492 }))
493 }
494 };
495
496 let to_owner_id = match Uuid::parse_str(&dto.to_owner_id) {
497 Ok(id) => id,
498 Err(_) => {
499 return HttpResponse::BadRequest().json(serde_json::json!({
500 "error": "Invalid to_owner_id format"
501 }))
502 }
503 };
504
505 match state
507 .unit_owner_use_cases
508 .transfer_ownership(from_owner_id, to_owner_id, unit_id)
509 .await
510 {
511 Ok((ended, created)) => {
512 if let Some(org_id) = user.organization_id {
514 AuditLogEntry::new(
515 AuditEventType::UnitOwnerUpdated,
516 Some(user.user_id),
517 Some(org_id),
518 )
519 .with_resource("UnitOwner", ended.id)
520 .with_metadata(serde_json::json!({
521 "transferred_to": created.id.to_string(),
522 "new_unit_owner_id": created.id.to_string()
523 }))
524 .log();
525 }
526
527 HttpResponse::Ok().json(serde_json::json!({
528 "ended_relationship": to_response_dto(&ended),
529 "new_relationship": to_response_dto(&created)
530 }))
531 }
532 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
533 "error": err
534 })),
535 }
536}
537
538#[get("/units/{unit_id}/owners/total-percentage")]
540pub async fn get_total_ownership_percentage(
541 state: web::Data<AppState>,
542 user: AuthenticatedUser,
543 unit_id: web::Path<String>,
544) -> impl Responder {
545 let unit_id = match Uuid::parse_str(&unit_id) {
547 Ok(id) => id,
548 Err(_) => {
549 return HttpResponse::BadRequest().json(serde_json::json!({
550 "error": "Invalid unit_id format"
551 }))
552 }
553 };
554
555 if let Err(err) = verify_unit_org_access(
561 &user,
562 unit_id,
563 &state.unit_use_cases,
564 &state.building_use_cases,
565 &state.acp_use_cases,
566 )
567 .await
568 {
569 return err.error_response();
570 }
571
572 match state
574 .unit_owner_use_cases
575 .get_total_ownership_percentage(unit_id)
576 .await
577 {
578 Ok(total) => HttpResponse::Ok().json(serde_json::json!({
579 "unit_id": unit_id.to_string(),
580 "total_ownership_percentage": total,
581 "percentage_display": format!("{:.2}%", total * rust_decimal_macros::dec!(100))
582 })),
583 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
584 "error": err
585 })),
586 }
587}
588
589#[utoipa::path(
596 post,
597 path = "/units/{unit_id}/voting-representative",
598 tag = "UnitOwners",
599 summary = "Désigner le représentant de vote d'un lot (Art. 3.87 §1er)",
600 params(("unit_id" = String, Path, description = "UUID du lot")),
601 request_body = DesignateVotingRepresentativeDto,
602 responses(
603 (status = 200, description = "Représentant désigné", body = crate::application::dto::unit_owner_dto::VotingRepresentativeResponseDto),
604 (status = 400, description = "Identifiant invalide"),
605 (status = 403, description = "Hors mandat sur ce lot"),
606 ),
607 security(("bearer_auth" = []))
608)]
609#[post("/units/{unit_id}/voting-representative")]
610pub async fn designate_voting_representative(
611 state: web::Data<AppState>,
612 user: AuthenticatedUser,
613 unit_id: web::Path<String>,
614 dto: web::Json<DesignateVotingRepresentativeDto>,
615) -> impl Responder {
616 if let Some(response) = verify_unit_ownership_permission(&user) {
617 return response;
618 }
619
620 if let Err(errors) = dto.validate() {
621 return HttpResponse::BadRequest().json(serde_json::json!({
622 "error": "Validation failed",
623 "details": errors.to_string()
624 }));
625 }
626
627 let unit_id = match Uuid::parse_str(&unit_id) {
628 Ok(id) => id,
629 Err(_) => {
630 return HttpResponse::BadRequest().json(serde_json::json!({
631 "error": "Invalid unit_id format"
632 }))
633 }
634 };
635
636 let owner_id = match Uuid::parse_str(&dto.owner_id) {
637 Ok(id) => id,
638 Err(_) => {
639 return HttpResponse::BadRequest().json(serde_json::json!({
640 "error": "Invalid owner_id format"
641 }))
642 }
643 };
644
645 if let Err(err) = verify_unit_org_access(
650 &user,
651 unit_id,
652 &state.unit_use_cases,
653 &state.building_use_cases,
654 &state.acp_use_cases,
655 )
656 .await
657 {
658 return err.error_response();
659 }
660
661 match state
662 .unit_owner_use_cases
663 .designate_voting_representative(unit_id, owner_id)
664 .await
665 {
666 Ok(target) => {
667 if let Some(org_id) = user.organization_id {
668 AuditLogEntry::new(
669 AuditEventType::UnitOwnerUpdated,
670 Some(user.user_id),
671 Some(org_id),
672 )
673 .with_resource("UnitOwner", target.id)
674 .with_metadata(serde_json::json!({
675 "voting_representative_designated": true,
676 "unit_id": unit_id.to_string(),
677 "owner_id": owner_id.to_string()
678 }))
679 .log();
680 }
681
682 HttpResponse::Ok().json(VotingRepresentativeResponseDto {
683 unit_id: unit_id.to_string(),
684 owner_id: owner_id.to_string(),
685 unit_owner_id: target.id.to_string(),
686 is_voting_representative: true,
687 })
688 }
689 Err(err) => err.error_response(),
690 }
691}
692
693fn to_response_dto(unit_owner: &UnitOwner) -> UnitOwnerResponseDto {
695 UnitOwnerResponseDto {
696 id: unit_owner.id.to_string(),
697 unit_id: unit_owner.unit_id.to_string(),
698 owner_id: unit_owner.owner_id.to_string(),
699 ownership_percentage: unit_owner.ownership_percentage,
700 start_date: unit_owner.start_date,
701 end_date: unit_owner.end_date,
702 is_primary_contact: unit_owner.is_primary_contact,
703 is_active: unit_owner.is_active(),
704 created_at: unit_owner.created_at,
705 updated_at: unit_owner.updated_at,
706 }
707}
708
709#[get("/unit-owners/{id}/export-contract-pdf")]
715pub async fn export_ownership_contract_pdf(
716 state: web::Data<AppState>,
717 user: AuthenticatedUser,
718 id: web::Path<Uuid>,
719) -> impl Responder {
720 use crate::domain::entities::{Building, Owner, Unit};
721 use crate::domain::services::OwnershipContractExporter;
722
723 let organization_id = match user.require_organization() {
724 Ok(org_id) => org_id,
725 Err(e) => {
726 return HttpResponse::Unauthorized().json(serde_json::json!({
727 "error": e.to_string()
728 }))
729 }
730 };
731
732 let relationship_id = *id;
733
734 let unit_owner = match state
736 .unit_owner_use_cases
737 .get_unit_owner(relationship_id)
738 .await
739 {
740 Ok(Some(uo)) => uo,
741 Ok(None) => {
742 return HttpResponse::NotFound().json(serde_json::json!({
743 "error": "Unit ownership relationship not found"
744 }))
745 }
746 Err(err) => {
747 return HttpResponse::InternalServerError().json(serde_json::json!({
748 "error": format!("Failed to get unit ownership: {}", err)
749 }))
750 }
751 };
752
753 let unit_dto = match state.unit_use_cases.get_unit(unit_owner.unit_id).await {
755 Ok(Some(dto)) => dto,
756 Ok(None) => {
757 return HttpResponse::NotFound().json(serde_json::json!({
758 "error": "Unit not found"
759 }))
760 }
761 Err(err) => {
762 return HttpResponse::InternalServerError().json(serde_json::json!({
763 "error": err
764 }))
765 }
766 };
767
768 let owner_dto = match state.owner_use_cases.get_owner(unit_owner.owner_id).await {
770 Ok(Some(dto)) => dto,
771 Ok(None) => {
772 return HttpResponse::NotFound().json(serde_json::json!({
773 "error": "Owner not found"
774 }))
775 }
776 Err(err) => {
777 return HttpResponse::InternalServerError().json(serde_json::json!({
778 "error": err
779 }))
780 }
781 };
782
783 let building_uuid = match Uuid::parse_str(&unit_dto.building_id) {
785 Ok(uuid) => uuid,
786 Err(_) => {
787 return HttpResponse::BadRequest().json(serde_json::json!({
788 "error": "Invalid building ID format"
789 }))
790 }
791 };
792 let building_dto = match state.building_use_cases.get_building(building_uuid).await {
793 Ok(Some(dto)) => dto,
794 Ok(None) => {
795 return HttpResponse::NotFound().json(serde_json::json!({
796 "error": "Building not found"
797 }))
798 }
799 Err(err) => {
800 return HttpResponse::InternalServerError().json(serde_json::json!({
801 "error": err
802 }))
803 }
804 };
805
806 let building_acp_id = Uuid::parse_str(&building_dto.acp_id).unwrap_or_else(|_| Uuid::new_v4());
808
809 let building_created_at = DateTime::parse_from_rfc3339(&building_dto.created_at)
810 .map(|dt| dt.with_timezone(&Utc))
811 .unwrap_or_else(|_| Utc::now());
812
813 let building_updated_at = DateTime::parse_from_rfc3339(&building_dto.updated_at)
814 .map(|dt| dt.with_timezone(&Utc))
815 .unwrap_or_else(|_| Utc::now());
816
817 let building_entity = Building {
818 id: Uuid::parse_str(&building_dto.id).unwrap_or(building_uuid),
819 name: building_dto.name.clone(),
820 address: building_dto.address,
821 city: building_dto.city,
822 postal_code: building_dto.postal_code,
823 country: building_dto.country,
824 total_units: building_dto.total_units,
825 total_tantiemes: building_dto.total_tantiemes,
826 construction_year: building_dto.construction_year,
827 syndic_name: None,
828 syndic_email: None,
829 syndic_phone: None,
830 syndic_address: None,
831 syndic_office_hours: None,
832 syndic_emergency_contact: None,
833 slug: None,
834 acp_id: building_acp_id,
835 created_at: building_created_at,
836 updated_at: building_updated_at,
837 };
838
839 let unit_entity = Unit {
840 id: Uuid::parse_str(&unit_dto.id).unwrap_or(unit_owner.unit_id),
841 acp_id: building_acp_id,
842 building_id: building_uuid,
843 unit_number: unit_dto.unit_number,
844 floor: unit_dto.floor,
845 unit_type: unit_dto.unit_type,
846 surface_area: unit_dto.surface_area,
847 quota: unit_dto.quota,
848 owner_id: unit_dto.owner_id.and_then(|s| Uuid::parse_str(&s).ok()),
849 created_at: Utc::now(), updated_at: Utc::now(),
851 };
852
853 let owner_entity = Owner {
854 id: Uuid::parse_str(&owner_dto.id).unwrap_or(unit_owner.owner_id),
855 organization_id: Uuid::parse_str(&owner_dto.organization_id).unwrap_or(organization_id),
856 first_name: owner_dto.first_name.clone(),
857 last_name: owner_dto.last_name.clone(),
858 email: owner_dto.email,
859 phone: owner_dto.phone,
860 address: owner_dto.address,
861 city: owner_dto.city,
862 postal_code: owner_dto.postal_code,
863 country: owner_dto.country,
864 user_id: owner_dto.user_id.and_then(|s| Uuid::parse_str(&s).ok()),
865 created_at: Utc::now(), updated_at: Utc::now(),
867 };
868
869 match OwnershipContractExporter::export_to_pdf(
871 &building_entity,
872 &unit_entity,
873 &owner_entity,
874 unit_owner.ownership_percentage,
875 unit_owner.start_date,
876 ) {
877 Ok(pdf_bytes) => {
878 AuditLogEntry::new(
880 AuditEventType::ReportGenerated,
881 Some(user.user_id),
882 Some(organization_id),
883 )
884 .with_resource("UnitOwner", relationship_id)
885 .with_metadata(serde_json::json!({
886 "report_type": "ownership_contract_pdf",
887 "building_name": building_entity.name,
888 "unit_number": unit_entity.unit_number,
889 "owner_name": format!("{} {}", owner_entity.first_name, owner_entity.last_name)
890 }))
891 .log();
892
893 HttpResponse::Ok()
894 .content_type("application/pdf")
895 .insert_header((
896 "Content-Disposition",
897 format!(
898 "attachment; filename=\"Contrat_Copropriete_{}_{}_{}.pdf\"",
899 building_entity.name.replace(' ', "_"),
900 unit_entity.unit_number.replace(' ', "_"),
901 owner_entity.last_name.replace(' ', "_")
902 ),
903 ))
904 .body(pdf_bytes)
905 }
906 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
907 "error": format!("Failed to generate PDF: {}", err)
908 })),
909 }
910}