Skip to main content

stateset_embedded/
lots.rs

1//! Lot/Batch tracking operations
2//!
3//! Comprehensive lot management system supporting:
4//! - Lot creation and lifecycle management
5//! - Lot transactions (consumption, adjustment, transfer)
6//! - Certificate management (COA, COC, MSDS)
7//! - Forward and backward traceability
8//!
9//! # Example
10//!
11//! ```rust,ignore
12//! use stateset_embedded::{Commerce, CreateLot};
13//! use chrono::{Utc, Duration};
14//! use rust_decimal_macros::dec;
15//!
16//! let commerce = Commerce::new("./store.db")?;
17//!
18//! // Create a lot for received materials
19//! let lot = commerce.lots().create(CreateLot {
20//!     lot_number: Some("LOT-2025-001".into()),
21//!     sku: "RAW-MAT-001".into(),
22//!     quantity_produced: dec!(1000),
23//!     expiration_date: Some(Utc::now() + Duration::days(365)),
24//!     ..Default::default()
25//! })?;
26//!
27//! println!("Created lot {}", lot.lot_number);
28//! # Ok::<(), stateset_embedded::CommerceError>(())
29//! ```
30
31use rust_decimal::Decimal;
32use stateset_core::{
33    AddLotCertificate, AdjustLot, ConsumeLot, CreateLot, Lot, LotCertificate, LotFilter,
34    LotLocation, LotStatus, LotTransaction, MergeLots, ReserveLot, Result, SplitLot,
35    TraceabilityResult, TransferLot, UpdateLot,
36};
37use stateset_db::Database;
38use std::sync::Arc;
39use uuid::Uuid;
40
41/// Lot/Batch tracking management interface.
42pub struct Lots {
43    db: Arc<dyn Database>,
44}
45
46impl std::fmt::Debug for Lots {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        f.debug_struct("Lots").finish_non_exhaustive()
49    }
50}
51
52impl Lots {
53    pub(crate) fn new(db: Arc<dyn Database>) -> Self {
54        Self { db }
55    }
56
57    // ========================================================================
58    // Basic CRUD
59    // ========================================================================
60
61    /// Create a new lot.
62    ///
63    /// # Example
64    ///
65    /// ```rust,ignore
66    /// use stateset_embedded::{Commerce, CreateLot};
67    /// use chrono::{Utc, Duration};
68    /// use rust_decimal_macros::dec;
69    ///
70    /// let commerce = Commerce::new(":memory:")?;
71    ///
72    /// let lot = commerce.lots().create(CreateLot {
73    ///     lot_number: Some("BATCH-001".into()),
74    ///     sku: "PROD-001".into(),
75    ///     quantity_produced: dec!(500),
76    ///     production_date: Some(Utc::now()),
77    ///     expiration_date: Some(Utc::now() + Duration::days(180)),
78    ///     ..Default::default()
79    /// })?;
80    /// # Ok::<(), stateset_embedded::CommerceError>(())
81    /// ```
82    pub fn create(&self, input: CreateLot) -> Result<Lot> {
83        self.db.lots().create(input)
84    }
85
86    /// Get a lot by ID.
87    pub fn get(&self, id: Uuid) -> Result<Option<Lot>> {
88        self.db.lots().get(id)
89    }
90
91    /// Get a lot by lot number.
92    pub fn get_by_number(&self, lot_number: &str) -> Result<Option<Lot>> {
93        self.db.lots().get_by_number(lot_number)
94    }
95
96    /// List lots with optional filtering.
97    ///
98    /// # Example
99    ///
100    /// ```rust,ignore
101    /// use stateset_embedded::{Commerce, LotFilter, LotStatus};
102    ///
103    /// let commerce = Commerce::new(":memory:")?;
104    ///
105    /// // Get all active lots for a SKU
106    /// let lots = commerce.lots().list(LotFilter {
107    ///     sku: Some("PROD-001".into()),
108    ///     status: Some(LotStatus::Active),
109    ///     ..Default::default()
110    /// })?;
111    /// # Ok::<(), stateset_embedded::CommerceError>(())
112    /// ```
113    pub fn list(&self, filter: LotFilter) -> Result<Vec<Lot>> {
114        self.db.lots().list(filter)
115    }
116
117    /// Update a lot.
118    pub fn update(&self, id: Uuid, input: UpdateLot) -> Result<Lot> {
119        self.db.lots().update(id, input)
120    }
121
122    /// Delete a lot (only if unused).
123    pub fn delete(&self, id: Uuid) -> Result<()> {
124        self.db.lots().delete(id)
125    }
126
127    // ========================================================================
128    // Status Management
129    // ========================================================================
130
131    /// Quarantine a lot (prevent usage).
132    ///
133    /// # Example
134    ///
135    /// ```rust,ignore
136    /// use stateset_embedded::Commerce;
137    /// use uuid::Uuid;
138    ///
139    /// let commerce = Commerce::new(":memory:")?;
140    ///
141    /// commerce.lots().quarantine(Uuid::new_v4(), "Quality issue detected")?;
142    /// # Ok::<(), stateset_embedded::CommerceError>(())
143    /// ```
144    pub fn quarantine(&self, id: Uuid, reason: &str) -> Result<Lot> {
145        self.db.lots().quarantine(id, reason)
146    }
147
148    /// Release a lot from quarantine.
149    pub fn release_quarantine(&self, id: Uuid) -> Result<Lot> {
150        self.db.lots().release_quarantine(id)
151    }
152
153    // ========================================================================
154    // Inventory Operations
155    // ========================================================================
156
157    /// Adjust lot quantity (positive or negative).
158    ///
159    /// # Example
160    ///
161    /// ```rust,ignore
162    /// use stateset_embedded::{Commerce, AdjustLot};
163    /// use rust_decimal_macros::dec;
164    /// use uuid::Uuid;
165    ///
166    /// let commerce = Commerce::new(":memory:")?;
167    ///
168    /// // Remove 10 units due to damage
169    /// commerce.lots().adjust(AdjustLot {
170    ///     lot_id: Uuid::new_v4(),
171    ///     quantity: dec!(-10),
172    ///     reason: "Damaged in storage".into(),
173    ///     performed_by: Some("warehouse_user".into()),
174    ///     ..Default::default()
175    /// })?;
176    /// # Ok::<(), stateset_embedded::CommerceError>(())
177    /// ```
178    pub fn adjust(&self, input: AdjustLot) -> Result<LotTransaction> {
179        self.db.lots().adjust(input)
180    }
181
182    /// Consume quantity from a lot.
183    ///
184    /// # Example
185    ///
186    /// ```rust,ignore
187    /// use stateset_embedded::{Commerce, ConsumeLot};
188    /// use rust_decimal_macros::dec;
189    /// use uuid::Uuid;
190    ///
191    /// let commerce = Commerce::new(":memory:")?;
192    ///
193    /// commerce.lots().consume(ConsumeLot {
194    ///     lot_id: Uuid::new_v4(),
195    ///     quantity: dec!(25),
196    ///     reference_type: "work_order".into(),
197    ///     reference_id: Uuid::new_v4(),
198    ///     ..Default::default()
199    /// })?;
200    /// # Ok::<(), stateset_embedded::CommerceError>(())
201    /// ```
202    pub fn consume(&self, input: ConsumeLot) -> Result<LotTransaction> {
203        self.db.lots().consume(input)
204    }
205
206    /// Reserve quantity in a lot.
207    ///
208    /// Returns the reservation ID which can be used to release or confirm the reservation.
209    pub fn reserve(&self, input: ReserveLot) -> Result<Uuid> {
210        self.db.lots().reserve(input)
211    }
212
213    /// Release a reservation (cancel it without consuming).
214    pub fn release_reservation(&self, reservation_id: Uuid) -> Result<()> {
215        self.db.lots().release_reservation(reservation_id)
216    }
217
218    /// Confirm a reservation (convert to actual consumption).
219    pub fn confirm_reservation(&self, reservation_id: Uuid) -> Result<LotTransaction> {
220        self.db.lots().confirm_reservation(reservation_id)
221    }
222
223    /// Transfer lot to a different location.
224    pub fn transfer(&self, input: TransferLot) -> Result<LotTransaction> {
225        self.db.lots().transfer(input)
226    }
227
228    /// Split a lot into two.
229    ///
230    /// # Example
231    ///
232    /// ```rust,ignore
233    /// use stateset_embedded::{Commerce, SplitLot};
234    /// use rust_decimal_macros::dec;
235    /// use uuid::Uuid;
236    ///
237    /// let commerce = Commerce::new(":memory:")?;
238    ///
239    /// // Split 100 units into a new lot
240    /// let new_lot = commerce.lots().split(SplitLot {
241    ///     source_lot_id: Uuid::new_v4(),
242    ///     new_lot_number: Some("LOT-2025-001B".into()),
243    ///     quantity: dec!(100),
244    ///     reason: Some("Customer allocation".into()),
245    ///     ..Default::default()
246    /// })?;
247    /// # Ok::<(), stateset_embedded::CommerceError>(())
248    /// ```
249    pub fn split(&self, input: SplitLot) -> Result<Lot> {
250        self.db.lots().split(input)
251    }
252
253    /// Merge multiple lots into one.
254    pub fn merge(&self, input: MergeLots) -> Result<Lot> {
255        self.db.lots().merge(input)
256    }
257
258    // ========================================================================
259    // Certificates
260    // ========================================================================
261
262    /// Add a certificate to a lot.
263    ///
264    /// # Example
265    ///
266    /// ```rust,ignore
267    /// use stateset_embedded::{Commerce, AddLotCertificate, CertificateType};
268    /// use uuid::Uuid;
269    ///
270    /// let commerce = Commerce::new(":memory:")?;
271    ///
272    /// commerce.lots().add_certificate(AddLotCertificate {
273    ///     lot_id: Uuid::new_v4(),
274    ///     certificate_type: CertificateType::Coa,
275    ///     document_url: Some("https://storage.example.com/certs/coa-123.pdf".into()),
276    ///     issued_by: Some("Quality Lab Inc.".into()),
277    ///     ..Default::default()
278    /// })?;
279    /// # Ok::<(), stateset_embedded::CommerceError>(())
280    /// ```
281    pub fn add_certificate(&self, input: AddLotCertificate) -> Result<LotCertificate> {
282        self.db.lots().add_certificate(input)
283    }
284
285    /// Get certificates for a lot.
286    pub fn get_certificates(&self, lot_id: Uuid) -> Result<Vec<LotCertificate>> {
287        self.db.lots().get_certificates(lot_id)
288    }
289
290    /// Remove a certificate from a lot.
291    pub fn delete_certificate(&self, certificate_id: Uuid) -> Result<()> {
292        self.db.lots().delete_certificate(certificate_id)
293    }
294
295    // ========================================================================
296    // Locations
297    // ========================================================================
298
299    /// Get lot quantities by location.
300    pub fn get_locations(&self, lot_id: Uuid) -> Result<Vec<LotLocation>> {
301        self.db.lots().get_lot_locations(lot_id)
302    }
303
304    /// Get quantity at a specific location.
305    pub fn get_quantity_at_location(
306        &self,
307        lot_id: Uuid,
308        location_id: i32,
309    ) -> Result<Option<Decimal>> {
310        self.db.lots().get_quantity_at_location(lot_id, location_id)
311    }
312
313    // ========================================================================
314    // Transactions
315    // ========================================================================
316
317    /// Get transaction history for a lot.
318    ///
319    /// # Example
320    ///
321    /// ```rust,ignore
322    /// use stateset_embedded::Commerce;
323    /// use uuid::Uuid;
324    ///
325    /// let commerce = Commerce::new(":memory:")?;
326    ///
327    /// let transactions = commerce.lots().get_transactions(
328    ///     Uuid::new_v4(),
329    ///     100,  // limit
330    /// )?;
331    ///
332    /// for tx in transactions {
333    ///     println!("{:?}: {} units", tx.transaction_type, tx.quantity);
334    /// }
335    /// # Ok::<(), stateset_embedded::CommerceError>(())
336    /// ```
337    pub fn get_transactions(&self, lot_id: Uuid, limit: u32) -> Result<Vec<LotTransaction>> {
338        self.db.lots().get_transactions(lot_id, limit)
339    }
340
341    // ========================================================================
342    // Traceability
343    // ========================================================================
344
345    /// Get full bidirectional traceability (upstream and downstream).
346    ///
347    /// # Example
348    ///
349    /// ```rust,ignore
350    /// use stateset_embedded::Commerce;
351    /// use uuid::Uuid;
352    ///
353    /// let commerce = Commerce::new(":memory:")?;
354    ///
355    /// let trace = commerce.lots().trace(Uuid::new_v4())?;
356    ///
357    /// println!("Upstream sources: {} nodes", trace.upstream.len());
358    /// println!("Downstream destinations: {} nodes", trace.downstream.len());
359    /// # Ok::<(), stateset_embedded::CommerceError>(())
360    /// ```
361    pub fn trace(&self, lot_id: Uuid) -> Result<TraceabilityResult> {
362        self.db.lots().trace(lot_id)
363    }
364
365    // ========================================================================
366    // Queries
367    // ========================================================================
368
369    /// Get lots expiring within a number of days.
370    pub fn get_expiring_lots(&self, days: i32) -> Result<Vec<Lot>> {
371        self.db.lots().get_expiring_lots(days)
372    }
373
374    /// Get already expired lots.
375    pub fn get_expired_lots(&self) -> Result<Vec<Lot>> {
376        self.db.lots().get_expired_lots()
377    }
378
379    /// Get lots with available quantity for a SKU.
380    ///
381    /// Returns lots ordered by production date (FIFO).
382    pub fn get_available_lots_for_sku(&self, sku: &str) -> Result<Vec<Lot>> {
383        self.db.lots().get_available_lots_for_sku(sku)
384    }
385
386    /// Count lots matching filter.
387    pub fn count(&self, filter: LotFilter) -> Result<u64> {
388        self.db.lots().count(filter)
389    }
390
391    // ========================================================================
392    // Batch Operations
393    // ========================================================================
394
395    /// Create multiple lots at once.
396    pub fn create_batch(&self, inputs: Vec<CreateLot>) -> Result<stateset_core::BatchResult<Lot>> {
397        self.db.lots().create_batch(inputs)
398    }
399
400    /// Get multiple lots by ID.
401    pub fn get_batch(&self, ids: Vec<Uuid>) -> Result<Vec<Lot>> {
402        self.db.lots().get_batch(ids)
403    }
404
405    // ========================================================================
406    // Convenience Methods
407    // ========================================================================
408
409    /// Get all active lots for a SKU.
410    pub fn get_active_lots(&self, sku: &str) -> Result<Vec<Lot>> {
411        self.list(LotFilter {
412            sku: Some(sku.to_string()),
413            status: Some(LotStatus::Active),
414            ..Default::default()
415        })
416    }
417
418    /// Get all quarantined lots.
419    pub fn get_quarantined(&self) -> Result<Vec<Lot>> {
420        self.list(LotFilter { status: Some(LotStatus::Quarantine), ..Default::default() })
421    }
422}