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}