Skip to main content

backbone_inventory/domain/entity/
location.rs

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