1use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10use chrono::{DateTime, Utc, NaiveDate};
11use rust_decimal::Decimal;
12
13#[cfg(feature = "openapi")]
14#[cfg(feature = "openapi")]
15use utoipa::ToSchema;
16
17use crate::domain::entity::ReorderingRule;
18use crate::domain::entity::AuditMetadata;
19use crate::domain::entity::OrderpointTrigger;
20
21#[derive(Debug, Clone, Deserialize)]
30#[cfg_attr(feature = "openapi", derive(ToSchema))]
31#[serde(rename_all = "camelCase")]
32pub struct CreateReorderingRuleDto {
33 #[cfg_attr(feature = "openapi", schema(example = "example"))]
34 pub name: String,
35 pub trigger: OrderpointTrigger,
36 #[cfg_attr(feature = "openapi", schema(example = true))]
37 pub active: bool,
38 #[serde(default, skip_serializing_if = "Option::is_none", alias = "snoozed_until")]
39 pub snoozed_until: Option<NaiveDate>,
40 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
41 #[serde(alias = "item_id")]
42 pub item_id: Uuid,
43 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
44 #[serde(alias = "location_id")]
45 pub location_id: Uuid,
46 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
47 #[serde(alias = "warehouse_id")]
48 pub warehouse_id: Uuid,
49 #[serde(alias = "item_min_qty")]
50 pub item_min_qty: Decimal,
51 #[serde(alias = "item_max_qty")]
52 pub item_max_qty: Decimal,
53 #[serde(default, skip_serializing_if = "Option::is_none", alias = "route_id")]
54 pub route_id: Option<Uuid>,
55 #[serde(alias = "qty_on_hand")]
56 pub qty_on_hand: Decimal,
57 #[serde(alias = "qty_forecast")]
58 pub qty_forecast: Decimal,
59 #[serde(alias = "qty_to_order")]
60 pub qty_to_order: Decimal,
61 #[serde(alias = "qty_to_order_manual")]
62 pub qty_to_order_manual: Decimal,
63 #[serde(alias = "lead_days")]
64 pub lead_days: Decimal,
65 #[serde(default, skip_serializing_if = "Option::is_none", alias = "deadline_date")]
66 pub deadline_date: Option<NaiveDate>,
67}
68
69#[derive(Debug, Clone, Deserialize)]
78#[cfg_attr(feature = "openapi", derive(ToSchema))]
79#[serde(rename_all = "camelCase")]
80pub struct UpdateReorderingRuleDto {
81 #[cfg_attr(feature = "openapi", schema(example = "example"))]
82 pub name: String,
83 pub trigger: OrderpointTrigger,
84 #[cfg_attr(feature = "openapi", schema(example = true))]
85 pub active: bool,
86 #[serde(default, skip_serializing_if = "Option::is_none", alias = "snoozed_until")]
87 pub snoozed_until: Option<NaiveDate>,
88 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
89 #[serde(alias = "item_id")]
90 pub item_id: Uuid,
91 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
92 #[serde(alias = "location_id")]
93 pub location_id: Uuid,
94 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
95 #[serde(alias = "warehouse_id")]
96 pub warehouse_id: Uuid,
97 #[serde(alias = "item_min_qty")]
98 pub item_min_qty: Decimal,
99 #[serde(alias = "item_max_qty")]
100 pub item_max_qty: Decimal,
101 #[serde(default, skip_serializing_if = "Option::is_none", alias = "route_id")]
102 pub route_id: Option<Uuid>,
103 #[serde(alias = "qty_on_hand")]
104 pub qty_on_hand: Decimal,
105 #[serde(alias = "qty_forecast")]
106 pub qty_forecast: Decimal,
107 #[serde(alias = "qty_to_order")]
108 pub qty_to_order: Decimal,
109 #[serde(alias = "qty_to_order_manual")]
110 pub qty_to_order_manual: Decimal,
111 #[serde(alias = "lead_days")]
112 pub lead_days: Decimal,
113 #[serde(default, skip_serializing_if = "Option::is_none", alias = "deadline_date")]
114 pub deadline_date: Option<NaiveDate>,
115}
116
117#[derive(Debug, Clone, Default, Deserialize)]
126#[cfg_attr(feature = "openapi", derive(ToSchema))]
127#[serde(rename_all = "camelCase")]
128pub struct PatchReorderingRuleDto {
129 #[cfg_attr(feature = "openapi", schema(example = "example"))]
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub name: Option<String>,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub trigger: Option<OrderpointTrigger>,
134 #[cfg_attr(feature = "openapi", schema(example = true))]
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub active: Option<bool>,
137 #[serde(skip_serializing_if = "Option::is_none", alias = "snoozed_until")]
138 pub snoozed_until: Option<NaiveDate>,
139 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
140 #[serde(skip_serializing_if = "Option::is_none", alias = "item_id")]
141 pub item_id: Option<Uuid>,
142 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
143 #[serde(skip_serializing_if = "Option::is_none", alias = "location_id")]
144 pub location_id: Option<Uuid>,
145 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
146 #[serde(skip_serializing_if = "Option::is_none", alias = "warehouse_id")]
147 pub warehouse_id: Option<Uuid>,
148 #[serde(skip_serializing_if = "Option::is_none", alias = "item_min_qty")]
149 pub item_min_qty: Option<Decimal>,
150 #[serde(skip_serializing_if = "Option::is_none", alias = "item_max_qty")]
151 pub item_max_qty: Option<Decimal>,
152 #[serde(skip_serializing_if = "Option::is_none", alias = "route_id")]
153 pub route_id: Option<Uuid>,
154 #[serde(skip_serializing_if = "Option::is_none", alias = "qty_on_hand")]
155 pub qty_on_hand: Option<Decimal>,
156 #[serde(skip_serializing_if = "Option::is_none", alias = "qty_forecast")]
157 pub qty_forecast: Option<Decimal>,
158 #[serde(skip_serializing_if = "Option::is_none", alias = "qty_to_order")]
159 pub qty_to_order: Option<Decimal>,
160 #[serde(skip_serializing_if = "Option::is_none", alias = "qty_to_order_manual")]
161 pub qty_to_order_manual: Option<Decimal>,
162 #[serde(skip_serializing_if = "Option::is_none", alias = "lead_days")]
163 pub lead_days: Option<Decimal>,
164 #[serde(skip_serializing_if = "Option::is_none", alias = "deadline_date")]
165 pub deadline_date: Option<NaiveDate>,
166}
167
168impl PatchReorderingRuleDto {
169 pub fn has_changes(&self) -> bool {
171 self.name.is_some() || self.trigger.is_some() || self.active.is_some() || self.snoozed_until.is_some() || self.item_id.is_some() || self.location_id.is_some() || self.warehouse_id.is_some() || self.item_min_qty.is_some() || self.item_max_qty.is_some() || self.route_id.is_some() || self.qty_on_hand.is_some() || self.qty_forecast.is_some() || self.qty_to_order.is_some() || self.qty_to_order_manual.is_some() || self.lead_days.is_some() || self.deadline_date.is_some()
172 }
173}
174
175#[derive(Debug, Clone, Serialize)]
184#[cfg_attr(feature = "openapi", derive(ToSchema))]
185#[serde(rename_all = "camelCase")]
186pub struct ReorderingRuleResponseDto {
187 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
188 pub id: Uuid,
189 #[cfg_attr(feature = "openapi", schema(example = "example"))]
190 pub name: String,
191 pub trigger: OrderpointTrigger,
192 #[cfg_attr(feature = "openapi", schema(example = true))]
193 pub active: bool,
194 pub snoozed_until: Option<NaiveDate>,
195 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
196 pub item_id: Uuid,
197 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
198 pub location_id: Uuid,
199 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
200 pub warehouse_id: Uuid,
201 pub item_min_qty: Decimal,
202 pub item_max_qty: Decimal,
203 pub route_id: Option<Uuid>,
204 pub qty_on_hand: Decimal,
205 pub qty_forecast: Decimal,
206 pub qty_to_order: Decimal,
207 pub qty_to_order_manual: Decimal,
208 pub lead_days: Decimal,
209 pub deadline_date: Option<NaiveDate>,
210 pub metadata: AuditMetadata,
211}
212
213#[derive(Debug, Clone, Serialize)]
222#[cfg_attr(feature = "openapi", derive(ToSchema))]
223#[serde(rename_all = "camelCase")]
224pub struct ReorderingRuleListResponseDto {
225 pub items: Vec<ReorderingRuleResponseDto>,
227 pub total: u64,
229 pub page: u32,
231 pub per_page: u32,
233 pub total_pages: u32,
235 pub has_next: bool,
237 pub has_prev: bool,
239}
240
241impl ReorderingRuleListResponseDto {
242 pub fn new(items: Vec<ReorderingRuleResponseDto>, total: u64, page: u32, per_page: u32) -> Self {
244 let total_pages = if per_page > 0 {
245 ((total as f64) / (per_page as f64)).ceil() as u32
246 } else {
247 0
248 };
249 Self {
250 items,
251 total,
252 page,
253 per_page,
254 total_pages,
255 has_next: page < total_pages,
256 has_prev: page > 1,
257 }
258 }
259}
260
261#[derive(Debug, Clone, Serialize)]
263#[cfg_attr(feature = "openapi", derive(ToSchema))]
264#[serde(rename_all = "camelCase")]
265pub struct ReorderingRuleSummaryDto {
266 pub id: Uuid,
267 pub name: String,
268 pub trigger: OrderpointTrigger,
269 pub active: bool,
270 pub created_at: Option<DateTime<Utc>>,
271}
272
273impl From<ReorderingRule> for ReorderingRuleResponseDto {
278 fn from(entity: ReorderingRule) -> Self {
279 Self {
280 id: entity.id,
281 name: entity.name,
282 trigger: entity.trigger,
283 active: entity.active,
284 snoozed_until: entity.snoozed_until,
285 item_id: entity.item_id,
286 location_id: entity.location_id,
287 warehouse_id: entity.warehouse_id,
288 item_min_qty: entity.item_min_qty,
289 item_max_qty: entity.item_max_qty,
290 route_id: entity.route_id,
291 qty_on_hand: entity.qty_on_hand,
292 qty_forecast: entity.qty_forecast,
293 qty_to_order: entity.qty_to_order,
294 qty_to_order_manual: entity.qty_to_order_manual,
295 lead_days: entity.lead_days,
296 deadline_date: entity.deadline_date,
297 metadata: entity.metadata,
298 }
299 }
300}
301
302impl From<ReorderingRule> for ReorderingRuleSummaryDto {
303 fn from(entity: ReorderingRule) -> Self {
304 let created_at = backbone_core::PersistentEntity::created_at(&entity);
305 Self {
306 id: entity.id,
307 name: entity.name,
308 trigger: entity.trigger,
309 active: entity.active,
310 created_at,
311 }
312 }
313}
314
315impl From<CreateReorderingRuleDto> for ReorderingRule {
316 fn from(dto: CreateReorderingRuleDto) -> Self {
317 Self {
318 id: Uuid::new_v4(),
319 name: dto.name,
320 trigger: dto.trigger,
321 active: dto.active,
322 snoozed_until: dto.snoozed_until,
323 item_id: dto.item_id,
324 location_id: dto.location_id,
325 warehouse_id: dto.warehouse_id,
326 item_min_qty: dto.item_min_qty,
327 item_max_qty: dto.item_max_qty,
328 route_id: dto.route_id,
329 qty_on_hand: dto.qty_on_hand,
330 qty_forecast: dto.qty_forecast,
331 qty_to_order: dto.qty_to_order,
332 qty_to_order_manual: dto.qty_to_order_manual,
333 lead_days: dto.lead_days,
334 deadline_date: dto.deadline_date,
335 metadata: AuditMetadata::default(),
336 }
337 }
338}
339
340impl From<&ReorderingRule> for ReorderingRuleResponseDto {
341 fn from(entity: &ReorderingRule) -> Self {
342 Self {
343 id: entity.id.clone(),
344 name: entity.name.clone(),
345 trigger: entity.trigger.clone(),
346 active: entity.active.clone(),
347 snoozed_until: entity.snoozed_until.clone(),
348 item_id: entity.item_id.clone(),
349 location_id: entity.location_id.clone(),
350 warehouse_id: entity.warehouse_id.clone(),
351 item_min_qty: entity.item_min_qty.clone(),
352 item_max_qty: entity.item_max_qty.clone(),
353 route_id: entity.route_id.clone(),
354 qty_on_hand: entity.qty_on_hand.clone(),
355 qty_forecast: entity.qty_forecast.clone(),
356 qty_to_order: entity.qty_to_order.clone(),
357 qty_to_order_manual: entity.qty_to_order_manual.clone(),
358 lead_days: entity.lead_days.clone(),
359 deadline_date: entity.deadline_date.clone(),
360 metadata: entity.metadata.clone(),
361 }
362 }
363}
364
365impl backbone_core::FromCreateDto<CreateReorderingRuleDto> for ReorderingRule {
366 fn from_create_dto(dto: CreateReorderingRuleDto) -> backbone_core::ServiceResult<Self> {
367 Ok(ReorderingRule::from(dto))
368 }
369}
370
371impl backbone_core::ApplyUpdateDto<UpdateReorderingRuleDto> for ReorderingRule {
372 fn apply_update(mut self, dto: UpdateReorderingRuleDto) -> backbone_core::ServiceResult<Self> {
373 self.name = dto.name;
374 self.trigger = dto.trigger;
375 self.active = dto.active;
376 self.snoozed_until = dto.snoozed_until;
377 self.item_id = dto.item_id;
378 self.location_id = dto.location_id;
379 self.warehouse_id = dto.warehouse_id;
380 self.item_min_qty = dto.item_min_qty;
381 self.item_max_qty = dto.item_max_qty;
382 self.route_id = dto.route_id;
383 self.qty_on_hand = dto.qty_on_hand;
384 self.qty_forecast = dto.qty_forecast;
385 self.qty_to_order = dto.qty_to_order;
386 self.qty_to_order_manual = dto.qty_to_order_manual;
387 self.lead_days = dto.lead_days;
388 self.deadline_date = dto.deadline_date;
389 Ok(self)
390 }
391}
392
393