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}
282
283impl super::Entity for Issue {
284    type Id = Uuid;
285
286    fn entity_id(&self) -> &Self::Id {
287        &self.id
288    }
289
290    fn entity_type() -> &'static str {
291        "Issue"
292    }
293}
294
295impl backbone_core::PersistentEntity for Issue {
296    fn entity_id(&self) -> String {
297        self.id.to_string()
298    }
299    fn set_entity_id(&mut self, id: String) {
300        if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
301            self.id = uuid;
302        }
303    }
304    fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
305        self.metadata.created_at
306    }
307    fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
308        self.metadata.created_at = Some(ts);
309    }
310    fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
311        self.metadata.updated_at
312    }
313    fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
314        self.metadata.updated_at = Some(ts);
315    }
316    fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
317        self.metadata.deleted_at
318    }
319    fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
320        self.metadata.deleted_at = ts;
321    }
322}
323
324impl backbone_orm::EntityRepoMeta for Issue {
325    fn column_types() -> std::collections::HashMap<String, String> {
326        let mut m = std::collections::HashMap::new();
327        m.insert("id".to_string(), "uuid".to_string());
328        m.insert("customer_id".to_string(), "uuid".to_string());
329        m.insert("sla_id".to_string(), "uuid".to_string());
330        m.insert("escalated_project_id".to_string(), "uuid".to_string());
331        m.insert("priority".to_string(), "issue_priority".to_string());
332        m.insert("status".to_string(), "issue_status".to_string());
333        m.insert("agreement_status".to_string(), "agreement_status".to_string());
334        m.insert("opened_at".to_string(), "timestamptz".to_string());
335        m.insert("response_by".to_string(), "timestamptz".to_string());
336        m.insert("resolution_by".to_string(), "timestamptz".to_string());
337        m.insert("first_responded_at".to_string(), "timestamptz".to_string());
338        m.insert("resolved_at".to_string(), "timestamptz".to_string());
339        m.insert("paused_at".to_string(), "timestamptz".to_string());
340        m
341    }
342    fn search_fields() -> &'static [&'static str] {
343        &["subject"]
344    }
345}
346
347/// Builder for Issue entity
348///
349/// Provides a fluent API for constructing Issue instances.
350/// System fields (id, metadata, timestamps) are auto-initialized.
351#[derive(Debug, Clone, Default)]
352pub struct IssueBuilder {
353    customer_id: Option<Uuid>,
354    subject: Option<String>,
355    description: Option<String>,
356    priority: Option<IssuePriority>,
357    sla_id: Option<Uuid>,
358    status: Option<IssueStatus>,
359    agreement_status: Option<AgreementStatus>,
360    opened_at: Option<DateTime<Utc>>,
361    response_by: Option<DateTime<Utc>>,
362    resolution_by: Option<DateTime<Utc>>,
363    first_responded_at: Option<DateTime<Utc>>,
364    response_breached: Option<bool>,
365    resolved_at: Option<DateTime<Utc>>,
366    paused_at: Option<DateTime<Utc>>,
367    total_paused_mins: Option<i32>,
368    escalated_project_id: Option<Uuid>,
369}
370
371impl IssueBuilder {
372    /// Set the customer_id field (optional)
373    pub fn customer_id(mut self, value: Uuid) -> Self {
374        self.customer_id = Some(value);
375        self
376    }
377
378    /// Set the subject field (required)
379    pub fn subject(mut self, value: String) -> Self {
380        self.subject = Some(value);
381        self
382    }
383
384    /// Set the description field (optional)
385    pub fn description(mut self, value: String) -> Self {
386        self.description = Some(value);
387        self
388    }
389
390    /// Set the priority field (default: `IssuePriority::default()`)
391    pub fn priority(mut self, value: IssuePriority) -> Self {
392        self.priority = Some(value);
393        self
394    }
395
396    /// Set the sla_id field (optional)
397    pub fn sla_id(mut self, value: Uuid) -> Self {
398        self.sla_id = Some(value);
399        self
400    }
401
402    /// Set the status field (default: `IssueStatus::default()`)
403    pub fn status(mut self, value: IssueStatus) -> Self {
404        self.status = Some(value);
405        self
406    }
407
408    /// Set the agreement_status field (default: `AgreementStatus::default()`)
409    pub fn agreement_status(mut self, value: AgreementStatus) -> Self {
410        self.agreement_status = Some(value);
411        self
412    }
413
414    /// Set the opened_at field (required)
415    pub fn opened_at(mut self, value: DateTime<Utc>) -> Self {
416        self.opened_at = Some(value);
417        self
418    }
419
420    /// Set the response_by field (optional)
421    pub fn response_by(mut self, value: DateTime<Utc>) -> Self {
422        self.response_by = Some(value);
423        self
424    }
425
426    /// Set the resolution_by field (optional)
427    pub fn resolution_by(mut self, value: DateTime<Utc>) -> Self {
428        self.resolution_by = Some(value);
429        self
430    }
431
432    /// Set the first_responded_at field (optional)
433    pub fn first_responded_at(mut self, value: DateTime<Utc>) -> Self {
434        self.first_responded_at = Some(value);
435        self
436    }
437
438    /// Set the response_breached field (default: `false`)
439    pub fn response_breached(mut self, value: bool) -> Self {
440        self.response_breached = Some(value);
441        self
442    }
443
444    /// Set the resolved_at field (optional)
445    pub fn resolved_at(mut self, value: DateTime<Utc>) -> Self {
446        self.resolved_at = Some(value);
447        self
448    }
449
450    /// Set the paused_at field (optional)
451    pub fn paused_at(mut self, value: DateTime<Utc>) -> Self {
452        self.paused_at = Some(value);
453        self
454    }
455
456    /// Set the total_paused_mins field (default: `0`)
457    pub fn total_paused_mins(mut self, value: i32) -> Self {
458        self.total_paused_mins = Some(value);
459        self
460    }
461
462    /// Set the escalated_project_id field (optional)
463    pub fn escalated_project_id(mut self, value: Uuid) -> Self {
464        self.escalated_project_id = Some(value);
465        self
466    }
467
468    /// Build the Issue entity
469    ///
470    /// Returns Err if any required field without a default is missing.
471    pub fn build(self) -> Result<Issue, String> {
472        let subject = self.subject.ok_or_else(|| "subject is required".to_string())?;
473        let opened_at = self.opened_at.ok_or_else(|| "opened_at is required".to_string())?;
474
475        Ok(Issue {
476            id: Uuid::new_v4(),
477            customer_id: self.customer_id,
478            subject,
479            description: self.description,
480            priority: self.priority.unwrap_or_default(),
481            sla_id: self.sla_id,
482            status: self.status.unwrap_or_default(),
483            agreement_status: self.agreement_status.unwrap_or_default(),
484            opened_at,
485            response_by: self.response_by,
486            resolution_by: self.resolution_by,
487            first_responded_at: self.first_responded_at,
488            response_breached: self.response_breached.unwrap_or(false),
489            resolved_at: self.resolved_at,
490            paused_at: self.paused_at,
491            total_paused_mins: self.total_paused_mins.unwrap_or(0),
492            escalated_project_id: self.escalated_project_id,
493            metadata: AuditMetadata::default(),
494        })
495    }
496}