Skip to main content

stateset_core/models/
inventory.rs

1//! Inventory domain models
2
3use chrono::{DateTime, Utc};
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use strum::{Display, EnumString};
7use uuid::Uuid;
8
9/// Inventory item (SKU master record)
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct InventoryItem {
12    pub id: i64,
13    pub sku: String,
14    pub name: String,
15    pub description: Option<String>,
16    pub unit_of_measure: String,
17    pub is_active: bool,
18    pub created_at: DateTime<Utc>,
19    pub updated_at: DateTime<Utc>,
20}
21
22/// Inventory balance at a location
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub struct InventoryBalance {
25    pub id: i64,
26    pub item_id: i64,
27    pub location_id: i32,
28    pub quantity_on_hand: Decimal,
29    pub quantity_allocated: Decimal,
30    pub quantity_available: Decimal,
31    pub reorder_point: Option<Decimal>,
32    pub safety_stock: Option<Decimal>,
33    pub version: i32,
34    pub last_counted_at: Option<DateTime<Utc>>,
35    pub updated_at: DateTime<Utc>,
36}
37
38/// Inventory transaction (audit trail)
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct InventoryTransaction {
41    pub id: i64,
42    pub item_id: i64,
43    pub location_id: i32,
44    pub transaction_type: TransactionType,
45    pub quantity: Decimal,
46    pub reference_type: Option<String>,
47    pub reference_id: Option<String>,
48    pub reason: Option<String>,
49    pub created_by: Option<String>,
50    pub created_at: DateTime<Utc>,
51}
52
53/// Inventory reservation
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct InventoryReservation {
56    pub id: Uuid,
57    pub item_id: i64,
58    pub location_id: i32,
59    pub quantity: Decimal,
60    pub status: ReservationStatus,
61    pub reference_type: String,
62    pub reference_id: String,
63    pub expires_at: Option<DateTime<Utc>>,
64    pub created_at: DateTime<Utc>,
65}
66
67/// Transaction type enumeration
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, EnumString)]
69#[serde(rename_all = "snake_case")]
70#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
71#[non_exhaustive]
72pub enum TransactionType {
73    Receipt,
74    Shipment,
75    Adjustment,
76    Transfer,
77    Return,
78    Allocation,
79    Deallocation,
80    CycleCount,
81}
82
83/// Reservation status enumeration
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, EnumString)]
85#[serde(rename_all = "snake_case")]
86#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
87#[non_exhaustive]
88pub enum ReservationStatus {
89    Pending,
90    Confirmed,
91    Allocated,
92    #[strum(serialize = "cancelled", serialize = "canceled")]
93    Cancelled,
94    Released,
95    Expired,
96}
97
98impl Default for ReservationStatus {
99    fn default() -> Self {
100        Self::Pending
101    }
102}
103
104/// Input for adjusting inventory
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct AdjustInventory {
107    pub sku: String,
108    pub location_id: Option<i32>,
109    pub quantity: Decimal,
110    pub reason: String,
111    pub reference_type: Option<String>,
112    pub reference_id: Option<String>,
113}
114
115/// Input for reserving inventory
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct ReserveInventory {
118    pub sku: String,
119    pub location_id: Option<i32>,
120    pub quantity: Decimal,
121    pub reference_type: String,
122    pub reference_id: String,
123    pub expires_in_seconds: Option<i64>,
124}
125
126/// Input for creating inventory item
127#[derive(Debug, Clone, Serialize, Deserialize, Default)]
128pub struct CreateInventoryItem {
129    pub sku: String,
130    pub name: String,
131    pub description: Option<String>,
132    pub unit_of_measure: Option<String>,
133    pub initial_quantity: Option<Decimal>,
134    pub location_id: Option<i32>,
135    pub reorder_point: Option<Decimal>,
136    pub safety_stock: Option<Decimal>,
137}
138
139/// Stock level summary
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct StockLevel {
142    pub sku: String,
143    pub name: String,
144    pub total_on_hand: Decimal,
145    pub total_allocated: Decimal,
146    pub total_available: Decimal,
147    pub locations: Vec<LocationStock>,
148}
149
150/// Stock at a specific location
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct LocationStock {
153    pub location_id: i32,
154    pub location_name: Option<String>,
155    pub on_hand: Decimal,
156    pub allocated: Decimal,
157    pub available: Decimal,
158}
159
160/// Inventory filter for querying
161#[derive(Debug, Clone, Default, Serialize, Deserialize)]
162pub struct InventoryFilter {
163    pub sku: Option<String>,
164    pub location_id: Option<i32>,
165    pub below_reorder_point: Option<bool>,
166    pub is_active: Option<bool>,
167    pub limit: Option<u32>,
168    pub offset: Option<u32>,
169}
170
171impl InventoryBalance {
172    /// Check if stock is below reorder point
173    #[must_use]
174    pub fn needs_reorder(&self) -> bool {
175        if let Some(reorder_point) = self.reorder_point {
176            self.quantity_available < reorder_point
177        } else {
178            false
179        }
180    }
181
182    /// Calculate available quantity
183    #[must_use]
184    pub fn calculate_available(&self) -> Decimal {
185        self.quantity_on_hand - self.quantity_allocated
186    }
187
188    /// Check if requested quantity can be allocated
189    #[must_use]
190    pub fn can_allocate(&self, quantity: Decimal) -> bool {
191        self.quantity_available >= quantity
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use std::str::FromStr;
199
200    #[test]
201    fn transaction_type_from_str() {
202        assert_eq!(TransactionType::from_str("receipt").unwrap(), TransactionType::Receipt);
203        assert!(TransactionType::from_str("unknown").is_err());
204    }
205
206    #[test]
207    fn reservation_status_from_str() {
208        assert_eq!(ReservationStatus::from_str("allocated").unwrap(), ReservationStatus::Allocated);
209        assert!(ReservationStatus::from_str("unknown").is_err());
210    }
211}