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}