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::Activity;
17use crate::domain::entity::AuditMetadata;
18use crate::domain::entity::ActivityDirection;
19use crate::domain::entity::ActivityStatus;
20use crate::domain::entity::ActivityType;
21
22#[derive(Debug, Clone, Deserialize)]
31#[cfg_attr(feature = "openapi", derive(ToSchema))]
32#[serde(rename_all = "camelCase")]
33pub struct CreateActivityDto {
34 #[cfg_attr(feature = "openapi", schema(example = "example"))]
35 pub subject: String,
36 #[serde(alias = "activity_type")]
37 pub activity_type: ActivityType,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub direction: Option<ActivityDirection>,
40 #[serde(default, skip_serializing_if = "Option::is_none", alias = "lead_id")]
41 pub lead_id: Option<Uuid>,
42 #[serde(default, skip_serializing_if = "Option::is_none", alias = "opportunity_id")]
43 pub opportunity_id: Option<Uuid>,
44 #[serde(default, skip_serializing_if = "Option::is_none", alias = "party_id")]
45 pub party_id: Option<Uuid>,
46 pub status: ActivityStatus,
47 #[serde(default, skip_serializing_if = "Option::is_none", alias = "scheduled_at")]
48 pub scheduled_at: Option<DateTime<Utc>>,
49 #[serde(default, skip_serializing_if = "Option::is_none", alias = "occurred_at")]
50 pub occurred_at: Option<DateTime<Utc>>,
51 #[serde(default, skip_serializing_if = "Option::is_none", alias = "assignee_id")]
52 pub assignee_id: Option<Uuid>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub outcome: Option<String>,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub notes: Option<String>,
57}
58
59#[derive(Debug, Clone, Deserialize)]
68#[cfg_attr(feature = "openapi", derive(ToSchema))]
69#[serde(rename_all = "camelCase")]
70pub struct UpdateActivityDto {
71 #[cfg_attr(feature = "openapi", schema(example = "example"))]
72 pub subject: String,
73 #[serde(alias = "activity_type")]
74 pub activity_type: ActivityType,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub direction: Option<ActivityDirection>,
77 #[serde(default, skip_serializing_if = "Option::is_none", alias = "lead_id")]
78 pub lead_id: Option<Uuid>,
79 #[serde(default, skip_serializing_if = "Option::is_none", alias = "opportunity_id")]
80 pub opportunity_id: Option<Uuid>,
81 #[serde(default, skip_serializing_if = "Option::is_none", alias = "party_id")]
82 pub party_id: Option<Uuid>,
83 pub status: ActivityStatus,
84 #[serde(default, skip_serializing_if = "Option::is_none", alias = "scheduled_at")]
85 pub scheduled_at: Option<DateTime<Utc>>,
86 #[serde(default, skip_serializing_if = "Option::is_none", alias = "occurred_at")]
87 pub occurred_at: Option<DateTime<Utc>>,
88 #[serde(default, skip_serializing_if = "Option::is_none", alias = "assignee_id")]
89 pub assignee_id: Option<Uuid>,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub outcome: Option<String>,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub notes: Option<String>,
94}
95
96#[derive(Debug, Clone, Default, Deserialize)]
105#[cfg_attr(feature = "openapi", derive(ToSchema))]
106#[serde(rename_all = "camelCase")]
107pub struct PatchActivityDto {
108 #[cfg_attr(feature = "openapi", schema(example = "example"))]
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub subject: Option<String>,
111 #[serde(skip_serializing_if = "Option::is_none", alias = "activity_type")]
112 pub activity_type: Option<ActivityType>,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub direction: Option<ActivityDirection>,
115 #[serde(skip_serializing_if = "Option::is_none", alias = "lead_id")]
116 pub lead_id: Option<Uuid>,
117 #[serde(skip_serializing_if = "Option::is_none", alias = "opportunity_id")]
118 pub opportunity_id: Option<Uuid>,
119 #[serde(skip_serializing_if = "Option::is_none", alias = "party_id")]
120 pub party_id: Option<Uuid>,
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub status: Option<ActivityStatus>,
123 #[serde(skip_serializing_if = "Option::is_none", alias = "scheduled_at")]
124 pub scheduled_at: Option<DateTime<Utc>>,
125 #[serde(skip_serializing_if = "Option::is_none", alias = "occurred_at")]
126 pub occurred_at: Option<DateTime<Utc>>,
127 #[serde(skip_serializing_if = "Option::is_none", alias = "assignee_id")]
128 pub assignee_id: Option<Uuid>,
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub outcome: Option<String>,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub notes: Option<String>,
133}
134
135impl PatchActivityDto {
136 pub fn has_changes(&self) -> bool {
138 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()
139 }
140}
141
142#[derive(Debug, Clone, Serialize)]
151#[cfg_attr(feature = "openapi", derive(ToSchema))]
152#[serde(rename_all = "camelCase")]
153pub struct ActivityResponseDto {
154 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
155 pub id: Uuid,
156 #[cfg_attr(feature = "openapi", schema(example = "example"))]
157 pub subject: String,
158 pub activity_type: ActivityType,
159 pub direction: Option<ActivityDirection>,
160 pub lead_id: Option<Uuid>,
161 pub opportunity_id: Option<Uuid>,
162 pub party_id: Option<Uuid>,
163 pub status: ActivityStatus,
164 pub scheduled_at: Option<DateTime<Utc>>,
165 pub occurred_at: Option<DateTime<Utc>>,
166 pub assignee_id: Option<Uuid>,
167 pub outcome: Option<String>,
168 pub notes: Option<String>,
169 pub metadata: AuditMetadata,
170}
171
172#[derive(Debug, Clone, Serialize)]
181#[cfg_attr(feature = "openapi", derive(ToSchema))]
182#[serde(rename_all = "camelCase")]
183pub struct ActivityListResponseDto {
184 pub items: Vec<ActivityResponseDto>,
186 pub total: u64,
188 pub page: u32,
190 pub per_page: u32,
192 pub total_pages: u32,
194 pub has_next: bool,
196 pub has_prev: bool,
198}
199
200impl ActivityListResponseDto {
201 pub fn new(items: Vec<ActivityResponseDto>, total: u64, page: u32, per_page: u32) -> Self {
203 let total_pages = if per_page > 0 {
204 ((total as f64) / (per_page as f64)).ceil() as u32
205 } else {
206 0
207 };
208 Self {
209 items,
210 total,
211 page,
212 per_page,
213 total_pages,
214 has_next: page < total_pages,
215 has_prev: page > 1,
216 }
217 }
218}
219
220#[derive(Debug, Clone, Serialize)]
222#[cfg_attr(feature = "openapi", derive(ToSchema))]
223#[serde(rename_all = "camelCase")]
224pub struct ActivitySummaryDto {
225 pub id: Uuid,
226 pub subject: String,
227 pub activity_type: ActivityType,
228 pub direction: Option<ActivityDirection>,
229 pub created_at: Option<DateTime<Utc>>,
230}
231
232impl From<Activity> for ActivityResponseDto {
237 fn from(entity: Activity) -> Self {
238 Self {
239 id: entity.id,
240 subject: entity.subject,
241 activity_type: entity.activity_type,
242 direction: entity.direction,
243 lead_id: entity.lead_id,
244 opportunity_id: entity.opportunity_id,
245 party_id: entity.party_id,
246 status: entity.status,
247 scheduled_at: entity.scheduled_at,
248 occurred_at: entity.occurred_at,
249 assignee_id: entity.assignee_id,
250 outcome: entity.outcome,
251 notes: entity.notes,
252 metadata: entity.metadata,
253 }
254 }
255}
256
257impl From<Activity> for ActivitySummaryDto {
258 fn from(entity: Activity) -> Self {
259 let created_at = backbone_core::PersistentEntity::created_at(&entity);
260 Self {
261 id: entity.id,
262 subject: entity.subject,
263 activity_type: entity.activity_type,
264 direction: entity.direction,
265 created_at,
266 }
267 }
268}
269
270impl From<CreateActivityDto> for Activity {
271 fn from(dto: CreateActivityDto) -> Self {
272 Self {
273 id: Uuid::new_v4(),
274 subject: dto.subject,
275 activity_type: dto.activity_type,
276 direction: dto.direction,
277 lead_id: dto.lead_id,
278 opportunity_id: dto.opportunity_id,
279 party_id: dto.party_id,
280 status: dto.status,
281 scheduled_at: dto.scheduled_at,
282 occurred_at: dto.occurred_at,
283 assignee_id: dto.assignee_id,
284 outcome: dto.outcome,
285 notes: dto.notes,
286 metadata: AuditMetadata::default(),
287 }
288 }
289}
290
291impl From<&Activity> for ActivityResponseDto {
292 fn from(entity: &Activity) -> Self {
293 Self {
294 id: entity.id.clone(),
295 subject: entity.subject.clone(),
296 activity_type: entity.activity_type.clone(),
297 direction: entity.direction.clone(),
298 lead_id: entity.lead_id.clone(),
299 opportunity_id: entity.opportunity_id.clone(),
300 party_id: entity.party_id.clone(),
301 status: entity.status.clone(),
302 scheduled_at: entity.scheduled_at.clone(),
303 occurred_at: entity.occurred_at.clone(),
304 assignee_id: entity.assignee_id.clone(),
305 outcome: entity.outcome.clone(),
306 notes: entity.notes.clone(),
307 metadata: entity.metadata.clone(),
308 }
309 }
310}
311
312impl backbone_core::FromCreateDto<CreateActivityDto> for Activity {
313 fn from_create_dto(dto: CreateActivityDto) -> backbone_core::ServiceResult<Self> {
314 Ok(Activity::from(dto))
315 }
316}
317
318impl backbone_core::ApplyUpdateDto<UpdateActivityDto> for Activity {
319 fn apply_update(mut self, dto: UpdateActivityDto) -> backbone_core::ServiceResult<Self> {
320 self.subject = dto.subject;
321 self.activity_type = dto.activity_type;
322 self.direction = dto.direction;
323 self.lead_id = dto.lead_id;
324 self.opportunity_id = dto.opportunity_id;
325 self.party_id = dto.party_id;
326 self.status = dto.status;
327 self.scheduled_at = dto.scheduled_at;
328 self.occurred_at = dto.occurred_at;
329 self.assignee_id = dto.assignee_id;
330 self.outcome = dto.outcome;
331 self.notes = dto.notes;
332 Ok(self)
333 }
334}
335
336