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}