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