Skip to main content

domain/retail/
recommendation.rs

1//! Retail recommendation contracts for personalized upsell candidates, review gates, and safe customer copy.
2
3use bon::Builder;
4use serde::{Deserialize, Serialize};
5
6use crate::entities::{CustomerId, LocationId, PetId};
7use crate::policy;
8
9use super::inventory::Availability;
10use super::product::Product;
11
12/// Rationale boundary for human-readable evidence explaining why a retail recommendation exists.
13pub mod rationale {
14    use nutype::nutype;
15    #[allow(unused_imports)]
16    use serde::{Deserialize, Serialize};
17
18    #[nutype(
19        sanitize(trim),
20        validate(not_empty, len_char_max = 500),
21        derive(
22            Debug,
23            Clone,
24            PartialEq,
25            Eq,
26            PartialOrd,
27            Ord,
28            Hash,
29            Serialize,
30            Deserialize
31        )
32    )]
33    pub struct Text(String);
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
37/// Recommendation rule that names the operational event that can produce an upsell candidate.
38pub enum Rule {
39    /// No additional workflow gate is required.
40    None,
41    /// Anxiety support after boarding retail operational signal for inventory, POS, reorder, recommendation, or review handling.
42    AnxietySupportAfterBoarding,
43    /// Diet support after boarding retail operational signal for inventory, POS, reorder, recommendation, or review handling.
44    DietSupportAfterBoarding,
45    /// Coat care after grooming retail operational signal for inventory, POS, reorder, recommendation, or review handling.
46    CoatCareAfterGrooming,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
50/// Retail recommendation candidate containing customer/pet context, product, rationale, inventory, preference, and care-safety signals.
51pub struct Candidate {
52    /// Source-derived customer id carried by this retail contract.
53    pub customer_id: CustomerId,
54    /// Pet receiving the grooming or care service.
55    pub pet_id: PetId,
56    /// Source-derived location id carried by this retail contract.
57    pub location_id: LocationId,
58    /// Source-derived product carried by this retail contract.
59    pub product: Product,
60    /// Business reason staff should review before proceeding.
61    pub reason: Reason,
62    /// Source-derived rationale carried by this retail contract.
63    pub rationale: rationale::Text,
64    /// Source-derived care sensitivity carried by this retail contract.
65    pub care_sensitivity: CareSensitivity,
66    /// Source-derived inventory carried by this retail contract.
67    pub inventory: Availability,
68    /// Source-derived customer preference carried by this retail contract.
69    pub customer_preference: Preference,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
73/// Business reason for recommending a product, kept separate from customer-facing copy.
74pub enum Reason {
75    /// Anxiety or stress support retail operational signal for inventory, POS, reorder, recommendation, or review handling.
76    AnxietyOrStressSupport,
77    /// Boarding diet continuity retail operational signal for inventory, POS, reorder, recommendation, or review handling.
78    BoardingDietContinuity,
79    /// Coat or skin care after grooming retail operational signal for inventory, POS, reorder, recommendation, or review handling.
80    CoatOrSkinCareAfterGrooming,
81    /// Prior purchase replenishment retail operational signal for inventory, POS, reorder, recommendation, or review handling.
82    PriorPurchaseReplenishment,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
86/// Care-sensitivity status that decides whether supplement/diet/product recommendations need care or manager review.
87pub enum CareSensitivity {
88    /// No known care conflict retail operational signal for inventory, POS, reorder, recommendation, or review handling.
89    NoKnownCareConflict,
90    /// Supplement or diet review required retail operational signal for inventory, POS, reorder, recommendation, or review handling.
91    SupplementOrDietReviewRequired,
92    /// Care plan conflict retail operational signal for inventory, POS, reorder, recommendation, or review handling.
93    CarePlanConflict,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
97/// Customer preference state used to suppress opted-out recommendations and review unknown consent.
98pub enum Preference {
99    /// Allows retail recommendations retail operational signal for inventory, POS, reorder, recommendation, or review handling.
100    AllowsRetailRecommendations,
101    /// Opted out retail operational signal for inventory, POS, reorder, recommendation, or review handling.
102    OptedOut,
103    /// Estimate confidence is unknown and must be reviewed.
104    UnknownRequiresReview,
105}
106
107#[derive(Debug, Clone, Default)]
108/// Represents the policy concept as a typed retail operational contract instead of a raw primitive.
109pub struct Policy;
110
111impl Policy {
112    /// Evaluates recommendation or customer copy safety without bypassing preference, inventory, or care-review gates.
113    pub fn evaluate(&self, candidate: &Candidate) -> Decision {
114        if matches!(candidate.customer_preference, Preference::OptedOut) {
115            return Decision::Suppressed {
116                reason: SuppressionReason::CustomerOptedOut,
117            };
118        }
119        if !matches!(candidate.inventory, Availability::Available) {
120            return Decision::Suppressed {
121                reason: SuppressionReason::InventoryUnavailable,
122            };
123        }
124        match candidate.care_sensitivity {
125            CareSensitivity::NoKnownCareConflict => Decision::DraftInternalCandidate,
126            CareSensitivity::SupplementOrDietReviewRequired => Decision::StaffReviewRequired {
127                reason: ReviewReason::CareSensitiveProduct,
128                gate: policy::ReviewGate::MedicalDocumentReview,
129            },
130            CareSensitivity::CarePlanConflict => Decision::ManagerReviewRequired {
131                reason: ReviewReason::CarePlanConflict,
132                gate: policy::ReviewGate::ManagerApproval,
133            },
134        }
135    }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139/// Recommendation decision that either drafts an internal candidate, requires review, or suppresses the upsell.
140pub enum Decision {
141    /// Draft internal candidate retail operational signal for inventory, POS, reorder, recommendation, or review handling.
142    DraftInternalCandidate,
143    /// Staff review required retail operational signal for inventory, POS, reorder, recommendation, or review handling.
144    StaffReviewRequired {
145        /// Business reason staff should review before proceeding.
146        reason: ReviewReason,
147        /// Source-derived gate carried by this retail contract.
148        gate: policy::ReviewGate,
149    },
150    /// Manager review required retail operational signal for inventory, POS, reorder, recommendation, or review handling.
151    ManagerReviewRequired {
152        /// Business reason staff should review before proceeding.
153        reason: ReviewReason,
154        /// Source-derived gate carried by this retail contract.
155        gate: policy::ReviewGate,
156    },
157    /// Suppressed retail operational signal for inventory, POS, reorder, recommendation, or review handling.
158    Suppressed {
159        /// Business reason staff should review before proceeding.
160        reason: SuppressionReason,
161    },
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
165/// Decision vocabulary for review reason in retail workflows.
166pub enum ReviewReason {
167    /// Care sensitive product retail operational signal for inventory, POS, reorder, recommendation, or review handling.
168    CareSensitiveProduct,
169    /// Care plan conflict retail operational signal for inventory, POS, reorder, recommendation, or review handling.
170    CarePlanConflict,
171    /// Unknown customer preference retail operational signal for inventory, POS, reorder, recommendation, or review handling.
172    UnknownCustomerPreference,
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
176/// Reason a recommendation is suppressed before staff/customer use.
177pub enum SuppressionReason {
178    /// Customer opted out retail operational signal for inventory, POS, reorder, recommendation, or review handling.
179    CustomerOptedOut,
180    /// Inventory unavailable retail operational signal for inventory, POS, reorder, recommendation, or review handling.
181    InventoryUnavailable,
182}
183
184/// Customer-copy boundary that prevents unsafe retail claims from leaving draft/review state.
185pub mod customer_copy {
186    use nutype::nutype;
187    use serde::{Deserialize, Serialize};
188
189    use crate::policy;
190
191    #[nutype(
192        sanitize(trim),
193        validate(not_empty, len_char_max = 500),
194        derive(
195            Debug,
196            Clone,
197            PartialEq,
198            Eq,
199            PartialOrd,
200            Ord,
201            Hash,
202            Serialize,
203            Deserialize
204        )
205    )]
206    pub struct SafeCopy(String);
207
208    #[derive(Debug, Clone, Default)]
209    /// Represents the policy concept as a typed retail operational contract instead of a raw primitive.
210    pub struct Policy;
211
212    impl Policy {
213        /// Evaluates recommendation or customer copy safety without bypassing preference, inventory, or care-review gates.
214        pub fn evaluate(&self, copy: &SafeCopy) -> Decision {
215            let normalized = copy.clone().into_inner().to_lowercase();
216            if ["treat", "diagnos", "cure", "prescrib", "medical"]
217                .iter()
218                .any(|term| normalized.contains(term))
219            {
220                Decision::Rejected {
221                    reason: RejectionReason::MedicalClaim,
222                    gate: policy::ReviewGate::CustomerMessageApproval,
223                }
224            } else {
225                Decision::DraftRequiresApproval {
226                    gate: policy::ReviewGate::CustomerMessageApproval,
227                }
228            }
229        }
230    }
231
232    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
233    /// Recommendation decision that either drafts an internal candidate, requires review, or suppresses the upsell.
234    pub enum Decision {
235        /// Draft requires approval retail operational signal for inventory, POS, reorder, recommendation, or review handling.
236        DraftRequiresApproval {
237            /// Source-derived gate carried by this retail contract.
238            gate: policy::ReviewGate,
239        },
240        /// Rejected retail operational signal for inventory, POS, reorder, recommendation, or review handling.
241        Rejected {
242            /// Business reason staff should review before proceeding.
243            reason: RejectionReason,
244            /// Source-derived gate carried by this retail contract.
245            gate: policy::ReviewGate,
246        },
247    }
248
249    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
250    /// Decision vocabulary for rejection reason in retail workflows.
251    pub enum RejectionReason {
252        /// Medical claim retail operational signal for inventory, POS, reorder, recommendation, or review handling.
253        MedicalClaim,
254        /// Unsupported promise retail operational signal for inventory, POS, reorder, recommendation, or review handling.
255        UnsupportedPromise,
256    }
257}