Skip to main content

backbone_inventory/domain/entity/
bin.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4use uuid::Uuid;
5use rust_decimal::Decimal;
6use super::AuditMetadata;
7
8/// Strongly-typed ID for Bin
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(transparent)]
11pub struct BinId(pub Uuid);
12
13impl BinId {
14    pub fn new(id: Uuid) -> Self { Self(id) }
15    pub fn generate() -> Self { Self(Uuid::new_v4()) }
16    pub fn into_inner(self) -> Uuid { self.0 }
17}
18
19impl std::fmt::Display for BinId {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        write!(f, "{}", self.0)
22    }
23}
24
25impl std::str::FromStr for BinId {
26    type Err = uuid::Error;
27    fn from_str(s: &str) -> Result<Self, Self::Err> {
28        Ok(Self(Uuid::parse_str(s)?))
29    }
30}
31
32impl From<Uuid> for BinId {
33    fn from(id: Uuid) -> Self { Self(id) }
34}
35
36impl From<BinId> for Uuid {
37    fn from(id: BinId) -> Self { id.0 }
38}
39
40impl AsRef<Uuid> for BinId {
41    fn as_ref(&self) -> &Uuid { &self.0 }
42}
43
44impl std::ops::Deref for BinId {
45    type Target = Uuid;
46    fn deref(&self) -> &Self::Target { &self.0 }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
50pub struct Bin {
51    pub id: Uuid,
52    pub item_id: Uuid,
53    pub warehouse_id: Uuid,
54    pub actual_qty: Decimal,
55    pub reserved_qty: Decimal,
56    pub valuation_rate: Decimal,
57    pub stock_value: Decimal,
58    #[serde(default)]
59    #[sqlx(json)]
60    pub metadata: AuditMetadata,
61}
62
63impl Bin {
64    /// Create a builder for Bin
65    pub fn builder() -> BinBuilder {
66        <BinBuilder as Default>::default()
67    }
68
69    /// Create a new Bin with required fields
70    pub fn new(item_id: Uuid, warehouse_id: Uuid, actual_qty: Decimal, reserved_qty: Decimal, valuation_rate: Decimal, stock_value: Decimal) -> Self {
71        Self {
72            id: Uuid::new_v4(),
73            item_id,
74            warehouse_id,
75            actual_qty,
76            reserved_qty,
77            valuation_rate,
78            stock_value,
79            metadata: AuditMetadata::default(),
80        }
81    }
82
83    /// Get the entity's unique identifier
84    pub fn id(&self) -> &Uuid {
85        &self.id
86    }
87
88    /// Get a strongly-typed ID for this entity
89    pub fn typed_id(&self) -> BinId {
90        BinId(self.id)
91    }
92
93    /// Get when this entity was created
94    pub fn created_at(&self) -> Option<&DateTime<Utc>> {
95        self.metadata.created_at.as_ref()
96    }
97
98    /// Get when this entity was last updated
99    pub fn updated_at(&self) -> Option<&DateTime<Utc>> {
100        self.metadata.updated_at.as_ref()
101    }
102
103    /// Check if this entity is soft deleted
104    pub fn is_deleted(&self) -> bool {
105        self.metadata.deleted_at.is_some()
106    }
107
108    /// Check if this entity is active (not deleted)
109    pub fn is_active(&self) -> bool {
110        self.metadata.deleted_at.is_none()
111    }
112
113    /// Get when this entity was deleted
114    pub fn deleted_at(&self) -> Option<&DateTime<Utc>> {
115        self.metadata.deleted_at.as_ref()
116    }
117
118    /// Get who created this entity
119    pub fn created_by(&self) -> Option<&Uuid> {
120        self.metadata.created_by.as_ref()
121    }
122
123    /// Get who last updated this entity
124    pub fn updated_by(&self) -> Option<&Uuid> {
125        self.metadata.updated_by.as_ref()
126    }
127
128    /// Get who deleted this entity
129    pub fn deleted_by(&self) -> Option<&Uuid> {
130        self.metadata.deleted_by.as_ref()
131    }
132
133
134    // ==========================================================
135    // Partial Update
136    // ==========================================================
137
138    /// Apply partial updates from a map of field name to JSON value
139    pub fn apply_patch(&mut self, fields: std::collections::HashMap<String, serde_json::Value>) {
140        for (key, value) in fields {
141            match key.as_str() {
142                "item_id" => {
143                    if let Ok(v) = serde_json::from_value(value) { self.item_id = v; }
144                }
145                "warehouse_id" => {
146                    if let Ok(v) = serde_json::from_value(value) { self.warehouse_id = v; }
147                }
148                "actual_qty" => {
149                    if let Ok(v) = serde_json::from_value(value) { self.actual_qty = v; }
150                }
151                "reserved_qty" => {
152                    if let Ok(v) = serde_json::from_value(value) { self.reserved_qty = v; }
153                }
154                "valuation_rate" => {
155                    if let Ok(v) = serde_json::from_value(value) { self.valuation_rate = v; }
156                }
157                "stock_value" => {
158                    if let Ok(v) = serde_json::from_value(value) { self.stock_value = v; }
159                }
160                _ => {} // ignore unknown fields
161            }
162        }
163    }
164
165}
166
167impl super::Entity for Bin {
168    type Id = Uuid;
169
170    fn entity_id(&self) -> &Self::Id {
171        &self.id
172    }
173
174    fn entity_type() -> &'static str {
175        "Bin"
176    }
177}
178
179impl backbone_core::PersistentEntity for Bin {
180    fn write_protected_fields() -> &'static [&'static str] {
181        &["actual_qty", "reserved_qty", "valuation_rate", "stock_value"]
182    }
183    fn entity_id(&self) -> String {
184        self.id.to_string()
185    }
186    fn set_entity_id(&mut self, id: String) {
187        if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
188            self.id = uuid;
189        }
190    }
191    fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
192        self.metadata.created_at
193    }
194    fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
195        self.metadata.created_at = Some(ts);
196    }
197    fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
198        self.metadata.updated_at
199    }
200    fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
201        self.metadata.updated_at = Some(ts);
202    }
203    fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
204        self.metadata.deleted_at
205    }
206    fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
207        self.metadata.deleted_at = ts;
208    }
209}
210
211impl backbone_orm::EntityRepoMeta for Bin {
212    fn column_types() -> std::collections::HashMap<String, String> {
213        let mut m = std::collections::HashMap::new();
214        m.insert("id".to_string(), "uuid".to_string());
215        m.insert("item_id".to_string(), "uuid".to_string());
216        m.insert("warehouse_id".to_string(), "uuid".to_string());
217        m
218    }
219    fn search_fields() -> &'static [&'static str] {
220        &[]
221    }
222    fn relations() -> &'static [(&'static str, &'static str, &'static str)] {
223        &[("warehouse", "warehouses", "warehouseId")]
224    }
225}
226
227/// Builder for Bin entity
228///
229/// Provides a fluent API for constructing Bin instances.
230/// System fields (id, metadata, timestamps) are auto-initialized.
231#[derive(Debug, Clone, Default)]
232pub struct BinBuilder {
233    item_id: Option<Uuid>,
234    warehouse_id: Option<Uuid>,
235    actual_qty: Option<Decimal>,
236    reserved_qty: Option<Decimal>,
237    valuation_rate: Option<Decimal>,
238    stock_value: Option<Decimal>,
239}
240
241impl BinBuilder {
242    /// Set the item_id field (required)
243    pub fn item_id(mut self, value: Uuid) -> Self {
244        self.item_id = Some(value);
245        self
246    }
247
248    /// Set the warehouse_id field (required)
249    pub fn warehouse_id(mut self, value: Uuid) -> Self {
250        self.warehouse_id = Some(value);
251        self
252    }
253
254    /// Set the actual_qty field (default: `Decimal::from(0)`)
255    pub fn actual_qty(mut self, value: Decimal) -> Self {
256        self.actual_qty = Some(value);
257        self
258    }
259
260    /// Set the reserved_qty field (default: `Decimal::from(0)`)
261    pub fn reserved_qty(mut self, value: Decimal) -> Self {
262        self.reserved_qty = Some(value);
263        self
264    }
265
266    /// Set the valuation_rate field (default: `Decimal::from(0)`)
267    pub fn valuation_rate(mut self, value: Decimal) -> Self {
268        self.valuation_rate = Some(value);
269        self
270    }
271
272    /// Set the stock_value field (default: `Decimal::from(0)`)
273    pub fn stock_value(mut self, value: Decimal) -> Self {
274        self.stock_value = Some(value);
275        self
276    }
277
278    /// Build the Bin entity
279    ///
280    /// Returns Err if any required field without a default is missing.
281    pub fn build(self) -> Result<Bin, String> {
282        let item_id = self.item_id.ok_or_else(|| "item_id is required".to_string())?;
283        let warehouse_id = self.warehouse_id.ok_or_else(|| "warehouse_id is required".to_string())?;
284
285        Ok(Bin {
286            id: Uuid::new_v4(),
287            item_id,
288            warehouse_id,
289            actual_qty: self.actual_qty.unwrap_or(Decimal::from(0)),
290            reserved_qty: self.reserved_qty.unwrap_or(Decimal::from(0)),
291            valuation_rate: self.valuation_rate.unwrap_or(Decimal::from(0)),
292            stock_value: self.stock_value.unwrap_or(Decimal::from(0)),
293            metadata: AuditMetadata::default(),
294        })
295    }
296}