Skip to main content

backbone_activity/domain/entity/
activity.rs

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