Skip to main content

domain/
daily_brief.rs

1//! Canonical domain contracts for cross-service resort daily briefs.
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 boundary for daily brief contracts.
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    /// Location id fact promoted into this daily brief contract.
71    pub location_id: LocationId,
72    /// Date fact promoted into this daily brief contract.
73    pub date: NaiveDate,
74    /// Snapshot id fact promoted into this daily brief contract.
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    /// Operating day fact promoted into this daily brief contract.
82    pub operating_day: ResortOperatingDay,
83    /// Sections fact promoted into this daily brief contract.
84    pub sections: Vec<Section>,
85    /// Recommended actions fact promoted into this daily brief contract.
86    pub recommended_actions: Vec<Action>,
87    /// Risks fact promoted into this daily brief contract.
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    /// Occupancy item surfaced for manager daily-brief triage.
106    Occupancy(OccupancySnapshot),
107    /// Arrivals and departures item surfaced for manager daily-brief triage.
108    ArrivalsAndDepartures(ArrivalDepartureSnapshot),
109    /// Labor item surfaced for manager daily-brief triage.
110    Labor(LaborSnapshot),
111    /// Customer follow ups item surfaced for manager daily-brief triage.
112    CustomerFollowUps(Vec<CustomerFollowUp>),
113    /// Pet care watchlist item surfaced for manager daily-brief triage.
114    PetCareWatchlist(Vec<PetCareWatch>),
115    /// Revenue opportunities item surfaced for manager daily-brief triage.
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 capacity fact promoted into this daily brief contract.
123    pub boarding_capacity: capacity::Metric,
124    /// Daycare capacity fact promoted into this daily brief contract.
125    pub daycare_capacity: capacity::Metric,
126    /// Grooming utilization fact promoted into this daily brief contract.
127    pub grooming_utilization: capacity::Metric,
128    /// Training utilization fact promoted into this daily brief contract.
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        /// Exposes the validated scalar for serialization and adapter boundaries.
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        /// Promotes boundary input into a validated daily brief domain value.
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        /// Exposes the validated scalar for serialization and adapter boundaries.
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    /// Domain vocabulary for limit error decisions in daily brief workflows.
182    pub enum LimitError {
183        #[error("capacity metrics require an explicit non-zero capacity limit")]
184        /// Zero capacity item surfaced for manager daily-brief triage.
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        /// Exposes the validated scalar for serialization and adapter boundaries.
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 this daily brief value's booked.
217        pub const fn booked(&self) -> Booked {
218            self.booked
219        }
220
221        /// Returns this daily brief value's capacity.
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    /// Exposes the validated scalar for serialization and adapter boundaries.
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    /// Check ins fact promoted into this daily brief contract.
255    pub check_ins: Vec<entities::reservation::Id>,
256    /// Check outs fact promoted into this daily brief contract.
257    pub check_outs: Vec<entities::reservation::Id>,
258    /// Late departure risk fact promoted into this daily brief contract.
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    /// Scheduled staff count fact promoted into this daily brief contract.
266    pub scheduled_staff_count: ScheduledStaffCount,
267    /// Labor risk fact promoted into this daily brief contract.
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    /// Understaffed item surfaced for manager daily-brief triage.
275    Understaffed,
276    /// On plan item surfaced for manager daily-brief triage.
277    OnPlan,
278    /// Overstaffed item surfaced for manager daily-brief triage.
279    Overstaffed,
280    /// Provider role or status could not be mapped confidently.
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 id fact promoted into this daily brief contract.
288    pub customer_id: CustomerId,
289    /// Business reason staff should review before proceeding.
290    pub reason: FollowUpReason,
291    /// Due at fact promoted into this daily brief contract.
292    pub due_at: DateTime<Utc>,
293}
294
295#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
296/// Domain vocabulary for follow up reason decisions in daily brief workflows.
297pub enum FollowUpReason {
298    /// Missing vaccine proof item surfaced for manager daily-brief triage.
299    MissingVaccineProof,
300    /// Deposit not paid item surfaced for manager daily-brief triage.
301    DepositNotPaid,
302    /// Reservation change requested item surfaced for manager daily-brief triage.
303    ReservationChangeRequested,
304    /// Lead needs response item surfaced for manager daily-brief triage.
305    LeadNeedsResponse,
306    /// Post stay check in item surfaced for manager daily-brief triage.
307    PostStayCheckIn,
308    /// Review response needed item surfaced for manager daily-brief triage.
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 receiving the grooming or care service.
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/// Domain vocabulary for pet care watch reason decisions in daily brief workflows.
323pub enum PetCareWatchReason {
324    /// Medication due item surfaced for manager daily-brief triage.
325    MedicationDue,
326    /// Feeding exception item surfaced for manager daily-brief triage.
327    FeedingException,
328    /// Anxiety or stress flag item surfaced for manager daily-brief triage.
329    AnxietyOrStressFlag,
330    /// Behavior review item surfaced for manager daily-brief triage.
331    BehaviorReview,
332    /// Incident follow up item surfaced for manager daily-brief triage.
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 id fact promoted into this daily brief contract.
340    pub customer_id: Option<CustomerId>,
341    /// Pet receiving the grooming or care service.
342    pub pet_id: Option<PetId>,
343    /// Requested service that drives scheduling and labor estimates.
344    pub service: ServiceKind,
345    /// Opportunity fact promoted into this daily brief contract.
346    pub opportunity: RevenueOpportunityKind,
347}
348
349#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
350/// Domain vocabulary for revenue opportunity kind decisions in daily brief workflows.
351pub enum RevenueOpportunityKind {
352    /// Exit bath after boarding item surfaced for manager daily-brief triage.
353    ExitBathAfterBoarding,
354    /// Grooming rebooking due item surfaced for manager daily-brief triage.
355    GroomingRebookingDue,
356    /// Daycare package candidate item surfaced for manager daily-brief triage.
357    DaycarePackageCandidate,
358    /// Training consult candidate item surfaced for manager daily-brief triage.
359    TrainingConsultCandidate,
360    /// Holiday boarding waitlist fill item surfaced for manager daily-brief triage.
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    /// Capacity constraint item surfaced for manager daily-brief triage.
368    CapacityConstraint {
369        /// Requested service that drives scheduling and labor estimates.
370        service: ServiceKind,
371    },
372    /// Labor mismatch item surfaced for manager daily-brief triage.
373    LaborMismatch {
374        /// Risk fact promoted into this daily brief contract.
375        risk: LaborRisk,
376    },
377    /// Customer experience risk item surfaced for manager daily-brief triage.
378    CustomerExperienceRisk {
379        /// Observation fact promoted into this daily brief contract.
380        observation: operations::operational::Observation,
381    },
382    /// Pet safety or care risk item surfaced for manager daily-brief triage.
383    PetSafetyOrCareRisk {
384        /// Observation fact promoted into this daily brief contract.
385        observation: operations::operational::Observation,
386    },
387    /// Revenue leakage item surfaced for manager daily-brief triage.
388    RevenueLeakage {
389        /// Observation fact promoted into this daily brief contract.
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 internal task item surfaced for manager daily-brief triage.
413    CreateInternalTask {
414        /// Recommendation fact promoted into this daily brief contract.
415        recommendation: operations::operational::Recommendation,
416    },
417    /// Draft customer message item surfaced for manager daily-brief triage.
418    DraftCustomerMessage {
419        /// Customer id fact promoted into this daily brief contract.
420        customer_id: CustomerId,
421        /// Business reason staff should review before proceeding.
422        reason: FollowUpReason,
423    },
424    /// Escalate to manager item surfaced for manager daily-brief triage.
425    EscalateToManager {
426        /// Business reason staff should review before proceeding.
427        reason: operations::operational::Observation,
428    },
429    /// Suggest schedule review item surfaced for manager daily-brief triage.
430    SuggestScheduleReview {
431        /// Risk fact promoted into this daily brief contract.
432        risk: LaborRisk,
433    },
434    /// Suggest revenue follow up item surfaced for manager daily-brief triage.
435    SuggestRevenueFollowUp {
436        /// Opportunity fact promoted into this daily brief contract.
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}