Skip to main content

backbone_inventory/domain/entity/
reordering_rule.rs

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