1use crate::application::dto::{CreateOwnerDto, PageRequest, PageResponse};
2use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
3use crate::infrastructure::web::{AppState, AuthenticatedUser};
4use actix_web::{get, post, put, web, HttpResponse, Responder};
5use chrono::{DateTime, Utc};
6use serde::Deserialize;
7use uuid::Uuid;
8use validator::Validate;
9
10#[derive(Debug, Deserialize, Validate)]
11pub struct UpdateOwnerDto {
12 #[validate(length(min = 1, message = "First name is required"))]
13 pub first_name: String,
14 #[validate(length(min = 1, message = "Last name is required"))]
15 pub last_name: String,
16 #[validate(email(message = "Invalid email format"))]
17 pub email: String,
18 pub phone: Option<String>,
19}
20
21#[derive(Debug, Deserialize)]
22pub struct LinkOwnerUserDto {
23 pub user_id: Option<String>, }
25
26#[post("/owners")]
27pub async fn create_owner(
28 state: web::Data<AppState>,
29 user: AuthenticatedUser, mut dto: web::Json<CreateOwnerDto>,
31) -> impl Responder {
32 if user.role == "owner" || user.role == "accountant" {
34 return HttpResponse::Forbidden().json(serde_json::json!({
35 "error": "Only SuperAdmin and Syndic can create owners"
36 }));
37 }
38
39 let organization_id = if user.is_superadmin() {
42 if dto.organization_id.is_empty() {
44 return HttpResponse::BadRequest().json(serde_json::json!({
45 "error": "SuperAdmin must specify organization_id"
46 }));
47 }
48 match Uuid::parse_str(&dto.organization_id) {
49 Ok(org_id) => org_id,
50 Err(_) => {
51 return HttpResponse::BadRequest().json(serde_json::json!({
52 "error": "Invalid organization_id format"
53 }))
54 }
55 }
56 } else {
57 match user.require_organization() {
59 Ok(org_id) => {
60 dto.organization_id = org_id.to_string();
61 org_id
62 }
63 Err(e) => {
64 return HttpResponse::Unauthorized().json(serde_json::json!({
65 "error": e.to_string()
66 }))
67 }
68 }
69 };
70
71 if let Err(errors) = dto.validate() {
72 return HttpResponse::BadRequest().json(serde_json::json!({
73 "error": "Validation failed",
74 "details": errors.to_string()
75 }));
76 }
77
78 match state.owner_use_cases.create_owner(dto.into_inner()).await {
79 Ok(owner) => {
80 AuditLogEntry::new(
82 AuditEventType::OwnerCreated,
83 Some(user.user_id),
84 Some(organization_id),
85 )
86 .with_resource("Owner", Uuid::parse_str(&owner.id).unwrap())
87 .log();
88
89 HttpResponse::Created().json(owner)
90 }
91 Err(err) => {
92 AuditLogEntry::new(
94 AuditEventType::OwnerCreated,
95 Some(user.user_id),
96 Some(organization_id),
97 )
98 .with_error(err.clone())
99 .log();
100
101 HttpResponse::BadRequest().json(serde_json::json!({
102 "error": err
103 }))
104 }
105 }
106}
107
108#[get("/owners")]
109pub async fn list_owners(
110 state: web::Data<AppState>,
111 user: AuthenticatedUser,
112 page_request: web::Query<PageRequest>,
113) -> impl Responder {
114 let organization_id = if user.is_superadmin() {
116 None } else {
118 user.organization_id };
120
121 match state
122 .owner_use_cases
123 .list_owners_paginated(&page_request, organization_id)
124 .await
125 {
126 Ok((owners, total)) => {
127 let response =
128 PageResponse::new(owners, page_request.page, page_request.per_page, total);
129 HttpResponse::Ok().json(response)
130 }
131 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
132 "error": err
133 })),
134 }
135}
136
137#[get("/owners/me")]
141pub async fn get_my_owner(state: web::Data<AppState>, user: AuthenticatedUser) -> impl Responder {
142 let result = if let Some(org_id) = user.organization_id {
143 state
144 .owner_use_cases
145 .find_owner_by_user_id_and_organization(user.user_id, org_id)
146 .await
147 } else {
148 state
149 .owner_use_cases
150 .find_owner_by_user_id(user.user_id)
151 .await
152 };
153
154 match result {
155 Ok(Some(owner)) => HttpResponse::Ok().json(owner),
156 Ok(None) => HttpResponse::Ok().json(serde_json::Value::Null),
174 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
175 "error": err
176 })),
177 }
178}
179
180#[get("/owners/{id}")]
181pub async fn get_owner(
182 state: web::Data<AppState>,
183 user: AuthenticatedUser,
184 id: web::Path<Uuid>,
185) -> impl Responder {
186 match state.owner_use_cases.get_owner(*id).await {
187 Ok(Some(owner)) => {
188 if let Ok(owner_org) = Uuid::parse_str(&owner.organization_id) {
190 if let Err(e) = user.verify_org_access(owner_org) {
191 return HttpResponse::Forbidden().json(serde_json::json!({ "error": e }));
192 }
193 }
194 HttpResponse::Ok().json(owner)
195 }
196 Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
197 "error": "Owner not found"
198 })),
199 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
200 "error": err
201 })),
202 }
203}
204
205#[put("/owners/{id}")]
206pub async fn update_owner(
207 state: web::Data<AppState>,
208 user: AuthenticatedUser,
209 id: web::Path<Uuid>,
210 dto: web::Json<UpdateOwnerDto>,
211) -> impl Responder {
212 if user.role == "owner" || user.role == "accountant" {
214 return HttpResponse::Forbidden().json(serde_json::json!({
215 "error": "Only SuperAdmin and Syndic can update owners"
216 }));
217 }
218
219 let user_organization_id = if !user.is_superadmin() {
221 match user.require_organization() {
222 Ok(org_id) => Some(org_id),
223 Err(e) => {
224 return HttpResponse::Unauthorized().json(serde_json::json!({
225 "error": e.to_string()
226 }))
227 }
228 }
229 } else {
230 None };
232
233 if let Err(errors) = dto.validate() {
234 return HttpResponse::BadRequest().json(serde_json::json!({
235 "error": "Validation failed",
236 "details": errors.to_string()
237 }));
238 }
239
240 let owner_id = *id;
241
242 match state.owner_use_cases.get_owner(owner_id).await {
244 Ok(Some(_existing_owner)) => {
245 match state
249 .owner_use_cases
250 .update_owner(
251 owner_id,
252 dto.first_name.clone(),
253 dto.last_name.clone(),
254 dto.email.clone(),
255 dto.phone.clone(),
256 )
257 .await
258 {
259 Ok(owner) => {
260 AuditLogEntry::new(
262 AuditEventType::OwnerUpdated,
263 Some(user.user_id),
264 user_organization_id,
265 )
266 .with_resource("Owner", owner_id)
267 .log();
268
269 HttpResponse::Ok().json(owner)
270 }
271 Err(err) => {
272 AuditLogEntry::new(
274 AuditEventType::OwnerUpdated,
275 Some(user.user_id),
276 user_organization_id,
277 )
278 .with_error(err.clone())
279 .log();
280
281 HttpResponse::BadRequest().json(serde_json::json!({
282 "error": err
283 }))
284 }
285 }
286 }
287 Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
288 "error": "Owner not found"
289 })),
290 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
291 "error": err
292 })),
293 }
294}
295
296#[put("/owners/{id}/link-user")]
298pub async fn link_owner_to_user(
299 state: web::Data<AppState>,
300 user: AuthenticatedUser,
301 id: web::Path<Uuid>,
302 dto: web::Json<LinkOwnerUserDto>,
303) -> impl Responder {
304 if !user.is_superadmin() {
306 return HttpResponse::Forbidden().json(serde_json::json!({
307 "error": "Only SuperAdmin can link users to owners"
308 }));
309 }
310
311 let owner_id = *id;
312
313 let user_id_to_link = if let Some(user_id_str) = &dto.user_id {
315 if user_id_str.is_empty() {
316 None } else {
318 match Uuid::parse_str(user_id_str) {
319 Ok(uid) => Some(uid),
320 Err(_) => {
321 return HttpResponse::BadRequest().json(serde_json::json!({
322 "error": "Invalid user_id format"
323 }))
324 }
325 }
326 }
327 } else {
328 None };
330
331 let _owner = match state.owner_use_cases.get_owner(owner_id).await {
333 Ok(Some(o)) => o,
334 Ok(None) => {
335 return HttpResponse::NotFound().json(serde_json::json!({
336 "error": "Owner not found"
337 }))
338 }
339 Err(err) => {
340 return HttpResponse::InternalServerError().json(serde_json::json!({
341 "error": err
342 }))
343 }
344 };
345
346 if let Some(uid) = user_id_to_link {
348 match state
349 .user_use_cases
350 .validate_user_has_role(uid, "owner")
351 .await
352 {
353 Ok(()) => {}
354 Err(e) if e == "User not found" => {
355 return HttpResponse::NotFound().json(serde_json::json!({ "error": e }));
356 }
357 Err(e) => {
358 return HttpResponse::BadRequest().json(serde_json::json!({ "error": e }));
359 }
360 }
361
362 match state.owner_use_cases.find_owner_by_user_id(uid).await {
364 Ok(Some(existing)) if existing.id != owner_id.to_string() => {
365 return HttpResponse::Conflict().json(serde_json::json!({
366 "error": format!(
367 "User is already linked to owner {} {} (ID: {})",
368 existing.first_name, existing.last_name, existing.id
369 )
370 }));
371 }
372 Ok(_) => {} Err(err) => {
374 return HttpResponse::InternalServerError().json(serde_json::json!({
375 "error": format!("Database error: {}", err)
376 }));
377 }
378 }
379 }
380
381 match state
383 .owner_use_cases
384 .link_user_to_owner(owner_id, user_id_to_link)
385 .await
386 {
387 Ok(()) => {
388 AuditLogEntry::new(
389 AuditEventType::OwnerUpdated,
390 Some(user.user_id),
391 user.organization_id,
392 )
393 .with_resource("Owner", owner_id)
394 .log();
395
396 let action = if user_id_to_link.is_some() {
397 "linked"
398 } else {
399 "unlinked"
400 };
401
402 HttpResponse::Ok().json(serde_json::json!({
403 "message": format!("Owner successfully {} to user", action),
404 "owner_id": owner_id,
405 "user_id": user_id_to_link
406 }))
407 }
408 Err(err) => {
409 AuditLogEntry::new(
410 AuditEventType::OwnerUpdated,
411 Some(user.user_id),
412 user.organization_id,
413 )
414 .with_error(err.clone())
415 .log();
416
417 HttpResponse::InternalServerError().json(serde_json::json!({ "error": err }))
418 }
419 }
420}
421
422#[derive(Debug, Deserialize)]
428pub struct ExportStatementQuery {
429 pub building_id: Uuid,
430 pub start_date: String, pub end_date: String, }
433
434#[get("/owners/{id}/export-statement-pdf")]
435pub async fn export_owner_statement_pdf(
436 state: web::Data<AppState>,
437 user: AuthenticatedUser,
438 id: web::Path<Uuid>,
439 query: web::Query<ExportStatementQuery>,
440) -> impl Responder {
441 use crate::domain::entities::{Building, Expense, Owner, Unit};
442 use crate::domain::services::{OwnerStatementExporter, UnitWithOwnership};
443
444 let organization_id = match user.require_organization() {
445 Ok(org_id) => org_id,
446 Err(e) => {
447 return HttpResponse::Unauthorized().json(serde_json::json!({
448 "error": e.to_string()
449 }))
450 }
451 };
452
453 let owner_id = *id;
454 let building_id = query.building_id;
455
456 let start_date = match DateTime::parse_from_rfc3339(&query.start_date) {
458 Ok(dt) => dt.with_timezone(&Utc),
459 Err(_) => {
460 return HttpResponse::BadRequest().json(serde_json::json!({
461 "error": "Invalid start_date format. Use ISO8601 (e.g., 2025-01-01T00:00:00Z)"
462 }))
463 }
464 };
465
466 let end_date = match DateTime::parse_from_rfc3339(&query.end_date) {
467 Ok(dt) => dt.with_timezone(&Utc),
468 Err(_) => {
469 return HttpResponse::BadRequest().json(serde_json::json!({
470 "error": "Invalid end_date format. Use ISO8601 (e.g., 2025-12-31T23:59:59Z)"
471 }))
472 }
473 };
474
475 let owner_dto = match state.owner_use_cases.get_owner(owner_id).await {
477 Ok(Some(dto)) => dto,
478 Ok(None) => {
479 return HttpResponse::NotFound().json(serde_json::json!({
480 "error": "Owner not found"
481 }))
482 }
483 Err(err) => {
484 return HttpResponse::InternalServerError().json(serde_json::json!({
485 "error": err
486 }))
487 }
488 };
489
490 let building_dto = match state.building_use_cases.get_building(building_id).await {
492 Ok(Some(dto)) => dto,
493 Ok(None) => {
494 return HttpResponse::NotFound().json(serde_json::json!({
495 "error": "Building not found"
496 }))
497 }
498 Err(err) => {
499 return HttpResponse::InternalServerError().json(serde_json::json!({
500 "error": err
501 }))
502 }
503 };
504
505 let unit_owners = match state.unit_owner_use_cases.get_owner_units(owner_id).await {
507 Ok(units) => units,
508 Err(err) => {
509 return HttpResponse::InternalServerError().json(serde_json::json!({
510 "error": format!("Failed to get owner units: {}", err)
511 }))
512 }
513 };
514
515 let mut building_unit_owners = Vec::new();
517 for uo in unit_owners {
518 if let Ok(Some(unit_dto)) = state.unit_use_cases.get_unit(uo.unit_id).await {
519 if let Ok(unit_building_id) = Uuid::parse_str(&unit_dto.building_id) {
521 if unit_building_id == building_id {
522 building_unit_owners.push((uo, unit_dto));
523 }
524 }
525 }
526 }
527
528 if building_unit_owners.is_empty() {
529 return HttpResponse::BadRequest().json(serde_json::json!({
530 "error": "Owner does not own any units in this building"
531 }));
532 }
533
534 let expenses_dto = match state
536 .expense_use_cases
537 .list_expenses_by_building(building_id)
538 .await
539 {
540 Ok(expenses) => expenses,
541 Err(err) => {
542 return HttpResponse::InternalServerError().json(serde_json::json!({
543 "error": format!("Failed to get expenses: {}", err)
544 }))
545 }
546 };
547
548 let period_expenses: Vec<_> = expenses_dto
550 .into_iter()
551 .filter(|e| {
552 if let Ok(exp_date) = DateTime::parse_from_rfc3339(&e.expense_date) {
554 let exp_date_utc = exp_date.with_timezone(&Utc);
555 exp_date_utc >= start_date && exp_date_utc <= end_date
556 } else {
557 false
558 }
559 })
560 .collect();
561
562 let owner_entity = Owner {
564 id: Uuid::parse_str(&owner_dto.id).unwrap_or(owner_id),
565 organization_id: Uuid::parse_str(&owner_dto.organization_id).unwrap_or(organization_id),
566 first_name: owner_dto.first_name,
567 last_name: owner_dto.last_name,
568 email: owner_dto.email,
569 phone: owner_dto.phone,
570 address: owner_dto.address,
571 city: owner_dto.city,
572 postal_code: owner_dto.postal_code,
573 country: owner_dto.country,
574 user_id: owner_dto.user_id.and_then(|s| Uuid::parse_str(&s).ok()),
575 created_at: Utc::now(), updated_at: Utc::now(),
577 };
578
579 let building_acp_id = Uuid::parse_str(&building_dto.acp_id).unwrap_or_else(|_| Uuid::new_v4());
581
582 let building_created_at = DateTime::parse_from_rfc3339(&building_dto.created_at)
583 .map(|dt| dt.with_timezone(&Utc))
584 .unwrap_or_else(|_| Utc::now());
585
586 let building_updated_at = DateTime::parse_from_rfc3339(&building_dto.updated_at)
587 .map(|dt| dt.with_timezone(&Utc))
588 .unwrap_or_else(|_| Utc::now());
589
590 let building_entity = Building {
591 id: Uuid::parse_str(&building_dto.id).unwrap_or(building_id),
592 name: building_dto.name.clone(),
593 address: building_dto.address,
594 city: building_dto.city,
595 postal_code: building_dto.postal_code,
596 country: building_dto.country,
597 total_units: building_dto.total_units,
598 total_tantiemes: building_dto.total_tantiemes,
599 construction_year: building_dto.construction_year,
600 syndic_name: None,
601 syndic_email: None,
602 syndic_phone: None,
603 syndic_address: None,
604 syndic_office_hours: None,
605 syndic_emergency_contact: None,
606 slug: None,
607 acp_id: building_acp_id,
608 created_at: building_created_at,
609 updated_at: building_updated_at,
610 };
611
612 let mut units_with_ownership = Vec::new();
614 for (uo, unit_dto) in building_unit_owners {
615 let unit_entity = Unit {
616 id: Uuid::parse_str(&unit_dto.id).unwrap_or(uo.unit_id),
617 acp_id: building_acp_id,
618 building_id: Uuid::parse_str(&unit_dto.building_id).unwrap_or(building_id),
619 unit_number: unit_dto.unit_number,
620 floor: unit_dto.floor,
621 unit_type: unit_dto.unit_type,
622 surface_area: unit_dto.surface_area,
623 quota: unit_dto.quota,
624 owner_id: unit_dto.owner_id.and_then(|s| Uuid::parse_str(&s).ok()),
625 created_at: Utc::now(), updated_at: Utc::now(),
627 };
628
629 units_with_ownership.push(UnitWithOwnership {
630 unit: unit_entity,
631 ownership_percentage: uo.ownership_percentage,
632 });
633 }
634
635 let expense_entities: Vec<Expense> = period_expenses
637 .iter()
638 .filter_map(|e| {
639 let exp_id = Uuid::parse_str(&e.id).ok()?;
640 let bldg_id = Uuid::parse_str(&e.building_id).ok()?;
641 let exp_date = DateTime::parse_from_rfc3339(&e.expense_date)
642 .ok()?
643 .with_timezone(&Utc);
644
645 Some(Expense {
646 id: exp_id,
647 acp_id: Uuid::parse_str(&e.acp_id).ok()?,
648 organization_id,
649 building_id: bldg_id,
650 category: e.category.clone(),
651 description: e.description.clone(),
652 amount: e.amount,
653 amount_excl_vat: None,
654 vat_rate: None,
655 vat_amount: None,
656 amount_incl_vat: None,
657 expense_date: exp_date,
658 invoice_date: None,
659 due_date: None,
660 paid_date: None,
661 approval_status: e.approval_status.clone(),
662 submitted_at: None,
663 approved_by: None,
664 approved_at: None,
665 rejection_reason: None,
666 payment_status: e.payment_status.clone(),
667 supplier: e.supplier.clone(),
668 invoice_number: e.invoice_number.clone(),
669 account_code: e.account_code.clone(),
670 contractor_report_id: None,
671 created_at: Utc::now(),
672 updated_at: Utc::now(),
673 })
674 })
675 .collect();
676
677 match OwnerStatementExporter::export_to_pdf(
679 &owner_entity,
680 &building_entity,
681 &units_with_ownership,
682 &expense_entities,
683 start_date,
684 end_date,
685 ) {
686 Ok(pdf_bytes) => {
687 AuditLogEntry::new(
689 AuditEventType::ReportGenerated,
690 Some(user.user_id),
691 Some(organization_id),
692 )
693 .with_resource("Owner", owner_id)
694 .with_metadata(serde_json::json!({
695 "report_type": "owner_statement_pdf",
696 "building_id": building_id,
697 "building_name": building_entity.name,
698 "start_date": start_date.to_rfc3339(),
699 "end_date": end_date.to_rfc3339()
700 }))
701 .log();
702
703 HttpResponse::Ok()
704 .content_type("application/pdf")
705 .insert_header((
706 "Content-Disposition",
707 format!(
708 "attachment; filename=\"Releve_Charges_{}_{}_{}_{}.pdf\"",
709 owner_entity.last_name.replace(' ', "_"),
710 building_entity.name.replace(' ', "_"),
711 start_date.format("%Y%m%d"),
712 end_date.format("%Y%m%d")
713 ),
714 ))
715 .body(pdf_bytes)
716 }
717 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
718 "error": format!("Failed to generate PDF: {}", err)
719 })),
720 }
721}