1use crate::application::dto::{
2 AddDocumentDto, AddPhotoDto, CreateWorkReportDto, PageRequest, UpdateWorkReportDto,
3 WarrantyStatusDto, WorkReportFilters, WorkReportListResponseDto, WorkReportResponseDto,
4};
5use crate::application::ports::WorkReportRepository;
6use crate::domain::entities::WorkReport;
7use chrono::DateTime;
8use std::sync::Arc;
9use uuid::Uuid;
10
11pub struct WorkReportUseCases {
12 repository: Arc<dyn WorkReportRepository>,
13}
14
15impl WorkReportUseCases {
16 pub fn new(repository: Arc<dyn WorkReportRepository>) -> Self {
17 Self { repository }
18 }
19
20 pub async fn create_work_report(
21 &self,
22 dto: CreateWorkReportDto,
23 ) -> Result<WorkReportResponseDto, String> {
24 let organization_id = Uuid::parse_str(&dto.organization_id)
25 .map_err(|_| "Invalid organization_id format".to_string())?;
26 let building_id = Uuid::parse_str(&dto.building_id)
27 .map_err(|_| "Invalid building_id format".to_string())?;
28
29 let work_date = DateTime::parse_from_rfc3339(&dto.work_date)
30 .map_err(|_| "Invalid work_date format".to_string())?
31 .with_timezone(&chrono::Utc);
32
33 let completion_date = if let Some(ref date_str) = dto.completion_date {
34 Some(
35 DateTime::parse_from_rfc3339(date_str)
36 .map_err(|_| "Invalid completion_date format".to_string())?
37 .with_timezone(&chrono::Utc),
38 )
39 } else {
40 None
41 };
42
43 let work_report = WorkReport::new(
44 organization_id,
45 building_id,
46 dto.title,
47 dto.description,
48 dto.work_type,
49 dto.contractor_name,
50 work_date,
51 dto.cost,
52 dto.warranty_type.clone(),
53 )?;
54
55 let mut work_report = work_report;
56 work_report.contractor_contact = dto.contractor_contact;
57 work_report.completion_date = completion_date;
58 work_report.invoice_number = dto.invoice_number;
59 work_report.notes = dto.notes;
60
61 let created = self.repository.create(&work_report).await?;
62 Ok(self.to_response_dto(&created))
63 }
64
65 pub async fn get_work_report(&self, id: Uuid) -> Result<Option<WorkReportResponseDto>, String> {
66 let work_report = self.repository.find_by_id(id).await?;
67 Ok(work_report.map(|w| self.to_response_dto(&w)))
68 }
69
70 pub async fn list_work_reports_by_building(
71 &self,
72 building_id: Uuid,
73 ) -> Result<Vec<WorkReportResponseDto>, String> {
74 let work_reports = self.repository.find_by_building(building_id).await?;
75 Ok(work_reports
76 .iter()
77 .map(|w| self.to_response_dto(w))
78 .collect())
79 }
80
81 pub async fn list_work_reports_by_organization(
82 &self,
83 organization_id: Uuid,
84 ) -> Result<Vec<WorkReportResponseDto>, String> {
85 let work_reports = self
86 .repository
87 .find_by_organization(organization_id)
88 .await?;
89 Ok(work_reports
90 .iter()
91 .map(|w| self.to_response_dto(w))
92 .collect())
93 }
94
95 pub async fn list_work_reports_paginated(
96 &self,
97 page_request: &PageRequest,
98 filters: &WorkReportFilters,
99 ) -> Result<WorkReportListResponseDto, String> {
100 let (work_reports, total) = self
101 .repository
102 .find_all_paginated(page_request, filters)
103 .await?;
104
105 let dtos = work_reports
106 .iter()
107 .map(|w| self.to_response_dto(w))
108 .collect();
109
110 Ok(WorkReportListResponseDto {
111 work_reports: dtos,
112 total,
113 page: page_request.page,
114 page_size: page_request.per_page,
115 })
116 }
117
118 pub async fn get_active_warranties(
119 &self,
120 building_id: Uuid,
121 ) -> Result<Vec<WarrantyStatusDto>, String> {
122 let work_reports = self
123 .repository
124 .find_with_active_warranty(building_id)
125 .await?;
126
127 Ok(work_reports
128 .iter()
129 .map(|w| WarrantyStatusDto {
130 work_report_id: w.id.to_string(),
131 title: w.title.clone(),
132 warranty_type: w.warranty_type.clone(),
133 warranty_expiry: w.warranty_expiry.to_rfc3339(),
134 is_valid: w.is_warranty_valid(),
135 days_remaining: w.warranty_days_remaining(),
136 })
137 .collect())
138 }
139
140 pub async fn get_expiring_warranties(
141 &self,
142 building_id: Uuid,
143 days: i32,
144 ) -> Result<Vec<WarrantyStatusDto>, String> {
145 let work_reports = self
146 .repository
147 .find_with_expiring_warranty(building_id, days)
148 .await?;
149
150 Ok(work_reports
151 .iter()
152 .map(|w| WarrantyStatusDto {
153 work_report_id: w.id.to_string(),
154 title: w.title.clone(),
155 warranty_type: w.warranty_type.clone(),
156 warranty_expiry: w.warranty_expiry.to_rfc3339(),
157 is_valid: w.is_warranty_valid(),
158 days_remaining: w.warranty_days_remaining(),
159 })
160 .collect())
161 }
162
163 pub async fn update_work_report(
164 &self,
165 id: Uuid,
166 dto: UpdateWorkReportDto,
167 ) -> Result<WorkReportResponseDto, String> {
168 let mut work_report = self
169 .repository
170 .find_by_id(id)
171 .await?
172 .ok_or_else(|| "Work report not found".to_string())?;
173
174 if let Some(title) = dto.title {
175 work_report.title = title;
176 }
177 if let Some(description) = dto.description {
178 work_report.description = description;
179 }
180 if let Some(work_type) = dto.work_type {
181 work_report.work_type = work_type;
182 }
183 if let Some(contractor_name) = dto.contractor_name {
184 work_report.contractor_name = contractor_name;
185 }
186 if let Some(contractor_contact) = dto.contractor_contact {
187 work_report.contractor_contact = Some(contractor_contact);
188 }
189 if let Some(work_date_str) = dto.work_date {
190 let work_date = DateTime::parse_from_rfc3339(&work_date_str)
191 .map_err(|_| "Invalid work_date format".to_string())?
192 .with_timezone(&chrono::Utc);
193 work_report.work_date = work_date;
194 }
195 if let Some(completion_date_str) = dto.completion_date {
196 let completion_date = DateTime::parse_from_rfc3339(&completion_date_str)
197 .map_err(|_| "Invalid completion_date format".to_string())?
198 .with_timezone(&chrono::Utc);
199 work_report.completion_date = Some(completion_date);
200 }
201 if let Some(cost) = dto.cost {
202 work_report.set_cost(cost)?;
203 }
204 if let Some(invoice_number) = dto.invoice_number {
205 work_report.invoice_number = Some(invoice_number);
206 }
207 if let Some(notes) = dto.notes {
208 work_report.notes = Some(notes);
209 }
210 if let Some(warranty_type) = dto.warranty_type {
211 work_report.warranty_type = warranty_type;
212 work_report.warranty_expiry = match work_report.warranty_type {
214 crate::domain::entities::WarrantyType::None => chrono::Utc::now(),
215 crate::domain::entities::WarrantyType::Standard => {
216 work_report.work_date + chrono::Duration::days(2 * 365)
217 }
218 crate::domain::entities::WarrantyType::Decennial => {
219 work_report.work_date + chrono::Duration::days(10 * 365)
220 }
221 crate::domain::entities::WarrantyType::Extended => {
222 work_report.work_date + chrono::Duration::days(3 * 365)
223 }
224 crate::domain::entities::WarrantyType::Custom { years } => {
225 work_report.work_date + chrono::Duration::days(years as i64 * 365)
226 }
227 };
228 }
229
230 work_report.updated_at = chrono::Utc::now();
231
232 let updated = self.repository.update(&work_report).await?;
233 Ok(self.to_response_dto(&updated))
234 }
235
236 pub async fn add_photo(
237 &self,
238 id: Uuid,
239 dto: AddPhotoDto,
240 ) -> Result<WorkReportResponseDto, String> {
241 let mut work_report = self
242 .repository
243 .find_by_id(id)
244 .await?
245 .ok_or_else(|| "Work report not found".to_string())?;
246
247 work_report.add_photo(dto.photo_path);
248
249 let updated = self.repository.update(&work_report).await?;
250 Ok(self.to_response_dto(&updated))
251 }
252
253 pub async fn add_document(
254 &self,
255 id: Uuid,
256 dto: AddDocumentDto,
257 ) -> Result<WorkReportResponseDto, String> {
258 let mut work_report = self
259 .repository
260 .find_by_id(id)
261 .await?
262 .ok_or_else(|| "Work report not found".to_string())?;
263
264 work_report.add_document(dto.document_path);
265
266 let updated = self.repository.update(&work_report).await?;
267 Ok(self.to_response_dto(&updated))
268 }
269
270 pub async fn delete_work_report(&self, id: Uuid) -> Result<bool, String> {
271 self.repository.delete(id).await
272 }
273
274 fn to_response_dto(&self, work_report: &WorkReport) -> WorkReportResponseDto {
275 WorkReportResponseDto {
276 id: work_report.id.to_string(),
277 organization_id: work_report.organization_id.to_string(),
278 building_id: work_report.building_id.to_string(),
279 title: work_report.title.clone(),
280 description: work_report.description.clone(),
281 work_type: work_report.work_type.clone(),
282 contractor_name: work_report.contractor_name.clone(),
283 contractor_contact: work_report.contractor_contact.clone(),
284 work_date: work_report.work_date.to_rfc3339(),
285 completion_date: work_report.completion_date.as_ref().map(|d| d.to_rfc3339()),
286 cost: work_report.cost,
287 invoice_number: work_report.invoice_number.clone(),
288 photos: work_report.photos.clone(),
289 documents: work_report.documents.clone(),
290 notes: work_report.notes.clone(),
291 warranty_type: work_report.warranty_type.clone(),
292 warranty_expiry: work_report.warranty_expiry.to_rfc3339(),
293 is_warranty_valid: work_report.is_warranty_valid(),
294 warranty_days_remaining: work_report.warranty_days_remaining(),
295 created_at: work_report.created_at.to_rfc3339(),
296 updated_at: work_report.updated_at.to_rfc3339(),
297 }
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use crate::application::dto::{CreateWorkReportDto, UpdateWorkReportDto, WorkReportFilters};
305 use crate::application::ports::WorkReportRepository;
306 use crate::domain::entities::{WarrantyType, WorkReport, WorkType};
307 use async_trait::async_trait;
308 use chrono::Utc;
309 use rust_decimal_macros::dec;
310 use std::sync::Arc;
311 use tokio::sync::Mutex;
312 use uuid::Uuid;
313
314 struct MockWorkReportRepository {
316 reports: Mutex<Vec<WorkReport>>,
317 }
318
319 impl MockWorkReportRepository {
320 fn new() -> Self {
321 Self {
322 reports: Mutex::new(Vec::new()),
323 }
324 }
325
326 fn with_reports(reports: Vec<WorkReport>) -> Self {
327 Self {
328 reports: Mutex::new(reports),
329 }
330 }
331 }
332
333 #[async_trait]
334 impl WorkReportRepository for MockWorkReportRepository {
335 async fn create(&self, work_report: &WorkReport) -> Result<WorkReport, String> {
336 let mut reports = self.reports.lock().await;
337 reports.push(work_report.clone());
338 Ok(work_report.clone())
339 }
340
341 async fn find_by_id(&self, id: Uuid) -> Result<Option<WorkReport>, String> {
342 let reports = self.reports.lock().await;
343 Ok(reports.iter().find(|r| r.id == id).cloned())
344 }
345
346 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<WorkReport>, String> {
347 let reports = self.reports.lock().await;
348 Ok(reports
349 .iter()
350 .filter(|r| r.building_id == building_id)
351 .cloned()
352 .collect())
353 }
354
355 async fn find_by_organization(
356 &self,
357 organization_id: Uuid,
358 ) -> Result<Vec<WorkReport>, String> {
359 let reports = self.reports.lock().await;
360 Ok(reports
361 .iter()
362 .filter(|r| r.organization_id == organization_id)
363 .cloned()
364 .collect())
365 }
366
367 async fn find_all_paginated(
368 &self,
369 _page_request: &crate::application::dto::PageRequest,
370 _filters: &WorkReportFilters,
371 ) -> Result<(Vec<WorkReport>, i64), String> {
372 let reports = self.reports.lock().await;
373 let total = reports.len() as i64;
374 Ok((reports.clone(), total))
375 }
376
377 async fn find_with_active_warranty(
378 &self,
379 building_id: Uuid,
380 ) -> Result<Vec<WorkReport>, String> {
381 let reports = self.reports.lock().await;
382 Ok(reports
383 .iter()
384 .filter(|r| r.building_id == building_id && r.is_warranty_valid())
385 .cloned()
386 .collect())
387 }
388
389 async fn find_with_expiring_warranty(
390 &self,
391 building_id: Uuid,
392 days: i32,
393 ) -> Result<Vec<WorkReport>, String> {
394 let reports = self.reports.lock().await;
395 Ok(reports
396 .iter()
397 .filter(|r| {
398 r.building_id == building_id
399 && r.is_warranty_valid()
400 && r.warranty_days_remaining() <= days as i64
401 })
402 .cloned()
403 .collect())
404 }
405
406 async fn update(&self, work_report: &WorkReport) -> Result<WorkReport, String> {
407 let mut reports = self.reports.lock().await;
408 if let Some(pos) = reports.iter().position(|r| r.id == work_report.id) {
409 reports[pos] = work_report.clone();
410 }
411 Ok(work_report.clone())
412 }
413
414 async fn delete(&self, id: Uuid) -> Result<bool, String> {
415 let mut reports = self.reports.lock().await;
416 let len_before = reports.len();
417 reports.retain(|r| r.id != id);
418 Ok(reports.len() < len_before)
419 }
420 }
421
422 fn make_org_and_building() -> (Uuid, Uuid) {
423 (Uuid::new_v4(), Uuid::new_v4())
424 }
425
426 #[tokio::test]
427 async fn test_create_work_report() {
428 let (org_id, building_id) = make_org_and_building();
429 let repo = Arc::new(MockWorkReportRepository::new());
430 let uc = WorkReportUseCases::new(repo);
431
432 let work_date = Utc::now().to_rfc3339();
433 let dto = CreateWorkReportDto {
434 organization_id: org_id.to_string(),
435 building_id: building_id.to_string(),
436 title: "Elevator repair".to_string(),
437 description: "Cable replacement".to_string(),
438 work_type: WorkType::Repair,
439 contractor_name: "Schindler".to_string(),
440 contractor_contact: None,
441 work_date,
442 completion_date: None,
443 cost: dec!(2500.00),
444 invoice_number: Some("INV-001".to_string()),
445 notes: None,
446 warranty_type: WarrantyType::Standard,
447 };
448
449 let result = uc.create_work_report(dto).await;
450 assert!(result.is_ok());
451 let resp = result.unwrap();
452 assert_eq!(resp.title, "Elevator repair");
453 assert_eq!(resp.cost, dec!(2500.00));
454 assert_eq!(resp.contractor_name, "Schindler");
455 assert!(resp.is_warranty_valid);
456 }
457
458 #[tokio::test]
459 async fn test_update_work_report() {
460 let (org_id, building_id) = make_org_and_building();
461 let report = WorkReport::new(
462 org_id,
463 building_id,
464 "Old title".to_string(),
465 "Old desc".to_string(),
466 WorkType::Maintenance,
467 "Contractor A".to_string(),
468 Utc::now(),
469 dec!(1000.00),
470 WarrantyType::None,
471 )
472 .expect("cout de test valide");
473 let report_id = report.id;
474
475 let repo = Arc::new(MockWorkReportRepository::with_reports(vec![report]));
476 let uc = WorkReportUseCases::new(repo);
477
478 let dto = UpdateWorkReportDto {
479 title: Some("Updated title".to_string()),
480 description: None,
481 work_type: None,
482 contractor_name: None,
483 contractor_contact: None,
484 work_date: None,
485 completion_date: None,
486 cost: Some(dec!(1500.00)),
487 invoice_number: None,
488 notes: None,
489 warranty_type: None,
490 };
491
492 let result = uc.update_work_report(report_id, dto).await;
493 assert!(result.is_ok());
494 let resp = result.unwrap();
495 assert_eq!(resp.title, "Updated title");
496 assert_eq!(resp.cost, dec!(1500.00));
497 }
498
499 #[tokio::test]
500 async fn test_list_work_reports_by_building() {
501 let (org_id, building_id) = make_org_and_building();
502 let other_building = Uuid::new_v4();
503
504 let r1 = WorkReport::new(
505 org_id,
506 building_id,
507 "Report 1".into(),
508 "Desc".into(),
509 WorkType::Repair,
510 "C1".into(),
511 Utc::now(),
512 dec!(100.00),
513 WarrantyType::None,
514 )
515 .expect("cout de test valide");
516 let r2 = WorkReport::new(
517 org_id,
518 building_id,
519 "Report 2".into(),
520 "Desc".into(),
521 WorkType::Maintenance,
522 "C2".into(),
523 Utc::now(),
524 dec!(200.00),
525 WarrantyType::None,
526 )
527 .expect("cout de test valide");
528 let r3 = WorkReport::new(
529 org_id,
530 other_building,
531 "Report 3".into(),
532 "Desc".into(),
533 WorkType::Emergency,
534 "C3".into(),
535 Utc::now(),
536 dec!(300.00),
537 WarrantyType::None,
538 )
539 .expect("cout de test valide");
540
541 let repo = Arc::new(MockWorkReportRepository::with_reports(vec![r1, r2, r3]));
542 let uc = WorkReportUseCases::new(repo);
543
544 let result = uc.list_work_reports_by_building(building_id).await;
545 assert!(result.is_ok());
546 let reports = result.unwrap();
547 assert_eq!(reports.len(), 2);
548 assert!(reports
549 .iter()
550 .all(|r| r.building_id == building_id.to_string()));
551 }
552
553 #[tokio::test]
554 async fn test_get_active_warranties() {
555 let (org_id, building_id) = make_org_and_building();
556
557 let r1 = WorkReport::new(
559 org_id,
560 building_id,
561 "Active warranty".into(),
562 "Desc".into(),
563 WorkType::Renovation,
564 "C1".into(),
565 Utc::now(),
566 dec!(5000.00),
567 WarrantyType::Standard,
568 )
569 .expect("cout de test valide");
570 let r2 = WorkReport::new(
572 org_id,
573 building_id,
574 "No warranty".into(),
575 "Desc".into(),
576 WorkType::Maintenance,
577 "C2".into(),
578 Utc::now(),
579 dec!(100.00),
580 WarrantyType::None,
581 )
582 .expect("cout de test valide");
583
584 let repo = Arc::new(MockWorkReportRepository::with_reports(vec![r1, r2]));
585 let uc = WorkReportUseCases::new(repo);
586
587 let result = uc.get_active_warranties(building_id).await;
588 assert!(result.is_ok());
589 let warranties = result.unwrap();
590 assert_eq!(warranties.len(), 1);
591 assert_eq!(warranties[0].title, "Active warranty");
592 assert!(warranties[0].is_valid);
593 assert!(warranties[0].days_remaining > 700);
594 }
595
596 #[tokio::test]
597 async fn test_get_expiring_warranties() {
598 let (org_id, building_id) = make_org_and_building();
599
600 let mut r1 = WorkReport::new(
602 org_id,
603 building_id,
604 "Expiring soon".into(),
605 "Desc".into(),
606 WorkType::Repair,
607 "C1".into(),
608 Utc::now(),
609 dec!(1000.00),
610 WarrantyType::Standard,
611 )
612 .expect("cout de test valide");
613 r1.warranty_expiry = Utc::now() + chrono::Duration::days(20);
615
616 let r2 = WorkReport::new(
618 org_id,
619 building_id,
620 "Long warranty".into(),
621 "Desc".into(),
622 WorkType::Renovation,
623 "C2".into(),
624 Utc::now(),
625 dec!(50000.00),
626 WarrantyType::Decennial,
627 )
628 .expect("cout de test valide");
629
630 let repo = Arc::new(MockWorkReportRepository::with_reports(vec![r1, r2]));
631 let uc = WorkReportUseCases::new(repo);
632
633 let result = uc.get_expiring_warranties(building_id, 30).await;
634 assert!(result.is_ok());
635 let expiring = result.unwrap();
636 assert_eq!(expiring.len(), 1);
637 assert_eq!(expiring[0].title, "Expiring soon");
638 assert!(expiring[0].days_remaining <= 30);
639 }
640}