waremax_storage/
inventory.rs1use crate::rack::BinAddress;
4use std::collections::HashMap;
5use thiserror::Error;
6use waremax_core::SkuId;
7
8#[derive(Error, Debug)]
10pub enum InventoryError {
11 #[error("Bin not found: {0}")]
12 BinNotFound(BinAddress),
13
14 #[error("Insufficient stock at {bin}: requested {requested}, available {available}")]
15 InsufficientStock {
16 bin: BinAddress,
17 requested: u32,
18 available: u32,
19 },
20}
21
22#[derive(Clone, Debug)]
24pub struct InventorySlot {
25 pub sku_id: SkuId,
26 pub quantity: u32,
27}
28
29#[derive(Clone, Default)]
31pub struct Inventory {
32 bins: HashMap<BinAddress, InventorySlot>,
33 sku_locations: HashMap<SkuId, Vec<BinAddress>>,
34 all_bins: Vec<BinAddress>,
36 replen_thresholds: HashMap<SkuId, u32>,
38}
39
40impl Inventory {
41 pub fn new() -> Self {
42 Self::default()
43 }
44
45 pub fn register_bin(&mut self, address: BinAddress) {
47 if !self.all_bins.contains(&address) {
48 self.all_bins.push(address);
49 }
50 }
51
52 pub fn set_replen_threshold(&mut self, sku_id: SkuId, threshold: u32) {
54 self.replen_thresholds.insert(sku_id, threshold);
55 }
56
57 pub fn get_replen_threshold(&self, sku_id: SkuId) -> Option<u32> {
59 self.replen_thresholds.get(&sku_id).copied()
60 }
61
62 pub fn add_placement(&mut self, address: BinAddress, sku_id: SkuId, quantity: u32) {
63 self.bins
64 .insert(address.clone(), InventorySlot { sku_id, quantity });
65 self.sku_locations
66 .entry(sku_id)
67 .or_default()
68 .push(address.clone());
69 self.register_bin(address);
70 }
71
72 pub fn get_slot(&self, address: &BinAddress) -> Option<&InventorySlot> {
73 self.bins.get(address)
74 }
75
76 pub fn get_quantity(&self, address: &BinAddress) -> Option<u32> {
77 self.bins.get(address).map(|s| s.quantity)
78 }
79
80 pub fn find_sku(&self, sku_id: SkuId) -> impl Iterator<Item = &BinAddress> {
81 self.sku_locations
82 .get(&sku_id)
83 .into_iter()
84 .flat_map(|v| v.iter())
85 }
86
87 pub fn find_sku_with_stock(&self, sku_id: SkuId, min_qty: u32) -> Option<&BinAddress> {
88 self.sku_locations
89 .get(&sku_id)?
90 .iter()
91 .find(|addr| self.bins.get(*addr).is_some_and(|s| s.quantity >= min_qty))
92 }
93
94 pub fn decrement(&mut self, address: &BinAddress, qty: u32) -> Result<(), InventoryError> {
95 let slot = self
96 .bins
97 .get_mut(address)
98 .ok_or_else(|| InventoryError::BinNotFound(address.clone()))?;
99
100 if slot.quantity < qty {
101 return Err(InventoryError::InsufficientStock {
102 bin: address.clone(),
103 requested: qty,
104 available: slot.quantity,
105 });
106 }
107
108 slot.quantity -= qty;
109 Ok(())
110 }
111
112 pub fn increment(&mut self, address: &BinAddress, qty: u32) -> Result<(), InventoryError> {
113 let slot = self
114 .bins
115 .get_mut(address)
116 .ok_or_else(|| InventoryError::BinNotFound(address.clone()))?;
117 slot.quantity += qty;
118 Ok(())
119 }
120
121 pub fn total_quantity(&self, sku_id: SkuId) -> u32 {
122 self.sku_locations
123 .get(&sku_id)
124 .map(|addrs| {
125 addrs
126 .iter()
127 .filter_map(|addr| self.bins.get(addr))
128 .map(|slot| slot.quantity)
129 .sum()
130 })
131 .unwrap_or(0)
132 }
133
134 pub fn get_empty_bins(&self) -> Vec<&BinAddress> {
136 self.all_bins
137 .iter()
138 .filter(|addr| self.bins.get(*addr).is_none_or(|slot| slot.quantity == 0))
139 .collect()
140 }
141
142 pub fn all_bins(&self) -> &[BinAddress] {
144 &self.all_bins
145 }
146
147 pub fn needs_replenishment(&self, sku_id: SkuId) -> Option<(BinAddress, u32, u32)> {
149 let threshold = self.replen_thresholds.get(&sku_id)?;
150
151 for addr in self.sku_locations.get(&sku_id)? {
153 if let Some(slot) = self.bins.get(addr) {
154 if slot.quantity < *threshold {
155 return Some((addr.clone(), slot.quantity, *threshold));
156 }
157 }
158 }
159 None
160 }
161
162 pub fn get_replenishment_needed(&self) -> Vec<(SkuId, BinAddress, u32, u32)> {
164 let mut results = Vec::new();
165 for (&sku_id, &threshold) in &self.replen_thresholds {
166 if let Some(locations) = self.sku_locations.get(&sku_id) {
167 for addr in locations {
168 if let Some(slot) = self.bins.get(addr) {
169 if slot.quantity < threshold {
170 results.push((sku_id, addr.clone(), slot.quantity, threshold));
171 }
172 }
173 }
174 }
175 }
176 results
177 }
178
179 pub fn create_empty_slot(&mut self, address: BinAddress, sku_id: SkuId) {
181 self.bins.insert(
182 address.clone(),
183 InventorySlot {
184 sku_id,
185 quantity: 0,
186 },
187 );
188 self.sku_locations
189 .entry(sku_id)
190 .or_default()
191 .push(address.clone());
192 self.register_bin(address);
193 }
194}