Skip to main content

domain/daycare/
front_desk.rs

1//! Daycare front-desk readiness decisions for faster, safer check-in lanes.
2//!
3//! ```
4//! use domain::{daycare, entities};
5//! use uuid::Uuid;
6//!
7//! let context = daycare::front_desk::ReadinessContext::builder()
8//!     .reservation_id(entities::reservation::Id(Uuid::nil()))
9//!     .service(daycare::ServiceVariant::DayBoarding)
10//!     .eligibility(daycare::front_desk::EligibilityReadiness::IndividualCareReady)
11//!     .coverage(daycare::coverage::Decision::Sufficient)
12//!     .care(daycare::front_desk::CareReadiness::Ready)
13//!     .package(daycare::front_desk::PackageReadiness::NeedsFrontDeskCollection)
14//!     .customer_message(daycare::front_desk::CustomerMessageReadiness::NoMessageNeeded)
15//!     .build();
16//! let decision = daycare::front_desk::ThroughputPolicy.evaluate(&context);
17//! let ticket = daycare::front_desk::QueueTicket::new(
18//!     daycare::front_desk::QueuePosition::try_new(1).unwrap(),
19//!     decision,
20//! );
21//!
22//! assert_eq!(ticket.lane(), daycare::front_desk::QueueLane::CollectionLane);
23//! ```
24
25use super::*;
26use crate::{entities, policy};
27
28positive_scalar!(
29    QueuePosition,
30    u16,
31    QueuePositionError,
32    "front-desk queue position requires at least one ticket position"
33);
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
36/// Combined daycare check-in evidence used to route a front-desk queue ticket.
37pub struct ReadinessContext {
38    /// Reservation being prepared for check-in or staff review.
39    pub reservation_id: entities::reservation::Id,
40    /// Requested service that drives scheduling and labor estimates.
41    pub service: ServiceVariant,
42    /// Eligibility readiness for the requested daycare care mode.
43    pub eligibility: EligibilityReadiness,
44    /// Staffing coverage state that may block or route check-in.
45    pub coverage: coverage::Decision,
46    /// Care-team readiness for special handling, medical, or behavior review.
47    pub care: CareReadiness,
48    /// Package/payment readiness controlling collection or manager review at check-in.
49    pub package: PackageReadiness,
50    /// Customer-message approval state for any drafted daycare communication.
51    pub customer_message: CustomerMessageReadiness,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55/// Eligibility readiness category used by front-desk routing.
56pub enum EligibilityReadiness {
57    /// Group-play add-on or accommodation feature.
58    GroupPlay(eligibility::GroupPlayDecision),
59    /// Individual-care service does not need group-play clearance for check-in.
60    IndividualCareReady,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64/// Care-team readiness state for daycare check-in.
65pub enum CareReadiness {
66    /// Care-team medical, behavior, and handling evidence has no unresolved check-in blocker.
67    ///
68    /// Package/payment and manager policy readiness are represented by `PackageReadiness`.
69    Ready,
70    /// Care team must clear medical, behavior, or handling evidence before check-in advances.
71    ///
72    /// This gate is not the package/payment or manager policy readiness gate.
73    NeedsCareTeamReview {
74        /// Specific care-team gate that must clear before daycare check-in advances.
75        gate: policy::ReviewGate,
76    },
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80/// Package/payment readiness state used to route daycare front-desk work.
81pub enum PackageReadiness {
82    /// Package and payment state is clear enough for front desk to skip collection.
83    Ready,
84    /// Front desk must collect payment, package visits, or missing account information.
85    NeedsFrontDeskCollection,
86    /// Manager must review package, payment, or policy ambiguity before check-in advances.
87    NeedsManagerReview {
88        /// Specific gate the responsible team must clear before daycare check-in advances.
89        gate: policy::ReviewGate,
90    },
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
94/// Approval status for daycare customer-message drafts.
95pub enum CustomerMessageReadiness {
96    /// No customer-facing message is required for this check-in path.
97    NoMessageNeeded,
98    /// A drafted customer message must be approved before it is sent or used.
99    DraftNeedsApproval,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103/// Front-desk routing outcome for a daycare check-in ticket.
104pub enum ReadinessDecision {
105    /// Ticket can move through the fast lane without extra collection or review.
106    ReadyToCheckIn,
107    /// Front desk must collect payment, package visits, or missing account information.
108    NeedsFrontDeskCollection,
109    /// Care team must clear medical, behavior, or handling evidence before check-in advances.
110    NeedsCareTeamReview {
111        /// Specific gate the responsible team must clear before daycare check-in advances.
112        gate: policy::ReviewGate,
113    },
114    /// Manager must clear operational or package/payment policy before check-in advances.
115    NeedsManagerReview {
116        /// Specific gate the responsible team must clear before daycare check-in advances.
117        gate: policy::ReviewGate,
118    },
119    /// Safety or policy block prevents check-in until the named gate is resolved.
120    BlockedForSafetyOrPolicy {
121        /// Specific gate the responsible team must clear before daycare check-in advances.
122        gate: policy::ReviewGate,
123    },
124}
125
126impl ReadinessDecision {
127    /// Returns any approval gate tied specifically to customer-message handling for this decision.
128    pub fn customer_message_gate(&self) -> Option<policy::ReviewGate> {
129        match self {
130            Self::ReadyToCheckIn
131            | Self::NeedsFrontDeskCollection
132            | Self::NeedsCareTeamReview { .. }
133            | Self::NeedsManagerReview { .. }
134            | Self::BlockedForSafetyOrPolicy { .. } => None,
135        }
136    }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
140/// Physical front-desk lane that routes arrivals, pickups, incidents, billing, and package help.
141pub enum QueueLane {
142    /// Queue lane for check-ins with all readiness gates clear.
143    FastLane,
144    /// Queue lane for tickets needing payment or package collection.
145    CollectionLane,
146    /// Queue lane for tickets needing care-team or behavior review.
147    CareTeamReviewLane,
148    /// Queue lane for manager approval before check-in proceeds.
149    ManagerReviewLane,
150    /// Queue lane for blocked, ineligible, or policy-limited attendance paths.
151    WaitlistOrPolicyLane,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155/// Front-desk queue ticket pairing a position with the readiness decision that determines lane routing.
156pub struct QueueTicket {
157    position: QueuePosition,
158    decision: ReadinessDecision,
159}
160
161impl QueueTicket {
162    /// Creates a queue ticket from a validated position and readiness decision.
163    pub const fn new(position: QueuePosition, decision: ReadinessDecision) -> Self {
164        Self { position, decision }
165    }
166
167    /// Maps the readiness decision to the operational queue lane staff should use.
168    pub const fn lane(&self) -> QueueLane {
169        match self.decision {
170            ReadinessDecision::ReadyToCheckIn => QueueLane::FastLane,
171            ReadinessDecision::NeedsFrontDeskCollection => QueueLane::CollectionLane,
172            ReadinessDecision::NeedsCareTeamReview { .. } => QueueLane::CareTeamReviewLane,
173            ReadinessDecision::NeedsManagerReview { .. } => QueueLane::ManagerReviewLane,
174            ReadinessDecision::BlockedForSafetyOrPolicy { .. } => QueueLane::WaitlistOrPolicyLane,
175        }
176    }
177}
178
179#[derive(Debug, Clone, Default)]
180/// Deterministic policy for turning daycare readiness evidence into front-desk routing.
181pub struct ThroughputPolicy;
182
183impl ThroughputPolicy {
184    /// Evaluates message, package, care, coverage, and eligibility gates in front-desk priority order.
185    pub fn evaluate(&self, context: &ReadinessContext) -> ReadinessDecision {
186        if let CustomerMessageReadiness::DraftNeedsApproval = context.customer_message {
187            return ReadinessDecision::NeedsManagerReview {
188                gate: policy::ReviewGate::CustomerMessageApproval,
189            };
190        }
191        if let PackageReadiness::NeedsManagerReview { gate } = &context.package {
192            return ReadinessDecision::NeedsManagerReview { gate: gate.clone() };
193        }
194        if let PackageReadiness::NeedsFrontDeskCollection = context.package {
195            return ReadinessDecision::NeedsFrontDeskCollection;
196        }
197        if let CareReadiness::NeedsCareTeamReview { gate } = &context.care {
198            return ReadinessDecision::NeedsCareTeamReview { gate: gate.clone() };
199        }
200        if let coverage::Decision::Insufficient { gate, .. }
201        | coverage::Decision::Unknown { gate } = &context.coverage
202        {
203            return ReadinessDecision::NeedsManagerReview { gate: gate.clone() };
204        }
205        match &context.eligibility {
206            EligibilityReadiness::IndividualCareReady => ReadinessDecision::ReadyToCheckIn,
207            EligibilityReadiness::GroupPlay(eligibility::GroupPlayDecision::Eligible {
208                ..
209            }) => ReadinessDecision::ReadyToCheckIn,
210            EligibilityReadiness::GroupPlay(eligibility::GroupPlayDecision::NeedsStaffReview {
211                gate,
212                ..
213            }) => ReadinessDecision::NeedsCareTeamReview { gate: gate.clone() },
214            EligibilityReadiness::GroupPlay(
215                eligibility::GroupPlayDecision::TemporarilySuspended { gate, .. },
216            ) => ReadinessDecision::NeedsManagerReview { gate: gate.clone() },
217            EligibilityReadiness::GroupPlay(eligibility::GroupPlayDecision::Ineligible {
218                ..
219            }) => ReadinessDecision::BlockedForSafetyOrPolicy {
220                gate: policy::ReviewGate::ManagerApproval,
221            },
222        }
223    }
224}