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}