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}