Skip to main content

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}