1use chrono::{DateTime, Utc};
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use strum::{Display, EnumString};
7use uuid::Uuid;
8
9#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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 #[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 #[must_use]
184 pub fn calculate_available(&self) -> Decimal {
185 self.quantity_on_hand - self.quantity_allocated
186 }
187
188 #[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}