Skip to main content

backbone_inventory/domain/entity/
transfer.rs

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