1use crate::application::dto::{CreateBuildingDto, PageRequest, PageResponse, UpdateBuildingDto};
2use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
3use crate::infrastructure::web::{AppState, AuthenticatedUser};
4use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
5use chrono::{DateTime, Datelike, Utc};
6use serde::Deserialize;
7use uuid::Uuid;
8use validator::Validate;
9
10#[utoipa::path(
11 post,
12 path = "/buildings",
13 tag = "Buildings",
14 summary = "Create a building",
15 request_body = CreateBuildingDto,
16 responses(
17 (status = 201, description = "Building created successfully"),
18 (status = 400, description = "Bad Request"),
19 (status = 403, description = "Forbidden - SuperAdmin only"),
20 (status = 500, description = "Internal Server Error"),
21 ),
22 security(("bearer_auth" = []))
23)]
24#[post("/buildings")]
25pub async fn create_building(
26 state: web::Data<AppState>,
27 user: AuthenticatedUser, mut dto: web::Json<CreateBuildingDto>,
29) -> impl Responder {
30 if user.role != "superadmin" {
32 return HttpResponse::Forbidden().json(serde_json::json!({
33 "error": "Only SuperAdmin can create buildings (structural data cannot be modified after creation)"
34 }));
35 }
36
37 let organization_id: Uuid;
40
41 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 organization_id = match Uuid::parse_str(&dto.organization_id) {
50 Ok(id) => id,
51 Err(_) => {
52 return HttpResponse::BadRequest().json(serde_json::json!({
53 "error": "Invalid organization_id format"
54 }));
55 }
56 };
57 } else {
58 organization_id = match user.require_organization() {
61 Ok(org_id) => org_id,
62 Err(e) => {
63 return HttpResponse::Unauthorized().json(serde_json::json!({
64 "error": e.to_string()
65 }))
66 }
67 };
68 dto.organization_id = organization_id.to_string();
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
79 .building_use_cases
80 .create_building(dto.into_inner())
81 .await
82 {
83 Ok(building) => {
84 AuditLogEntry::new(
86 AuditEventType::BuildingCreated,
87 Some(user.user_id),
88 Some(organization_id),
89 )
90 .with_resource("Building", Uuid::parse_str(&building.id).unwrap())
91 .log();
92
93 HttpResponse::Created().json(building)
94 }
95 Err(err) => {
96 AuditLogEntry::new(
98 AuditEventType::BuildingCreated,
99 Some(user.user_id),
100 Some(organization_id),
101 )
102 .with_error(err.clone())
103 .log();
104
105 HttpResponse::BadRequest().json(serde_json::json!({
106 "error": err
107 }))
108 }
109 }
110}
111
112#[utoipa::path(
113 get,
114 path = "/buildings",
115 tag = "Buildings",
116 summary = "List buildings (paginated)",
117 params(PageRequest),
118 responses(
119 (status = 200, description = "Paginated list of buildings"),
120 (status = 500, description = "Internal Server Error"),
121 ),
122 security(("bearer_auth" = []))
123)]
124#[get("/buildings")]
125pub async fn list_buildings(
126 state: web::Data<AppState>,
127 user: AuthenticatedUser,
128 page_request: web::Query<PageRequest>,
129) -> impl Responder {
130 let organization_id = user.organization_id;
132
133 let owner_user_id = if user.role == "owner" {
136 Some(user.user_id)
137 } else {
138 None
139 };
140
141 match state
142 .building_use_cases
143 .list_buildings_paginated_for_user(&page_request, organization_id, owner_user_id)
144 .await
145 {
146 Ok((buildings, total)) => {
147 let response =
148 PageResponse::new(buildings, page_request.page, page_request.per_page, total);
149 HttpResponse::Ok().json(response)
150 }
151 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
152 "error": err
153 })),
154 }
155}
156
157#[utoipa::path(
158 get,
159 path = "/buildings/{id}",
160 tag = "Buildings",
161 summary = "Get a building by ID",
162 params(
163 ("id" = Uuid, Path, description = "Building UUID")
164 ),
165 responses(
166 (status = 200, description = "Building found"),
167 (status = 404, description = "Building not found"),
168 (status = 500, description = "Internal Server Error"),
169 ),
170 security(("bearer_auth" = []))
171)]
172#[get("/buildings/{id}")]
173pub async fn get_building(
174 state: web::Data<AppState>,
175 user: AuthenticatedUser,
176 id: web::Path<Uuid>,
177) -> impl Responder {
178 match state.building_use_cases.get_building(*id).await {
179 Ok(Some(building)) => {
180 if let Ok(building_org) = Uuid::parse_str(&building.organization_id) {
182 if let Err(e) = user.verify_org_access(building_org) {
183 return HttpResponse::Forbidden().json(serde_json::json!({ "error": e }));
184 }
185 }
186 HttpResponse::Ok().json(building)
187 }
188 Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
189 "error": "Building not found"
190 })),
191 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
192 "error": err
193 })),
194 }
195}
196
197#[utoipa::path(
198 put,
199 path = "/buildings/{id}",
200 tag = "Buildings",
201 summary = "Update a building",
202 params(
203 ("id" = Uuid, Path, description = "Building UUID")
204 ),
205 request_body = UpdateBuildingDto,
206 responses(
207 (status = 200, description = "Building updated successfully"),
208 (status = 400, description = "Bad Request"),
209 (status = 403, description = "Forbidden - SuperAdmin only"),
210 (status = 404, description = "Building not found"),
211 (status = 500, description = "Internal Server Error"),
212 ),
213 security(("bearer_auth" = []))
214)]
215#[put("/buildings/{id}")]
216pub async fn update_building(
217 state: web::Data<AppState>,
218 user: AuthenticatedUser,
219 id: web::Path<Uuid>,
220 dto: web::Json<UpdateBuildingDto>,
221) -> impl Responder {
222 if user.role != "superadmin" {
224 return HttpResponse::Forbidden().json(serde_json::json!({
225 "error": "Only SuperAdmin can update buildings (structural data)"
226 }));
227 }
228
229 if let Err(errors) = dto.validate() {
230 return HttpResponse::BadRequest().json(serde_json::json!({
231 "error": "Validation failed",
232 "details": errors.to_string()
233 }));
234 }
235
236 if dto.organization_id.is_some() && user.role != "superadmin" {
238 return HttpResponse::Forbidden().json(serde_json::json!({
239 "error": "Only SuperAdmins can change building organization"
240 }));
241 }
242
243 if user.role != "superadmin" {
245 match state.building_use_cases.get_building(*id).await {
246 Ok(Some(building)) => {
247 let building_org_id = match Uuid::parse_str(&building.organization_id) {
248 Ok(id) => id,
249 Err(_) => {
250 return HttpResponse::InternalServerError().json(serde_json::json!({
251 "error": "Invalid building organization_id"
252 }));
253 }
254 };
255
256 let user_org_id = match user.require_organization() {
257 Ok(id) => id,
258 Err(e) => {
259 return HttpResponse::Unauthorized().json(serde_json::json!({
260 "error": e.to_string()
261 }));
262 }
263 };
264
265 if building_org_id != user_org_id {
266 return HttpResponse::Forbidden().json(serde_json::json!({
267 "error": "You can only update buildings in your own organization"
268 }));
269 }
270 }
271 Ok(None) => {
272 return HttpResponse::NotFound().json(serde_json::json!({
273 "error": "Building not found"
274 }));
275 }
276 Err(err) => {
277 return HttpResponse::InternalServerError().json(serde_json::json!({
278 "error": err
279 }));
280 }
281 }
282 }
283
284 match state
285 .building_use_cases
286 .update_building(*id, dto.into_inner())
287 .await
288 {
289 Ok(building) => {
290 AuditLogEntry::new(
292 AuditEventType::BuildingUpdated,
293 Some(user.user_id),
294 user.organization_id,
295 )
296 .with_resource("Building", *id)
297 .log();
298
299 HttpResponse::Ok().json(building)
300 }
301 Err(err) => {
302 AuditLogEntry::new(
304 AuditEventType::BuildingUpdated,
305 Some(user.user_id),
306 user.organization_id,
307 )
308 .with_resource("Building", *id)
309 .with_error(err.clone())
310 .log();
311
312 HttpResponse::BadRequest().json(serde_json::json!({
313 "error": err
314 }))
315 }
316 }
317}
318
319#[utoipa::path(
320 delete,
321 path = "/buildings/{id}",
322 tag = "Buildings",
323 summary = "Delete a building",
324 params(
325 ("id" = Uuid, Path, description = "Building UUID")
326 ),
327 responses(
328 (status = 204, description = "Building deleted successfully"),
329 (status = 403, description = "Forbidden - SuperAdmin only"),
330 (status = 404, description = "Building not found"),
331 (status = 500, description = "Internal Server Error"),
332 ),
333 security(("bearer_auth" = []))
334)]
335#[delete("/buildings/{id}")]
336pub async fn delete_building(
337 state: web::Data<AppState>,
338 user: AuthenticatedUser,
339 id: web::Path<Uuid>,
340) -> impl Responder {
341 if user.role != "superadmin" {
343 return HttpResponse::Forbidden().json(serde_json::json!({
344 "error": "Only SuperAdmin can delete buildings"
345 }));
346 }
347
348 match state.building_use_cases.delete_building(*id).await {
349 Ok(true) => {
350 AuditLogEntry::new(
352 AuditEventType::BuildingDeleted,
353 Some(user.user_id),
354 user.organization_id,
355 )
356 .with_resource("Building", *id)
357 .log();
358
359 HttpResponse::NoContent().finish()
360 }
361 Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
362 "error": "Building not found"
363 })),
364 Err(err) => {
365 AuditLogEntry::new(
367 AuditEventType::BuildingDeleted,
368 Some(user.user_id),
369 user.organization_id,
370 )
371 .with_resource("Building", *id)
372 .with_error(err.clone())
373 .log();
374
375 HttpResponse::InternalServerError().json(serde_json::json!({
376 "error": err
377 }))
378 }
379 }
380}
381
382#[derive(Debug, Deserialize, utoipa::IntoParams)]
388pub struct ExportAnnualReportQuery {
389 pub year: i32,
390 #[serde(default)]
391 pub reserve_fund: Option<f64>, #[serde(default)]
393 pub total_income: Option<f64>, }
395
396#[utoipa::path(
397 get,
398 path = "/buildings/{id}/export-annual-report-pdf",
399 tag = "Buildings",
400 summary = "Export annual financial report as PDF",
401 params(
402 ("id" = Uuid, Path, description = "Building UUID"),
403 ExportAnnualReportQuery
404 ),
405 responses(
406 (status = 200, description = "PDF generated successfully", content_type = "application/pdf"),
407 (status = 401, description = "Unauthorized"),
408 (status = 404, description = "Building not found"),
409 (status = 500, description = "Internal Server Error"),
410 ),
411 security(("bearer_auth" = []))
412)]
413#[get("/buildings/{id}/export-annual-report-pdf")]
414pub async fn export_annual_report_pdf(
415 state: web::Data<AppState>,
416 user: AuthenticatedUser,
417 id: web::Path<Uuid>,
418 query: web::Query<ExportAnnualReportQuery>,
419) -> impl Responder {
420 use crate::domain::entities::{Building, Expense};
421 use crate::domain::services::{AnnualReportExporter, BudgetItem};
422
423 let organization_id = match user.require_organization() {
424 Ok(org_id) => org_id,
425 Err(e) => {
426 return HttpResponse::Unauthorized().json(serde_json::json!({
427 "error": e.to_string()
428 }))
429 }
430 };
431
432 let building_id = *id;
433 let year = query.year;
434
435 let building_dto = match state.building_use_cases.get_building(building_id).await {
437 Ok(Some(dto)) => dto,
438 Ok(None) => {
439 return HttpResponse::NotFound().json(serde_json::json!({
440 "error": "Building not found"
441 }))
442 }
443 Err(err) => {
444 return HttpResponse::InternalServerError().json(serde_json::json!({
445 "error": err
446 }))
447 }
448 };
449
450 let expenses_dto = match state
452 .expense_use_cases
453 .list_expenses_by_building(building_id)
454 .await
455 {
456 Ok(expenses) => expenses,
457 Err(err) => {
458 return HttpResponse::InternalServerError().json(serde_json::json!({
459 "error": format!("Failed to get expenses: {}", err)
460 }))
461 }
462 };
463
464 let year_expenses: Vec<_> = expenses_dto
466 .into_iter()
467 .filter(|e| {
468 DateTime::parse_from_rfc3339(&e.expense_date)
470 .map(|dt| dt.year() == year)
471 .unwrap_or(false)
472 })
473 .collect();
474
475 use crate::domain::entities::PaymentStatus;
477 let total_income = query.total_income.unwrap_or_else(|| {
478 year_expenses
479 .iter()
480 .filter(|e| e.payment_status == PaymentStatus::Paid)
481 .map(|e| e.amount)
482 .sum()
483 });
484
485 let reserve_fund = query.reserve_fund.unwrap_or(0.0);
487
488 let building_org_id = Uuid::parse_str(&building_dto.organization_id).unwrap_or(organization_id);
490
491 let building_created_at = DateTime::parse_from_rfc3339(&building_dto.created_at)
492 .map(|dt| dt.with_timezone(&Utc))
493 .unwrap_or_else(|_| Utc::now());
494
495 let building_updated_at = DateTime::parse_from_rfc3339(&building_dto.updated_at)
496 .map(|dt| dt.with_timezone(&Utc))
497 .unwrap_or_else(|_| Utc::now());
498
499 let building_entity = Building {
500 id: Uuid::parse_str(&building_dto.id).unwrap_or(building_id),
501 name: building_dto.name.clone(),
502 address: building_dto.address,
503 city: building_dto.city,
504 postal_code: building_dto.postal_code,
505 country: building_dto.country,
506 total_units: building_dto.total_units,
507 total_tantiemes: building_dto.total_tantiemes,
508 construction_year: building_dto.construction_year,
509 syndic_name: None,
510 syndic_email: None,
511 syndic_phone: None,
512 syndic_address: None,
513 syndic_office_hours: None,
514 syndic_emergency_contact: None,
515 slug: None,
516 organization_id: building_org_id,
517 created_at: building_created_at,
518 updated_at: building_updated_at,
519 };
520
521 let expense_entities: Vec<Expense> = year_expenses
523 .iter()
524 .filter_map(|e| {
525 let exp_id = Uuid::parse_str(&e.id).ok()?;
527 let bldg_id = Uuid::parse_str(&e.building_id).ok()?;
528 let exp_date = DateTime::parse_from_rfc3339(&e.expense_date)
529 .ok()?
530 .with_timezone(&Utc);
531
532 Some(Expense {
533 id: exp_id,
534 organization_id,
535 building_id: bldg_id,
536 category: e.category.clone(),
537 description: e.description.clone(),
538 amount: e.amount,
539 amount_excl_vat: None,
540 vat_rate: None,
541 vat_amount: None,
542 amount_incl_vat: None,
543 expense_date: exp_date,
544 invoice_date: None,
545 due_date: None,
546 paid_date: None,
547 approval_status: e.approval_status.clone(),
548 submitted_at: None,
549 approved_by: None,
550 approved_at: None,
551 rejection_reason: None,
552 payment_status: e.payment_status.clone(),
553 supplier: e.supplier.clone(),
554 invoice_number: e.invoice_number.clone(),
555 account_code: e.account_code.clone(),
556 contractor_report_id: None,
557 created_at: Utc::now(), updated_at: Utc::now(), })
560 })
561 .collect();
562
563 let budget_items: Vec<BudgetItem> = Vec::new();
565
566 match AnnualReportExporter::export_to_pdf(
568 &building_entity,
569 year,
570 &expense_entities,
571 &budget_items,
572 total_income,
573 reserve_fund,
574 ) {
575 Ok(pdf_bytes) => {
576 AuditLogEntry::new(
578 AuditEventType::ReportGenerated,
579 Some(user.user_id),
580 Some(organization_id),
581 )
582 .with_resource("Building", building_id)
583 .with_metadata(serde_json::json!({
584 "report_type": "annual_report_pdf",
585 "building_name": building_entity.name,
586 "year": year,
587 "total_income": total_income,
588 "reserve_fund": reserve_fund
589 }))
590 .log();
591
592 HttpResponse::Ok()
593 .content_type("application/pdf")
594 .insert_header((
595 "Content-Disposition",
596 format!(
597 "attachment; filename=\"Rapport_Annuel_{}_{}.pdf\"",
598 building_entity.name.replace(' ', "_"),
599 year
600 ),
601 ))
602 .body(pdf_bytes)
603 }
604 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
605 "error": format!("Failed to generate PDF: {}", err)
606 })),
607 }
608}