Skip to main content

backbone_inventory/domain/entity/
stock_item.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4use uuid::Uuid;
5use rust_decimal::Decimal;
6
7use super::ValuationMethod;
8use super::ServiceTrackingType;
9use super::AuditMetadata;
10
11/// Strongly-typed ID for StockItem
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(transparent)]
14pub struct StockItemId(pub Uuid);
15
16impl StockItemId {
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 StockItemId {
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 StockItemId {
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 StockItemId {
36    fn from(id: Uuid) -> Self { Self(id) }
37}
38
39impl From<StockItemId> for Uuid {
40    fn from(id: StockItemId) -> Self { id.0 }
41}
42
43impl AsRef<Uuid> for StockItemId {
44    fn as_ref(&self) -> &Uuid { &self.0 }
45}
46
47impl std::ops::Deref for StockItemId {
48    type Target = Uuid;
49    fn deref(&self) -> &Self::Target { &self.0 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
53pub struct StockItem {
54    pub id: Uuid,
55    pub item_id: Uuid,
56    pub stock_uom: String,
57    pub is_stock_item: bool,
58    pub has_batch: bool,
59    pub valuation_method: ValuationMethod,
60    pub reorder_level: Decimal,
61    pub weight_per_unit: Decimal,
62    pub service_tracking: ServiceTrackingType,
63    pub service_project_id: Option<Uuid>,
64    pub service_project_template_id: Option<Uuid>,
65    #[serde(default)]
66    #[sqlx(json)]
67    pub metadata: AuditMetadata,
68}
69
70impl StockItem {
71    /// Create a builder for StockItem
72    pub fn builder() -> StockItemBuilder {
73        <StockItemBuilder as Default>::default()
74    }
75
76    /// Create a new StockItem with required fields
77    pub fn new(item_id: Uuid, stock_uom: String, is_stock_item: bool, has_batch: bool, valuation_method: ValuationMethod, reorder_level: Decimal, weight_per_unit: Decimal, service_tracking: ServiceTrackingType) -> Self {
78        Self {
79            id: Uuid::new_v4(),
80            item_id,
81            stock_uom,
82            is_stock_item,
83            has_batch,
84            valuation_method,
85            reorder_level,
86            weight_per_unit,
87            service_tracking,
88            service_project_id: None,
89            service_project_template_id: None,
90            metadata: AuditMetadata::default(),
91        }
92    }
93
94    /// Get the entity's unique identifier
95    pub fn id(&self) -> &Uuid {
96        &self.id
97    }
98
99    /// Get a strongly-typed ID for this entity
100    pub fn typed_id(&self) -> StockItemId {
101        StockItemId(self.id)
102    }
103
104    /// Get when this entity was created
105    pub fn created_at(&self) -> Option<&DateTime<Utc>> {
106        self.metadata.created_at.as_ref()
107    }
108
109    /// Get when this entity was last updated
110    pub fn updated_at(&self) -> Option<&DateTime<Utc>> {
111        self.metadata.updated_at.as_ref()
112    }
113
114    /// Check if this entity is soft deleted
115    pub fn is_deleted(&self) -> bool {
116        self.metadata.deleted_at.is_some()
117    }
118
119    /// Check if this entity is active (not deleted)
120    pub fn is_active(&self) -> bool {
121        self.metadata.deleted_at.is_none()
122    }
123
124    /// Get when this entity was deleted
125    pub fn deleted_at(&self) -> Option<&DateTime<Utc>> {
126        self.metadata.deleted_at.as_ref()
127    }
128
129    /// Get who created this entity
130    pub fn created_by(&self) -> Option<&Uuid> {
131        self.metadata.created_by.as_ref()
132    }
133
134    /// Get who last updated this entity
135    pub fn updated_by(&self) -> Option<&Uuid> {
136        self.metadata.updated_by.as_ref()
137    }
138
139    /// Get who deleted this entity
140    pub fn deleted_by(&self) -> Option<&Uuid> {
141        self.metadata.deleted_by.as_ref()
142    }
143
144
145    // ==========================================================
146    // Fluent Setters (with_* for optional fields)
147    // ==========================================================
148
149    /// Set the service_project_id field (chainable)
150    pub fn with_service_project_id(mut self, value: Uuid) -> Self {
151        self.service_project_id = Some(value);
152        self
153    }
154
155    /// Set the service_project_template_id field (chainable)
156    pub fn with_service_project_template_id(mut self, value: Uuid) -> Self {
157        self.service_project_template_id = Some(value);
158        self
159    }
160
161    // ==========================================================
162    // Partial Update
163    // ==========================================================
164
165    /// Apply partial updates from a map of field name to JSON value
166    pub fn apply_patch(&mut self, fields: std::collections::HashMap<String, serde_json::Value>) {
167        for (key, value) in fields {
168            match key.as_str() {
169                "item_id" => {
170                    if let Ok(v) = serde_json::from_value(value) { self.item_id = v; }
171                }
172                "stock_uom" => {
173                    if let Ok(v) = serde_json::from_value(value) { self.stock_uom = v; }
174                }
175                "is_stock_item" => {
176                    if let Ok(v) = serde_json::from_value(value) { self.is_stock_item = v; }
177                }
178                "has_batch" => {
179                    if let Ok(v) = serde_json::from_value(value) { self.has_batch = v; }
180                }
181                "valuation_method" => {
182                    if let Ok(v) = serde_json::from_value(value) { self.valuation_method = v; }
183                }
184                "reorder_level" => {
185                    if let Ok(v) = serde_json::from_value(value) { self.reorder_level = v; }
186                }
187                "weight_per_unit" => {
188                    if let Ok(v) = serde_json::from_value(value) { self.weight_per_unit = v; }
189                }
190                "service_tracking" => {
191                    if let Ok(v) = serde_json::from_value(value) { self.service_tracking = v; }
192                }
193                "service_project_id" => {
194                    if let Ok(v) = serde_json::from_value(value) { self.service_project_id = v; }
195                }
196                "service_project_template_id" => {
197                    if let Ok(v) = serde_json::from_value(value) { self.service_project_template_id = v; }
198                }
199                _ => {} // ignore unknown fields
200            }
201        }
202    }
203
204}
205
206impl super::Entity for StockItem {
207    type Id = Uuid;
208
209    fn entity_id(&self) -> &Self::Id {
210        &self.id
211    }
212
213    fn entity_type() -> &'static str {
214        "StockItem"
215    }
216}
217
218impl backbone_core::PersistentEntity for StockItem {
219    fn entity_id(&self) -> String {
220        self.id.to_string()
221    }
222    fn set_entity_id(&mut self, id: String) {
223        if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
224            self.id = uuid;
225        }
226    }
227    fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
228        self.metadata.created_at
229    }
230    fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
231        self.metadata.created_at = Some(ts);
232    }
233    fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
234        self.metadata.updated_at
235    }
236    fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
237        self.metadata.updated_at = Some(ts);
238    }
239    fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
240        self.metadata.deleted_at
241    }
242    fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
243        self.metadata.deleted_at = ts;
244    }
245}
246
247impl backbone_orm::EntityRepoMeta for StockItem {
248    fn column_types() -> std::collections::HashMap<String, String> {
249        let mut m = std::collections::HashMap::new();
250        m.insert("id".to_string(), "uuid".to_string());
251        m.insert("item_id".to_string(), "uuid".to_string());
252        m.insert("service_project_id".to_string(), "uuid".to_string());
253        m.insert("service_project_template_id".to_string(), "uuid".to_string());
254        m.insert("valuation_method".to_string(), "valuation_method".to_string());
255        m.insert("service_tracking".to_string(), "service_tracking_type".to_string());
256        m
257    }
258    fn search_fields() -> &'static [&'static str] {
259        &["stock_uom"]
260    }
261}
262
263/// Builder for StockItem entity
264///
265/// Provides a fluent API for constructing StockItem instances.
266/// System fields (id, metadata, timestamps) are auto-initialized.
267#[derive(Debug, Clone, Default)]
268pub struct StockItemBuilder {
269    item_id: Option<Uuid>,
270    stock_uom: Option<String>,
271    is_stock_item: Option<bool>,
272    has_batch: Option<bool>,
273    valuation_method: Option<ValuationMethod>,
274    reorder_level: Option<Decimal>,
275    weight_per_unit: Option<Decimal>,
276    service_tracking: Option<ServiceTrackingType>,
277    service_project_id: Option<Uuid>,
278    service_project_template_id: Option<Uuid>,
279}
280
281impl StockItemBuilder {
282    /// Set the item_id field (required)
283    pub fn item_id(mut self, value: Uuid) -> Self {
284        self.item_id = Some(value);
285        self
286    }
287
288    /// Set the stock_uom field (required)
289    pub fn stock_uom(mut self, value: String) -> Self {
290        self.stock_uom = Some(value);
291        self
292    }
293
294    /// Set the is_stock_item field (default: `true`)
295    pub fn is_stock_item(mut self, value: bool) -> Self {
296        self.is_stock_item = Some(value);
297        self
298    }
299
300    /// Set the has_batch field (default: `false`)
301    pub fn has_batch(mut self, value: bool) -> Self {
302        self.has_batch = Some(value);
303        self
304    }
305
306    /// Set the valuation_method field (default: `ValuationMethod::default()`)
307    pub fn valuation_method(mut self, value: ValuationMethod) -> Self {
308        self.valuation_method = Some(value);
309        self
310    }
311
312    /// Set the reorder_level field (default: `Decimal::from(0)`)
313    pub fn reorder_level(mut self, value: Decimal) -> Self {
314        self.reorder_level = Some(value);
315        self
316    }
317
318    /// Set the weight_per_unit field (default: `Decimal::from(0)`)
319    pub fn weight_per_unit(mut self, value: Decimal) -> Self {
320        self.weight_per_unit = Some(value);
321        self
322    }
323
324    /// Set the service_tracking field (default: `ServiceTrackingType::default()`)
325    pub fn service_tracking(mut self, value: ServiceTrackingType) -> Self {
326        self.service_tracking = Some(value);
327        self
328    }
329
330    /// Set the service_project_id field (optional)
331    pub fn service_project_id(mut self, value: Uuid) -> Self {
332        self.service_project_id = Some(value);
333        self
334    }
335
336    /// Set the service_project_template_id field (optional)
337    pub fn service_project_template_id(mut self, value: Uuid) -> Self {
338        self.service_project_template_id = Some(value);
339        self
340    }
341
342    /// Build the StockItem entity
343    ///
344    /// Returns Err if any required field without a default is missing.
345    pub fn build(self) -> Result<StockItem, String> {
346        let item_id = self.item_id.ok_or_else(|| "item_id is required".to_string())?;
347        let stock_uom = self.stock_uom.ok_or_else(|| "stock_uom is required".to_string())?;
348
349        Ok(StockItem {
350            id: Uuid::new_v4(),
351            item_id,
352            stock_uom,
353            is_stock_item: self.is_stock_item.unwrap_or(true),
354            has_batch: self.has_batch.unwrap_or(false),
355            valuation_method: self.valuation_method.unwrap_or_default(),
356            reorder_level: self.reorder_level.unwrap_or(Decimal::from(0)),
357            weight_per_unit: self.weight_per_unit.unwrap_or(Decimal::from(0)),
358            service_tracking: self.service_tracking.unwrap_or_default(),
359            service_project_id: self.service_project_id,
360            service_project_template_id: self.service_project_template_id,
361            metadata: AuditMetadata::default(),
362        })
363    }
364}