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 match state
134 .building_use_cases
135 .list_buildings_paginated(&page_request, organization_id)
136 .await
137 {
138 Ok((buildings, total)) => {
139 let response =
140 PageResponse::new(buildings, page_request.page, page_request.per_page, total);
141 HttpResponse::Ok().json(response)
142 }
143 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
144 "error": err
145 })),
146 }
147}
148
149#[utoipa::path(
150 get,
151 path = "/buildings/{id}",
152 tag = "Buildings",
153 summary = "Get a building by ID",
154 params(
155 ("id" = Uuid, Path, description = "Building UUID")
156 ),
157 responses(
158 (status = 200, description = "Building found"),
159 (status = 404, description = "Building not found"),
160 (status = 500, description = "Internal Server Error"),
161 ),
162 security(("bearer_auth" = []))
163)]
164#[get("/buildings/{id}")]
165pub async fn get_building(
166 state: web::Data<AppState>,
167 user: AuthenticatedUser,
168 id: web::Path<Uuid>,
169) -> impl Responder {
170 match state.building_use_cases.get_building(*id).await {
171 Ok(Some(building)) => {
172 if let Ok(building_org) = Uuid::parse_str(&building.organization_id) {
174 if let Err(e) = user.verify_org_access(building_org) {
175 return HttpResponse::Forbidden().json(serde_json::json!({ "error": e }));
176 }
177 }
178 HttpResponse::Ok().json(building)
179 }
180 Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
181 "error": "Building not found"
182 })),
183 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
184 "error": err
185 })),
186 }
187}
188
189#[utoipa::path(
190 put,
191 path = "/buildings/{id}",
192 tag = "Buildings",
193 summary = "Update a building",
194 params(
195 ("id" = Uuid, Path, description = "Building UUID")
196 ),
197 request_body = UpdateBuildingDto,
198 responses(
199 (status = 200, description = "Building updated successfully"),
200 (status = 400, description = "Bad Request"),
201 (status = 403, description = "Forbidden - SuperAdmin only"),
202 (status = 404, description = "Building not found"),
203 (status = 500, description = "Internal Server Error"),
204 ),
205 security(("bearer_auth" = []))
206)]
207#[put("/buildings/{id}")]
208pub async fn update_building(
209 state: web::Data<AppState>,
210 user: AuthenticatedUser,
211 id: web::Path<Uuid>,
212 dto: web::Json<UpdateBuildingDto>,
213) -> impl Responder {
214 if user.role != "superadmin" {
216 return HttpResponse::Forbidden().json(serde_json::json!({
217 "error": "Only SuperAdmin can update buildings (structural data)"
218 }));
219 }
220
221 if let Err(errors) = dto.validate() {
222 return HttpResponse::BadRequest().json(serde_json::json!({
223 "error": "Validation failed",
224 "details": errors.to_string()
225 }));
226 }
227
228 if dto.organization_id.is_some() && user.role != "superadmin" {
230 return HttpResponse::Forbidden().json(serde_json::json!({
231 "error": "Only SuperAdmins can change building organization"
232 }));
233 }
234
235 if user.role != "superadmin" {
237 match state.building_use_cases.get_building(*id).await {
238 Ok(Some(building)) => {
239 let building_org_id = match Uuid::parse_str(&building.organization_id) {
240 Ok(id) => id,
241 Err(_) => {
242 return HttpResponse::InternalServerError().json(serde_json::json!({
243 "error": "Invalid building organization_id"
244 }));
245 }
246 };
247
248 let user_org_id = match user.require_organization() {
249 Ok(id) => id,
250 Err(e) => {
251 return HttpResponse::Unauthorized().json(serde_json::json!({
252 "error": e.to_string()
253 }));
254 }
255 };
256
257 if building_org_id != user_org_id {
258 return HttpResponse::Forbidden().json(serde_json::json!({
259 "error": "You can only update buildings in your own organization"
260 }));
261 }
262 }
263 Ok(None) => {
264 return HttpResponse::NotFound().json(serde_json::json!({
265 "error": "Building not found"
266 }));
267 }
268 Err(err) => {
269 return HttpResponse::InternalServerError().json(serde_json::json!({
270 "error": err
271 }));
272 }
273 }
274 }
275
276 match state
277 .building_use_cases
278 .update_building(*id, dto.into_inner())
279 .await
280 {
281 Ok(building) => {
282 AuditLogEntry::new(
284 AuditEventType::BuildingUpdated,
285 Some(user.user_id),
286 user.organization_id,
287 )
288 .with_resource("Building", *id)
289 .log();
290
291 HttpResponse::Ok().json(building)
292 }
293 Err(err) => {
294 AuditLogEntry::new(
296 AuditEventType::BuildingUpdated,
297 Some(user.user_id),
298 user.organization_id,
299 )
300 .with_resource("Building", *id)
301 .with_error(err.clone())
302 .log();
303
304 HttpResponse::BadRequest().json(serde_json::json!({
305 "error": err
306 }))
307 }
308 }
309}
310
311#[utoipa::path(
312 delete,
313 path = "/buildings/{id}",
314 tag = "Buildings",
315 summary = "Delete a building",
316 params(
317 ("id" = Uuid, Path, description = "Building UUID")
318 ),
319 responses(
320 (status = 204, description = "Building deleted successfully"),
321 (status = 403, description = "Forbidden - SuperAdmin only"),
322 (status = 404, description = "Building not found"),
323 (status = 500, description = "Internal Server Error"),
324 ),
325 security(("bearer_auth" = []))
326)]
327#[delete("/buildings/{id}")]
328pub async fn delete_building(
329 state: web::Data<AppState>,
330 user: AuthenticatedUser,
331 id: web::Path<Uuid>,
332) -> impl Responder {
333 if user.role != "superadmin" {
335 return HttpResponse::Forbidden().json(serde_json::json!({
336 "error": "Only SuperAdmin can delete buildings"
337 }));
338 }
339
340 match state.building_use_cases.delete_building(*id).await {
341 Ok(true) => {
342 AuditLogEntry::new(
344 AuditEventType::BuildingDeleted,
345 Some(user.user_id),
346 user.organization_id,
347 )
348 .with_resource("Building", *id)
349 .log();
350
351 HttpResponse::NoContent().finish()
352 }
353 Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
354 "error": "Building not found"
355 })),
356 Err(err) => {
357 AuditLogEntry::new(
359 AuditEventType::BuildingDeleted,
360 Some(user.user_id),
361 user.organization_id,
362 )
363 .with_resource("Building", *id)
364 .with_error(err.clone())
365 .log();
366
367 HttpResponse::InternalServerError().json(serde_json::json!({
368 "error": err
369 }))
370 }
371 }
372}
373
374#[derive(Debug, Deserialize, utoipa::IntoParams)]
380pub struct ExportAnnualReportQuery {
381 pub year: i32,
382 #[serde(default)]
383 pub reserve_fund: Option<f64>, #[serde(default)]
385 pub total_income: Option<f64>, }
387
388#[utoipa::path(
389 get,
390 path = "/buildings/{id}/export-annual-report-pdf",
391 tag = "Buildings",
392 summary = "Export annual financial report as PDF",
393 params(
394 ("id" = Uuid, Path, description = "Building UUID"),
395 ExportAnnualReportQuery
396 ),
397 responses(
398 (status = 200, description = "PDF generated successfully", content_type = "application/pdf"),
399 (status = 401, description = "Unauthorized"),
400 (status = 404, description = "Building not found"),
401 (status = 500, description = "Internal Server Error"),
402 ),
403 security(("bearer_auth" = []))
404)]
405#[get("/buildings/{id}/export-annual-report-pdf")]
406pub async fn export_annual_report_pdf(
407 state: web::Data<AppState>,
408 user: AuthenticatedUser,
409 id: web::Path<Uuid>,
410 query: web::Query<ExportAnnualReportQuery>,
411) -> impl Responder {
412 use crate::domain::entities::{Building, Expense};
413 use crate::domain::services::{AnnualReportExporter, BudgetItem};
414
415 let organization_id = match user.require_organization() {
416 Ok(org_id) => org_id,
417 Err(e) => {
418 return HttpResponse::Unauthorized().json(serde_json::json!({
419 "error": e.to_string()
420 }))
421 }
422 };
423
424 let building_id = *id;
425 let year = query.year;
426
427 let building_dto = match state.building_use_cases.get_building(building_id).await {
429 Ok(Some(dto)) => dto,
430 Ok(None) => {
431 return HttpResponse::NotFound().json(serde_json::json!({
432 "error": "Building not found"
433 }))
434 }
435 Err(err) => {
436 return HttpResponse::InternalServerError().json(serde_json::json!({
437 "error": err
438 }))
439 }
440 };
441
442 let expenses_dto = match state
444 .expense_use_cases
445 .list_expenses_by_building(building_id)
446 .await
447 {
448 Ok(expenses) => expenses,
449 Err(err) => {
450 return HttpResponse::InternalServerError().json(serde_json::json!({
451 "error": format!("Failed to get expenses: {}", err)
452 }))
453 }
454 };
455
456 let year_expenses: Vec<_> = expenses_dto
458 .into_iter()
459 .filter(|e| {
460 DateTime::parse_from_rfc3339(&e.expense_date)
462 .map(|dt| dt.year() == year)
463 .unwrap_or(false)
464 })
465 .collect();
466
467 use crate::domain::entities::PaymentStatus;
469 let total_income = query.total_income.unwrap_or_else(|| {
470 year_expenses
471 .iter()
472 .filter(|e| e.payment_status == PaymentStatus::Paid)
473 .map(|e| e.amount)
474 .sum()
475 });
476
477 let reserve_fund = query.reserve_fund.unwrap_or(0.0);
479
480 let building_org_id = Uuid::parse_str(&building_dto.organization_id).unwrap_or(organization_id);
482
483 let building_created_at = DateTime::parse_from_rfc3339(&building_dto.created_at)
484 .map(|dt| dt.with_timezone(&Utc))
485 .unwrap_or_else(|_| Utc::now());
486
487 let building_updated_at = DateTime::parse_from_rfc3339(&building_dto.updated_at)
488 .map(|dt| dt.with_timezone(&Utc))
489 .unwrap_or_else(|_| Utc::now());
490
491 let building_entity = Building {
492 id: Uuid::parse_str(&building_dto.id).unwrap_or(building_id),
493 name: building_dto.name.clone(),
494 address: building_dto.address,
495 city: building_dto.city,
496 postal_code: building_dto.postal_code,
497 country: building_dto.country,
498 total_units: building_dto.total_units,
499 total_tantiemes: building_dto.total_tantiemes,
500 construction_year: building_dto.construction_year,
501 syndic_name: None,
502 syndic_email: None,
503 syndic_phone: None,
504 syndic_address: None,
505 syndic_office_hours: None,
506 syndic_emergency_contact: None,
507 slug: None,
508 organization_id: building_org_id,
509 created_at: building_created_at,
510 updated_at: building_updated_at,
511 };
512
513 let expense_entities: Vec<Expense> = year_expenses
515 .iter()
516 .filter_map(|e| {
517 let exp_id = Uuid::parse_str(&e.id).ok()?;
519 let bldg_id = Uuid::parse_str(&e.building_id).ok()?;
520 let exp_date = DateTime::parse_from_rfc3339(&e.expense_date)
521 .ok()?
522 .with_timezone(&Utc);
523
524 Some(Expense {
525 id: exp_id,
526 organization_id,
527 building_id: bldg_id,
528 category: e.category.clone(),
529 description: e.description.clone(),
530 amount: e.amount,
531 amount_excl_vat: None,
532 vat_rate: None,
533 vat_amount: None,
534 amount_incl_vat: None,
535 expense_date: exp_date,
536 invoice_date: None,
537 due_date: None,
538 paid_date: None,
539 approval_status: e.approval_status.clone(),
540 submitted_at: None,
541 approved_by: None,
542 approved_at: None,
543 rejection_reason: None,
544 payment_status: e.payment_status.clone(),
545 supplier: e.supplier.clone(),
546 invoice_number: e.invoice_number.clone(),
547 account_code: e.account_code.clone(),
548 contractor_report_id: None,
549 created_at: Utc::now(), updated_at: Utc::now(), })
552 })
553 .collect();
554
555 let budget_items: Vec<BudgetItem> = Vec::new();
557
558 match AnnualReportExporter::export_to_pdf(
560 &building_entity,
561 year,
562 &expense_entities,
563 &budget_items,
564 total_income,
565 reserve_fund,
566 ) {
567 Ok(pdf_bytes) => {
568 AuditLogEntry::new(
570 AuditEventType::ReportGenerated,
571 Some(user.user_id),
572 Some(organization_id),
573 )
574 .with_resource("Building", building_id)
575 .with_metadata(serde_json::json!({
576 "report_type": "annual_report_pdf",
577 "building_name": building_entity.name,
578 "year": year,
579 "total_income": total_income,
580 "reserve_fund": reserve_fund
581 }))
582 .log();
583
584 HttpResponse::Ok()
585 .content_type("application/pdf")
586 .insert_header((
587 "Content-Disposition",
588 format!(
589 "attachment; filename=\"Rapport_Annuel_{}_{}.pdf\"",
590 building_entity.name.replace(' ', "_"),
591 year
592 ),
593 ))
594 .body(pdf_bytes)
595 }
596 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
597 "error": format!("Failed to generate PDF: {}", err)
598 })),
599 }
600}