Skip to main content

domain/retail/
inventory.rs

1//! Inventory contracts 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    /// Promotes boundary input into a validated retail domain value.
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    /// Exposes the validated scalar for serialization and adapter boundaries.
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/// Decision vocabulary for unit count error in retail workflows.
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    /// Assembles this retail value from already-validated domain parts.
52    pub const fn new(value: u32) -> Self {
53        Self(value)
54    }
55
56    /// Exposes the validated scalar for serialization and adapter boundaries.
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    /// Assembles this retail value from already-validated domain parts.
68    pub const fn new(value: u32) -> Self {
69        Self(value)
70    }
71
72    /// Exposes the validated scalar for serialization and adapter boundaries.
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    /// Assembles this retail value from already-validated domain parts.
84    pub const fn new(value: u32) -> Self {
85        Self(value)
86    }
87
88    /// Exposes the validated scalar for serialization and adapter boundaries.
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    /// Source-derived location id carried by this retail contract.
98    pub location_id: LocationId,
99    /// Source-derived sku carried by this retail contract.
100    pub sku: Sku,
101    /// Source-derived on hand carried by this retail contract.
102    pub on_hand: OnHandUnits,
103    /// Source-derived reserved carried by this retail contract.
104    pub reserved: ReservedUnits,
105    /// Source-derived reorder at carried by this retail contract.
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    /// Source-derived location id carried by this retail contract.
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 evidence recorded on this retail contract.
136    pub fn sku(&self) -> &Sku {
137        &self.sku
138    }
139
140    /// Returns the on hand evidence recorded on this retail contract.
141    pub const fn on_hand(&self) -> OnHandUnits {
142        self.on_hand
143    }
144
145    /// Returns the reserved evidence recorded on this retail contract.
146    pub const fn reserved(&self) -> ReservedUnits {
147        self.reserved
148    }
149
150    /// Returns the reorder at evidence recorded on this retail contract.
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/// Groomer-assignment policies used when booking grooming work.
168pub enum Policy {
169    /// Not tracked retail operational signal for inventory, POS, reorder, recommendation, or review handling.
170    NotTracked,
171    /// Tracked retail operational signal for inventory, POS, reorder, recommendation, or review handling.
172    Tracked {
173        /// Source-derived on hand carried by this retail contract.
174        on_hand: UnitCount,
175        /// Source-derived reorder at carried by this retail contract.
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    /// Available retail operational signal for inventory, POS, reorder, recommendation, or review handling.
184    Available,
185    /// Out of stock retail operational signal for inventory, POS, reorder, recommendation, or review handling.
186    OutOfStock,
187    /// Backordered retail operational signal for inventory, POS, reorder, recommendation, or review handling.
188    Backordered,
189    /// Provider role or status could not be mapped confidently.
190    Unknown,
191}