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