domain/retail/product.rs
1//! Product catalog contracts for SKUs, categories, location offerings, and sellability rules.
2
3use bon::Builder;
4use nutype::nutype;
5use serde::{Deserialize, Serialize};
6
7use crate::entities::LocationId;
8
9use super::{inventory, pos, reorder};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12/// Product category used to distinguish supplements, boarding diets, and personalized upsell items.
13pub enum Category {
14 /// Supplement retail operational signal for inventory, POS, reorder, recommendation, or review handling.
15 Supplement,
16 /// In house diet retail operational signal for inventory, POS, reorder, recommendation, or review handling.
17 InHouseDiet,
18 /// Personalized upsell retail operational signal for inventory, POS, reorder, recommendation, or review handling.
19 PersonalizedUpsell,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
23/// Non-empty SKU identifier promoted from POS/catalog data for inventory and reorder workflows.
24pub struct Sku(String);
25
26impl Sku {
27 /// Validates and creates the retail value.
28 pub fn try_new(value: impl Into<String>) -> std::result::Result<Self, SkuError> {
29 let value = value.into().trim().to_owned();
30 if value.is_empty() {
31 return Err(SkuError::Empty);
32 }
33 Ok(Self(value))
34 }
35
36 /// Returns the owned inner string for storage or outbound mapping.
37 pub fn into_inner(self) -> String {
38 self.0
39 }
40
41 /// Returns the provider or domain identifier as a string slice.
42 pub fn as_str(&self) -> &str {
43 &self.0
44 }
45}
46
47impl<'de> Deserialize<'de> for Sku {
48 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
49 where
50 D: serde::Deserializer<'de>,
51 {
52 Self::try_new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
57/// Decision vocabulary for sku error in retail workflows.
58pub enum SkuError {
59 #[error("retail SKU cannot be empty")]
60 /// Empty retail operational signal for inventory, POS, reorder, recommendation, or review handling.
61 Empty,
62}
63
64#[nutype(
65 sanitize(trim),
66 validate(not_empty, len_char_max = 160),
67 derive(
68 Debug,
69 Clone,
70 PartialEq,
71 Eq,
72 PartialOrd,
73 Ord,
74 Hash,
75 Serialize,
76 Deserialize
77 )
78)]
79pub struct Name(String);
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82/// Retail product with a SKU and category used across POS, inventory, and recommendation contracts.
83pub struct Product {
84 sku: Sku,
85 /// Source-derived category carried by this retail contract.
86 pub category: Category,
87}
88
89impl Product {
90 /// Assembles this retail value from already-validated domain parts.
91 pub fn new(sku: Sku, category: Category) -> Self {
92 Self { sku, category }
93 }
94
95 /// Returns the sku evidence recorded on this retail contract.
96 pub fn sku(&self) -> &Sku {
97 &self.sku
98 }
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
102/// Location-level offering status used to prevent inactive or discontinued products from being sold.
103pub enum OfferingStatus {
104 /// Active retail operational signal for inventory, POS, reorder, recommendation, or review handling.
105 Active,
106 /// Inactive retail operational signal for inventory, POS, reorder, recommendation, or review handling.
107 Inactive,
108 /// Discontinued retail operational signal for inventory, POS, reorder, recommendation, or review handling.
109 Discontinued,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
113/// Usage policy distinguishing customer-sellable items from in-house consumables such as boarding diets.
114pub enum Usage {
115 /// Customer sellable retail operational signal for inventory, POS, reorder, recommendation, or review handling.
116 CustomerSellable,
117 /// In house consumable retail operational signal for inventory, POS, reorder, recommendation, or review handling.
118 InHouseConsumable,
119 /// Sellable and in house consumable retail operational signal for inventory, POS, reorder, recommendation, or review handling.
120 SellableAndInHouseConsumable,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
124/// Location-specific product offering with POS, inventory, and reorder policies attached.
125pub struct LocationOffering {
126 /// Source-derived location id carried by this retail contract.
127 pub location_id: LocationId,
128 /// Source-derived product carried by this retail contract.
129 pub product: Product,
130 /// Source-derived status carried by this retail contract.
131 pub status: OfferingStatus,
132 /// Source-derived usage carried by this retail contract.
133 pub usage: Usage,
134 /// Source-derived pos carried by this retail contract.
135 pub pos: pos::Policy,
136 /// Source-derived inventory carried by this retail contract.
137 pub inventory: inventory::Policy,
138 /// Source-derived reorder carried by this retail contract.
139 pub reorder: reorder::Policy,
140}
141
142impl LocationOffering {
143 /// Reports whether the product is active and customer-sellable at this location.
144 pub fn can_be_sold_to_customer(&self) -> bool {
145 matches!(self.status, OfferingStatus::Active)
146 && matches!(
147 self.usage,
148 Usage::CustomerSellable | Usage::SellableAndInHouseConsumable
149 )
150 }
151
152 /// Checks tracked inventory before allowing a POS sale draft for the requested quantity.
153 pub fn has_available_sale_units(&self, quantity: pos::Quantity) -> bool {
154 match self.inventory {
155 inventory::Policy::NotTracked => true,
156 inventory::Policy::Tracked { on_hand, .. } => on_hand.get() >= quantity.get(),
157 }
158 }
159}