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.role == "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.role == "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::NotFound().json(serde_json::json!({
157 "error": "No owner record linked to this user"
158 })),
159 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
160 "error": err
161 })),
162 }
163}
164
165#[get("/owners/{id}")]
166pub async fn get_owner(
167 state: web::Data<AppState>,
168 user: AuthenticatedUser,
169 id: web::Path<Uuid>,
170) -> impl Responder {
171 match state.owner_use_cases.get_owner(*id).await {
172 Ok(Some(owner)) => {
173 if let Ok(owner_org) = Uuid::parse_str(&owner.organization_id) {
175 if let Err(e) = user.verify_org_access(owner_org) {
176 return HttpResponse::Forbidden().json(serde_json::json!({ "error": e }));
177 }
178 }
179 HttpResponse::Ok().json(owner)
180 }
181 Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
182 "error": "Owner not found"
183 })),
184 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
185 "error": err
186 })),
187 }
188}
189
190#[put("/owners/{id}")]
191pub async fn update_owner(
192 state: web::Data<AppState>,
193 user: AuthenticatedUser,
194 id: web::Path<Uuid>,
195 dto: web::Json<UpdateOwnerDto>,
196) -> impl Responder {
197 if user.role == "owner" || user.role == "accountant" {
199 return HttpResponse::Forbidden().json(serde_json::json!({
200 "error": "Only SuperAdmin and Syndic can update owners"
201 }));
202 }
203
204 let user_organization_id = if user.role != "superadmin" {
206 match user.require_organization() {
207 Ok(org_id) => Some(org_id),
208 Err(e) => {
209 return HttpResponse::Unauthorized().json(serde_json::json!({
210 "error": e.to_string()
211 }))
212 }
213 }
214 } else {
215 None };
217
218 if let Err(errors) = dto.validate() {
219 return HttpResponse::BadRequest().json(serde_json::json!({
220 "error": "Validation failed",
221 "details": errors.to_string()
222 }));
223 }
224
225 let owner_id = *id;
226
227 match state.owner_use_cases.get_owner(owner_id).await {
229 Ok(Some(_existing_owner)) => {
230 match state
234 .owner_use_cases
235 .update_owner(
236 owner_id,
237 dto.first_name.clone(),
238 dto.last_name.clone(),
239 dto.email.clone(),
240 dto.phone.clone(),
241 )
242 .await
243 {
244 Ok(owner) => {
245 AuditLogEntry::new(
247 AuditEventType::OwnerUpdated,
248 Some(user.user_id),
249 user_organization_id,
250 )
251 .with_resource("Owner", owner_id)
252 .log();
253
254 HttpResponse::Ok().json(owner)
255 }
256 Err(err) => {
257 AuditLogEntry::new(
259 AuditEventType::OwnerUpdated,
260 Some(user.user_id),
261 user_organization_id,
262 )
263 .with_error(err.clone())
264 .log();
265
266 HttpResponse::BadRequest().json(serde_json::json!({
267 "error": err
268 }))
269 }
270 }
271 }
272 Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
273 "error": "Owner not found"
274 })),
275 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
276 "error": err
277 })),
278 }
279}
280
281#[put("/owners/{id}/link-user")]
283pub async fn link_owner_to_user(
284 state: web::Data<AppState>,
285 user: AuthenticatedUser,
286 id: web::Path<Uuid>,
287 dto: web::Json<LinkOwnerUserDto>,
288) -> impl Responder {
289 if user.role != "superadmin" {
291 return HttpResponse::Forbidden().json(serde_json::json!({
292 "error": "Only SuperAdmin can link users to owners"
293 }));
294 }
295
296 let owner_id = *id;
297
298 let user_id_to_link = if let Some(user_id_str) = &dto.user_id {
300 if user_id_str.is_empty() {
301 None } else {
303 match Uuid::parse_str(user_id_str) {
304 Ok(uid) => Some(uid),
305 Err(_) => {
306 return HttpResponse::BadRequest().json(serde_json::json!({
307 "error": "Invalid user_id format"
308 }))
309 }
310 }
311 }
312 } else {
313 None };
315
316 let _owner = match state.owner_use_cases.get_owner(owner_id).await {
318 Ok(Some(o)) => o,
319 Ok(None) => {
320 return HttpResponse::NotFound().json(serde_json::json!({
321 "error": "Owner not found"
322 }))
323 }
324 Err(err) => {
325 return HttpResponse::InternalServerError().json(serde_json::json!({
326 "error": err
327 }))
328 }
329 };
330
331 if let Some(uid) = user_id_to_link {
333 let user_check = sqlx::query!("SELECT id FROM users WHERE id = $1", uid)
335 .fetch_optional(&state.pool)
336 .await;
337
338 match user_check {
339 Ok(Some(_user_record)) => {
340 let role_check = sqlx::query!(
342 "SELECT COUNT(*) as count FROM user_roles WHERE user_id = $1 AND role = $2",
343 uid,
344 "owner"
345 )
346 .fetch_one(&state.pool)
347 .await;
348
349 match role_check {
350 Ok(record) => {
351 if record.count.unwrap_or(0) == 0 {
352 return HttpResponse::BadRequest().json(serde_json::json!({
353 "error": "User must have role 'owner' to be linked to an owner entity"
354 }));
355 }
356 }
357 Err(err) => {
358 return HttpResponse::InternalServerError().json(serde_json::json!({
359 "error": format!("Database error checking roles: {}", err)
360 }));
361 }
362 }
363 }
364 Ok(None) => {
365 return HttpResponse::NotFound().json(serde_json::json!({
366 "error": "User not found"
367 }));
368 }
369 Err(err) => {
370 return HttpResponse::InternalServerError().json(serde_json::json!({
371 "error": format!("Database error: {}", err)
372 }));
373 }
374 }
375
376 let existing_link = sqlx::query!(
378 "SELECT id, first_name, last_name FROM owners WHERE user_id = $1 AND id != $2",
379 uid,
380 owner_id
381 )
382 .fetch_optional(&state.pool)
383 .await;
384
385 match existing_link {
386 Ok(Some(existing)) => {
387 return HttpResponse::Conflict().json(serde_json::json!({
388 "error": format!("User is already linked to owner {} {} (ID: {})",
389 existing.first_name, existing.last_name, existing.id)
390 }));
391 }
392 Ok(None) => {} Err(err) => {
394 return HttpResponse::InternalServerError().json(serde_json::json!({
395 "error": format!("Database error: {}", err)
396 }));
397 }
398 }
399 }
400
401 let update_result = sqlx::query!(
403 "UPDATE owners SET user_id = $1, updated_at = NOW() WHERE id = $2",
404 user_id_to_link,
405 owner_id
406 )
407 .execute(&state.pool)
408 .await;
409
410 match update_result {
411 Ok(_) => {
412 AuditLogEntry::new(
414 AuditEventType::OwnerUpdated,
415 Some(user.user_id),
416 user.organization_id,
417 )
418 .with_resource("Owner", owner_id)
419 .log();
420
421 let action = if user_id_to_link.is_some() {
422 "linked"
423 } else {
424 "unlinked"
425 };
426
427 HttpResponse::Ok().json(serde_json::json!({
428 "message": format!("Owner successfully {} to user", action),
429 "owner_id": owner_id,
430 "user_id": user_id_to_link
431 }))
432 }
433 Err(err) => {
434 AuditLogEntry::new(
436 AuditEventType::OwnerUpdated,
437 Some(user.user_id),
438 user.organization_id,
439 )
440 .with_error(err.to_string())
441 .log();
442
443 HttpResponse::InternalServerError().json(serde_json::json!({
444 "error": format!("Database error: {}", err)
445 }))
446 }
447 }
448}
449
450#[derive(Debug, Deserialize)]
456pub struct ExportStatementQuery {
457 pub building_id: Uuid,
458 pub start_date: String, pub end_date: String, }
461
462#[get("/owners/{id}/export-statement-pdf")]
463pub async fn export_owner_statement_pdf(
464 state: web::Data<AppState>,
465 user: AuthenticatedUser,
466 id: web::Path<Uuid>,
467 query: web::Query<ExportStatementQuery>,
468) -> impl Responder {
469 use crate::domain::entities::{Building, Expense, Owner, Unit};
470 use crate::domain::services::{OwnerStatementExporter, UnitWithOwnership};
471
472 let organization_id = match user.require_organization() {
473 Ok(org_id) => org_id,
474 Err(e) => {
475 return HttpResponse::Unauthorized().json(serde_json::json!({
476 "error": e.to_string()
477 }))
478 }
479 };
480
481 let owner_id = *id;
482 let building_id = query.building_id;
483
484 let start_date = match DateTime::parse_from_rfc3339(&query.start_date) {
486 Ok(dt) => dt.with_timezone(&Utc),
487 Err(_) => {
488 return HttpResponse::BadRequest().json(serde_json::json!({
489 "error": "Invalid start_date format. Use ISO8601 (e.g., 2025-01-01T00:00:00Z)"
490 }))
491 }
492 };
493
494 let end_date = match DateTime::parse_from_rfc3339(&query.end_date) {
495 Ok(dt) => dt.with_timezone(&Utc),
496 Err(_) => {
497 return HttpResponse::BadRequest().json(serde_json::json!({
498 "error": "Invalid end_date format. Use ISO8601 (e.g., 2025-12-31T23:59:59Z)"
499 }))
500 }
501 };
502
503 let owner_dto = match state.owner_use_cases.get_owner(owner_id).await {
505 Ok(Some(dto)) => dto,
506 Ok(None) => {
507 return HttpResponse::NotFound().json(serde_json::json!({
508 "error": "Owner not found"
509 }))
510 }
511 Err(err) => {
512 return HttpResponse::InternalServerError().json(serde_json::json!({
513 "error": err
514 }))
515 }
516 };
517
518 let building_dto = match state.building_use_cases.get_building(building_id).await {
520 Ok(Some(dto)) => dto,
521 Ok(None) => {
522 return HttpResponse::NotFound().json(serde_json::json!({
523 "error": "Building not found"
524 }))
525 }
526 Err(err) => {
527 return HttpResponse::InternalServerError().json(serde_json::json!({
528 "error": err
529 }))
530 }
531 };
532
533 let unit_owners = match state.unit_owner_use_cases.get_owner_units(owner_id).await {
535 Ok(units) => units,
536 Err(err) => {
537 return HttpResponse::InternalServerError().json(serde_json::json!({
538 "error": format!("Failed to get owner units: {}", err)
539 }))
540 }
541 };
542
543 let mut building_unit_owners = Vec::new();
545 for uo in unit_owners {
546 if let Ok(Some(unit_dto)) = state.unit_use_cases.get_unit(uo.unit_id).await {
547 if let Ok(unit_building_id) = Uuid::parse_str(&unit_dto.building_id) {
549 if unit_building_id == building_id {
550 building_unit_owners.push((uo, unit_dto));
551 }
552 }
553 }
554 }
555
556 if building_unit_owners.is_empty() {
557 return HttpResponse::BadRequest().json(serde_json::json!({
558 "error": "Owner does not own any units in this building"
559 }));
560 }
561
562 let expenses_dto = match state
564 .expense_use_cases
565 .list_expenses_by_building(building_id)
566 .await
567 {
568 Ok(expenses) => expenses,
569 Err(err) => {
570 return HttpResponse::InternalServerError().json(serde_json::json!({
571 "error": format!("Failed to get expenses: {}", err)
572 }))
573 }
574 };
575
576 let period_expenses: Vec<_> = expenses_dto
578 .into_iter()
579 .filter(|e| {
580 if let Ok(exp_date) = DateTime::parse_from_rfc3339(&e.expense_date) {
582 let exp_date_utc = exp_date.with_timezone(&Utc);
583 exp_date_utc >= start_date && exp_date_utc <= end_date
584 } else {
585 false
586 }
587 })
588 .collect();
589
590 let owner_entity = Owner {
592 id: Uuid::parse_str(&owner_dto.id).unwrap_or(owner_id),
593 organization_id: Uuid::parse_str(&owner_dto.organization_id).unwrap_or(organization_id),
594 first_name: owner_dto.first_name,
595 last_name: owner_dto.last_name,
596 email: owner_dto.email,
597 phone: owner_dto.phone,
598 address: owner_dto.address,
599 city: owner_dto.city,
600 postal_code: owner_dto.postal_code,
601 country: owner_dto.country,
602 user_id: owner_dto.user_id.and_then(|s| Uuid::parse_str(&s).ok()),
603 created_at: Utc::now(), updated_at: Utc::now(),
605 };
606
607 let building_org_id = Uuid::parse_str(&building_dto.organization_id).unwrap_or(organization_id);
608
609 let building_created_at = DateTime::parse_from_rfc3339(&building_dto.created_at)
610 .map(|dt| dt.with_timezone(&Utc))
611 .unwrap_or_else(|_| Utc::now());
612
613 let building_updated_at = DateTime::parse_from_rfc3339(&building_dto.updated_at)
614 .map(|dt| dt.with_timezone(&Utc))
615 .unwrap_or_else(|_| Utc::now());
616
617 let building_entity = Building {
618 id: Uuid::parse_str(&building_dto.id).unwrap_or(building_id),
619 name: building_dto.name.clone(),
620 address: building_dto.address,
621 city: building_dto.city,
622 postal_code: building_dto.postal_code,
623 country: building_dto.country,
624 total_units: building_dto.total_units,
625 total_tantiemes: building_dto.total_tantiemes,
626 construction_year: building_dto.construction_year,
627 syndic_name: None,
628 syndic_email: None,
629 syndic_phone: None,
630 syndic_address: None,
631 syndic_office_hours: None,
632 syndic_emergency_contact: None,
633 slug: None,
634 organization_id: building_org_id,
635 created_at: building_created_at,
636 updated_at: building_updated_at,
637 };
638
639 let mut units_with_ownership = Vec::new();
641 for (uo, unit_dto) in building_unit_owners {
642 let unit_entity = Unit {
643 id: Uuid::parse_str(&unit_dto.id).unwrap_or(uo.unit_id),
644 organization_id,
645 building_id: Uuid::parse_str(&unit_dto.building_id).unwrap_or(building_id),
646 unit_number: unit_dto.unit_number,
647 floor: unit_dto.floor,
648 unit_type: unit_dto.unit_type,
649 surface_area: unit_dto.surface_area,
650 quota: unit_dto.quota,
651 owner_id: unit_dto.owner_id.and_then(|s| Uuid::parse_str(&s).ok()),
652 created_at: Utc::now(), updated_at: Utc::now(),
654 };
655
656 units_with_ownership.push(UnitWithOwnership {
657 unit: unit_entity,
658 ownership_percentage: uo.ownership_percentage,
659 });
660 }
661
662 let expense_entities: Vec<Expense> = period_expenses
664 .iter()
665 .filter_map(|e| {
666 let exp_id = Uuid::parse_str(&e.id).ok()?;
667 let bldg_id = Uuid::parse_str(&e.building_id).ok()?;
668 let exp_date = DateTime::parse_from_rfc3339(&e.expense_date)
669 .ok()?
670 .with_timezone(&Utc);
671
672 Some(Expense {
673 id: exp_id,
674 organization_id,
675 building_id: bldg_id,
676 category: e.category.clone(),
677 description: e.description.clone(),
678 amount: e.amount,
679 amount_excl_vat: None,
680 vat_rate: None,
681 vat_amount: None,
682 amount_incl_vat: None,
683 expense_date: exp_date,
684 invoice_date: None,
685 due_date: None,
686 paid_date: None,
687 approval_status: e.approval_status.clone(),
688 submitted_at: None,
689 approved_by: None,
690 approved_at: None,
691 rejection_reason: None,
692 payment_status: e.payment_status.clone(),
693 supplier: e.supplier.clone(),
694 invoice_number: e.invoice_number.clone(),
695 account_code: e.account_code.clone(),
696 contractor_report_id: None,
697 created_at: Utc::now(),
698 updated_at: Utc::now(),
699 })
700 })
701 .collect();
702
703 match OwnerStatementExporter::export_to_pdf(
705 &owner_entity,
706 &building_entity,
707 &units_with_ownership,
708 &expense_entities,
709 start_date,
710 end_date,
711 ) {
712 Ok(pdf_bytes) => {
713 AuditLogEntry::new(
715 AuditEventType::ReportGenerated,
716 Some(user.user_id),
717 Some(organization_id),
718 )
719 .with_resource("Owner", owner_id)
720 .with_metadata(serde_json::json!({
721 "report_type": "owner_statement_pdf",
722 "building_id": building_id,
723 "building_name": building_entity.name,
724 "start_date": start_date.to_rfc3339(),
725 "end_date": end_date.to_rfc3339()
726 }))
727 .log();
728
729 HttpResponse::Ok()
730 .content_type("application/pdf")
731 .insert_header((
732 "Content-Disposition",
733 format!(
734 "attachment; filename=\"Releve_Charges_{}_{}_{}_{}.pdf\"",
735 owner_entity.last_name.replace(' ', "_"),
736 building_entity.name.replace(' ', "_"),
737 start_date.format("%Y%m%d"),
738 end_date.format("%Y%m%d")
739 ),
740 ))
741 .body(pdf_bytes)
742 }
743 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
744 "error": format!("Failed to generate PDF: {}", err)
745 })),
746 }
747}