1use crate::application::dto::{
2 AssignTicketRequest, CancelTicketRequest, CreateTicketRequest, ReopenTicketRequest,
3 ResolveTicketRequest, TicketResponse,
4};
5use crate::application::ports::TicketRepository;
6use crate::domain::entities::{Ticket, TicketStatus};
7use std::sync::Arc;
8use uuid::Uuid;
9
10pub struct TicketUseCases {
11 ticket_repository: Arc<dyn TicketRepository>,
12}
13
14impl TicketUseCases {
15 pub fn new(ticket_repository: Arc<dyn TicketRepository>) -> Self {
16 Self { ticket_repository }
17 }
18
19 pub async fn create_ticket(
21 &self,
22 organization_id: Uuid,
23 created_by: Uuid,
24 request: CreateTicketRequest,
25 ) -> Result<TicketResponse, String> {
26 let ticket = Ticket::new(
27 organization_id,
28 request.building_id,
29 request.unit_id,
30 created_by,
31 request.title,
32 request.description,
33 request.category,
34 request.priority,
35 )?;
36
37 let created = self.ticket_repository.create(&ticket).await?;
38 Ok(TicketResponse::from(created))
39 }
40
41 pub async fn get_ticket(&self, id: Uuid) -> Result<Option<TicketResponse>, String> {
43 match self.ticket_repository.find_by_id(id).await? {
44 Some(ticket) => Ok(Some(TicketResponse::from(ticket))),
45 None => Ok(None),
46 }
47 }
48
49 pub async fn list_tickets_by_building(
51 &self,
52 building_id: Uuid,
53 ) -> Result<Vec<TicketResponse>, String> {
54 let tickets = self.ticket_repository.find_by_building(building_id).await?;
55 Ok(tickets.into_iter().map(TicketResponse::from).collect())
56 }
57
58 pub async fn list_tickets_by_organization(
60 &self,
61 organization_id: Uuid,
62 ) -> Result<Vec<TicketResponse>, String> {
63 let tickets = self
64 .ticket_repository
65 .find_by_organization(organization_id)
66 .await?;
67 Ok(tickets.into_iter().map(TicketResponse::from).collect())
68 }
69
70 pub async fn list_my_tickets(&self, created_by: Uuid) -> Result<Vec<TicketResponse>, String> {
72 let tickets = self
73 .ticket_repository
74 .find_by_created_by(created_by)
75 .await?;
76 Ok(tickets.into_iter().map(TicketResponse::from).collect())
77 }
78
79 pub async fn list_assigned_tickets(
81 &self,
82 assigned_to: Uuid,
83 ) -> Result<Vec<TicketResponse>, String> {
84 let tickets = self
85 .ticket_repository
86 .find_by_assigned_to(assigned_to)
87 .await?;
88 Ok(tickets.into_iter().map(TicketResponse::from).collect())
89 }
90
91 pub async fn list_tickets_by_status(
93 &self,
94 building_id: Uuid,
95 status: TicketStatus,
96 ) -> Result<Vec<TicketResponse>, String> {
97 let tickets = self
98 .ticket_repository
99 .find_by_status(building_id, status)
100 .await?;
101 Ok(tickets.into_iter().map(TicketResponse::from).collect())
102 }
103
104 pub async fn assign_ticket(
106 &self,
107 id: Uuid,
108 request: AssignTicketRequest,
109 ) -> Result<TicketResponse, String> {
110 let mut ticket = self
111 .ticket_repository
112 .find_by_id(id)
113 .await?
114 .ok_or_else(|| "Ticket not found".to_string())?;
115
116 ticket.assign(request.assigned_to)?;
117
118 let updated = self.ticket_repository.update(&ticket).await?;
119 Ok(TicketResponse::from(updated))
120 }
121
122 pub async fn start_work(&self, id: Uuid) -> Result<TicketResponse, String> {
124 let mut ticket = self
125 .ticket_repository
126 .find_by_id(id)
127 .await?
128 .ok_or_else(|| "Ticket not found".to_string())?;
129
130 ticket.start_work()?;
131
132 let updated = self.ticket_repository.update(&ticket).await?;
133 Ok(TicketResponse::from(updated))
134 }
135
136 pub async fn resolve_ticket(
138 &self,
139 id: Uuid,
140 request: ResolveTicketRequest,
141 ) -> Result<TicketResponse, String> {
142 let mut ticket = self
143 .ticket_repository
144 .find_by_id(id)
145 .await?
146 .ok_or_else(|| "Ticket not found".to_string())?;
147
148 ticket.resolve(request.resolution_notes)?;
149
150 let updated = self.ticket_repository.update(&ticket).await?;
151 Ok(TicketResponse::from(updated))
152 }
153
154 pub async fn close_ticket(&self, id: Uuid) -> Result<TicketResponse, String> {
156 let mut ticket = self
157 .ticket_repository
158 .find_by_id(id)
159 .await?
160 .ok_or_else(|| "Ticket not found".to_string())?;
161
162 ticket.close()?;
163
164 let updated = self.ticket_repository.update(&ticket).await?;
165 Ok(TicketResponse::from(updated))
166 }
167
168 pub async fn cancel_ticket(
170 &self,
171 id: Uuid,
172 request: CancelTicketRequest,
173 ) -> Result<TicketResponse, String> {
174 let mut ticket = self
175 .ticket_repository
176 .find_by_id(id)
177 .await?
178 .ok_or_else(|| "Ticket not found".to_string())?;
179
180 ticket.cancel(request.reason)?;
181
182 let updated = self.ticket_repository.update(&ticket).await?;
183 Ok(TicketResponse::from(updated))
184 }
185
186 pub async fn reopen_ticket(
188 &self,
189 id: Uuid,
190 request: ReopenTicketRequest,
191 ) -> Result<TicketResponse, String> {
192 let mut ticket = self
193 .ticket_repository
194 .find_by_id(id)
195 .await?
196 .ok_or_else(|| "Ticket not found".to_string())?;
197
198 ticket.reopen(request.reason)?;
199
200 let updated = self.ticket_repository.update(&ticket).await?;
201 Ok(TicketResponse::from(updated))
202 }
203
204 pub async fn delete_ticket(&self, id: Uuid) -> Result<bool, String> {
206 let ticket = self
207 .ticket_repository
208 .find_by_id(id)
209 .await?
210 .ok_or_else(|| "Ticket not found".to_string())?;
211
212 if ticket.status != TicketStatus::Open {
214 return Err("Can only delete tickets with Open status".to_string());
215 }
216
217 self.ticket_repository.delete(id).await
218 }
219
220 pub async fn get_ticket_statistics(
222 &self,
223 building_id: Uuid,
224 ) -> Result<TicketStatistics, String> {
225 let total = self
226 .ticket_repository
227 .count_by_building(building_id)
228 .await?;
229
230 let open = self
231 .ticket_repository
232 .count_by_status(building_id, TicketStatus::Open)
233 .await?;
234
235 let in_progress = self
236 .ticket_repository
237 .count_by_status(building_id, TicketStatus::InProgress)
238 .await?;
239
240 let resolved = self
241 .ticket_repository
242 .count_by_status(building_id, TicketStatus::Resolved)
243 .await?;
244
245 let closed = self
246 .ticket_repository
247 .count_by_status(building_id, TicketStatus::Closed)
248 .await?;
249
250 let cancelled = self
251 .ticket_repository
252 .count_by_status(building_id, TicketStatus::Cancelled)
253 .await?;
254
255 Ok(TicketStatistics {
256 total,
257 open,
258 in_progress,
259 resolved,
260 closed,
261 cancelled,
262 })
263 }
264
265 pub async fn get_overdue_tickets(
267 &self,
268 building_id: Uuid,
269 max_days: i64,
270 ) -> Result<Vec<TicketResponse>, String> {
271 let tickets = self.ticket_repository.find_by_building(building_id).await?;
272
273 let overdue: Vec<TicketResponse> = tickets
274 .into_iter()
275 .filter(|ticket| ticket.is_overdue(max_days))
276 .map(TicketResponse::from)
277 .collect();
278
279 Ok(overdue)
280 }
281
282 pub async fn get_ticket_statistics_by_organization(
284 &self,
285 organization_id: Uuid,
286 ) -> Result<TicketStatistics, String> {
287 let total = self
288 .ticket_repository
289 .count_by_organization(organization_id)
290 .await?;
291
292 let open = self
293 .ticket_repository
294 .count_by_organization_and_status(organization_id, TicketStatus::Open)
295 .await?;
296
297 let in_progress = self
298 .ticket_repository
299 .count_by_organization_and_status(organization_id, TicketStatus::InProgress)
300 .await?;
301
302 let resolved = self
303 .ticket_repository
304 .count_by_organization_and_status(organization_id, TicketStatus::Resolved)
305 .await?;
306
307 let closed = self
308 .ticket_repository
309 .count_by_organization_and_status(organization_id, TicketStatus::Closed)
310 .await?;
311
312 let cancelled = self
313 .ticket_repository
314 .count_by_organization_and_status(organization_id, TicketStatus::Cancelled)
315 .await?;
316
317 Ok(TicketStatistics {
318 total,
319 open,
320 in_progress,
321 resolved,
322 closed,
323 cancelled,
324 })
325 }
326
327 pub async fn get_overdue_tickets_by_organization(
329 &self,
330 organization_id: Uuid,
331 max_days: i64,
332 ) -> Result<Vec<TicketResponse>, String> {
333 let tickets = self
334 .ticket_repository
335 .find_by_organization(organization_id)
336 .await?;
337
338 let overdue: Vec<TicketResponse> = tickets
339 .into_iter()
340 .filter(|ticket| ticket.is_overdue(max_days))
341 .map(TicketResponse::from)
342 .collect();
343
344 Ok(overdue)
345 }
346
347 pub async fn send_work_order(&self, ticket_id: Uuid) -> Result<TicketResponse, String> {
350 let mut ticket = self
351 .ticket_repository
352 .find_by_id(ticket_id)
353 .await?
354 .ok_or_else(|| "Ticket not found".to_string())?;
355
356 ticket.send_work_order_to_contractor()?;
357 let updated = self.ticket_repository.update(&ticket).await?;
358
359 Ok(TicketResponse::from(updated))
360 }
361}
362
363#[derive(Debug, serde::Serialize)]
365pub struct TicketStatistics {
366 pub total: i64,
367 pub open: i64,
368 pub in_progress: i64,
369 pub resolved: i64,
370 pub closed: i64,
371 pub cancelled: i64,
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377 use crate::application::ports::TicketRepository;
378 use crate::domain::entities::{Ticket, TicketCategory, TicketPriority, TicketStatus};
379 use async_trait::async_trait;
380 use std::collections::HashMap;
381 use std::sync::Mutex;
382
383 struct MockTicketRepository {
386 tickets: Mutex<HashMap<Uuid, Ticket>>,
387 }
388
389 impl MockTicketRepository {
390 fn new() -> Self {
391 Self {
392 tickets: Mutex::new(HashMap::new()),
393 }
394 }
395 }
396
397 #[async_trait]
398 impl TicketRepository for MockTicketRepository {
399 async fn create(&self, ticket: &Ticket) -> Result<Ticket, String> {
400 self.tickets
401 .lock()
402 .unwrap()
403 .insert(ticket.id, ticket.clone());
404 Ok(ticket.clone())
405 }
406
407 async fn find_by_id(&self, id: Uuid) -> Result<Option<Ticket>, String> {
408 Ok(self.tickets.lock().unwrap().get(&id).cloned())
409 }
410
411 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Ticket>, String> {
412 Ok(self
413 .tickets
414 .lock()
415 .unwrap()
416 .values()
417 .filter(|t| t.building_id == building_id)
418 .cloned()
419 .collect())
420 }
421
422 async fn find_by_organization(&self, organization_id: Uuid) -> Result<Vec<Ticket>, String> {
423 Ok(self
424 .tickets
425 .lock()
426 .unwrap()
427 .values()
428 .filter(|t| t.organization_id == organization_id)
429 .cloned()
430 .collect())
431 }
432
433 async fn find_by_created_by(&self, created_by: Uuid) -> Result<Vec<Ticket>, String> {
434 Ok(self
435 .tickets
436 .lock()
437 .unwrap()
438 .values()
439 .filter(|t| t.created_by == created_by)
440 .cloned()
441 .collect())
442 }
443
444 async fn find_by_assigned_to(&self, assigned_to: Uuid) -> Result<Vec<Ticket>, String> {
445 Ok(self
446 .tickets
447 .lock()
448 .unwrap()
449 .values()
450 .filter(|t| t.assigned_to == Some(assigned_to))
451 .cloned()
452 .collect())
453 }
454
455 async fn find_by_status(
456 &self,
457 building_id: Uuid,
458 status: TicketStatus,
459 ) -> Result<Vec<Ticket>, String> {
460 Ok(self
461 .tickets
462 .lock()
463 .unwrap()
464 .values()
465 .filter(|t| t.building_id == building_id && t.status == status)
466 .cloned()
467 .collect())
468 }
469
470 async fn update(&self, ticket: &Ticket) -> Result<Ticket, String> {
471 self.tickets
472 .lock()
473 .unwrap()
474 .insert(ticket.id, ticket.clone());
475 Ok(ticket.clone())
476 }
477
478 async fn delete(&self, id: Uuid) -> Result<bool, String> {
479 Ok(self.tickets.lock().unwrap().remove(&id).is_some())
480 }
481
482 async fn count_by_building(&self, building_id: Uuid) -> Result<i64, String> {
483 Ok(self
484 .tickets
485 .lock()
486 .unwrap()
487 .values()
488 .filter(|t| t.building_id == building_id)
489 .count() as i64)
490 }
491
492 async fn count_by_status(
493 &self,
494 building_id: Uuid,
495 status: TicketStatus,
496 ) -> Result<i64, String> {
497 Ok(self
498 .tickets
499 .lock()
500 .unwrap()
501 .values()
502 .filter(|t| t.building_id == building_id && t.status == status)
503 .count() as i64)
504 }
505
506 async fn count_by_organization(&self, organization_id: Uuid) -> Result<i64, String> {
507 Ok(self
508 .tickets
509 .lock()
510 .unwrap()
511 .values()
512 .filter(|t| t.organization_id == organization_id)
513 .count() as i64)
514 }
515
516 async fn count_by_organization_and_status(
517 &self,
518 organization_id: Uuid,
519 status: TicketStatus,
520 ) -> Result<i64, String> {
521 Ok(self
522 .tickets
523 .lock()
524 .unwrap()
525 .values()
526 .filter(|t| t.organization_id == organization_id && t.status == status)
527 .count() as i64)
528 }
529 }
530
531 fn make_use_cases(repo: Arc<dyn TicketRepository>) -> TicketUseCases {
534 TicketUseCases::new(repo)
535 }
536
537 fn make_create_request(building_id: Uuid) -> CreateTicketRequest {
538 CreateTicketRequest {
539 building_id,
540 unit_id: None,
541 title: "Fuite d'eau cuisine".to_string(),
542 description: "L'eau coule sous l'évier de la cuisine commune".to_string(),
543 category: TicketCategory::Plumbing,
544 priority: TicketPriority::High,
545 }
546 }
547
548 async fn create_ticket_helper(
550 use_cases: &TicketUseCases,
551 org_id: Uuid,
552 building_id: Uuid,
553 ) -> TicketResponse {
554 let user_id = Uuid::new_v4();
555 let request = make_create_request(building_id);
556 use_cases
557 .create_ticket(org_id, user_id, request)
558 .await
559 .expect("create_ticket should succeed")
560 }
561
562 #[tokio::test]
565 async fn test_create_ticket_success() {
566 let repo = Arc::new(MockTicketRepository::new());
567 let use_cases = make_use_cases(repo);
568
569 let org_id = Uuid::new_v4();
570 let building_id = Uuid::new_v4();
571 let user_id = Uuid::new_v4();
572 let request = make_create_request(building_id);
573
574 let result = use_cases.create_ticket(org_id, user_id, request).await;
575
576 assert!(result.is_ok());
577 let ticket = result.unwrap();
578 assert_eq!(ticket.status, TicketStatus::Open);
579 assert_eq!(ticket.building_id, building_id);
580 assert_eq!(ticket.organization_id, org_id);
581 assert_eq!(ticket.created_by, user_id);
582 assert_eq!(ticket.title, "Fuite d'eau cuisine");
583 assert!(ticket.assigned_to.is_none());
584 assert!(ticket.resolution_notes.is_none());
585 }
586
587 #[tokio::test]
588 async fn test_create_ticket_empty_title_fails() {
589 let repo = Arc::new(MockTicketRepository::new());
590 let use_cases = make_use_cases(repo);
591
592 let request = CreateTicketRequest {
593 building_id: Uuid::new_v4(),
594 unit_id: None,
595 title: " ".to_string(),
596 description: "Some description".to_string(),
597 category: TicketCategory::Electrical,
598 priority: TicketPriority::Low,
599 };
600
601 let result = use_cases
602 .create_ticket(Uuid::new_v4(), Uuid::new_v4(), request)
603 .await;
604
605 assert!(result.is_err());
606 assert!(result.unwrap_err().contains("Title cannot be empty"));
607 }
608
609 #[tokio::test]
610 async fn test_assign_contractor_transitions_to_in_progress() {
611 let repo = Arc::new(MockTicketRepository::new());
612 let use_cases = make_use_cases(repo);
613
614 let org_id = Uuid::new_v4();
615 let building_id = Uuid::new_v4();
616 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
617 assert_eq!(ticket.status, TicketStatus::Open);
618
619 let contractor_id = Uuid::new_v4();
620 let assign_request = AssignTicketRequest {
621 assigned_to: contractor_id,
622 };
623
624 let result = use_cases.assign_ticket(ticket.id, assign_request).await;
625
626 assert!(result.is_ok());
627 let updated = result.unwrap();
628 assert_eq!(updated.status, TicketStatus::InProgress);
629 assert_eq!(updated.assigned_to, Some(contractor_id));
630 }
631
632 #[tokio::test]
633 async fn test_start_work_open_to_in_progress() {
634 let repo = Arc::new(MockTicketRepository::new());
635 let use_cases = make_use_cases(repo);
636
637 let org_id = Uuid::new_v4();
638 let building_id = Uuid::new_v4();
639 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
640
641 let result = use_cases.start_work(ticket.id).await;
642
643 assert!(result.is_ok());
644 assert_eq!(result.unwrap().status, TicketStatus::InProgress);
645 }
646
647 #[tokio::test]
648 async fn test_resolve_ticket_with_notes() {
649 let repo = Arc::new(MockTicketRepository::new());
650 let use_cases = make_use_cases(repo);
651
652 let org_id = Uuid::new_v4();
653 let building_id = Uuid::new_v4();
654 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
655
656 use_cases.start_work(ticket.id).await.unwrap();
658
659 let resolve_request = ResolveTicketRequest {
660 resolution_notes: "Fuite réparée, joint remplacé".to_string(),
661 };
662 let result = use_cases.resolve_ticket(ticket.id, resolve_request).await;
663
664 assert!(result.is_ok());
665 let resolved = result.unwrap();
666 assert_eq!(resolved.status, TicketStatus::Resolved);
667 assert_eq!(
668 resolved.resolution_notes.as_deref(),
669 Some("Fuite réparée, joint remplacé")
670 );
671 assert!(resolved.resolved_at.is_some());
672 }
673
674 #[tokio::test]
675 async fn test_close_ticket_after_resolution() {
676 let repo = Arc::new(MockTicketRepository::new());
677 let use_cases = make_use_cases(repo);
678
679 let org_id = Uuid::new_v4();
680 let building_id = Uuid::new_v4();
681 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
682
683 use_cases.start_work(ticket.id).await.unwrap();
685 use_cases
686 .resolve_ticket(
687 ticket.id,
688 ResolveTicketRequest {
689 resolution_notes: "Done".to_string(),
690 },
691 )
692 .await
693 .unwrap();
694
695 let result = use_cases.close_ticket(ticket.id).await;
696
697 assert!(result.is_ok());
698 let closed = result.unwrap();
699 assert_eq!(closed.status, TicketStatus::Closed);
700 assert!(closed.closed_at.is_some());
701 }
702
703 #[tokio::test]
704 async fn test_close_open_ticket_fails() {
705 let repo = Arc::new(MockTicketRepository::new());
706 let use_cases = make_use_cases(repo);
707
708 let org_id = Uuid::new_v4();
709 let building_id = Uuid::new_v4();
710 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
711
712 let result = use_cases.close_ticket(ticket.id).await;
714
715 assert!(result.is_err());
716 assert!(result.unwrap_err().contains("Must be Resolved first"));
717 }
718
719 #[tokio::test]
720 async fn test_cancel_ticket_with_reason() {
721 let repo = Arc::new(MockTicketRepository::new());
722 let use_cases = make_use_cases(repo);
723
724 let org_id = Uuid::new_v4();
725 let building_id = Uuid::new_v4();
726 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
727
728 let cancel_request = CancelTicketRequest {
729 reason: "Erreur de déclaration, problème déjà résolu".to_string(),
730 };
731 let result = use_cases.cancel_ticket(ticket.id, cancel_request).await;
732
733 assert!(result.is_ok());
734 let cancelled = result.unwrap();
735 assert_eq!(cancelled.status, TicketStatus::Cancelled);
736 assert!(cancelled
737 .resolution_notes
738 .as_deref()
739 .unwrap()
740 .contains("CANCELLED"));
741 }
742
743 #[tokio::test]
744 async fn test_reopen_resolved_ticket() {
745 let repo = Arc::new(MockTicketRepository::new());
746 let use_cases = make_use_cases(repo);
747
748 let org_id = Uuid::new_v4();
749 let building_id = Uuid::new_v4();
750 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
751
752 use_cases.start_work(ticket.id).await.unwrap();
754 use_cases
755 .resolve_ticket(
756 ticket.id,
757 ResolveTicketRequest {
758 resolution_notes: "Fixed".to_string(),
759 },
760 )
761 .await
762 .unwrap();
763
764 let reopen_request = ReopenTicketRequest {
766 reason: "Problème persiste après intervention".to_string(),
767 };
768 let result = use_cases.reopen_ticket(ticket.id, reopen_request).await;
769
770 assert!(result.is_ok());
771 let reopened = result.unwrap();
772 assert_eq!(reopened.status, TicketStatus::InProgress);
773 assert!(reopened.resolved_at.is_none());
774 assert!(reopened.closed_at.is_none());
775 assert!(reopened
776 .resolution_notes
777 .as_deref()
778 .unwrap()
779 .contains("REOPENED"));
780 }
781
782 #[tokio::test]
783 async fn test_reopen_open_ticket_fails() {
784 let repo = Arc::new(MockTicketRepository::new());
785 let use_cases = make_use_cases(repo);
786
787 let org_id = Uuid::new_v4();
788 let building_id = Uuid::new_v4();
789 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
790
791 let reopen_request = ReopenTicketRequest {
793 reason: "Some reason".to_string(),
794 };
795 let result = use_cases.reopen_ticket(ticket.id, reopen_request).await;
796
797 assert!(result.is_err());
798 assert!(result
799 .unwrap_err()
800 .contains("Can only reopen resolved or closed tickets"));
801 }
802
803 #[tokio::test]
804 async fn test_send_work_order_in_progress_ticket() {
805 let repo = Arc::new(MockTicketRepository::new());
806 let use_cases = make_use_cases(repo);
807
808 let org_id = Uuid::new_v4();
809 let building_id = Uuid::new_v4();
810 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
811
812 let contractor_id = Uuid::new_v4();
814 use_cases
815 .assign_ticket(
816 ticket.id,
817 AssignTicketRequest {
818 assigned_to: contractor_id,
819 },
820 )
821 .await
822 .unwrap();
823
824 let result = use_cases.send_work_order(ticket.id).await;
826
827 assert!(result.is_ok());
828 let updated = result.unwrap();
829 assert!(updated.work_order_sent_at.is_some());
830 assert_eq!(updated.status, TicketStatus::InProgress);
831 }
832
833 #[tokio::test]
834 async fn test_send_work_order_open_ticket_fails() {
835 let repo = Arc::new(MockTicketRepository::new());
836 let use_cases = make_use_cases(repo);
837
838 let org_id = Uuid::new_v4();
839 let building_id = Uuid::new_v4();
840 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
841
842 let result = use_cases.send_work_order(ticket.id).await;
844
845 assert!(result.is_err());
846 assert!(result.unwrap_err().contains("InProgress"));
847 }
848
849 #[tokio::test]
850 async fn test_get_overdue_tickets() {
851 let repo = Arc::new(MockTicketRepository::new());
852 let use_cases = make_use_cases(repo.clone());
853
854 let org_id = Uuid::new_v4();
855 let building_id = Uuid::new_v4();
856 let user_id = Uuid::new_v4();
857
858 let request = make_create_request(building_id);
860 let created = use_cases
861 .create_ticket(org_id, user_id, request)
862 .await
863 .unwrap();
864
865 {
867 let mut store = repo.tickets.lock().unwrap();
868 if let Some(ticket) = store.get_mut(&created.id) {
869 ticket.created_at = chrono::Utc::now() - chrono::Duration::days(10);
870 }
871 }
872
873 let overdue = use_cases.get_overdue_tickets(building_id, 5).await.unwrap();
874 assert_eq!(overdue.len(), 1);
875 assert_eq!(overdue[0].id, created.id);
876
877 let not_overdue = use_cases
879 .get_overdue_tickets(building_id, 15)
880 .await
881 .unwrap();
882 assert_eq!(not_overdue.len(), 0);
883 }
884
885 #[tokio::test]
886 async fn test_get_ticket_statistics() {
887 let repo = Arc::new(MockTicketRepository::new());
888 let use_cases = make_use_cases(repo);
889
890 let org_id = Uuid::new_v4();
891 let building_id = Uuid::new_v4();
892
893 let t1 = create_ticket_helper(&use_cases, org_id, building_id).await; let t2 = create_ticket_helper(&use_cases, org_id, building_id).await;
896 let t3 = create_ticket_helper(&use_cases, org_id, building_id).await;
897
898 use_cases.start_work(t2.id).await.unwrap();
900 use_cases
901 .resolve_ticket(
902 t2.id,
903 ResolveTicketRequest {
904 resolution_notes: "Done".to_string(),
905 },
906 )
907 .await
908 .unwrap();
909
910 use_cases
912 .cancel_ticket(
913 t3.id,
914 CancelTicketRequest {
915 reason: "Duplicate".to_string(),
916 },
917 )
918 .await
919 .unwrap();
920
921 let stats = use_cases.get_ticket_statistics(building_id).await.unwrap();
922
923 assert_eq!(stats.total, 3);
924 assert_eq!(stats.open, 1); assert_eq!(stats.in_progress, 0);
926 assert_eq!(stats.resolved, 1); assert_eq!(stats.closed, 0);
928 assert_eq!(stats.cancelled, 1); }
930
931 #[tokio::test]
932 async fn test_delete_open_ticket_succeeds() {
933 let repo = Arc::new(MockTicketRepository::new());
934 let use_cases = make_use_cases(repo);
935
936 let org_id = Uuid::new_v4();
937 let building_id = Uuid::new_v4();
938 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
939
940 let result = use_cases.delete_ticket(ticket.id).await;
941
942 assert!(result.is_ok());
943 assert!(result.unwrap());
944
945 let fetched = use_cases.get_ticket(ticket.id).await.unwrap();
947 assert!(fetched.is_none());
948 }
949
950 #[tokio::test]
951 async fn test_delete_in_progress_ticket_fails() {
952 let repo = Arc::new(MockTicketRepository::new());
953 let use_cases = make_use_cases(repo);
954
955 let org_id = Uuid::new_v4();
956 let building_id = Uuid::new_v4();
957 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
958
959 use_cases.start_work(ticket.id).await.unwrap();
961
962 let result = use_cases.delete_ticket(ticket.id).await;
963
964 assert!(result.is_err());
965 assert!(result
966 .unwrap_err()
967 .contains("Can only delete tickets with Open status"));
968 }
969}