Skip to main content

domain/retail/
pos.rs

1//! POS models 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    /// Accepts a positive sale quantity so POS drafts never create zero-unit retail line items.
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    /// Returns the quantity for checkout mapping, inventory checks, and audit records.
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/// Quantity validation errors that prevent unusable POS sale drafts.
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    /// Allows staff to draft an in-person retail sale that remains separate from reservation checkout.
50    StandaloneSale,
51    /// Allows a sale to be proposed during reservation checkout but still requires customer-message approval.
52    IntegratedWithReservationCheckout,
53    /// Forces manager approval before any comp, refund, or discount reaches POS workflow.
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    /// Location offering being checked for sellability, usage policy, and available units.
97    pub offering: LocationOffering,
98    /// Positive unit count staff want to sell or attach to checkout.
99    pub quantity: Quantity,
100    /// Sale origin used to block unsupported POS or reservation mutations.
101    pub source: Source,
102    /// Discount, comp, refund, or reversal context that may require manager approval.
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    /// Staff-originated counter sale that may draft when policy and inventory allow it.
110    StandaloneStaffSale {
111        /// Staff member accountable for the standalone sale draft.
112        staff_id: entities::StaffId,
113    },
114    /// Reservation checkout context that can propose an attachment but cannot send customer copy without approval.
115    ReservationCheckout {
116        /// Reservation receiving a proposed retail attachment after customer-message approval.
117        reservation_id: entities::reservation::Id,
118    },
119    /// Imported POS reconciliation source that is recorded for review instead of mutating checkout from domain code.
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 discount, comp, refund, or reversal is requested.
127    None,
128    /// Approval reason shown to staff or managers before checkout work proceeds.
129    PolicyDiscount {
130        /// Manager-readable reason for the discount, comp, refund, or reversal request.
131        reason: PriceExceptionReason,
132    },
133    /// Approval reason shown to staff or managers before checkout work proceeds.
134    ManagerComp {
135        /// Manager-readable reason for the discount, comp, refund, or reversal request.
136        reason: PriceExceptionReason,
137    },
138    /// Approval reason shown to staff or managers before checkout work proceeds.
139    RefundOrReversal {
140        /// Manager-readable reason for the discount, comp, refund, or reversal request.
141        reason: PriceExceptionReason,
142    },
143}
144
145impl PriceAdjustment {
146    /// Reports whether this price action must stop for manager approval before checkout changes.
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/// Price-exception reasons that explain why a manager must approve the checkout change.
154pub enum PriceExceptionReason {
155    /// Discount or comp requested to recover from a customer complaint, requiring manager review.
156    ComplaintRecovery,
157    /// Courtesy adjustment requested by staff, requiring manager review before POS action.
158    StaffCourtesy,
159    /// Refund or reversal correction that must be approved before money movement.
160    RefundCorrection,
161    /// Manager override reason documenting why an exception may proceed after approval.
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    /// Sale may be drafted internally because product, inventory, source, and price policy all passed.
169    DraftAllowed,
170    /// Sale must pause for the named approval gate before POS, reservation, payment, refund, or discount action.
171    ReviewRequired {
172        /// Approval reason shown to staff or managers before checkout work proceeds.
173        reason: ReviewReason,
174        /// Approval gate that must be satisfied before the retail workflow can proceed.
175        gate: policy::ReviewGate,
176    },
177    /// Sale is blocked before checkout because product, inventory, or source policy failed.
178    Denied {
179        /// Denial reason explaining why checkout work must not proceed.
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 adjustment requires manager approval before any discount, comp, refund, or reversal.
188    PriceException,
189    /// Reservation attachment requires customer-message approval before staff can proceed.
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    /// Product is inactive, discontinued, or not customer-sellable at this location.
197    OfferingNotSellable,
198    /// Available units cannot satisfy the requested quantity, so checkout must not promise the item.
199    InventoryUnavailable,
200    /// Sale origin is not allowed by this POS policy, preventing unsupported POS or reservation writes.
201    SourceNotAllowed,
202}