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::Activity;
20use crate::domain::entity::AuditMetadata;
21use crate::domain::entity::ActivityDirection;
22use crate::domain::entity::ActivityStatus;
23use crate::domain::entity::ActivityType;
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 CreateActivityDto {
38 #[cfg_attr(feature = "validation", validate(length(max = 200)))]
39 #[cfg_attr(feature = "openapi", schema(example = "example"))]
40 pub subject: String,
41 #[serde(alias = "activity_type")]
42 pub activity_type: ActivityType,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub direction: Option<ActivityDirection>,
45 #[serde(default, skip_serializing_if = "Option::is_none", alias = "lead_id")]
46 pub lead_id: Option<Uuid>,
47 #[serde(default, skip_serializing_if = "Option::is_none", alias = "opportunity_id")]
48 pub opportunity_id: Option<Uuid>,
49 #[serde(default, skip_serializing_if = "Option::is_none", alias = "party_id")]
50 pub party_id: Option<Uuid>,
51 pub status: ActivityStatus,
52 #[serde(default, skip_serializing_if = "Option::is_none", alias = "scheduled_at")]
53 pub scheduled_at: Option<DateTime<Utc>>,
54 #[serde(default, skip_serializing_if = "Option::is_none", alias = "occurred_at")]
55 pub occurred_at: Option<DateTime<Utc>>,
56 #[serde(default, skip_serializing_if = "Option::is_none", alias = "assignee_id")]
57 pub assignee_id: Option<Uuid>,
58 #[cfg_attr(feature = "validation", validate(length(max = 280)))]
59 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub outcome: Option<String>,
61 #[cfg_attr(feature = "validation", validate(length(max = 4000)))]
62 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub notes: Option<String>,
64}
65
66#[derive(Debug, Clone, Deserialize)]
75#[cfg_attr(feature = "openapi", derive(ToSchema))]
76#[cfg_attr(feature = "validation", derive(Validate))]
77#[serde(rename_all = "camelCase")]
78pub struct UpdateActivityDto {
79 #[cfg_attr(feature = "validation", validate(length(max = 200)))]
80 #[cfg_attr(feature = "openapi", schema(example = "example"))]
81 pub subject: String,
82 #[serde(alias = "activity_type")]
83 pub activity_type: ActivityType,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub direction: Option<ActivityDirection>,
86 #[serde(default, skip_serializing_if = "Option::is_none", alias = "lead_id")]
87 pub lead_id: Option<Uuid>,
88 #[serde(default, skip_serializing_if = "Option::is_none", alias = "opportunity_id")]
89 pub opportunity_id: Option<Uuid>,
90 #[serde(default, skip_serializing_if = "Option::is_none", alias = "party_id")]
91 pub party_id: Option<Uuid>,
92 pub status: ActivityStatus,
93 #[serde(default, skip_serializing_if = "Option::is_none", alias = "scheduled_at")]
94 pub scheduled_at: Option<DateTime<Utc>>,
95 #[serde(default, skip_serializing_if = "Option::is_none", alias = "occurred_at")]
96 pub occurred_at: Option<DateTime<Utc>>,
97 #[serde(default, skip_serializing_if = "Option::is_none", alias = "assignee_id")]
98 pub assignee_id: Option<Uuid>,
99 #[cfg_attr(feature = "validation", validate(length(max = 280)))]
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub outcome: Option<String>,
102 #[cfg_attr(feature = "validation", validate(length(max = 4000)))]
103 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub notes: Option<String>,
105}
106
107#[derive(Debug, Clone, Default, Deserialize)]
116#[cfg_attr(feature = "openapi", derive(ToSchema))]
117#[cfg_attr(feature = "validation", derive(Validate))]
118#[serde(rename_all = "camelCase")]
119pub struct PatchActivityDto {
120 #[cfg_attr(feature = "validation", validate(length(max = 200)))]
121 #[cfg_attr(feature = "openapi", schema(example = "example"))]
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub subject: Option<String>,
124 #[serde(skip_serializing_if = "Option::is_none", alias = "activity_type")]
125 pub activity_type: Option<ActivityType>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub direction: Option<ActivityDirection>,
128 #[serde(skip_serializing_if = "Option::is_none", alias = "lead_id")]
129 pub lead_id: Option<Uuid>,
130 #[serde(skip_serializing_if = "Option::is_none", alias = "opportunity_id")]
131 pub opportunity_id: Option<Uuid>,
132 #[serde(skip_serializing_if = "Option::is_none", alias = "party_id")]
133 pub party_id: Option<Uuid>,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub status: Option<ActivityStatus>,
136 #[serde(skip_serializing_if = "Option::is_none", alias = "scheduled_at")]
137 pub scheduled_at: Option<DateTime<Utc>>,
138 #[serde(skip_serializing_if = "Option::is_none", alias = "occurred_at")]
139 pub occurred_at: Option<DateTime<Utc>>,
140 #[serde(skip_serializing_if = "Option::is_none", alias = "assignee_id")]
141 pub assignee_id: Option<Uuid>,
142 #[cfg_attr(feature = "validation", validate(length(max = 280)))]
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub outcome: Option<String>,
145 #[cfg_attr(feature = "validation", validate(length(max = 4000)))]
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub notes: Option<String>,
148}
149
150impl PatchActivityDto {
151 pub fn has_changes(&self) -> bool {
153 self.subject.is_some() || self.activity_type.is_some() || self.direction.is_some() || self.lead_id.is_some() || self.opportunity_id.is_some() || self.party_id.is_some() || self.status.is_some() || self.scheduled_at.is_some() || self.occurred_at.is_some() || self.assignee_id.is_some() || self.outcome.is_some() || self.notes.is_some()
154 }
155}
156
157#[derive(Debug, Clone, Serialize)]
166#[cfg_attr(feature = "openapi", derive(ToSchema))]
167#[serde(rename_all = "camelCase")]
168pub struct ActivityResponseDto {
169 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
170 pub id: Uuid,
171 #[cfg_attr(feature = "openapi", schema(example = "example"))]
172 pub subject: String,
173 pub activity_type: ActivityType,
174 pub direction: Option<ActivityDirection>,
175 pub lead_id: Option<Uuid>,
176 pub opportunity_id: Option<Uuid>,
177 pub party_id: Option<Uuid>,
178 pub status: ActivityStatus,
179 pub scheduled_at: Option<DateTime<Utc>>,
180 pub occurred_at: Option<DateTime<Utc>>,
181 pub assignee_id: Option<Uuid>,
182 pub outcome: Option<String>,
183 pub notes: Option<String>,
184 pub metadata: AuditMetadata,
185}
186
187#[derive(Debug, Clone, Serialize)]
196#[cfg_attr(feature = "openapi", derive(ToSchema))]
197#[serde(rename_all = "camelCase")]
198pub struct ActivityListResponseDto {
199 pub items: Vec<ActivityResponseDto>,
201 pub total: u64,
203 pub page: u32,
205 pub per_page: u32,
207 pub total_pages: u32,
209 pub has_next: bool,
211 pub has_prev: bool,
213}
214
215impl ActivityListResponseDto {
216 pub fn new(items: Vec<ActivityResponseDto>, total: u64, page: u32, per_page: u32) -> Self {
218 let total_pages = if per_page > 0 {
219 ((total as f64) / (per_page as f64)).ceil() as u32
220 } else {
221 0
222 };
223 Self {
224 items,
225 total,
226 page,
227 per_page,
228 total_pages,
229 has_next: page < total_pages,
230 has_prev: page > 1,
231 }
232 }
233}
234
235#[derive(Debug, Clone, Serialize)]
237#[cfg_attr(feature = "openapi", derive(ToSchema))]
238#[serde(rename_all = "camelCase")]
239pub struct ActivitySummaryDto {
240 pub id: Uuid,
241 pub subject: String,
242 pub activity_type: ActivityType,
243 pub direction: Option<ActivityDirection>,
244 pub created_at: Option<DateTime<Utc>>,
245}
246
247impl From<Activity> for ActivityResponseDto {
252 fn from(entity: Activity) -> Self {
253 Self {
254 id: entity.id,
255 subject: entity.subject,
256 activity_type: entity.activity_type,
257 direction: entity.direction,
258 lead_id: entity.lead_id,
259 opportunity_id: entity.opportunity_id,
260 party_id: entity.party_id,
261 status: entity.status,
262 scheduled_at: entity.scheduled_at,
263 occurred_at: entity.occurred_at,
264 assignee_id: entity.assignee_id,
265 outcome: entity.outcome,
266 notes: entity.notes,
267 metadata: entity.metadata,
268 }
269 }
270}
271
272impl From<Activity> for ActivitySummaryDto {
273 fn from(entity: Activity) -> Self {
274 let created_at = backbone_core::PersistentEntity::created_at(&entity);
275 Self {
276 id: entity.id,
277 subject: entity.subject,
278 activity_type: entity.activity_type,
279 direction: entity.direction,
280 created_at,
281 }
282 }
283}
284
285impl From<CreateActivityDto> for Activity {
286 fn from(dto: CreateActivityDto) -> Self {
287 Self {
288 id: Uuid::new_v4(),
289 subject: dto.subject,
290 activity_type: dto.activity_type,
291 direction: dto.direction,
292 lead_id: dto.lead_id,
293 opportunity_id: dto.opportunity_id,
294 party_id: dto.party_id,
295 status: dto.status,
296 scheduled_at: dto.scheduled_at,
297 occurred_at: dto.occurred_at,
298 assignee_id: dto.assignee_id,
299 outcome: dto.outcome,
300 notes: dto.notes,
301 metadata: AuditMetadata::default(),
302 }
303 }
304}
305
306impl From<&Activity> for ActivityResponseDto {
307 fn from(entity: &Activity) -> Self {
308 Self {
309 id: entity.id.clone(),
310 subject: entity.subject.clone(),
311 activity_type: entity.activity_type.clone(),
312 direction: entity.direction.clone(),
313 lead_id: entity.lead_id.clone(),
314 opportunity_id: entity.opportunity_id.clone(),
315 party_id: entity.party_id.clone(),
316 status: entity.status.clone(),
317 scheduled_at: entity.scheduled_at.clone(),
318 occurred_at: entity.occurred_at.clone(),
319 assignee_id: entity.assignee_id.clone(),
320 outcome: entity.outcome.clone(),
321 notes: entity.notes.clone(),
322 metadata: entity.metadata.clone(),
323 }
324 }
325}
326
327impl backbone_core::FromCreateDto<CreateActivityDto> for Activity {
328 fn from_create_dto(dto: CreateActivityDto) -> backbone_core::ServiceResult<Self> {
329 Ok(Activity::from(dto))
330 }
331}
332
333impl backbone_core::ApplyUpdateDto<UpdateActivityDto> for Activity {
334 fn apply_update(mut self, dto: UpdateActivityDto) -> backbone_core::ServiceResult<Self> {
335 self.subject = dto.subject;
336 self.activity_type = dto.activity_type;
337 self.direction = dto.direction;
338 self.lead_id = dto.lead_id;
339 self.opportunity_id = dto.opportunity_id;
340 self.party_id = dto.party_id;
341 self.status = dto.status;
342 self.scheduled_at = dto.scheduled_at;
343 self.occurred_at = dto.occurred_at;
344 self.assignee_id = dto.assignee_id;
345 self.outcome = dto.outcome;
346 self.notes = dto.notes;
347 Ok(self)
348 }
349}
350
351