Skip to main content

backbone_support/domain/entity/
issue.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4use uuid::Uuid;
5
6use super::IssuePriority;
7use super::IssueStatus;
8use super::AgreementStatus;
9use super::AuditMetadata;
10
11/// Strongly-typed ID for Issue
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(transparent)]
14pub struct IssueId(pub Uuid);
15
16impl IssueId {
17    pub fn new(id: Uuid) -> Self { Self(id) }
18    pub fn generate() -> Self { Self(Uuid::new_v4()) }
19    pub fn into_inner(self) -> Uuid { self.0 }
20}
21
22impl std::fmt::Display for IssueId {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        write!(f, "{}", self.0)
25    }
26}
27
28impl std::str::FromStr for IssueId {
29    type Err = uuid::Error;
30    fn from_str(s: &str) -> Result<Self, Self::Err> {
31        Ok(Self(Uuid::parse_str(s)?))
32    }
33}
34
35impl From<Uuid> for IssueId {
36    fn from(id: Uuid) -> Self { Self(id) }
37}
38
39impl From<IssueId> for Uuid {
40    fn from(id: IssueId) -> Self { id.0 }
41}
42
43impl AsRef<Uuid> for IssueId {
44    fn as_ref(&self) -> &Uuid { &self.0 }
45}
46
47impl std::ops::Deref for IssueId {
48    type Target = Uuid;
49    fn deref(&self) -> &Self::Target { &self.0 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
53pub struct Issue {
54    pub id: Uuid,
55    pub customer_id: Option<Uuid>,
56    pub subject: String,
57    pub description: Option<String>,
58    pub priority: IssuePriority,
59    pub sla_id: Option<Uuid>,
60    pub status: IssueStatus,
61    pub agreement_status: AgreementStatus,
62    pub opened_at: DateTime<Utc>,
63    pub response_by: Option<DateTime<Utc>>,
64    pub resolution_by: Option<DateTime<Utc>>,
65    pub first_responded_at: Option<DateTime<Utc>>,
66    pub response_breached: bool,
67    pub resolved_at: Option<DateTime<Utc>>,
68    pub paused_at: Option<DateTime<Utc>>,
69    pub total_paused_mins: i32,
70    pub escalated_project_id: Option<Uuid>,
71    #[serde(default)]
72    #[sqlx(json)]
73    pub metadata: AuditMetadata,
74}
75
76impl Issue {
77    /// Create a builder for Issue
78    pub fn builder() -> IssueBuilder {
79        <IssueBuilder as Default>::default()
80    }
81
82    /// Create a new Issue with required fields
83    pub fn new(subject: String, priority: IssuePriority, status: IssueStatus, agreement_status: AgreementStatus, opened_at: DateTime<Utc>, response_breached: bool, total_paused_mins: i32) -> Self {
84        Self {
85            id: Uuid::new_v4(),
86            customer_id: None,
87            subject,
88            description: None,
89            priority,
90            sla_id: None,
91            status,
92            agreement_status,
93            opened_at,
94            response_by: None,
95            resolution_by: None,
96            first_responded_at: None,
97            response_breached,
98            resolved_at: None,
99            paused_at: None,
100            total_paused_mins,
101            escalated_project_id: None,
102            metadata: AuditMetadata::default(),
103        }
104    }
105
106    /// Get the entity's unique identifier
107    pub fn id(&self) -> &Uuid {
108        &self.id
109    }
110
111    /// Get a strongly-typed ID for this entity
112    pub fn typed_id(&self) -> IssueId {
113        IssueId(self.id)
114    }
115
116    /// Get when this entity was created
117    pub fn created_at(&self) -> Option<&DateTime<Utc>> {
118        self.metadata.created_at.as_ref()
119    }
120
121    /// Get when this entity was last updated
122    pub fn updated_at(&self) -> Option<&DateTime<Utc>> {
123        self.metadata.updated_at.as_ref()
124    }
125
126    /// Check if this entity is soft deleted
127    pub fn is_deleted(&self) -> bool {
128        self.metadata.deleted_at.is_some()
129    }
130
131    /// Check if this entity is active (not deleted)
132    pub fn is_active(&self) -> bool {
133        self.metadata.deleted_at.is_none()
134    }
135
136    /// Get when this entity was deleted
137    pub fn deleted_at(&self) -> Option<&DateTime<Utc>> {
138        self.metadata.deleted_at.as_ref()
139    }
140
141    /// Get who created this entity
142    pub fn created_by(&self) -> Option<&Uuid> {
143        self.metadata.created_by.as_ref()
144    }
145
146    /// Get who last updated this entity
147    pub fn updated_by(&self) -> Option<&Uuid> {
148        self.metadata.updated_by.as_ref()
149    }
150
151    /// Get who deleted this entity
152    pub fn deleted_by(&self) -> Option<&Uuid> {
153        self.metadata.deleted_by.as_ref()
154    }
155
156    /// Get the current status
157    pub fn status(&self) -> &IssueStatus {
158        &self.status
159    }
160
161
162    // ==========================================================
163    // Fluent Setters (with_* for optional fields)
164    // ==========================================================
165
166    /// Set the customer_id field (chainable)
167    pub fn with_customer_id(mut self, value: Uuid) -> Self {
168        self.customer_id = Some(value);
169        self
170    }
171
172    /// Set the description field (chainable)
173    pub fn with_description(mut self, value: String) -> Self {
174        self.description = Some(value);
175        self
176    }
177
178    /// Set the sla_id field (chainable)
179    pub fn with_sla_id(mut self, value: Uuid) -> Self {
180        self.sla_id = Some(value);
181        self
182    }
183
184    /// Set the response_by field (chainable)
185    pub fn with_response_by(mut self, value: DateTime<Utc>) -> Self {
186        self.response_by = Some(value);
187        self
188    }
189
190    /// Set the resolution_by field (chainable)
191    pub fn with_resolution_by(mut self, value: DateTime<Utc>) -> Self {
192        self.resolution_by = Some(value);
193        self
194    }
195
196    /// Set the first_responded_at field (chainable)
197    pub fn with_first_responded_at(mut self, value: DateTime<Utc>) -> Self {
198        self.first_responded_at = Some(value);
199        self
200    }
201
202    /// Set the resolved_at field (chainable)
203    pub fn with_resolved_at(mut self, value: DateTime<Utc>) -> Self {
204        self.resolved_at = Some(value);
205        self
206    }
207
208    /// Set the paused_at field (chainable)
209    pub fn with_paused_at(mut self, value: DateTime<Utc>) -> Self {
210        self.paused_at = Some(value);
211        self
212    }
213
214    /// Set the escalated_project_id field (chainable)
215    pub fn with_escalated_project_id(mut self, value: Uuid) -> Self {
216        self.escalated_project_id = Some(value);
217        self
218    }
219
220    // ==========================================================
221    // Partial Update
222    // ==========================================================
223
224    /// Apply partial updates from a map of field name to JSON value
225    pub fn apply_patch(&mut self, fields: std::collections::HashMap<String, serde_json::Value>) {
226        for (key, value) in fields {
227            match key.as_str() {
228                "customer_id" => {
229                    if let Ok(v) = serde_json::from_value(value) { self.customer_id = v; }
230                }
231                "subject" => {
232                    if let Ok(v) = serde_json::from_value(value) { self.subject = v; }
233                }
234                "description" => {
235                    if let Ok(v) = serde_json::from_value(value) { self.description = v; }
236                }
237                "priority" => {
238                    if let Ok(v) = serde_json::from_value(value) { self.priority = v; }
239                }
240                "sla_id" => {
241                    if let Ok(v) = serde_json::from_value(value) { self.sla_id = v; }
242                }
243                "status" => {
244                    if let Ok(v) = serde_json::from_value(value) { self.status = v; }
245                }
246                "agreement_status" => {
247                    if let Ok(v) = serde_json::from_value(value) { self.agreement_status = v; }
248                }
249                "opened_at" => {
250                    if let Ok(v) = serde_json::from_value(value) { self.opened_at = v; }
251                }
252                "response_by" => {
253                    if let Ok(v) = serde_json::from_value(value) { self.response_by = v; }
254                }
255                "resolution_by" => {
256                    if let Ok(v) = serde_json::from_value(value) { self.resolution_by = v; }
257                }
258                "first_responded_at" => {
259                    if let Ok(v) = serde_json::from_value(value) { self.first_responded_at = v; }
260                }
261                "response_breached" => {
262                    if let Ok(v) = serde_json::from_value(value) { self.response_breached = v; }
263                }
264                "resolved_at" => {
265                    if let Ok(v) = serde_json::from_value(value) { self.resolved_at = v; }
266                }
267                "paused_at" => {
268                    if let Ok(v) = serde_json::from_value(value) { self.paused_at = v; }
269                }
270                "total_paused_mins" => {
271                    if let Ok(v) = serde_json::from_value(value) { self.total_paused_mins = v; }
272                }
273                "escalated_project_id" => {
274                    if let Ok(v) = serde_json::from_value(value) { self.escalated_project_id = v; }
275                }
276                _ => {} // ignore unknown fields
277            }
278        }
279    }
280
281    // <<< CUSTOM METHODS START >>>
282    // <<< CUSTOM METHODS END >>>
283}
284
285impl super::Entity for Issue {
286    type Id = Uuid;
287
288    fn entity_id(&self) -> &Self::Id {
289        &self.id
290    }
291
292    fn entity_type() -> &'static str {
293        "Issue"
294    }
295}
296
297impl backbone_core::PersistentEntity for Issue {
298    fn entity_id(&self) -> String {
299        self.id.to_string()
300    }
301    fn set_entity_id(&mut self, id: String) {
302        if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
303            self.id = uuid;
304        }
305    }
306    fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
307        self.metadata.created_at
308    }
309    fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
310        self.metadata.created_at = Some(ts);
311    }
312    fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
313        self.metadata.updated_at
314    }
315    fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
316        self.metadata.updated_at = Some(ts);
317    }
318    fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
319        self.metadata.deleted_at
320    }
321    fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
322        self.metadata.deleted_at = ts;
323    }
324}
325
326impl backbone_orm::EntityRepoMeta for Issue {
327    fn column_types() -> std::collections::HashMap<String, String> {
328        let mut m = std::collections::HashMap::new();
329        m.insert("id".to_string(), "uuid".to_string());
330        m.insert("customer_id".to_string(), "uuid".to_string());
331        m.insert("sla_id".to_string(), "uuid".to_string());
332        m.insert("escalated_project_id".to_string(), "uuid".to_string());
333        m.insert("priority".to_string(), "issue_priority".to_string());
334        m.insert("status".to_string(), "issue_status".to_string());
335        m.insert("agreement_status".to_string(), "agreement_status".to_string());
336        m.insert("opened_at".to_string(), "timestamptz".to_string());
337        m.insert("response_by".to_string(), "timestamptz".to_string());
338        m.insert("resolution_by".to_string(), "timestamptz".to_string());
339        m.insert("first_responded_at".to_string(), "timestamptz".to_string());
340        m.insert("resolved_at".to_string(), "timestamptz".to_string());
341        m.insert("paused_at".to_string(), "timestamptz".to_string());
342        m
343    }
344    fn search_fields() -> &'static [&'static str] {
345        &["subject"]
346    }
347}
348
349/// Builder for Issue entity
350///
351/// Provides a fluent API for constructing Issue instances.
352/// System fields (id, metadata, timestamps) are auto-initialized.
353#[derive(Debug, Clone, Default)]
354pub struct IssueBuilder {
355    customer_id: Option<Uuid>,
356    subject: Option<String>,
357    description: Option<String>,
358    priority: Option<IssuePriority>,
359    sla_id: Option<Uuid>,
360    status: Option<IssueStatus>,
361    agreement_status: Option<AgreementStatus>,
362    opened_at: Option<DateTime<Utc>>,
363    response_by: Option<DateTime<Utc>>,
364    resolution_by: Option<DateTime<Utc>>,
365    first_responded_at: Option<DateTime<Utc>>,
366    response_breached: Option<bool>,
367    resolved_at: Option<DateTime<Utc>>,
368    paused_at: Option<DateTime<Utc>>,
369    total_paused_mins: Option<i32>,
370    escalated_project_id: Option<Uuid>,
371}
372
373impl IssueBuilder {
374    /// Set the customer_id field (optional)
375    pub fn customer_id(mut self, value: Uuid) -> Self {
376        self.customer_id = Some(value);
377        self
378    }
379
380    /// Set the subject field (required)
381    pub fn subject(mut self, value: String) -> Self {
382        self.subject = Some(value);
383        self
384    }
385
386    /// Set the description field (optional)
387    pub fn description(mut self, value: String) -> Self {
388        self.description = Some(value);
389        self
390    }
391
392    /// Set the priority field (default: `IssuePriority::default()`)
393    pub fn priority(mut self, value: IssuePriority) -> Self {
394        self.priority = Some(value);
395        self
396    }
397
398    /// Set the sla_id field (optional)
399    pub fn sla_id(mut self, value: Uuid) -> Self {
400        self.sla_id = Some(value);
401        self
402    }
403
404    /// Set the status field (default: `IssueStatus::default()`)
405    pub fn status(mut self, value: IssueStatus) -> Self {
406        self.status = Some(value);
407        self
408    }
409
410    /// Set the agreement_status field (default: `AgreementStatus::default()`)
411    pub fn agreement_status(mut self, value: AgreementStatus) -> Self {
412        self.agreement_status = Some(value);
413        self
414    }
415
416    /// Set the opened_at field (required)
417    pub fn opened_at(mut self, value: DateTime<Utc>) -> Self {
418        self.opened_at = Some(value);
419        self
420    }
421
422    /// Set the response_by field (optional)
423    pub fn response_by(mut self, value: DateTime<Utc>) -> Self {
424        self.response_by = Some(value);
425        self
426    }
427
428    /// Set the resolution_by field (optional)
429    pub fn resolution_by(mut self, value: DateTime<Utc>) -> Self {
430        self.resolution_by = Some(value);
431        self
432    }
433
434    /// Set the first_responded_at field (optional)
435    pub fn first_responded_at(mut self, value: DateTime<Utc>) -> Self {
436        self.first_responded_at = Some(value);
437        self
438    }
439
440    /// Set the response_breached field (default: `false`)
441    pub fn response_breached(mut self, value: bool) -> Self {
442        self.response_breached = Some(value);
443        self
444    }
445
446    /// Set the resolved_at field (optional)
447    pub fn resolved_at(mut self, value: DateTime<Utc>) -> Self {
448        self.resolved_at = Some(value);
449        self
450    }
451
452    /// Set the paused_at field (optional)
453    pub fn paused_at(mut self, value: DateTime<Utc>) -> Self {
454        self.paused_at = Some(value);
455        self
456    }
457
458    /// Set the total_paused_mins field (default: `0`)
459    pub fn total_paused_mins(mut self, value: i32) -> Self {
460        self.total_paused_mins = Some(value);
461        self
462    }
463
464    /// Set the escalated_project_id field (optional)
465    pub fn escalated_project_id(mut self, value: Uuid) -> Self {
466        self.escalated_project_id = Some(value);
467        self
468    }
469
470    /// Build the Issue entity
471    ///
472    /// Returns Err if any required field without a default is missing.
473    pub fn build(self) -> Result<Issue, String> {
474        let subject = self.subject.ok_or_else(|| "subject is required".to_string())?;
475        let opened_at = self.opened_at.ok_or_else(|| "opened_at is required".to_string())?;
476
477        Ok(Issue {
478            id: Uuid::new_v4(),
479            customer_id: self.customer_id,
480            subject,
481            description: self.description,
482            priority: self.priority.unwrap_or_default(),
483            sla_id: self.sla_id,
484            status: self.status.unwrap_or_default(),
485            agreement_status: self.agreement_status.unwrap_or_default(),
486            opened_at,
487            response_by: self.response_by,
488            resolution_by: self.resolution_by,
489            first_responded_at: self.first_responded_at,
490            response_breached: self.response_breached.unwrap_or(false),
491            resolved_at: self.resolved_at,
492            paused_at: self.paused_at,
493            total_paused_mins: self.total_paused_mins.unwrap_or(0),
494            escalated_project_id: self.escalated_project_id,
495            metadata: AuditMetadata::default(),
496        })
497    }
498}