koprogo_api/application/use_cases/
ticket_use_cases.rs

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    /// Create a new maintenance request ticket
20    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    /// Get a ticket by ID
42    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    /// List all tickets for a building
50    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    /// List all tickets for an organization
59    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    /// List tickets created by a specific owner
71    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    /// List tickets assigned to a specific user (syndic/contractor)
80    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    /// List tickets by status for a building
92    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    /// Assign a ticket to a user (syndic or contractor)
105    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    /// Start working on a ticket (transition to InProgress)
123    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    /// Resolve a ticket (mark as work completed)
137    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    /// Close a ticket (final validation by requester or syndic)
155    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    /// Cancel a ticket with a reason
169    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    /// Reopen a ticket (if incorrectly resolved)
187    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    /// Delete a ticket (only allowed for Open tickets)
205    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        // Only allow deletion of Open tickets
213        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    /// Get ticket statistics for a building
221    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    /// Check for overdue tickets (open for more than specified days)
266    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
283/// Ticket statistics for a building
284#[derive(Debug, serde::Serialize)]
285pub struct TicketStatistics {
286    pub total: i64,
287    pub open: i64,
288    pub in_progress: i64,
289    pub resolved: i64,
290    pub closed: i64,
291    pub cancelled: i64,
292}