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 assigned: i64 = 0;
237
238 let in_progress = self
239 .ticket_repository
240 .count_by_status(building_id, TicketStatus::InProgress)
241 .await?;
242
243 let resolved = self
244 .ticket_repository
245 .count_by_status(building_id, TicketStatus::Resolved)
246 .await?;
247
248 let closed = self
249 .ticket_repository
250 .count_by_status(building_id, TicketStatus::Closed)
251 .await?;
252
253 let cancelled = self
254 .ticket_repository
255 .count_by_status(building_id, TicketStatus::Cancelled)
256 .await?;
257
258 let overdue_tickets = self
259 .ticket_repository
260 .find_by_building(building_id)
261 .await?
262 .iter()
263 .filter(|t| t.is_overdue(7))
264 .count() as i64;
265
266 Ok(TicketStatistics {
267 total,
268 open,
269 assigned,
270 in_progress,
271 resolved,
272 closed,
273 cancelled,
274 overdue_tickets,
275 })
276 }
277
278 pub async fn get_overdue_tickets(
280 &self,
281 building_id: Uuid,
282 max_days: i64,
283 ) -> Result<Vec<TicketResponse>, String> {
284 let tickets = self.ticket_repository.find_by_building(building_id).await?;
285
286 let overdue: Vec<TicketResponse> = tickets
287 .into_iter()
288 .filter(|ticket| ticket.is_overdue(max_days))
289 .map(TicketResponse::from)
290 .collect();
291
292 Ok(overdue)
293 }
294
295 pub async fn get_ticket_statistics_by_organization(
297 &self,
298 organization_id: Uuid,
299 ) -> Result<TicketStatistics, String> {
300 let total = self
301 .ticket_repository
302 .count_by_organization(organization_id)
303 .await?;
304
305 let open = self
306 .ticket_repository
307 .count_by_organization_and_status(organization_id, TicketStatus::Open)
308 .await?;
309
310 let assigned: i64 = 0;
311
312 let in_progress = self
313 .ticket_repository
314 .count_by_organization_and_status(organization_id, TicketStatus::InProgress)
315 .await?;
316
317 let resolved = self
318 .ticket_repository
319 .count_by_organization_and_status(organization_id, TicketStatus::Resolved)
320 .await?;
321
322 let closed = self
323 .ticket_repository
324 .count_by_organization_and_status(organization_id, TicketStatus::Closed)
325 .await?;
326
327 let cancelled = self
328 .ticket_repository
329 .count_by_organization_and_status(organization_id, TicketStatus::Cancelled)
330 .await?;
331
332 let overdue_tickets = self
333 .ticket_repository
334 .find_by_organization(organization_id)
335 .await?
336 .iter()
337 .filter(|t| t.is_overdue(7))
338 .count() as i64;
339
340 Ok(TicketStatistics {
341 total,
342 open,
343 assigned,
344 in_progress,
345 resolved,
346 closed,
347 cancelled,
348 overdue_tickets,
349 })
350 }
351
352 pub async fn get_overdue_tickets_by_organization(
354 &self,
355 organization_id: Uuid,
356 max_days: i64,
357 ) -> Result<Vec<TicketResponse>, String> {
358 let tickets = self
359 .ticket_repository
360 .find_by_organization(organization_id)
361 .await?;
362
363 let overdue: Vec<TicketResponse> = tickets
364 .into_iter()
365 .filter(|ticket| ticket.is_overdue(max_days))
366 .map(TicketResponse::from)
367 .collect();
368
369 Ok(overdue)
370 }
371
372 pub async fn send_work_order(&self, ticket_id: Uuid) -> Result<TicketResponse, String> {
375 let mut ticket = self
376 .ticket_repository
377 .find_by_id(ticket_id)
378 .await?
379 .ok_or_else(|| "Ticket not found".to_string())?;
380
381 ticket.send_work_order_to_contractor()?;
382 let updated = self.ticket_repository.update(&ticket).await?;
383
384 Ok(TicketResponse::from(updated))
385 }
386}
387
388#[derive(Debug, serde::Serialize)]
390pub struct TicketStatistics {
391 #[serde(rename = "total_tickets")]
392 pub total: i64,
393 #[serde(rename = "open_tickets")]
394 pub open: i64,
395 #[serde(rename = "assigned_tickets")]
396 pub assigned: i64,
397 #[serde(rename = "in_progress_tickets")]
398 pub in_progress: i64,
399 #[serde(rename = "resolved_tickets")]
400 pub resolved: i64,
401 #[serde(rename = "closed_tickets")]
402 pub closed: i64,
403 #[serde(rename = "cancelled_tickets")]
404 pub cancelled: i64,
405 pub overdue_tickets: i64,
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411 use crate::application::ports::TicketRepository;
412 use crate::domain::entities::{Ticket, TicketCategory, TicketPriority, TicketStatus};
413 use async_trait::async_trait;
414 use std::collections::HashMap;
415 use std::sync::Mutex;
416
417 struct MockTicketRepository {
420 tickets: Mutex<HashMap<Uuid, Ticket>>,
421 }
422
423 impl MockTicketRepository {
424 fn new() -> Self {
425 Self {
426 tickets: Mutex::new(HashMap::new()),
427 }
428 }
429 }
430
431 #[async_trait]
432 impl TicketRepository for MockTicketRepository {
433 async fn create(&self, ticket: &Ticket) -> Result<Ticket, String> {
434 self.tickets
435 .lock()
436 .unwrap()
437 .insert(ticket.id, ticket.clone());
438 Ok(ticket.clone())
439 }
440
441 async fn find_by_id(&self, id: Uuid) -> Result<Option<Ticket>, String> {
442 Ok(self.tickets.lock().unwrap().get(&id).cloned())
443 }
444
445 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Ticket>, String> {
446 Ok(self
447 .tickets
448 .lock()
449 .unwrap()
450 .values()
451 .filter(|t| t.building_id == building_id)
452 .cloned()
453 .collect())
454 }
455
456 async fn find_by_organization(&self, organization_id: Uuid) -> Result<Vec<Ticket>, String> {
457 Ok(self
458 .tickets
459 .lock()
460 .unwrap()
461 .values()
462 .filter(|t| t.organization_id == organization_id)
463 .cloned()
464 .collect())
465 }
466
467 async fn find_by_created_by(&self, created_by: Uuid) -> Result<Vec<Ticket>, String> {
468 Ok(self
469 .tickets
470 .lock()
471 .unwrap()
472 .values()
473 .filter(|t| t.created_by == created_by)
474 .cloned()
475 .collect())
476 }
477
478 async fn find_by_assigned_to(&self, assigned_to: Uuid) -> Result<Vec<Ticket>, String> {
479 Ok(self
480 .tickets
481 .lock()
482 .unwrap()
483 .values()
484 .filter(|t| t.assigned_to == Some(assigned_to))
485 .cloned()
486 .collect())
487 }
488
489 async fn find_by_status(
490 &self,
491 building_id: Uuid,
492 status: TicketStatus,
493 ) -> Result<Vec<Ticket>, String> {
494 Ok(self
495 .tickets
496 .lock()
497 .unwrap()
498 .values()
499 .filter(|t| t.building_id == building_id && t.status == status)
500 .cloned()
501 .collect())
502 }
503
504 async fn update(&self, ticket: &Ticket) -> Result<Ticket, String> {
505 self.tickets
506 .lock()
507 .unwrap()
508 .insert(ticket.id, ticket.clone());
509 Ok(ticket.clone())
510 }
511
512 async fn delete(&self, id: Uuid) -> Result<bool, String> {
513 Ok(self.tickets.lock().unwrap().remove(&id).is_some())
514 }
515
516 async fn count_by_building(&self, building_id: Uuid) -> Result<i64, String> {
517 Ok(self
518 .tickets
519 .lock()
520 .unwrap()
521 .values()
522 .filter(|t| t.building_id == building_id)
523 .count() as i64)
524 }
525
526 async fn count_by_status(
527 &self,
528 building_id: Uuid,
529 status: TicketStatus,
530 ) -> Result<i64, String> {
531 Ok(self
532 .tickets
533 .lock()
534 .unwrap()
535 .values()
536 .filter(|t| t.building_id == building_id && t.status == status)
537 .count() as i64)
538 }
539
540 async fn count_by_organization(&self, organization_id: Uuid) -> Result<i64, String> {
541 Ok(self
542 .tickets
543 .lock()
544 .unwrap()
545 .values()
546 .filter(|t| t.organization_id == organization_id)
547 .count() as i64)
548 }
549
550 async fn count_by_organization_and_status(
551 &self,
552 organization_id: Uuid,
553 status: TicketStatus,
554 ) -> Result<i64, String> {
555 Ok(self
556 .tickets
557 .lock()
558 .unwrap()
559 .values()
560 .filter(|t| t.organization_id == organization_id && t.status == status)
561 .count() as i64)
562 }
563 }
564
565 fn make_use_cases(repo: Arc<dyn TicketRepository>) -> TicketUseCases {
568 TicketUseCases::new(repo)
569 }
570
571 fn make_create_request(building_id: Uuid) -> CreateTicketRequest {
572 CreateTicketRequest {
573 building_id,
574 unit_id: None,
575 title: "Fuite d'eau cuisine".to_string(),
576 description: "L'eau coule sous l'évier de la cuisine commune".to_string(),
577 category: TicketCategory::Plumbing,
578 priority: TicketPriority::High,
579 }
580 }
581
582 async fn create_ticket_helper(
584 use_cases: &TicketUseCases,
585 org_id: Uuid,
586 building_id: Uuid,
587 ) -> TicketResponse {
588 let user_id = Uuid::new_v4();
589 let request = make_create_request(building_id);
590 use_cases
591 .create_ticket(org_id, user_id, request)
592 .await
593 .expect("create_ticket should succeed")
594 }
595
596 #[tokio::test]
599 async fn test_create_ticket_success() {
600 let repo = Arc::new(MockTicketRepository::new());
601 let use_cases = make_use_cases(repo);
602
603 let org_id = Uuid::new_v4();
604 let building_id = Uuid::new_v4();
605 let user_id = Uuid::new_v4();
606 let request = make_create_request(building_id);
607
608 let result = use_cases.create_ticket(org_id, user_id, request).await;
609
610 assert!(result.is_ok());
611 let ticket = result.unwrap();
612 assert_eq!(ticket.status, TicketStatus::Open);
613 assert_eq!(ticket.building_id, building_id);
614 assert_eq!(ticket.organization_id, org_id);
615 assert_eq!(ticket.created_by, user_id);
616 assert_eq!(ticket.title, "Fuite d'eau cuisine");
617 assert!(ticket.assigned_to.is_none());
618 assert!(ticket.resolution_notes.is_none());
619 }
620
621 #[tokio::test]
622 async fn test_create_ticket_empty_title_fails() {
623 let repo = Arc::new(MockTicketRepository::new());
624 let use_cases = make_use_cases(repo);
625
626 let request = CreateTicketRequest {
627 building_id: Uuid::new_v4(),
628 unit_id: None,
629 title: " ".to_string(),
630 description: "Some description".to_string(),
631 category: TicketCategory::Electrical,
632 priority: TicketPriority::Low,
633 };
634
635 let result = use_cases
636 .create_ticket(Uuid::new_v4(), Uuid::new_v4(), request)
637 .await;
638
639 assert!(result.is_err());
640 assert!(result.unwrap_err().contains("Title cannot be empty"));
641 }
642
643 #[tokio::test]
644 async fn test_assign_contractor_transitions_to_in_progress() {
645 let repo = Arc::new(MockTicketRepository::new());
646 let use_cases = make_use_cases(repo);
647
648 let org_id = Uuid::new_v4();
649 let building_id = Uuid::new_v4();
650 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
651 assert_eq!(ticket.status, TicketStatus::Open);
652
653 let contractor_id = Uuid::new_v4();
654 let assign_request = AssignTicketRequest {
655 assigned_to: contractor_id,
656 };
657
658 let result = use_cases.assign_ticket(ticket.id, assign_request).await;
659
660 assert!(result.is_ok());
661 let updated = result.unwrap();
662 assert_eq!(updated.status, TicketStatus::InProgress);
663 assert_eq!(updated.assigned_to, Some(contractor_id));
664 }
665
666 #[tokio::test]
667 async fn test_start_work_open_to_in_progress() {
668 let repo = Arc::new(MockTicketRepository::new());
669 let use_cases = make_use_cases(repo);
670
671 let org_id = Uuid::new_v4();
672 let building_id = Uuid::new_v4();
673 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
674
675 let result = use_cases.start_work(ticket.id).await;
676
677 assert!(result.is_ok());
678 assert_eq!(result.unwrap().status, TicketStatus::InProgress);
679 }
680
681 #[tokio::test]
682 async fn test_resolve_ticket_with_notes() {
683 let repo = Arc::new(MockTicketRepository::new());
684 let use_cases = make_use_cases(repo);
685
686 let org_id = Uuid::new_v4();
687 let building_id = Uuid::new_v4();
688 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
689
690 use_cases.start_work(ticket.id).await.unwrap();
692
693 let resolve_request = ResolveTicketRequest {
694 resolution_notes: "Fuite réparée, joint remplacé".to_string(),
695 };
696 let result = use_cases.resolve_ticket(ticket.id, resolve_request).await;
697
698 assert!(result.is_ok());
699 let resolved = result.unwrap();
700 assert_eq!(resolved.status, TicketStatus::Resolved);
701 assert_eq!(
702 resolved.resolution_notes.as_deref(),
703 Some("Fuite réparée, joint remplacé")
704 );
705 assert!(resolved.resolved_at.is_some());
706 }
707
708 #[tokio::test]
709 async fn test_close_ticket_after_resolution() {
710 let repo = Arc::new(MockTicketRepository::new());
711 let use_cases = make_use_cases(repo);
712
713 let org_id = Uuid::new_v4();
714 let building_id = Uuid::new_v4();
715 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
716
717 use_cases.start_work(ticket.id).await.unwrap();
719 use_cases
720 .resolve_ticket(
721 ticket.id,
722 ResolveTicketRequest {
723 resolution_notes: "Done".to_string(),
724 },
725 )
726 .await
727 .unwrap();
728
729 let result = use_cases.close_ticket(ticket.id).await;
730
731 assert!(result.is_ok());
732 let closed = result.unwrap();
733 assert_eq!(closed.status, TicketStatus::Closed);
734 assert!(closed.closed_at.is_some());
735 }
736
737 #[tokio::test]
738 async fn test_close_open_ticket_fails() {
739 let repo = Arc::new(MockTicketRepository::new());
740 let use_cases = make_use_cases(repo);
741
742 let org_id = Uuid::new_v4();
743 let building_id = Uuid::new_v4();
744 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
745
746 let result = use_cases.close_ticket(ticket.id).await;
748
749 assert!(result.is_err());
750 assert!(result.unwrap_err().contains("Must be Resolved first"));
751 }
752
753 #[tokio::test]
754 async fn test_cancel_ticket_with_reason() {
755 let repo = Arc::new(MockTicketRepository::new());
756 let use_cases = make_use_cases(repo);
757
758 let org_id = Uuid::new_v4();
759 let building_id = Uuid::new_v4();
760 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
761
762 let cancel_request = CancelTicketRequest {
763 reason: "Erreur de déclaration, problème déjà résolu".to_string(),
764 };
765 let result = use_cases.cancel_ticket(ticket.id, cancel_request).await;
766
767 assert!(result.is_ok());
768 let cancelled = result.unwrap();
769 assert_eq!(cancelled.status, TicketStatus::Cancelled);
770 assert!(cancelled
771 .resolution_notes
772 .as_deref()
773 .unwrap()
774 .contains("CANCELLED"));
775 }
776
777 #[tokio::test]
778 async fn test_reopen_resolved_ticket() {
779 let repo = Arc::new(MockTicketRepository::new());
780 let use_cases = make_use_cases(repo);
781
782 let org_id = Uuid::new_v4();
783 let building_id = Uuid::new_v4();
784 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
785
786 use_cases.start_work(ticket.id).await.unwrap();
788 use_cases
789 .resolve_ticket(
790 ticket.id,
791 ResolveTicketRequest {
792 resolution_notes: "Fixed".to_string(),
793 },
794 )
795 .await
796 .unwrap();
797
798 let reopen_request = ReopenTicketRequest {
800 reason: "Problème persiste après intervention".to_string(),
801 };
802 let result = use_cases.reopen_ticket(ticket.id, reopen_request).await;
803
804 assert!(result.is_ok());
805 let reopened = result.unwrap();
806 assert_eq!(reopened.status, TicketStatus::InProgress);
807 assert!(reopened.resolved_at.is_none());
808 assert!(reopened.closed_at.is_none());
809 assert!(reopened
810 .resolution_notes
811 .as_deref()
812 .unwrap()
813 .contains("REOPENED"));
814 }
815
816 #[tokio::test]
817 async fn test_reopen_open_ticket_fails() {
818 let repo = Arc::new(MockTicketRepository::new());
819 let use_cases = make_use_cases(repo);
820
821 let org_id = Uuid::new_v4();
822 let building_id = Uuid::new_v4();
823 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
824
825 let reopen_request = ReopenTicketRequest {
827 reason: "Some reason".to_string(),
828 };
829 let result = use_cases.reopen_ticket(ticket.id, reopen_request).await;
830
831 assert!(result.is_err());
832 assert!(result
833 .unwrap_err()
834 .contains("Can only reopen resolved or closed tickets"));
835 }
836
837 #[tokio::test]
838 async fn test_send_work_order_in_progress_ticket() {
839 let repo = Arc::new(MockTicketRepository::new());
840 let use_cases = make_use_cases(repo);
841
842 let org_id = Uuid::new_v4();
843 let building_id = Uuid::new_v4();
844 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
845
846 let contractor_id = Uuid::new_v4();
848 use_cases
849 .assign_ticket(
850 ticket.id,
851 AssignTicketRequest {
852 assigned_to: contractor_id,
853 },
854 )
855 .await
856 .unwrap();
857
858 let result = use_cases.send_work_order(ticket.id).await;
860
861 assert!(result.is_ok());
862 let updated = result.unwrap();
863 assert!(updated.work_order_sent_at.is_some());
864 assert_eq!(updated.status, TicketStatus::InProgress);
865 }
866
867 #[tokio::test]
868 async fn test_send_work_order_open_ticket_fails() {
869 let repo = Arc::new(MockTicketRepository::new());
870 let use_cases = make_use_cases(repo);
871
872 let org_id = Uuid::new_v4();
873 let building_id = Uuid::new_v4();
874 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
875
876 let result = use_cases.send_work_order(ticket.id).await;
878
879 assert!(result.is_err());
880 assert!(result.unwrap_err().contains("InProgress"));
881 }
882
883 #[tokio::test]
884 async fn test_get_overdue_tickets() {
885 let repo = Arc::new(MockTicketRepository::new());
886 let use_cases = make_use_cases(repo.clone());
887
888 let org_id = Uuid::new_v4();
889 let building_id = Uuid::new_v4();
890 let user_id = Uuid::new_v4();
891
892 let request = make_create_request(building_id);
894 let created = use_cases
895 .create_ticket(org_id, user_id, request)
896 .await
897 .unwrap();
898
899 {
901 let mut store = repo.tickets.lock().unwrap();
902 if let Some(ticket) = store.get_mut(&created.id) {
903 ticket.created_at = chrono::Utc::now() - chrono::Duration::days(10);
904 }
905 }
906
907 let overdue = use_cases.get_overdue_tickets(building_id, 5).await.unwrap();
908 assert_eq!(overdue.len(), 1);
909 assert_eq!(overdue[0].id, created.id);
910
911 let not_overdue = use_cases
913 .get_overdue_tickets(building_id, 15)
914 .await
915 .unwrap();
916 assert_eq!(not_overdue.len(), 0);
917 }
918
919 #[tokio::test]
920 async fn test_get_ticket_statistics() {
921 let repo = Arc::new(MockTicketRepository::new());
922 let use_cases = make_use_cases(repo);
923
924 let org_id = Uuid::new_v4();
925 let building_id = Uuid::new_v4();
926
927 let _t1 = create_ticket_helper(&use_cases, org_id, building_id).await; let t2 = create_ticket_helper(&use_cases, org_id, building_id).await;
930 let t3 = create_ticket_helper(&use_cases, org_id, building_id).await;
931
932 use_cases.start_work(t2.id).await.unwrap();
934 use_cases
935 .resolve_ticket(
936 t2.id,
937 ResolveTicketRequest {
938 resolution_notes: "Done".to_string(),
939 },
940 )
941 .await
942 .unwrap();
943
944 use_cases
946 .cancel_ticket(
947 t3.id,
948 CancelTicketRequest {
949 reason: "Duplicate".to_string(),
950 },
951 )
952 .await
953 .unwrap();
954
955 let stats = use_cases.get_ticket_statistics(building_id).await.unwrap();
956
957 assert_eq!(stats.total, 3);
958 assert_eq!(stats.open, 1); assert_eq!(stats.in_progress, 0);
960 assert_eq!(stats.resolved, 1); assert_eq!(stats.closed, 0);
962 assert_eq!(stats.cancelled, 1); }
964
965 #[tokio::test]
966 async fn test_delete_open_ticket_succeeds() {
967 let repo = Arc::new(MockTicketRepository::new());
968 let use_cases = make_use_cases(repo);
969
970 let org_id = Uuid::new_v4();
971 let building_id = Uuid::new_v4();
972 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
973
974 let result = use_cases.delete_ticket(ticket.id).await;
975
976 assert!(result.is_ok());
977 assert!(result.unwrap());
978
979 let fetched = use_cases.get_ticket(ticket.id).await.unwrap();
981 assert!(fetched.is_none());
982 }
983
984 #[tokio::test]
985 async fn test_delete_in_progress_ticket_fails() {
986 let repo = Arc::new(MockTicketRepository::new());
987 let use_cases = make_use_cases(repo);
988
989 let org_id = Uuid::new_v4();
990 let building_id = Uuid::new_v4();
991 let ticket = create_ticket_helper(&use_cases, org_id, building_id).await;
992
993 use_cases.start_work(ticket.id).await.unwrap();
995
996 let result = use_cases.delete_ticket(ticket.id).await;
997
998 assert!(result.is_err());
999 assert!(result
1000 .unwrap_err()
1001 .contains("Can only delete tickets with Open status"));
1002 }
1003}