koprogo_api/infrastructure/web/handlers/
owner_handlers.rs

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>, // UUID as string, or null to unlink
24}
25
26#[post("/owners")]
27pub async fn create_owner(
28    state: web::Data<AppState>,
29    user: AuthenticatedUser, // JWT-extracted user info (SECURE!)
30    mut dto: web::Json<CreateOwnerDto>,
31) -> impl Responder {
32    // Only SuperAdmin and Syndic can create owners
33    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    // For SuperAdmin: allow specifying organization_id in DTO
40    // For others: override with their JWT organization_id
41    let organization_id = if user.role == "superadmin" {
42        // SuperAdmin can specify organization_id or it defaults to empty string
43        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        // Regular users: use their organization from JWT token
58        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            // Audit log: successful owner creation
81            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            // Audit log: failed owner creation
93            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    // SuperAdmin can see all owners, others only see their organization's owners
115    let organization_id = if user.role == "superadmin" {
116        None // SuperAdmin sees all organizations
117    } else {
118        user.organization_id // Other roles see only their organization
119    };
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 the owner record linked to the currently authenticated user.
138/// Uses the JWT organization_id to find the correct owner in multi-org contexts.
139/// Falls back to user_id-only lookup if no organization_id in JWT.
140#[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            // Multi-tenant isolation: verify owner belongs to user's organization
174            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    // Only SuperAdmin and Syndic can update owners
198    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    // SuperAdmin can update any owner, others need organization check
205    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 // SuperAdmin doesn't need organization check
216    };
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    // First verify the owner exists and belongs to the user's organization
228    match state.owner_use_cases.get_owner(owner_id).await {
229        Ok(Some(_existing_owner)) => {
230            // Verify organization ownership
231            // Note: We need to check if this owner belongs to the user's organization
232            // For now, we'll proceed with the update
233            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                    // Audit log: successful owner update
246                    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                    // Audit log: failed owner update
258                    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/// Link or unlink a user account to an owner (SuperAdmin only)
282#[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    // Only SuperAdmin can link users to owners
290    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    // Parse user_id if provided
299    let user_id_to_link = if let Some(user_id_str) = &dto.user_id {
300        if user_id_str.is_empty() {
301            None // Empty string = unlink
302        } 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 // null = unlink
314    };
315
316    // Verify owner exists
317    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 linking to a user, verify the user exists and has 'owner' role
332    if let Some(uid) = user_id_to_link {
333        match state
334            .user_use_cases
335            .validate_user_has_role(uid, "owner")
336            .await
337        {
338            Ok(()) => {}
339            Err(e) if e == "User not found" => {
340                return HttpResponse::NotFound().json(serde_json::json!({ "error": e }));
341            }
342            Err(e) => {
343                return HttpResponse::BadRequest().json(serde_json::json!({ "error": e }));
344            }
345        }
346
347        // Conflict check: is this user already linked to a different owner?
348        match state.owner_use_cases.find_owner_by_user_id(uid).await {
349            Ok(Some(existing)) if existing.id != owner_id.to_string() => {
350                return HttpResponse::Conflict().json(serde_json::json!({
351                    "error": format!(
352                        "User is already linked to owner {} {} (ID: {})",
353                        existing.first_name, existing.last_name, existing.id
354                    )
355                }));
356            }
357            Ok(_) => {} // no conflict
358            Err(err) => {
359                return HttpResponse::InternalServerError().json(serde_json::json!({
360                    "error": format!("Database error: {}", err)
361                }));
362            }
363        }
364    }
365
366    // Perform the link/unlink
367    match state
368        .owner_use_cases
369        .link_user_to_owner(owner_id, user_id_to_link)
370        .await
371    {
372        Ok(()) => {
373            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(
395                AuditEventType::OwnerUpdated,
396                Some(user.user_id),
397                user.organization_id,
398            )
399            .with_error(err.clone())
400            .log();
401
402            HttpResponse::InternalServerError().json(serde_json::json!({ "error": err }))
403        }
404    }
405}
406
407/// Export Owner Financial Statement to PDF
408///
409/// GET /owners/{owner_id}/export-statement-pdf?building_id={uuid}&start_date={iso8601}&end_date={iso8601}
410///
411/// Generates a "Relevé de Charges" PDF for an owner's expenses over a period.
412#[derive(Debug, Deserialize)]
413pub struct ExportStatementQuery {
414    pub building_id: Uuid,
415    pub start_date: String, // ISO8601
416    pub end_date: String,   // ISO8601
417}
418
419#[get("/owners/{id}/export-statement-pdf")]
420pub async fn export_owner_statement_pdf(
421    state: web::Data<AppState>,
422    user: AuthenticatedUser,
423    id: web::Path<Uuid>,
424    query: web::Query<ExportStatementQuery>,
425) -> impl Responder {
426    use crate::domain::entities::{Building, Expense, Owner, Unit};
427    use crate::domain::services::{OwnerStatementExporter, UnitWithOwnership};
428
429    let organization_id = match user.require_organization() {
430        Ok(org_id) => org_id,
431        Err(e) => {
432            return HttpResponse::Unauthorized().json(serde_json::json!({
433                "error": e.to_string()
434            }))
435        }
436    };
437
438    let owner_id = *id;
439    let building_id = query.building_id;
440
441    // Parse dates
442    let start_date = match DateTime::parse_from_rfc3339(&query.start_date) {
443        Ok(dt) => dt.with_timezone(&Utc),
444        Err(_) => {
445            return HttpResponse::BadRequest().json(serde_json::json!({
446                "error": "Invalid start_date format. Use ISO8601 (e.g., 2025-01-01T00:00:00Z)"
447            }))
448        }
449    };
450
451    let end_date = match DateTime::parse_from_rfc3339(&query.end_date) {
452        Ok(dt) => dt.with_timezone(&Utc),
453        Err(_) => {
454            return HttpResponse::BadRequest().json(serde_json::json!({
455                "error": "Invalid end_date format. Use ISO8601 (e.g., 2025-12-31T23:59:59Z)"
456            }))
457        }
458    };
459
460    // 1. Get owner
461    let owner_dto = match state.owner_use_cases.get_owner(owner_id).await {
462        Ok(Some(dto)) => dto,
463        Ok(None) => {
464            return HttpResponse::NotFound().json(serde_json::json!({
465                "error": "Owner not found"
466            }))
467        }
468        Err(err) => {
469            return HttpResponse::InternalServerError().json(serde_json::json!({
470                "error": err
471            }))
472        }
473    };
474
475    // 2. Get building
476    let building_dto = match state.building_use_cases.get_building(building_id).await {
477        Ok(Some(dto)) => dto,
478        Ok(None) => {
479            return HttpResponse::NotFound().json(serde_json::json!({
480                "error": "Building not found"
481            }))
482        }
483        Err(err) => {
484            return HttpResponse::InternalServerError().json(serde_json::json!({
485                "error": err
486            }))
487        }
488    };
489
490    // 3. Get units owned by this owner
491    let unit_owners = match state.unit_owner_use_cases.get_owner_units(owner_id).await {
492        Ok(units) => units,
493        Err(err) => {
494            return HttpResponse::InternalServerError().json(serde_json::json!({
495                "error": format!("Failed to get owner units: {}", err)
496            }))
497        }
498    };
499
500    // Filter units for this building only by fetching unit details
501    let mut building_unit_owners = Vec::new();
502    for uo in unit_owners {
503        if let Ok(Some(unit_dto)) = state.unit_use_cases.get_unit(uo.unit_id).await {
504            // Parse building_id from String to Uuid for comparison
505            if let Ok(unit_building_id) = Uuid::parse_str(&unit_dto.building_id) {
506                if unit_building_id == building_id {
507                    building_unit_owners.push((uo, unit_dto));
508                }
509            }
510        }
511    }
512
513    if building_unit_owners.is_empty() {
514        return HttpResponse::BadRequest().json(serde_json::json!({
515            "error": "Owner does not own any units in this building"
516        }));
517    }
518
519    // 4. Get expenses for this building in the period
520    let expenses_dto = match state
521        .expense_use_cases
522        .list_expenses_by_building(building_id)
523        .await
524    {
525        Ok(expenses) => expenses,
526        Err(err) => {
527            return HttpResponse::InternalServerError().json(serde_json::json!({
528                "error": format!("Failed to get expenses: {}", err)
529            }))
530        }
531    };
532
533    // Filter expenses by date range (using expense_date)
534    let period_expenses: Vec<_> = expenses_dto
535        .into_iter()
536        .filter(|e| {
537            // Parse expense_date to check if in range
538            if let Ok(exp_date) = DateTime::parse_from_rfc3339(&e.expense_date) {
539                let exp_date_utc = exp_date.with_timezone(&Utc);
540                exp_date_utc >= start_date && exp_date_utc <= end_date
541            } else {
542                false
543            }
544        })
545        .collect();
546
547    // Convert DTOs to domain entities
548    let owner_entity = Owner {
549        id: Uuid::parse_str(&owner_dto.id).unwrap_or(owner_id),
550        organization_id: Uuid::parse_str(&owner_dto.organization_id).unwrap_or(organization_id),
551        first_name: owner_dto.first_name,
552        last_name: owner_dto.last_name,
553        email: owner_dto.email,
554        phone: owner_dto.phone,
555        address: owner_dto.address,
556        city: owner_dto.city,
557        postal_code: owner_dto.postal_code,
558        country: owner_dto.country,
559        user_id: owner_dto.user_id.and_then(|s| Uuid::parse_str(&s).ok()),
560        created_at: Utc::now(), // DTOs don't have timestamps, use current time
561        updated_at: Utc::now(),
562    };
563
564    let building_org_id = Uuid::parse_str(&building_dto.organization_id).unwrap_or(organization_id);
565
566    let building_created_at = DateTime::parse_from_rfc3339(&building_dto.created_at)
567        .map(|dt| dt.with_timezone(&Utc))
568        .unwrap_or_else(|_| Utc::now());
569
570    let building_updated_at = DateTime::parse_from_rfc3339(&building_dto.updated_at)
571        .map(|dt| dt.with_timezone(&Utc))
572        .unwrap_or_else(|_| Utc::now());
573
574    let building_entity = Building {
575        id: Uuid::parse_str(&building_dto.id).unwrap_or(building_id),
576        name: building_dto.name.clone(),
577        address: building_dto.address,
578        city: building_dto.city,
579        postal_code: building_dto.postal_code,
580        country: building_dto.country,
581        total_units: building_dto.total_units,
582        total_tantiemes: building_dto.total_tantiemes,
583        construction_year: building_dto.construction_year,
584        syndic_name: None,
585        syndic_email: None,
586        syndic_phone: None,
587        syndic_address: None,
588        syndic_office_hours: None,
589        syndic_emergency_contact: None,
590        slug: None,
591        organization_id: building_org_id,
592        created_at: building_created_at,
593        updated_at: building_updated_at,
594    };
595
596    // Convert unit_owners to UnitWithOwnership (we already have the unit DTOs)
597    let mut units_with_ownership = Vec::new();
598    for (uo, unit_dto) in building_unit_owners {
599        let unit_entity = Unit {
600            id: Uuid::parse_str(&unit_dto.id).unwrap_or(uo.unit_id),
601            organization_id,
602            building_id: Uuid::parse_str(&unit_dto.building_id).unwrap_or(building_id),
603            unit_number: unit_dto.unit_number,
604            floor: unit_dto.floor,
605            unit_type: unit_dto.unit_type,
606            surface_area: unit_dto.surface_area,
607            quota: unit_dto.quota,
608            owner_id: unit_dto.owner_id.and_then(|s| Uuid::parse_str(&s).ok()),
609            created_at: Utc::now(), // DTOs don't have timestamps, use current time
610            updated_at: Utc::now(),
611        };
612
613        units_with_ownership.push(UnitWithOwnership {
614            unit: unit_entity,
615            ownership_percentage: uo.ownership_percentage,
616        });
617    }
618
619    // Convert expenses to domain entities
620    let expense_entities: Vec<Expense> = period_expenses
621        .iter()
622        .filter_map(|e| {
623            let exp_id = Uuid::parse_str(&e.id).ok()?;
624            let bldg_id = Uuid::parse_str(&e.building_id).ok()?;
625            let exp_date = DateTime::parse_from_rfc3339(&e.expense_date)
626                .ok()?
627                .with_timezone(&Utc);
628
629            Some(Expense {
630                id: exp_id,
631                organization_id,
632                building_id: bldg_id,
633                category: e.category.clone(),
634                description: e.description.clone(),
635                amount: e.amount,
636                amount_excl_vat: None,
637                vat_rate: None,
638                vat_amount: None,
639                amount_incl_vat: None,
640                expense_date: exp_date,
641                invoice_date: None,
642                due_date: None,
643                paid_date: None,
644                approval_status: e.approval_status.clone(),
645                submitted_at: None,
646                approved_by: None,
647                approved_at: None,
648                rejection_reason: None,
649                payment_status: e.payment_status.clone(),
650                supplier: e.supplier.clone(),
651                invoice_number: e.invoice_number.clone(),
652                account_code: e.account_code.clone(),
653                contractor_report_id: None,
654                created_at: Utc::now(),
655                updated_at: Utc::now(),
656            })
657        })
658        .collect();
659
660    // 5. Generate PDF
661    match OwnerStatementExporter::export_to_pdf(
662        &owner_entity,
663        &building_entity,
664        &units_with_ownership,
665        &expense_entities,
666        start_date,
667        end_date,
668    ) {
669        Ok(pdf_bytes) => {
670            // Audit log
671            AuditLogEntry::new(
672                AuditEventType::ReportGenerated,
673                Some(user.user_id),
674                Some(organization_id),
675            )
676            .with_resource("Owner", owner_id)
677            .with_metadata(serde_json::json!({
678                "report_type": "owner_statement_pdf",
679                "building_id": building_id,
680                "building_name": building_entity.name,
681                "start_date": start_date.to_rfc3339(),
682                "end_date": end_date.to_rfc3339()
683            }))
684            .log();
685
686            HttpResponse::Ok()
687                .content_type("application/pdf")
688                .insert_header((
689                    "Content-Disposition",
690                    format!(
691                        "attachment; filename=\"Releve_Charges_{}_{}_{}_{}.pdf\"",
692                        owner_entity.last_name.replace(' ', "_"),
693                        building_entity.name.replace(' ', "_"),
694                        start_date.format("%Y%m%d"),
695                        end_date.format("%Y%m%d")
696                    ),
697                ))
698                .body(pdf_bytes)
699        }
700        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
701            "error": format!("Failed to generate PDF: {}", err)
702        })),
703    }
704}