Skip to main content

stateset_embedded/
receiving.rs

1//! Receiving and goods receipt operations
2//!
3//! Comprehensive receiving management supporting:
4//! - ASN (Advanced Shipping Notice) processing
5//! - Goods receipt from purchase orders
6//! - Quality inspection integration
7//! - Put-away task management
8//!
9//! # Example
10//!
11//! ```ignore
12//! use stateset_embedded::{Commerce, CreateReceipt, CreateReceiptItem, ReceiptType};
13//! use rust_decimal_macros::dec;
14//!
15//! let commerce = Commerce::new("./store.db")?;
16//!
17//! // Create a receipt from an ASN
18//! let receipt = commerce.receiving().create_receipt(CreateReceipt {
19//!     receipt_type: ReceiptType::PurchaseOrder,
20//!     warehouse_id: 1,
21//!     items: vec![CreateReceiptItem {
22//!         sku: "PROD-001".into(),
23//!         expected_quantity: dec!(100),
24//!         ..Default::default()
25//!     }],
26//!     ..Default::default()
27//! })?;
28//!
29//! println!("Created receipt {}", receipt.receipt_number);
30//! # Ok::<(), stateset_embedded::CommerceError>(())
31//! ```
32
33use stateset_core::{
34    BatchResult, CompletePutAway, CreatePutAway, CreateReceipt, PutAway, PutAwayFilter, Receipt,
35    ReceiptFilter, ReceiptItem, ReceiveItems, Result, UpdateReceipt,
36};
37use stateset_db::Database;
38use std::sync::Arc;
39use uuid::Uuid;
40
41/// Receiving and goods receipt management interface.
42pub struct Receiving {
43    db: Arc<dyn Database>,
44}
45
46impl std::fmt::Debug for Receiving {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        f.debug_struct("Receiving").finish_non_exhaustive()
49    }
50}
51
52impl Receiving {
53    pub(crate) fn new(db: Arc<dyn Database>) -> Self {
54        Self { db }
55    }
56
57    // ========================================================================
58    // Receipt Operations
59    // ========================================================================
60
61    /// Create a new receipt (ASN/Goods Receipt).
62    ///
63    /// # Example
64    ///
65    /// ```rust,no_run
66    /// use stateset_embedded::{Commerce, CreateReceipt, CreateReceiptItem, ReceiptType};
67    /// use rust_decimal_macros::dec;
68    /// use chrono::{Utc, Duration};
69    ///
70    /// let commerce = Commerce::new(":memory:")?;
71    ///
72    /// let receipt = commerce.receiving().create_receipt(CreateReceipt {
73    ///     receipt_type: ReceiptType::PurchaseOrder,
74    ///     warehouse_id: 1,
75    ///     carrier: Some("UPS".into()),
76    ///     tracking_number: Some("1Z999AA10123456784".into()),
77    ///     expected_date: Some(Utc::now() + Duration::days(3)),
78    ///     items: vec![
79    ///         CreateReceiptItem {
80    ///             sku: "WIDGET-001".into(),
81    ///             description: Some("Standard Widget".into()),
82    ///             expected_quantity: dec!(50),
83    ///             unit_cost: Some(dec!(10.00)),
84    ///             ..Default::default()
85    ///         },
86    ///         CreateReceiptItem {
87    ///             sku: "GADGET-002".into(),
88    ///             description: Some("Premium Gadget".into()),
89    ///             expected_quantity: dec!(25),
90    ///             unit_cost: Some(dec!(25.00)),
91    ///             ..Default::default()
92    ///         },
93    ///     ],
94    ///     ..Default::default()
95    /// })?;
96    /// # Ok::<(), stateset_embedded::CommerceError>(())
97    /// ```
98    pub fn create_receipt(&self, input: CreateReceipt) -> Result<Receipt> {
99        self.db.receiving().create_receipt(input)
100    }
101
102    /// Get a receipt by ID.
103    pub fn get_receipt(&self, id: Uuid) -> Result<Option<Receipt>> {
104        self.db.receiving().get_receipt(id)
105    }
106
107    /// Get a receipt by receipt number.
108    pub fn get_receipt_by_number(&self, number: &str) -> Result<Option<Receipt>> {
109        self.db.receiving().get_receipt_by_number(number)
110    }
111
112    /// Update receipt details (carrier, tracking, expected date).
113    pub fn update_receipt(&self, id: Uuid, input: UpdateReceipt) -> Result<Receipt> {
114        self.db.receiving().update_receipt(id, input)
115    }
116
117    /// List receipts with optional filtering.
118    ///
119    /// # Example
120    ///
121    /// ```rust,no_run
122    /// use stateset_embedded::{Commerce, ReceiptFilter, ReceiptStatus};
123    ///
124    /// let commerce = Commerce::new(":memory:")?;
125    ///
126    /// // Get all in-progress receipts for a warehouse
127    /// let receipts = commerce.receiving().list_receipts(ReceiptFilter {
128    ///     warehouse_id: Some(1),
129    ///     status: Some(ReceiptStatus::InProgress),
130    ///     limit: Some(50),
131    ///     ..Default::default()
132    /// })?;
133    /// # Ok::<(), stateset_embedded::CommerceError>(())
134    /// ```
135    pub fn list_receipts(&self, filter: ReceiptFilter) -> Result<Vec<Receipt>> {
136        self.db.receiving().list_receipts(filter)
137    }
138
139    /// Delete a receipt (only if still in 'expected' status).
140    pub fn delete_receipt(&self, id: Uuid) -> Result<()> {
141        self.db.receiving().delete_receipt(id)
142    }
143
144    /// Start receiving goods (transition from 'expected' to '`in_progress`').
145    ///
146    /// Call this when goods arrive and receiving process begins.
147    pub fn start_receiving(&self, id: Uuid) -> Result<Receipt> {
148        self.db.receiving().start_receiving(id)
149    }
150
151    /// Receive items against a receipt.
152    ///
153    /// Updates receipt item quantities with actual received amounts.
154    /// Can be called multiple times for partial receipts.
155    ///
156    /// # Example
157    ///
158    /// ```rust,no_run
159    /// use stateset_embedded::{Commerce, ReceiveItems, ReceiveItemLine};
160    /// use rust_decimal_macros::dec;
161    /// use uuid::Uuid;
162    ///
163    /// let commerce = Commerce::new(":memory:")?;
164    ///
165    /// // Receive items with lot tracking
166    /// let receipt = commerce.receiving().receive_items(ReceiveItems {
167    ///     receipt_id: Uuid::new_v4(), // receipt ID
168    ///     items: vec![ReceiveItemLine {
169    ///         receipt_item_id: Uuid::new_v4(), // receipt item ID
170    ///         quantity_received: dec!(48),
171    ///         quantity_rejected: Some(dec!(2)),
172    ///         rejection_reason: Some("Damaged in transit".into()),
173    ///         lot_number: Some("LOT-2025-001".into()),
174    ///         serial_numbers: None,
175    ///         expiration_date: None,
176    ///         notes: None,
177    ///     }],
178    ///     receiving_location_id: Some(1),
179    ///     received_by: Some("warehouse_user".into()),
180    /// })?;
181    /// # Ok::<(), stateset_embedded::CommerceError>(())
182    /// ```
183    pub fn receive_items(&self, input: ReceiveItems) -> Result<Receipt> {
184        self.db.receiving().receive_items(input)
185    }
186
187    /// Complete receiving (all items received).
188    ///
189    /// Transitions receipt to 'received' status.
190    pub fn complete_receiving(&self, id: Uuid) -> Result<Receipt> {
191        self.db.receiving().complete_receiving(id)
192    }
193
194    /// Cancel a receipt.
195    pub fn cancel_receipt(&self, id: Uuid) -> Result<Receipt> {
196        self.db.receiving().cancel_receipt(id)
197    }
198
199    /// Get all line items for a receipt.
200    pub fn get_receipt_items(&self, receipt_id: Uuid) -> Result<Vec<ReceiptItem>> {
201        self.db.receiving().get_receipt_items(receipt_id)
202    }
203
204    /// Count receipts matching the filter.
205    pub fn count_receipts(&self, filter: ReceiptFilter) -> Result<u64> {
206        self.db.receiving().count_receipts(filter)
207    }
208
209    // ========================================================================
210    // Put-Away Operations
211    // ========================================================================
212
213    /// Create a put-away task for received items.
214    ///
215    /// Put-away tasks direct warehouse workers to move received goods
216    /// from the receiving area to storage locations.
217    ///
218    /// # Example
219    ///
220    /// ```rust,no_run
221    /// use stateset_embedded::{Commerce, CreatePutAway};
222    /// use rust_decimal_macros::dec;
223    /// use uuid::Uuid;
224    ///
225    /// let commerce = Commerce::new(":memory:")?;
226    ///
227    /// let put_away = commerce.receiving().create_put_away(CreatePutAway {
228    ///     receipt_id: Uuid::new_v4(),
229    ///     receipt_item_id: Uuid::new_v4(),
230    ///     sku: "WIDGET-001".into(),
231    ///     from_location_id: Some(1), // Receiving dock
232    ///     to_location_id: 5,         // Storage location A-01-02
233    ///     quantity: dec!(50),
234    ///     lot_id: None,
235    ///     assigned_to: Some("forklift_operator".into()),
236    ///     notes: None,
237    /// })?;
238    /// # Ok::<(), stateset_embedded::CommerceError>(())
239    /// ```
240    pub fn create_put_away(&self, input: CreatePutAway) -> Result<PutAway> {
241        self.db.receiving().create_put_away(input)
242    }
243
244    /// Get a put-away task by ID.
245    pub fn get_put_away(&self, id: Uuid) -> Result<Option<PutAway>> {
246        self.db.receiving().get_put_away(id)
247    }
248
249    /// List put-away tasks with optional filtering.
250    pub fn list_put_aways(&self, filter: PutAwayFilter) -> Result<Vec<PutAway>> {
251        self.db.receiving().list_put_aways(filter)
252    }
253
254    /// Assign a put-away task to a user.
255    pub fn assign_put_away(&self, id: Uuid, assigned_to: &str) -> Result<PutAway> {
256        self.db.receiving().assign_put_away(id, assigned_to)
257    }
258
259    /// Start a put-away task.
260    pub fn start_put_away(&self, id: Uuid) -> Result<PutAway> {
261        self.db.receiving().start_put_away(id)
262    }
263
264    /// Complete a put-away task.
265    ///
266    /// # Example
267    ///
268    /// ```rust,no_run
269    /// use stateset_embedded::{Commerce, CompletePutAway};
270    /// use uuid::Uuid;
271    ///
272    /// let commerce = Commerce::new(":memory:")?;
273    ///
274    /// // Complete put-away (optionally with different actual location)
275    /// let put_away = commerce.receiving().complete_put_away(CompletePutAway {
276    ///     put_away_id: Uuid::new_v4(),
277    ///     actual_location_id: Some(6), // Different from planned if needed
278    ///     completed_by: Some("forklift_operator".into()),
279    ///     notes: Some("Placed in alternate bin due to space".into()),
280    /// })?;
281    /// # Ok::<(), stateset_embedded::CommerceError>(())
282    /// ```
283    pub fn complete_put_away(&self, input: CompletePutAway) -> Result<PutAway> {
284        self.db.receiving().complete_put_away(input)
285    }
286
287    /// Cancel a put-away task.
288    pub fn cancel_put_away(&self, id: Uuid) -> Result<PutAway> {
289        self.db.receiving().cancel_put_away(id)
290    }
291
292    /// Get pending put-away tasks for a receipt.
293    pub fn get_pending_put_aways(&self, receipt_id: Uuid) -> Result<Vec<PutAway>> {
294        self.db.receiving().get_pending_put_aways(receipt_id)
295    }
296
297    /// Count put-away tasks matching the filter.
298    pub fn count_put_aways(&self, filter: PutAwayFilter) -> Result<u64> {
299        self.db.receiving().count_put_aways(filter)
300    }
301
302    // ========================================================================
303    // Integration Operations
304    // ========================================================================
305
306    /// Create a receipt directly from a purchase order.
307    ///
308    /// Copies PO line items to create expected receipt items.
309    ///
310    /// # Example
311    ///
312    /// ```rust,no_run
313    /// use stateset_embedded::Commerce;
314    /// use uuid::Uuid;
315    ///
316    /// let commerce = Commerce::new(":memory:")?;
317    ///
318    /// let receipt = commerce.receiving().create_receipt_from_po(
319    ///     Uuid::new_v4(), // PO ID
320    ///     1,              // Warehouse ID
321    /// )?;
322    ///
323    /// println!("Created receipt {} from PO", receipt.receipt_number);
324    /// # Ok::<(), stateset_embedded::CommerceError>(())
325    /// ```
326    pub fn create_receipt_from_po(&self, po_id: Uuid, warehouse_id: i32) -> Result<Receipt> {
327        self.db.receiving().create_receipt_from_po(po_id, warehouse_id)
328    }
329
330    // ========================================================================
331    // Batch Operations
332    // ========================================================================
333
334    /// Create multiple receipts in a batch.
335    pub fn create_receipts_batch(
336        &self,
337        inputs: Vec<CreateReceipt>,
338    ) -> Result<BatchResult<Receipt>> {
339        self.db.receiving().create_receipts_batch(inputs)
340    }
341
342    /// Get multiple receipts by ID.
343    pub fn get_receipts_batch(&self, ids: Vec<Uuid>) -> Result<Vec<Receipt>> {
344        self.db.receiving().get_receipts_batch(ids)
345    }
346}