domain/retail/pos.rs
1//! POS contracts for attaching retail sales to staff transactions or reservation checkout while preserving approval gates.
2
3use bon::Builder;
4use serde::{Deserialize, Deserializer, Serialize};
5
6use crate::{entities, policy};
7
8use super::product::LocationOffering;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
11/// Positive sale quantity used to ensure retail checkout never drafts zero-unit line items.
12pub struct Quantity(u32);
13
14impl Quantity {
15 /// Promotes boundary input into a validated retail domain value.
16 pub const fn try_new(value: u32) -> std::result::Result<Self, QuantityError> {
17 if value == 0 {
18 return Err(QuantityError::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 Quantity {
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 quantity error in retail workflows.
40pub enum QuantityError {
41 #[error("retail sale quantity 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, Serialize, Deserialize)]
47/// POS policy deciding which retail sources are allowed and which price actions need manager review.
48pub enum Policy {
49 /// Standalone sale retail operational signal for inventory, POS, reorder, recommendation, or review handling.
50 StandaloneSale,
51 /// Integrated with reservation checkout retail operational signal for inventory, POS, reorder, recommendation, or review handling.
52 IntegratedWithReservationCheckout,
53 /// Manager only comp retail operational signal for inventory, POS, reorder, recommendation, or review handling.
54 ManagerOnlyComp,
55}
56
57impl Policy {
58 /// Evaluates sale eligibility from offering status, inventory, source, and price-exception policy.
59 pub fn evaluate(&self, request: &Request) -> Decision {
60 if !request.offering.can_be_sold_to_customer() {
61 return Decision::Denied {
62 reason: DenialReason::OfferingNotSellable,
63 };
64 }
65 if !request.offering.has_available_sale_units(request.quantity) {
66 return Decision::Denied {
67 reason: DenialReason::InventoryUnavailable,
68 };
69 }
70 if request.price_adjustment.requires_manager_approval()
71 || matches!(self, Self::ManagerOnlyComp)
72 {
73 return Decision::ReviewRequired {
74 reason: ReviewReason::PriceException,
75 gate: policy::ReviewGate::ManagerApproval,
76 };
77 }
78 match (self, &request.source) {
79 (Self::StandaloneSale, Source::StandaloneStaffSale { .. }) => Decision::DraftAllowed,
80 (Self::IntegratedWithReservationCheckout, Source::ReservationCheckout { .. }) => {
81 Decision::ReviewRequired {
82 reason: ReviewReason::ReservationCheckoutAttachment,
83 gate: policy::ReviewGate::CustomerMessageApproval,
84 }
85 }
86 _ => Decision::Denied {
87 reason: DenialReason::SourceNotAllowed,
88 },
89 }
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
94/// Retail sale request combining offering, quantity, source, and price-adjustment context.
95pub struct Request {
96 /// Source-derived offering carried by this retail contract.
97 pub offering: LocationOffering,
98 /// Source-derived quantity carried by this retail contract.
99 pub quantity: Quantity,
100 /// Source-derived source carried by this retail contract.
101 pub source: Source,
102 /// Source-derived price adjustment carried by this retail contract.
103 pub price_adjustment: PriceAdjustment,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107/// Source of the retail sale attempt, used to prevent unsupported POS or reservation mutations.
108pub enum Source {
109 /// Standalone staff sale retail operational signal for inventory, POS, reorder, recommendation, or review handling.
110 StandaloneStaffSale {
111 /// Source-derived staff id carried by this retail contract.
112 staff_id: entities::StaffId,
113 },
114 /// Reservation checkout retail operational signal for inventory, POS, reorder, recommendation, or review handling.
115 ReservationCheckout {
116 /// Source-derived reservation id carried by this retail contract.
117 reservation_id: entities::reservation::Id,
118 },
119 /// External pos reconciliation retail operational signal for inventory, POS, reorder, recommendation, or review handling.
120 ExternalPosReconciliation,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
124/// Price adjustment or comp that triggers manager review before checkout mutation.
125pub enum PriceAdjustment {
126 /// No additional workflow gate is required.
127 None,
128 /// Business reason staff should review before proceeding.
129 PolicyDiscount {
130 /// Reason value carried by this review or workflow variant.
131 reason: PriceExceptionReason,
132 },
133 /// Business reason staff should review before proceeding.
134 ManagerComp {
135 /// Reason value carried by this review or workflow variant.
136 reason: PriceExceptionReason,
137 },
138 /// Business reason staff should review before proceeding.
139 RefundOrReversal {
140 /// Reason value carried by this review or workflow variant.
141 reason: PriceExceptionReason,
142 },
143}
144
145impl PriceAdjustment {
146 /// Returns the requires manager approval evidence recorded on this retail contract.
147 pub const fn requires_manager_approval(self) -> bool {
148 !matches!(self, Self::None)
149 }
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
153/// Decision vocabulary for price exception reason in retail workflows.
154pub enum PriceExceptionReason {
155 /// Complaint recovery retail operational signal for inventory, POS, reorder, recommendation, or review handling.
156 ComplaintRecovery,
157 /// Staff courtesy retail operational signal for inventory, POS, reorder, recommendation, or review handling.
158 StaffCourtesy,
159 /// Refund correction retail operational signal for inventory, POS, reorder, recommendation, or review handling.
160 RefundCorrection,
161 /// Manager override retail operational signal for inventory, POS, reorder, recommendation, or review handling.
162 ManagerOverride,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166/// POS decision describing whether a sale draft is allowed, needs review, or is denied.
167pub enum Decision {
168 /// Draft allowed retail operational signal for inventory, POS, reorder, recommendation, or review handling.
169 DraftAllowed,
170 /// Review required retail operational signal for inventory, POS, reorder, recommendation, or review handling.
171 ReviewRequired {
172 /// Business reason staff should review before proceeding.
173 reason: ReviewReason,
174 /// Source-derived gate carried by this retail contract.
175 gate: policy::ReviewGate,
176 },
177 /// Denied retail operational signal for inventory, POS, reorder, recommendation, or review handling.
178 Denied {
179 /// Business reason staff should review before proceeding.
180 reason: DenialReason,
181 },
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
185/// Reason a POS draft must be reviewed before checkout action.
186pub enum ReviewReason {
187 /// Price exception retail operational signal for inventory, POS, reorder, recommendation, or review handling.
188 PriceException,
189 /// Reservation checkout attachment retail operational signal for inventory, POS, reorder, recommendation, or review handling.
190 ReservationCheckoutAttachment,
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
194/// Reason a retail sale is denied before reaching checkout.
195pub enum DenialReason {
196 /// Offering not sellable retail operational signal for inventory, POS, reorder, recommendation, or review handling.
197 OfferingNotSellable,
198 /// Inventory unavailable retail operational signal for inventory, POS, reorder, recommendation, or review handling.
199 InventoryUnavailable,
200 /// Source not allowed retail operational signal for inventory, POS, reorder, recommendation, or review handling.
201 SourceNotAllowed,
202}