Skip to main content

domain/daycare/
package_opportunity.rs

1//! Daycare package-opportunity policy for review-gated membership/pass recommendations.
2//!
3//! ```
4//! use domain::{daycare, entities, policy};
5//! use uuid::Uuid;
6//!
7//! let evidence = daycare::package_opportunity::Evidence::builder()
8//!     .customer_id(entities::CustomerId(Uuid::nil()))
9//!     .pet_id(entities::PetId(Uuid::nil()))
10//!     .attendance_visits(daycare::package_opportunity::AttendanceVisitCount::new(8))
11//!     .eligibility(daycare::package_opportunity::CareEligibility::Cleared)
12//!     .package_state(daycare::package_opportunity::PackageState::PayPerVisit)
13//!     .payment_state(daycare::package_opportunity::PaymentState::Current)
14//!     .build();
15//!
16//! assert_eq!(
17//!     daycare::package_opportunity::Policy.classify(&evidence),
18//!     daycare::package_opportunity::Decision::RecommendStaffReview {
19//!         score: daycare::package_opportunity::OpportunityScore::Strong,
20//!         gate: policy::ReviewGate::CustomerMessageApproval,
21//!     },
22//! );
23//! ```
24
25use super::*;
26use crate::policy;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
29/// Count of recent daycare visits used to score pass or membership opportunities.
30pub struct AttendanceVisitCount(u16);
31
32impl AttendanceVisitCount {
33    /// Creates an attendance visit count from prior daycare check-in history.
34    pub const fn new(value: u16) -> Self {
35        Self(value)
36    }
37
38    /// Returns the raw visit count for reporting, scoring, and serialization.
39    pub const fn get(self) -> u16 {
40        self.0
41    }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45/// Care eligibility state used before recommending daycare packages.
46pub enum CareEligibility {
47    /// Care and safety gates are clear enough to consider package recommendations.
48    Cleared,
49    /// Safety or care review blocks sales recommendations until staff clear it.
50    BlockedBySafetyReview,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
54/// Existing package coverage state for a customer and pet.
55pub enum PackageState {
56    /// Customer currently pays per visit and may benefit from a package recommendation.
57    PayPerVisit,
58    /// Existing package or membership already covers the daycare need.
59    AlreadyCovered,
60    /// Package coverage could not be mapped confidently, so recommendations should not assume need.
61    Unknown,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65/// Billing state used to suppress recommendations when collection or review is needed.
66pub enum PaymentState {
67    /// Payment status is current enough to allow staff-reviewed recommendations.
68    Current,
69    /// Billing state requires review before staff suggest another package or membership.
70    NeedsBillingReview,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Builder)]
74/// Source evidence used to classify daycare package or membership opportunities.
75pub struct Evidence {
76    /// Customer account that would receive the package recommendation.
77    pub customer_id: CustomerId,
78    /// Pet whose attendance history and care eligibility drive the recommendation.
79    pub pet_id: PetId,
80    /// Recent visit count used as the demand signal for package scoring.
81    pub attendance_visits: AttendanceVisitCount,
82    /// Care/safety eligibility that can suppress recommendations.
83    pub eligibility: CareEligibility,
84    /// Existing package coverage used to avoid duplicate sales prompts.
85    pub package_state: PackageState,
86    /// Billing readiness used to suppress recommendations needing collection review.
87    pub payment_state: PaymentState,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91/// Package-opportunity decision staff may review before contacting a customer.
92pub enum Decision {
93    /// Recommendation is promising enough to show staff, but customer messaging remains approval-gated.
94    RecommendStaffReview {
95        /// Opportunity strength derived from attendance history.
96        score: OpportunityScore,
97        /// Review gate required before staff use or send a recommendation.
98        gate: policy::ReviewGate,
99    },
100    /// Recommendation is intentionally hidden because safety, care, or billing review comes first.
101    Suppressed {
102        /// Reason evidence does not allow a direct package recommendation.
103        reason: SuppressionReason,
104        /// Review gate required before staff use or send a recommendation.
105        gate: policy::ReviewGate,
106    },
107    /// Evidence does not justify a package recommendation.
108    NoOpportunity {
109        /// Reason evidence does not allow a direct package recommendation.
110        reason: NoOpportunityReason,
111    },
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
115/// Strength of a daycare package opportunity from recent attendance evidence.
116pub enum OpportunityScore {
117    /// Attendance history suggests a possible package fit, but not enough for the strongest score.
118    Moderate,
119    /// Attendance history strongly suggests staff should review a package or membership offer.
120    Strong,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
124/// Reasons daycare package recommendations are suppressed before staff review.
125pub enum SuppressionReason {
126    /// Care or behavior review must be handled before sales recommendations.
127    SafetyOrCareReviewRequired,
128    /// Billing issue must be handled before sales recommendations.
129    PaymentOrBillingReviewRequired,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
133/// Reasons evidence does not indicate a new daycare package opportunity.
134pub enum NoOpportunityReason {
135    /// Existing package or membership already covers the daycare need.
136    AlreadyCovered,
137    /// Recent visits are too low to justify a package recommendation.
138    NotEnoughAttendanceHistory,
139}
140
141#[derive(Debug, Clone, Default)]
142/// Deterministic policy that scores daycare package opportunities from evidence.
143pub struct Policy;
144
145impl Policy {
146    /// Classifies package opportunity evidence into recommend, suppress, or no-opportunity outcomes.
147    pub fn classify(&self, evidence: &Evidence) -> Decision {
148        if matches!(evidence.eligibility, CareEligibility::BlockedBySafetyReview) {
149            return Decision::Suppressed {
150                reason: SuppressionReason::SafetyOrCareReviewRequired,
151                gate: policy::ReviewGate::BehaviorReview,
152            };
153        }
154        if matches!(evidence.payment_state, PaymentState::NeedsBillingReview) {
155            return Decision::Suppressed {
156                reason: SuppressionReason::PaymentOrBillingReviewRequired,
157                gate: policy::ReviewGate::RefundOrDepositException,
158            };
159        }
160        if matches!(evidence.package_state, PackageState::AlreadyCovered) {
161            return Decision::NoOpportunity {
162                reason: NoOpportunityReason::AlreadyCovered,
163            };
164        }
165        if evidence.attendance_visits.get() >= 8 {
166            Decision::RecommendStaffReview {
167                score: OpportunityScore::Strong,
168                gate: policy::ReviewGate::CustomerMessageApproval,
169            }
170        } else {
171            Decision::NoOpportunity {
172                reason: NoOpportunityReason::NotEnoughAttendanceHistory,
173            }
174        }
175    }
176}