Skip to main content

domain/
daily_brief.rs

1//! Manager-facing daily brief values for cross-service resort operations.
2//!
3//! Brief sections, occupancy, labor, revenue, watchlists, and recommended manager actions
4//! are owned here rather than hidden behind broader operations vocabulary. A daily brief
5//! is the manager-facing read model in the source-fact → validated-domain → workflow
6//! chain: it exposes labor-cost levers such as scheduled staff count, utilization,
7//! over/understaffing, follow-up queues, safety watchlists, and revenue opportunities
8//! without taking live customer or staffing action on its own.
9//!
10//! ```
11//! use domain::{daily_brief, entities};
12//!
13//! let brief = daily_brief::Resort {
14//!     operating_day: daily_brief::ResortOperatingDay {
15//!         location_id: entities::LocationId(uuid::Uuid::nil()),
16//!         date: chrono::NaiveDate::from_ymd_opt(2026, 6, 18).unwrap(),
17//!         snapshot_id: daily_brief::snapshot::Id::try_new("loc-1-2026-06-18").unwrap(),
18//!     },
19//!     sections: vec![daily_brief::Section::Labor(daily_brief::LaborSnapshot {
20//!         scheduled_staff_count: daily_brief::ScheduledStaffCount::new(4),
21//!         labor_risk: daily_brief::LaborRisk::Understaffed,
22//!     })],
23//!     recommended_actions: vec![daily_brief::Action::SuggestScheduleReview {
24//!         risk: daily_brief::LaborRisk::Understaffed,
25//!     }],
26//!     risks: vec![daily_brief::Risk::LaborMismatch {
27//!         risk: daily_brief::LaborRisk::Understaffed,
28//!     }],
29//! };
30//!
31//! assert!(brief.has_manager_attention_required());
32//! assert!(brief.recommended_actions[0].requires_manager_approval());
33//! ```
34
35use chrono::{DateTime, NaiveDate, Utc};
36use nutype::nutype;
37#[allow(unused_imports)]
38use serde::{Deserialize, Deserializer, Serialize};
39
40use crate::entities::{self, CustomerId, LocationId, PetId, ServiceKind};
41use crate::operations;
42
43pub use snapshot::Id as Snapshot;
44
45/// Snapshot identifiers used to tie a daily brief back to the source/read-model extract.
46pub mod snapshot {
47    use super::*;
48
49    #[nutype(
50        sanitize(trim),
51        validate(not_empty, len_char_max = 120),
52        derive(
53            Debug,
54            Clone,
55            PartialEq,
56            Eq,
57            PartialOrd,
58            Ord,
59            Hash,
60            Serialize,
61            Deserialize
62        )
63    )]
64    pub struct Id(String);
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68/// Source snapshot key for one resort's manager brief on an operating day.
69pub struct ResortOperatingDay {
70    /// Resort location whose manager owns this operating-day brief.
71    pub location_id: LocationId,
72    /// Operating day the brief summarizes for staffing, arrivals, care, and follow-up decisions.
73    pub date: NaiveDate,
74    /// Source snapshot id retained so every brief item can be traced back to the read-model extract.
75    pub snapshot_id: snapshot::Id,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79/// Manager-facing daily brief assembled from validated operational read models.
80pub struct Resort {
81    /// Location/date/snapshot key that scopes the entire manager brief.
82    pub operating_day: ResortOperatingDay,
83    /// Manager-facing sections that organize occupancy, labor, customer, care, and revenue evidence.
84    pub sections: Vec<Section>,
85    /// Draft recommendations that may create tasks, messages, escalations, schedule reviews, or revenue follow-up.
86    pub recommended_actions: Vec<Action>,
87    /// Risks requiring manager attention before staff rely on the brief for operational decisions.
88    pub risks: Vec<Risk>,
89}
90
91impl Resort {
92    /// Returns whether risks or proposed actions require manager attention before work starts.
93    pub fn has_manager_attention_required(&self) -> bool {
94        self.risks.iter().any(Risk::requires_manager_attention)
95            || self
96                .recommended_actions
97                .iter()
98                .any(Action::requires_manager_approval)
99    }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103/// Section of the daily brief that turns source/read-model evidence into manager focus.
104pub enum Section {
105    /// Booked-vs-capacity view showing where demand may exceed service limits.
106    Occupancy(OccupancySnapshot),
107    /// Check-in/check-out workload that helps front desk and care teams plan the day.
108    ArrivalsAndDepartures(ArrivalDepartureSnapshot),
109    /// Staffing snapshot comparing scheduled labor with expected demand.
110    Labor(LaborSnapshot),
111    /// Customer follow-up queue for missing proof, changes, reviews, or service recovery.
112    CustomerFollowUps(Vec<CustomerFollowUp>),
113    /// Pet-care watchlist for medication, feeding, behavior, or incident attention.
114    PetCareWatchlist(Vec<PetCareWatch>),
115    /// Revenue opportunities that need staff review before customer follow-up.
116    RevenueOpportunities(Vec<RevenueOpportunity>),
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120/// Occupancy/utilization snapshot used to compare booked demand with service capacity.
121pub struct OccupancySnapshot {
122    /// Boarding booked-vs-capacity metric used to identify occupancy pressure.
123    pub boarding_capacity: capacity::Metric,
124    /// Daycare booked-vs-capacity metric used for playgroup and staffing review.
125    pub daycare_capacity: capacity::Metric,
126    /// Grooming utilization metric used to spot schedule pressure or rebooking capacity.
127    pub grooming_utilization: capacity::Metric,
128    /// Training utilization metric used to plan trainer workload and consult capacity.
129    pub training_utilization: capacity::Metric,
130}
131
132/// Capacity metrics used by daily briefs to expose utilization and labor-pressure signals.
133pub mod capacity {
134    use super::*;
135
136    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
137    /// Number of booked units contributing to a capacity metric.
138    pub struct Booked(u32);
139
140    impl Booked {
141        /// Assembles this daily brief value from already-validated domain parts.
142        pub const fn new(value: u32) -> Self {
143            Self(value)
144        }
145
146        /// Returns the checked value for storage, reporting, or adapter output.
147        pub const fn get(self) -> u32 {
148            self.0
149        }
150    }
151
152    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
153    /// Nonzero service capacity limit used as the denominator for utilization.
154    pub struct Limit(u32);
155
156    impl Limit {
157        /// Rejects unusable daily-brief input before managers see capacity or labor metrics.
158        pub const fn try_new(value: u32) -> Result<Self, LimitError> {
159            if value == 0 {
160                return Err(LimitError::ZeroCapacity);
161            }
162            Ok(Self(value))
163        }
164
165        /// Returns the checked value for storage, reporting, or adapter output.
166        pub const fn get(self) -> u32 {
167            self.0
168        }
169    }
170
171    impl<'de> Deserialize<'de> for Limit {
172        fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
173        where
174            D: Deserializer<'de>,
175        {
176            Self::try_new(u32::deserialize(deserializer)?).map_err(serde::de::Error::custom)
177        }
178    }
179
180    #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
181    /// Limit validation error that protects daily-brief ranking from empty or oversized queues.
182    pub enum LimitError {
183        #[error("capacity metrics require an explicit non-zero capacity limit")]
184        /// Zero capacity would make utilization meaningless and blocks the metric before manager display.
185        ZeroCapacity,
186    }
187
188    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
189    /// Capacity saturation expressed in basis points for stable BI/reporting comparisons.
190    pub struct SaturationBasisPoints(u32);
191
192    impl SaturationBasisPoints {
193        /// Assembles this daily brief value from already-validated domain parts.
194        pub const fn new(value: u32) -> Self {
195            Self(value)
196        }
197
198        /// Returns the checked value for storage, reporting, or adapter output.
199        pub const fn get(self) -> u32 {
200            self.0
201        }
202    }
203    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
204    /// Booked-vs-capacity metric that makes service utilization visible to managers.
205    pub struct Metric {
206        booked: Booked,
207        capacity: Limit,
208    }
209
210    impl Metric {
211        /// Assembles this daily brief value from already-validated domain parts.
212        pub const fn new(booked: Booked, capacity: Limit) -> Self {
213            Self { booked, capacity }
214        }
215
216        /// Returns booked units so occupancy pressure can be compared with capacity.
217        pub const fn booked(&self) -> Booked {
218            self.booked
219        }
220
221        /// Returns available capacity used to rank overbooking risk and labor pressure.
222        pub const fn capacity(&self) -> Limit {
223            self.capacity
224        }
225
226        /// Returns the saturation basis points for this daily brief value.
227        pub fn saturation_basis_points(&self) -> SaturationBasisPoints {
228            SaturationBasisPoints::new(
229                self.booked.get().saturating_mul(10_000) / self.capacity.get(),
230            )
231        }
232    }
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
236/// Count of scheduled staff used to reason about over/understaffing labor risk.
237pub struct ScheduledStaffCount(u16);
238
239impl ScheduledStaffCount {
240    /// Assembles this daily brief value from already-validated domain parts.
241    pub const fn new(value: u16) -> Self {
242        Self(value)
243    }
244
245    /// Returns the checked value for storage, reporting, or adapter output.
246    pub const fn get(self) -> u16 {
247        self.0
248    }
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252/// Check-in/check-out workload snapshot for front-desk and care-team planning.
253pub struct ArrivalDepartureSnapshot {
254    /// Reservations expected to arrive, driving front-desk preparation and care-team intake labor.
255    pub check_ins: Vec<entities::reservation::Id>,
256    /// Reservations expected to depart, driving pickup, billing, belongings, and room turnover work.
257    pub check_outs: Vec<entities::reservation::Id>,
258    /// Departures at risk of running late and affecting capacity, labor, or customer communication.
259    pub late_departure_risk: Vec<entities::reservation::Id>,
260}
261
262#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
263/// Labor summary comparing scheduled staff against expected demand and risk.
264pub struct LaborSnapshot {
265    /// Number of scheduled staff used to compare labor coverage with expected demand.
266    pub scheduled_staff_count: ScheduledStaffCount,
267    /// Staffing posture that tells managers whether to review coverage before the day starts.
268    pub labor_risk: LaborRisk,
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
272/// Staffing posture surfaced as a labor-cost and service-quality lever.
273pub enum LaborRisk {
274    /// Expected demand exceeds scheduled coverage and should trigger staffing review.
275    Understaffed,
276    /// Scheduled coverage appears aligned with expected demand.
277    OnPlan,
278    /// Scheduled coverage may exceed demand and can inform cost review or reassignment.
279    Overstaffed,
280    /// Labor coverage evidence is missing or unclear, so managers should verify staffing before acting.
281    Unknown,
282}
283
284#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
285/// Customer follow-up item generated from validated operational evidence.
286pub struct CustomerFollowUp {
287    /// Customer whose follow-up or revenue item needs staff-owned review.
288    pub customer_id: CustomerId,
289    /// Business reason staff should review before proceeding.
290    pub reason: FollowUpReason,
291    /// Deadline for completing or escalating the follow-up before it becomes stale.
292    pub due_at: DateTime<Utc>,
293}
294
295#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
296/// Follow-up reason that tells managers why the daily brief surfaced a pet, customer, or revenue task.
297pub enum FollowUpReason {
298    /// Vaccine proof is missing and must be collected or verified before care eligibility is trusted.
299    MissingVaccineProof,
300    /// Deposit is unpaid, requiring billing review or customer follow-up before relying on the booking.
301    DepositNotPaid,
302    /// Customer requested a reservation change that staff must confirm before schedule or capacity changes.
303    ReservationChangeRequested,
304    /// Sales/intake lead is waiting for response and may affect conversion or capacity planning.
305    LeadNeedsResponse,
306    /// Post-stay check-in is due for customer experience or service recovery.
307    PostStayCheckIn,
308    /// Reputation response is needed but must follow review and approval boundaries.
309    ReviewResponseNeeded,
310}
311
312#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
313/// Pet care/safety watch item that protects staff handoff and manager review.
314pub struct PetCareWatch {
315    /// Pet whose care/safety watch item needs staff attention.
316    pub pet_id: PetId,
317    /// Business reason staff should review before proceeding.
318    pub reason: PetCareWatchReason,
319}
320
321#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
322/// Pet-care watch reason used to flag vaccination, incident, medication, temperament, or feeding attention.
323pub enum PetCareWatchReason {
324    /// Medication task is due and needs care-team completion evidence.
325    MedicationDue,
326    /// Feeding instructions or exceptions need care-team attention before normal workflow proceeds.
327    FeedingException,
328    /// Anxiety or stress evidence may affect handling, staffing, and customer updates.
329    AnxietyOrStressFlag,
330    /// Behavior evidence requires review before playgroup, handling, or customer messaging changes.
331    BehaviorReview,
332    /// Incident follow-up is due and may affect safety review or customer communication.
333    IncidentFollowUp,
334}
335
336#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
337/// Revenue opportunity that may justify staff follow-up without bypassing approval gates.
338pub struct RevenueOpportunity {
339    /// Customer connected to a possible revenue follow-up, if known.
340    pub customer_id: Option<CustomerId>,
341    /// Pet connected to the revenue opportunity, if known.
342    pub pet_id: Option<PetId>,
343    /// Requested service that drives scheduling and labor estimates.
344    pub service: ServiceKind,
345    /// Opportunity category staff should review before drafting or making a customer offer.
346    pub opportunity: RevenueOpportunityKind,
347}
348
349#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
350/// Revenue opportunity kind used to separate package, add-on, retail, reactivation, and training work.
351pub enum RevenueOpportunityKind {
352    /// Boarding stay may be eligible for an exit bath offer after staff confirm service fit and timing.
353    ExitBathAfterBoarding,
354    /// Grooming customer may be due for rebooking, subject to schedule and customer preference review.
355    GroomingRebookingDue,
356    /// Daycare usage suggests package discussion, but staff must verify attendance and payment context.
357    DaycarePackageCandidate,
358    /// Care or behavior evidence suggests a training consult may be useful after staff review.
359    TrainingConsultCandidate,
360    /// Waitlist or cancellation gap may allow boarding revenue after capacity and policy review.
361    HolidayBoardingWaitlistFill,
362}
363
364#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
365/// Manager-visible risk derived from occupancy, labor, customer, care, or revenue evidence.
366pub enum Risk {
367    /// Demand may exceed capacity for the service and should interrupt normal planning.
368    CapacityConstraint {
369        /// Requested service that drives scheduling and labor estimates.
370        service: ServiceKind,
371    },
372    /// Staffing coverage may not match demand and should drive schedule review.
373    LaborMismatch {
374        /// Labor-risk value that explains why schedule review is recommended.
375        risk: LaborRisk,
376    },
377    /// Customer-experience signal needing manager review before follow-up.
378    CustomerExperienceRisk {
379        /// Source-backed observation explaining the risk for reviewers.
380        observation: operations::operational::Observation,
381    },
382    /// Pet safety or care signal that should route to care/manager review.
383    PetSafetyOrCareRisk {
384        /// Source-backed care observation explaining the safety concern.
385        observation: operations::operational::Observation,
386    },
387    /// Revenue signal that may deserve follow-up but stays review-gated.
388    RevenueLeakage {
389        /// Source-backed revenue observation for manager review.
390        observation: operations::operational::Observation,
391    },
392}
393
394impl Risk {
395    /// Returns whether this risk should interrupt normal staff workflow for manager review.
396    pub fn requires_manager_attention(&self) -> bool {
397        matches!(
398            self,
399            Self::CapacityConstraint { .. }
400                | Self::LaborMismatch {
401                    risk: LaborRisk::Understaffed
402                }
403                | Self::CustomerExperienceRisk { .. }
404                | Self::PetSafetyOrCareRisk { .. }
405        )
406    }
407}
408
409#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
410/// Proposed action that remains draft/recommendation until the workflow gate approves it.
411pub enum Action {
412    /// Create a staff task from brief evidence without taking external customer action.
413    CreateInternalTask {
414        /// Recommendation text/evidence used to create the internal task.
415        recommendation: operations::operational::Recommendation,
416    },
417    /// Draft a customer message only; approval and channel rules still control sending.
418    DraftCustomerMessage {
419        /// Customer who would receive the drafted follow-up after approval.
420        customer_id: CustomerId,
421        /// Business reason staff should review before proceeding.
422        reason: FollowUpReason,
423    },
424    /// Escalate source-backed concern to a manager before staff act.
425    EscalateToManager {
426        /// Business reason staff should review before proceeding.
427        reason: operations::operational::Observation,
428    },
429    /// Suggest manager review of staffing or schedule coverage.
430    SuggestScheduleReview {
431        /// Labor risk that justifies schedule review.
432        risk: LaborRisk,
433    },
434    /// Suggest review-gated revenue follow-up rather than direct sales outreach.
435    SuggestRevenueFollowUp {
436        /// Revenue opportunity to verify before any customer-facing offer.
437        opportunity: RevenueOpportunityKind,
438    },
439}
440
441impl Action {
442    /// Returns whether this action affects staffing/escalation enough to require approval.
443    pub fn requires_manager_approval(&self) -> bool {
444        matches!(
445            self,
446            Self::EscalateToManager { .. } | Self::SuggestScheduleReview { .. }
447        )
448    }
449}