1use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10use chrono::{DateTime, Utc};
11
12#[cfg(feature = "openapi")]
13#[cfg(feature = "openapi")]
14use utoipa::ToSchema;
15
16#[cfg(feature = "validation")]
17use validator::Validate;
18
19use crate::domain::entity::Issue;
20use crate::domain::entity::AuditMetadata;
21use crate::domain::entity::AgreementStatus;
22use crate::domain::entity::IssuePriority;
23use crate::domain::entity::IssueStatus;
24
25#[derive(Debug, Clone, Deserialize)]
34#[cfg_attr(feature = "openapi", derive(ToSchema))]
35#[cfg_attr(feature = "validation", derive(Validate))]
36#[serde(rename_all = "camelCase")]
37pub struct CreateIssueDto {
38 #[serde(default, skip_serializing_if = "Option::is_none", alias = "customer_id")]
39 pub customer_id: Option<Uuid>,
40 #[cfg_attr(feature = "validation", validate(length(max = 200)))]
41 #[cfg_attr(feature = "openapi", schema(example = "example"))]
42 pub subject: String,
43 #[cfg_attr(feature = "validation", validate(length(max = 4000)))]
44 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub description: Option<String>,
46 pub priority: IssuePriority,
47 #[serde(default, skip_serializing_if = "Option::is_none", alias = "sla_id")]
48 pub sla_id: Option<Uuid>,
49 pub status: IssueStatus,
50 #[serde(alias = "agreement_status")]
51 pub agreement_status: AgreementStatus,
52 #[cfg_attr(feature = "openapi", schema(example = "2024-01-01T00:00:00Z"))]
53 #[serde(alias = "opened_at")]
54 pub opened_at: DateTime<Utc>,
55 #[serde(default, skip_serializing_if = "Option::is_none", alias = "response_by")]
56 pub response_by: Option<DateTime<Utc>>,
57 #[serde(default, skip_serializing_if = "Option::is_none", alias = "resolution_by")]
58 pub resolution_by: Option<DateTime<Utc>>,
59 #[serde(default, skip_serializing_if = "Option::is_none", alias = "first_responded_at")]
60 pub first_responded_at: Option<DateTime<Utc>>,
61 #[cfg_attr(feature = "openapi", schema(example = true))]
62 #[serde(alias = "response_breached")]
63 pub response_breached: bool,
64 #[serde(default, skip_serializing_if = "Option::is_none", alias = "resolved_at")]
65 pub resolved_at: Option<DateTime<Utc>>,
66 #[serde(default, skip_serializing_if = "Option::is_none", alias = "paused_at")]
67 pub paused_at: Option<DateTime<Utc>>,
68 #[cfg_attr(feature = "openapi", schema(example = 42))]
69 #[serde(alias = "total_paused_mins")]
70 pub total_paused_mins: i32,
71 #[serde(default, skip_serializing_if = "Option::is_none", alias = "escalated_project_id")]
72 pub escalated_project_id: Option<Uuid>,
73}
74
75#[derive(Debug, Clone, Deserialize)]
84#[cfg_attr(feature = "openapi", derive(ToSchema))]
85#[cfg_attr(feature = "validation", derive(Validate))]
86#[serde(rename_all = "camelCase")]
87pub struct UpdateIssueDto {
88 #[serde(default, skip_serializing_if = "Option::is_none", alias = "customer_id")]
89 pub customer_id: Option<Uuid>,
90 #[cfg_attr(feature = "validation", validate(length(max = 200)))]
91 #[cfg_attr(feature = "openapi", schema(example = "example"))]
92 pub subject: String,
93 #[cfg_attr(feature = "validation", validate(length(max = 4000)))]
94 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub description: Option<String>,
96 pub priority: IssuePriority,
97 #[serde(default, skip_serializing_if = "Option::is_none", alias = "sla_id")]
98 pub sla_id: Option<Uuid>,
99 pub status: IssueStatus,
100 #[serde(alias = "agreement_status")]
101 pub agreement_status: AgreementStatus,
102 #[cfg_attr(feature = "openapi", schema(example = "2024-01-01T00:00:00Z"))]
103 #[serde(alias = "opened_at")]
104 pub opened_at: DateTime<Utc>,
105 #[serde(default, skip_serializing_if = "Option::is_none", alias = "response_by")]
106 pub response_by: Option<DateTime<Utc>>,
107 #[serde(default, skip_serializing_if = "Option::is_none", alias = "resolution_by")]
108 pub resolution_by: Option<DateTime<Utc>>,
109 #[serde(default, skip_serializing_if = "Option::is_none", alias = "first_responded_at")]
110 pub first_responded_at: Option<DateTime<Utc>>,
111 #[cfg_attr(feature = "openapi", schema(example = true))]
112 #[serde(alias = "response_breached")]
113 pub response_breached: bool,
114 #[serde(default, skip_serializing_if = "Option::is_none", alias = "resolved_at")]
115 pub resolved_at: Option<DateTime<Utc>>,
116 #[serde(default, skip_serializing_if = "Option::is_none", alias = "paused_at")]
117 pub paused_at: Option<DateTime<Utc>>,
118 #[cfg_attr(feature = "openapi", schema(example = 42))]
119 #[serde(alias = "total_paused_mins")]
120 pub total_paused_mins: i32,
121 #[serde(default, skip_serializing_if = "Option::is_none", alias = "escalated_project_id")]
122 pub escalated_project_id: Option<Uuid>,
123}
124
125#[derive(Debug, Clone, Default, Deserialize)]
134#[cfg_attr(feature = "openapi", derive(ToSchema))]
135#[cfg_attr(feature = "validation", derive(Validate))]
136#[serde(rename_all = "camelCase")]
137pub struct PatchIssueDto {
138 #[serde(skip_serializing_if = "Option::is_none", alias = "customer_id")]
139 pub customer_id: Option<Uuid>,
140 #[cfg_attr(feature = "validation", validate(length(max = 200)))]
141 #[cfg_attr(feature = "openapi", schema(example = "example"))]
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub subject: Option<String>,
144 #[cfg_attr(feature = "validation", validate(length(max = 4000)))]
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub description: Option<String>,
147 #[serde(skip_serializing_if = "Option::is_none")]
148 pub priority: Option<IssuePriority>,
149 #[serde(skip_serializing_if = "Option::is_none", alias = "sla_id")]
150 pub sla_id: Option<Uuid>,
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub status: Option<IssueStatus>,
153 #[serde(skip_serializing_if = "Option::is_none", alias = "agreement_status")]
154 pub agreement_status: Option<AgreementStatus>,
155 #[cfg_attr(feature = "openapi", schema(example = "2024-01-01T00:00:00Z"))]
156 #[serde(skip_serializing_if = "Option::is_none", alias = "opened_at")]
157 pub opened_at: Option<DateTime<Utc>>,
158 #[serde(skip_serializing_if = "Option::is_none", alias = "response_by")]
159 pub response_by: Option<DateTime<Utc>>,
160 #[serde(skip_serializing_if = "Option::is_none", alias = "resolution_by")]
161 pub resolution_by: Option<DateTime<Utc>>,
162 #[serde(skip_serializing_if = "Option::is_none", alias = "first_responded_at")]
163 pub first_responded_at: Option<DateTime<Utc>>,
164 #[cfg_attr(feature = "openapi", schema(example = true))]
165 #[serde(skip_serializing_if = "Option::is_none", alias = "response_breached")]
166 pub response_breached: Option<bool>,
167 #[serde(skip_serializing_if = "Option::is_none", alias = "resolved_at")]
168 pub resolved_at: Option<DateTime<Utc>>,
169 #[serde(skip_serializing_if = "Option::is_none", alias = "paused_at")]
170 pub paused_at: Option<DateTime<Utc>>,
171 #[cfg_attr(feature = "openapi", schema(example = 42))]
172 #[serde(skip_serializing_if = "Option::is_none", alias = "total_paused_mins")]
173 pub total_paused_mins: Option<i32>,
174 #[serde(skip_serializing_if = "Option::is_none", alias = "escalated_project_id")]
175 pub escalated_project_id: Option<Uuid>,
176}
177
178impl PatchIssueDto {
179 pub fn has_changes(&self) -> bool {
181 self.customer_id.is_some() || self.subject.is_some() || self.description.is_some() || self.priority.is_some() || self.sla_id.is_some() || self.status.is_some() || self.agreement_status.is_some() || self.opened_at.is_some() || self.response_by.is_some() || self.resolution_by.is_some() || self.first_responded_at.is_some() || self.response_breached.is_some() || self.resolved_at.is_some() || self.paused_at.is_some() || self.total_paused_mins.is_some() || self.escalated_project_id.is_some()
182 }
183}
184
185#[derive(Debug, Clone, Serialize)]
194#[cfg_attr(feature = "openapi", derive(ToSchema))]
195#[serde(rename_all = "camelCase")]
196pub struct IssueResponseDto {
197 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
198 pub id: Uuid,
199 pub customer_id: Option<Uuid>,
200 #[cfg_attr(feature = "openapi", schema(example = "example"))]
201 pub subject: String,
202 pub description: Option<String>,
203 pub priority: IssuePriority,
204 pub sla_id: Option<Uuid>,
205 pub status: IssueStatus,
206 pub agreement_status: AgreementStatus,
207 #[cfg_attr(feature = "openapi", schema(example = "2024-01-01T00:00:00Z"))]
208 pub opened_at: DateTime<Utc>,
209 pub response_by: Option<DateTime<Utc>>,
210 pub resolution_by: Option<DateTime<Utc>>,
211 pub first_responded_at: Option<DateTime<Utc>>,
212 #[cfg_attr(feature = "openapi", schema(example = true))]
213 pub response_breached: bool,
214 pub resolved_at: Option<DateTime<Utc>>,
215 pub paused_at: Option<DateTime<Utc>>,
216 #[cfg_attr(feature = "openapi", schema(example = 42))]
217 pub total_paused_mins: i32,
218 pub escalated_project_id: Option<Uuid>,
219 pub metadata: AuditMetadata,
220}
221
222#[derive(Debug, Clone, Serialize)]
231#[cfg_attr(feature = "openapi", derive(ToSchema))]
232#[serde(rename_all = "camelCase")]
233pub struct IssueListResponseDto {
234 pub items: Vec<IssueResponseDto>,
236 pub total: u64,
238 pub page: u32,
240 pub per_page: u32,
242 pub total_pages: u32,
244 pub has_next: bool,
246 pub has_prev: bool,
248}
249
250impl IssueListResponseDto {
251 pub fn new(items: Vec<IssueResponseDto>, total: u64, page: u32, per_page: u32) -> Self {
253 let total_pages = if per_page > 0 {
254 ((total as f64) / (per_page as f64)).ceil() as u32
255 } else {
256 0
257 };
258 Self {
259 items,
260 total,
261 page,
262 per_page,
263 total_pages,
264 has_next: page < total_pages,
265 has_prev: page > 1,
266 }
267 }
268}
269
270#[derive(Debug, Clone, Serialize)]
272#[cfg_attr(feature = "openapi", derive(ToSchema))]
273#[serde(rename_all = "camelCase")]
274pub struct IssueSummaryDto {
275 pub id: Uuid,
276 pub customer_id: Option<Uuid>,
277 pub subject: String,
278 pub description: Option<String>,
279 pub created_at: Option<DateTime<Utc>>,
280}
281
282impl From<Issue> for IssueResponseDto {
287 fn from(entity: Issue) -> Self {
288 Self {
289 id: entity.id,
290 customer_id: entity.customer_id,
291 subject: entity.subject,
292 description: entity.description,
293 priority: entity.priority,
294 sla_id: entity.sla_id,
295 status: entity.status,
296 agreement_status: entity.agreement_status,
297 opened_at: entity.opened_at,
298 response_by: entity.response_by,
299 resolution_by: entity.resolution_by,
300 first_responded_at: entity.first_responded_at,
301 response_breached: entity.response_breached,
302 resolved_at: entity.resolved_at,
303 paused_at: entity.paused_at,
304 total_paused_mins: entity.total_paused_mins,
305 escalated_project_id: entity.escalated_project_id,
306 metadata: entity.metadata,
307 }
308 }
309}
310
311impl From<Issue> for IssueSummaryDto {
312 fn from(entity: Issue) -> Self {
313 let created_at = backbone_core::PersistentEntity::created_at(&entity);
314 Self {
315 id: entity.id,
316 customer_id: entity.customer_id,
317 subject: entity.subject,
318 description: entity.description,
319 created_at,
320 }
321 }
322}
323
324impl From<CreateIssueDto> for Issue {
325 fn from(dto: CreateIssueDto) -> Self {
326 Self {
327 id: Uuid::new_v4(),
328 customer_id: dto.customer_id,
329 subject: dto.subject,
330 description: dto.description,
331 priority: dto.priority,
332 sla_id: dto.sla_id,
333 status: dto.status,
334 agreement_status: dto.agreement_status,
335 opened_at: dto.opened_at,
336 response_by: dto.response_by,
337 resolution_by: dto.resolution_by,
338 first_responded_at: dto.first_responded_at,
339 response_breached: dto.response_breached,
340 resolved_at: dto.resolved_at,
341 paused_at: dto.paused_at,
342 total_paused_mins: dto.total_paused_mins,
343 escalated_project_id: dto.escalated_project_id,
344 metadata: AuditMetadata::default(),
345 }
346 }
347}
348
349impl From<&Issue> for IssueResponseDto {
350 fn from(entity: &Issue) -> Self {
351 Self {
352 id: entity.id.clone(),
353 customer_id: entity.customer_id.clone(),
354 subject: entity.subject.clone(),
355 description: entity.description.clone(),
356 priority: entity.priority.clone(),
357 sla_id: entity.sla_id.clone(),
358 status: entity.status.clone(),
359 agreement_status: entity.agreement_status.clone(),
360 opened_at: entity.opened_at.clone(),
361 response_by: entity.response_by.clone(),
362 resolution_by: entity.resolution_by.clone(),
363 first_responded_at: entity.first_responded_at.clone(),
364 response_breached: entity.response_breached.clone(),
365 resolved_at: entity.resolved_at.clone(),
366 paused_at: entity.paused_at.clone(),
367 total_paused_mins: entity.total_paused_mins.clone(),
368 escalated_project_id: entity.escalated_project_id.clone(),
369 metadata: entity.metadata.clone(),
370 }
371 }
372}
373
374impl backbone_core::FromCreateDto<CreateIssueDto> for Issue {
375 fn from_create_dto(dto: CreateIssueDto) -> backbone_core::ServiceResult<Self> {
376 Ok(Issue::from(dto))
377 }
378}
379
380impl backbone_core::ApplyUpdateDto<UpdateIssueDto> for Issue {
381 fn apply_update(mut self, dto: UpdateIssueDto) -> backbone_core::ServiceResult<Self> {
382 self.customer_id = dto.customer_id;
383 self.subject = dto.subject;
384 self.description = dto.description;
385 self.priority = dto.priority;
386 self.sla_id = dto.sla_id;
387 self.status = dto.status;
388 self.agreement_status = dto.agreement_status;
389 self.opened_at = dto.opened_at;
390 self.response_by = dto.response_by;
391 self.resolution_by = dto.resolution_by;
392 self.first_responded_at = dto.first_responded_at;
393 self.response_breached = dto.response_breached;
394 self.resolved_at = dto.resolved_at;
395 self.paused_at = dto.paused_at;
396 self.total_paused_mins = dto.total_paused_mins;
397 self.escalated_project_id = dto.escalated_project_id;
398 Ok(self)
399 }
400}
401
402