Skip to main content

domain/retail/
inventory.rs

1//! Inventory models for retail stock counts, available units, and reorder threshold decisions.
2
3use serde::{Deserialize, Deserializer, Serialize};
4
5use crate::entities::LocationId;
6
7use super::product::Sku;
8use super::{Error, Result};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
11/// Positive unit threshold used for reorder-at quantities and tracked inventory policy.
12pub struct UnitCount(u32);
13
14impl UnitCount {
15    /// Accepts a positive reorder threshold so staff tasks are not created from zero-unit policy values.
16    pub const fn try_new(value: u32) -> std::result::Result<Self, UnitCountError> {
17        if value == 0 {
18            return Err(UnitCountError::Zero);
19        }
20        Ok(Self(value))
21    }
22
23    /// Returns the unit count for storage records, POS mappings, and threshold comparisons.
24    pub const fn get(self) -> u32 {
25        self.0
26    }
27}
28
29impl<'de> Deserialize<'de> for UnitCount {
30    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
31    where
32        D: Deserializer<'de>,
33    {
34        Self::try_new(u32::deserialize(deserializer)?).map_err(serde::de::Error::custom)
35    }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
39/// Unit-count validation errors that keep reorder thresholds and tracked inventory policy usable by staff.
40pub enum UnitCountError {
41    #[error("retail inventory count requires at least one unit")]
42    /// Rejects zero where the pet-resort workflow requires a positive quantity.
43    Zero,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
47/// Units physically on hand at a location before reservations or holds are subtracted.
48pub struct OnHandUnits(u32);
49
50impl OnHandUnits {
51    /// Records the count reported by the inventory source before availability math is applied.
52    pub const fn new(value: u32) -> Self {
53        Self(value)
54    }
55
56    /// Returns the unit count for storage records, POS mappings, and threshold comparisons.
57    pub const fn get(self) -> u32 {
58        self.0
59    }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
63/// Units already reserved for checkout, service bundles, or staff-held transactions.
64pub struct ReservedUnits(u32);
65
66impl ReservedUnits {
67    /// Records the count reported by the inventory source before availability math is applied.
68    pub const fn new(value: u32) -> Self {
69        Self(value)
70    }
71
72    /// Returns the unit count for storage records, POS mappings, and threshold comparisons.
73    pub const fn get(self) -> u32 {
74        self.0
75    }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
79/// Units available to sell after reserved units are subtracted from on-hand stock.
80pub struct AvailableUnits(u32);
81
82impl AvailableUnits {
83    /// Records the count reported by the inventory source before availability math is applied.
84    pub const fn new(value: u32) -> Self {
85        Self(value)
86    }
87
88    /// Returns the unit count for storage records, POS mappings, and threshold comparisons.
89    pub const fn get(self) -> u32 {
90        self.0
91    }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95/// Source stock record promoted from POS/inventory data before invariant checks are applied.
96pub struct Stock {
97    /// Location whose shelves or retail stockroom own this inventory count.
98    pub location_id: LocationId,
99    /// SKU whose on-hand, reserved, and reorder quantities are being evaluated.
100    pub sku: Sku,
101    /// Physical units counted at the location before reservations or holds are subtracted.
102    pub on_hand: OnHandUnits,
103    /// Units held for checkout, service bundles, or staff workflows before sale availability is calculated.
104    pub reserved: ReservedUnits,
105    /// Minimum available units that should prompt manager or vendor reorder attention.
106    pub reorder_at: UnitCount,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110/// Validated inventory position that guarantees reserved units do not exceed on-hand units.
111pub struct Position {
112    /// Location whose shelves or retail stockroom own this inventory count.
113    pub location_id: LocationId,
114    sku: Sku,
115    on_hand: OnHandUnits,
116    reserved: ReservedUnits,
117    reorder_at: UnitCount,
118}
119
120impl Position {
121    /// Records a stock position after rejecting impossible inventory math.
122    pub fn record(stock: Stock) -> Result<Self> {
123        if stock.reserved.get() > stock.on_hand.get() {
124            return Err(Error::ReservedUnitsExceedOnHand);
125        }
126        Ok(Self {
127            location_id: stock.location_id,
128            sku: stock.sku,
129            on_hand: stock.on_hand,
130            reserved: stock.reserved,
131            reorder_at: stock.reorder_at,
132        })
133    }
134
135    /// Returns the SKU used to connect this position to catalog, POS, recommendation, and reorder workflows.
136    pub fn sku(&self) -> &Sku {
137        &self.sku
138    }
139
140    /// Returns the physical stock count that anchors oversell and reorder decisions.
141    pub const fn on_hand(&self) -> OnHandUnits {
142        self.on_hand
143    }
144
145    /// Returns held units so staff drafts do not promise inventory already reserved elsewhere.
146    pub const fn reserved(&self) -> ReservedUnits {
147        self.reserved
148    }
149
150    /// Returns the low-stock threshold used to decide whether manager/vendor follow-up is due.
151    pub const fn reorder_at(&self) -> UnitCount {
152        self.reorder_at
153    }
154
155    /// Computes sellable units so POS and recommendation workflows do not oversell stock.
156    pub const fn available_units(&self) -> AvailableUnits {
157        AvailableUnits(self.on_hand.get() - self.reserved.get())
158    }
159
160    /// Reports whether available inventory has fallen to the reorder threshold.
161    pub const fn is_at_or_below_reorder_threshold(&self) -> bool {
162        self.available_units().get() <= self.reorder_at.get()
163    }
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
167/// Inventory policy used by POS, recommendation, and reorder workflows to decide whether stock checks apply.
168pub enum Policy {
169    /// Treats inventory as untracked, allowing sale drafts while leaving stock verification to staff/POS workflow.
170    NotTracked,
171    /// Uses explicit on-hand and reorder-at counts to prevent oversells and surface low-stock work.
172    Tracked {
173        /// Physical units counted at the location before reservations or holds are subtracted.
174        on_hand: UnitCount,
175        /// Minimum available units that should prompt manager or vendor reorder attention.
176        reorder_at: UnitCount,
177    },
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
181/// Availability status used by recommendation policy to suppress unavailable products.
182pub enum Availability {
183    /// Product is available for sale drafts and recommendation candidates.
184    Available,
185    /// Product is unavailable, suppressing POS sale drafts and customer-facing recommendations.
186    OutOfStock,
187    /// Product is on backorder, so staff can see demand but automation must not promise fulfillment.
188    Backordered,
189    /// Inventory source did not provide a confident availability status, so staff should verify before promising stock.
190    Unknown,
191}