app/manager_daily_brief.rs
1//! Manager Daily Brief workflow contracts for labor-saving internal review.
2//!
3//! The workflow starts from app-owned, source-grounded context and produces a
4//! deterministic packet that an agent may summarize or rank, but not use as
5//! authority to mutate schedules, provider/PMS records, customer channels, or
6//! money movement. Outcome records then capture whether the reviewed action
7//! actually reduced manager/front-desk labor.
8//!
9//! ```
10//! use app::manager_daily_brief as brief;
11//! use chrono::NaiveDate;
12//! use domain::{analytics, entities, operations, source};
13//! use uuid::Uuid;
14//!
15//! let location_id = entities::LocationId(Uuid::from_u128(0x170));
16//! let operating_day = operations::operating_day::Date::try_new(
17//! NaiveDate::from_ymd_opt(2026, 6, 18).expect("fixture date is valid"),
18//! )?;
19//! let source_ref = source::RecordRef::new(
20//! source::System::BusinessIntelligence,
21//! source::record::Id::try_new("labor-read-model:boarding-demand:2026-06-18")?,
22//! );
23//! let demand_fact = analytics::service_demand::Fact::try_new(
24//! analytics::service_demand::Id::try_new("boarding-demand-risk")?,
25//! operations::operating_day::Key::new(
26//! location_id,
27//! operations::service_core::ServiceLine::Boarding,
28//! operating_day,
29//! ),
30//! analytics::service_demand::DemandUnits::try_new(42)?,
31//! vec![source_ref.clone()],
32//! analytics::ProjectionVersion::try_new("manager-brief-fixture-v1")?,
33//! vec![],
34//! )?;
35//!
36//! let request = brief::Request::builder()
37//! .location_id(location_id)
38//! .operating_day(operating_day)
39//! .prepared_for(brief::ManagerBriefPersona::GeneralManager)
40//! .demand_attention_threshold(brief::DemandThresholdUnits::try_new(25)?)
41//! .service_demand_facts(vec![demand_fact])
42//! .build();
43//!
44//! let packet = brief::Workflow::evaluate(request);
45//!
46//! assert_eq!(packet.actions().len(), 1);
47//! assert!(packet.all_actions_are_source_grounded());
48//! assert!(packet.safe_agent_actions().contains(&brief::SafeAgentAction::RankManagerActions));
49//! assert!(packet.blocked_actions().contains(&brief::BlockedAction::ChangeStaffSchedule));
50//! assert!(packet.blocked_actions().contains(&brief::BlockedAction::MutateProviderOrPmsRecord));
51//! assert!(packet.minutes_saved() > 0);
52//!
53//! let outcome = brief::OutcomeRecord::builder()
54//! .action_id(packet.actions()[0].id().clone())
55//! .recorded_by(entities::ActorRef::Manager {
56//! manager_id: entities::ManagerId::try_new("gm-fixture")?,
57//! })
58//! .outcome(brief::FeedbackOutcome::Completed)
59//! .before_minutes(brief::LaborMinutes::try_new(45)?)
60//! .actual_minutes(brief::LaborMinutes::try_new(12)?)
61//! .source_record_refs(vec![source_ref])
62//! .build();
63//!
64//! assert!(outcome.records_feedback_without_external_mutation());
65//! assert!(outcome.blocked_actions().contains(&brief::BlockedAction::SendCustomerMessage));
66//! assert_eq!(outcome.actual_minutes_saved(), 33);
67//! # Ok::<(), Box<dyn std::error::Error>>(())
68//! ```
69use domain::{analytics, entities, operations, policy, source};
70use nutype::nutype;
71use serde::{Deserialize, Serialize};
72
73use crate::{checkout_completion, crm_retention};
74
75#[nutype(
76 sanitize(trim),
77 validate(not_empty, len_char_max = 1200),
78 derive(
79 Debug,
80 Clone,
81 PartialEq,
82 Eq,
83 PartialOrd,
84 Ord,
85 Hash,
86 Serialize,
87 Deserialize
88 )
89)]
90pub struct BriefSummary(String);
91
92#[nutype(
93 sanitize(trim),
94 validate(not_empty, len_char_max = 120),
95 derive(
96 Debug,
97 Clone,
98 PartialEq,
99 Eq,
100 PartialOrd,
101 Ord,
102 Hash,
103 Serialize,
104 Deserialize
105 )
106)]
107pub struct ActionId(String);
108
109#[nutype(
110 sanitize(trim),
111 validate(not_empty, len_char_max = 500),
112 derive(
113 Debug,
114 Clone,
115 PartialEq,
116 Eq,
117 PartialOrd,
118 Ord,
119 Hash,
120 Serialize,
121 Deserialize
122 )
123)]
124pub struct ActionRationale(String);
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
127/// Labor minutes carried by the manager daily brief workflow; it assembles reviewable manager brief packets from deterministic context and agent drafts.
128pub struct LaborMinutes(u16);
129
130impl LaborMinutes {
131 /// Builds try new for the manager daily brief workflow contract from validated source facts while preserving review gates and draft-only side-effect boundaries.
132 pub const fn try_new(value: u16) -> Result<Self> {
133 if value == 0 {
134 return Err(Error::ZeroLaborMinutes);
135 }
136 Ok(Self(value))
137 }
138
139 /// Returns the get source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
140 pub const fn get(self) -> u16 {
141 self.0
142 }
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
146/// Aggregate labor minutes carried by the manager daily brief workflow; it assembles reviewable manager brief packets from deterministic context and agent drafts.
147pub struct AggregateLaborMinutes(u16);
148
149impl AggregateLaborMinutes {
150 /// Builds new for the manager daily brief workflow contract from validated source facts while preserving review gates and draft-only side-effect boundaries.
151 pub const fn new(value: u16) -> Self {
152 Self(value)
153 }
154
155 /// Returns the get source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
156 pub const fn get(self) -> u16 {
157 self.0
158 }
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
162/// Demand threshold units carried by the manager daily brief workflow; it assembles reviewable manager brief packets from deterministic context and agent drafts.
163pub struct DemandThresholdUnits(u32);
164
165impl DemandThresholdUnits {
166 /// Builds try new for the manager daily brief workflow contract from validated source facts while preserving review gates and draft-only side-effect boundaries.
167 pub const fn try_new(value: u32) -> Result<Self> {
168 if value == 0 {
169 return Err(Error::ZeroDemandThresholdUnits);
170 }
171 Ok(Self(value))
172 }
173
174 /// Returns the get source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
175 pub const fn get(self) -> u32 {
176 self.0
177 }
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
181/// Decision taxonomy for manager brief persona in the manager daily brief workflow; each value carries operational meaning for source-grounded routing and review.
182pub enum ManagerBriefPersona {
183 /// Represents general manager in the manager brief decision model so the app can choose the correct evidence, review, or draft path without taking live action.
184 GeneralManager,
185 /// Represents assistant general manager in the manager brief decision model so the app can choose the correct evidence, review, or draft path without taking live action.
186 AssistantGeneralManager,
187 /// Represents front desk lead in the manager brief decision model so the app can choose the correct evidence, review, or draft path without taking live action.
188 FrontDeskLead,
189 /// Represents front desk agent in the manager brief decision model so the app can choose the correct evidence, review, or draft path without taking live action.
190 FrontDeskAgent,
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
194/// Decision taxonomy for removed manual work in the manager daily brief workflow; each value carries operational meaning for source-grounded routing and review.
195pub enum RemovedManualWork {
196 /// Represents morning dashboard reconciliation in the manager brief decision model so the app can choose the correct evidence, review, or draft path without taking live action.
197 MorningDashboardReconciliation,
198 /// Represents demand versus staffing scan in the manager brief decision model so the app can choose the correct evidence, review, or draft path without taking live action.
199 DemandVersusStaffingScan,
200 /// Represents checkout exception audit in the manager brief decision model so the app can choose the correct evidence, review, or draft path without taking live action.
201 CheckoutExceptionAudit,
202 /// Represents retention follow up queue prioritization in the manager brief decision model so the app can choose the correct evidence, review, or draft path without taking live action.
203 RetentionFollowUpQueuePrioritization,
204 /// Represents data quality exception triage in the manager brief decision model so the app can choose the correct evidence, review, or draft path without taking live action.
205 DataQualityExceptionTriage,
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
209/// Decision taxonomy for source fact kind in the manager daily brief workflow; each value carries operational meaning for source-grounded routing and review.
210pub enum SourceFactKind {
211 /// Represents service demand forecast in the manager brief decision model so the app can choose the correct evidence, review, or draft path without taking live action.
212 ServiceDemandForecast,
213 /// Represents checkout completion status in the manager brief decision model so the app can choose the correct evidence, review, or draft path without taking live action.
214 CheckoutCompletionStatus,
215 /// Represents retention follow up eligibility in the manager brief decision model so the app can choose the correct evidence, review, or draft path without taking live action.
216 RetentionFollowUpEligibility,
217 /// Represents source data quality issue in the manager brief decision model so the app can choose the correct evidence, review, or draft path without taking live action.
218 SourceDataQualityIssue,
219}
220
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
222/// Source fact carried by the manager daily brief workflow; it assembles reviewable manager brief packets from deterministic context and agent drafts.
223pub struct SourceFact {
224 kind: SourceFactKind,
225 summary: BriefSummary,
226 #[builder(default)]
227 source_record_refs: Vec<source::RecordRef>,
228}
229
230impl SourceFact {
231 /// Returns the kind source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
232 pub const fn kind(&self) -> SourceFactKind {
233 self.kind
234 }
235
236 /// Returns the summary source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
237 pub const fn summary(&self) -> &BriefSummary {
238 &self.summary
239 }
240
241 /// Returns the source record refs source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
242 pub fn source_record_refs(&self) -> &[source::RecordRef] {
243 &self.source_record_refs
244 }
245
246 /// Reports whether the manager daily brief workflow satisfies the has source evidence safety condition.
247 pub fn has_source_evidence(&self) -> bool {
248 !self.source_record_refs.is_empty()
249 }
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
253/// Decision taxonomy for brief action kind in the manager daily brief workflow; each value carries operational meaning for source-grounded routing and review.
254pub enum BriefActionKind {
255 /// Represents review demand against staffing plan in the manager brief decision model so the app can choose the correct evidence, review, or draft path without taking live action.
256 ReviewDemandAgainstStaffingPlan,
257 /// Represents resolve checkout exception in the manager brief decision model so the app can choose the correct evidence, review, or draft path without taking live action.
258 ResolveCheckoutException,
259 /// Represents approve retention follow up draft in the manager brief decision model so the app can choose the correct evidence, review, or draft path without taking live action.
260 ApproveRetentionFollowUpDraft,
261 /// Represents investigate source data quality issue in the manager brief decision model so the app can choose the correct evidence, review, or draft path without taking live action.
262 InvestigateSourceDataQualityIssue,
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
266/// Decision taxonomy for brief action priority in the manager daily brief workflow; each value carries operational meaning for source-grounded routing and review.
267pub enum BriefActionPriority {
268 /// Represents high in the manager brief decision model so the app can choose the correct evidence, review, or draft path without taking live action.
269 High,
270 /// Represents medium in the manager brief decision model so the app can choose the correct evidence, review, or draft path without taking live action.
271 Medium,
272 /// Represents low in the manager brief decision model so the app can choose the correct evidence, review, or draft path without taking live action.
273 Low,
274}
275
276#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
277/// Review-safe agent tasks allowed to save staff time without crossing mutation or send boundaries.
278pub enum SafeAgentAction {
279 /// Allows agents to summarize source evidence for staff review without mutating records or contacting customers.
280 SummarizeSourceEvidence,
281 /// Allows agents to rank manager actions for staff review without mutating records or contacting customers.
282 RankManagerActions,
283 /// Allows agents to draft internal task for review for staff review without mutating records or contacting customers.
284 DraftInternalTaskForReview,
285 /// Allows agents to record manager feedback for staff review without mutating records or contacting customers.
286 RecordManagerFeedback,
287 /// Allows agents to estimate labor minutes saved for staff review without mutating records or contacting customers.
288 EstimateLaborMinutesSaved,
289}
290
291#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
292/// Actions the agent must never perform without a human/operator system of record.
293pub enum BlockedAction {
294 /// Blocks agents from change staff schedule until staff or the system of record performs the action.
295 ChangeStaffSchedule,
296 /// Blocks agents from mutate provider or pms record until staff or the system of record performs the action.
297 MutateProviderOrPmsRecord,
298 /// Blocks agents from send customer message until staff or the system of record performs the action.
299 SendCustomerMessage,
300 /// Blocks agents from move refund discount or payment until staff or the system of record performs the action.
301 MoveRefundDiscountOrPayment,
302 /// Blocks agents from hide source data quality issue until staff or the system of record performs the action.
303 HideSourceDataQualityIssue,
304}
305
306impl BlockedAction {
307 /// Returns the code source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
308 pub const fn code(self) -> &'static str {
309 match self {
310 Self::ChangeStaffSchedule => "change_staff_schedule",
311 Self::MutateProviderOrPmsRecord => "mutate_provider_or_pms_record",
312 Self::SendCustomerMessage => "send_customer_message",
313 Self::MoveRefundDiscountOrPayment => "move_refund_discount_or_payment",
314 Self::HideSourceDataQualityIssue => "hide_source_data_quality_issue",
315 }
316 }
317
318 /// Builds from requested side effect code for the manager daily brief workflow contract from validated source facts while preserving review gates and draft-only side-effect boundaries.
319 pub fn from_requested_side_effect_code(code: &str) -> Option<Self> {
320 match code {
321 "change_staff_schedule" => Some(Self::ChangeStaffSchedule),
322 "mutate_provider_or_pms_record" => Some(Self::MutateProviderOrPmsRecord),
323 "send_customer_message" => Some(Self::SendCustomerMessage),
324 "move_refund_discount_or_payment" => Some(Self::MoveRefundDiscountOrPayment),
325 "hide_source_data_quality_issue" => Some(Self::HideSourceDataQualityIssue),
326 _ => None,
327 }
328 }
329}
330
331/// Produces the requested side effect rejection reason contract for the manager daily brief workflow.
332pub fn requested_side_effect_rejection_reason(side_effect: &str) -> String {
333 if BlockedAction::from_requested_side_effect_code(side_effect).is_some() {
334 format!("blocked_side_effect:{side_effect}")
335 } else {
336 format!("unsupported_side_effect:{side_effect}")
337 }
338}
339
340#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
341/// Labor impact estimate carried by the manager daily brief workflow; it assembles reviewable manager brief packets from deterministic context and agent drafts.
342pub struct LaborImpactEstimate {
343 before_minutes: LaborMinutes,
344 after_minutes: LaborMinutes,
345}
346
347impl LaborImpactEstimate {
348 /// Builds new for the manager daily brief workflow contract from validated source facts while preserving review gates and draft-only side-effect boundaries.
349 pub const fn new(before_minutes: LaborMinutes, after_minutes: LaborMinutes) -> Self {
350 Self {
351 before_minutes,
352 after_minutes,
353 }
354 }
355
356 /// Returns the before minutes source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
357 pub const fn before_minutes(&self) -> LaborMinutes {
358 self.before_minutes
359 }
360
361 /// Returns the after minutes source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
362 pub const fn after_minutes(&self) -> LaborMinutes {
363 self.after_minutes
364 }
365
366 /// Returns the minutes saved source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
367 pub const fn minutes_saved(&self) -> u16 {
368 self.before_minutes.0.saturating_sub(self.after_minutes.0)
369 }
370}
371
372#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
373/// Brief action carried by the manager daily brief workflow; it assembles reviewable manager brief packets from deterministic context and agent drafts.
374pub struct BriefAction {
375 id: ActionId,
376 kind: BriefActionKind,
377 priority: BriefActionPriority,
378 owner_persona: ManagerBriefPersona,
379 removed_manual_work: RemovedManualWork,
380 rationale: ActionRationale,
381 source_facts: Vec<SourceFact>,
382 labor_impact: LaborImpactEstimate,
383 #[builder(default)]
384 required_review_gates: Vec<policy::ReviewGate>,
385}
386
387impl BriefAction {
388 /// Returns the id source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
389 pub const fn id(&self) -> &ActionId {
390 &self.id
391 }
392
393 /// Returns the kind source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
394 pub const fn kind(&self) -> BriefActionKind {
395 self.kind
396 }
397
398 /// Returns the priority source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
399 pub const fn priority(&self) -> BriefActionPriority {
400 self.priority
401 }
402
403 /// Returns the owner persona source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
404 pub const fn owner_persona(&self) -> ManagerBriefPersona {
405 self.owner_persona
406 }
407
408 /// Returns the removed manual work source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
409 pub const fn removed_manual_work(&self) -> RemovedManualWork {
410 self.removed_manual_work
411 }
412
413 /// Returns the rationale source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
414 pub const fn rationale(&self) -> &ActionRationale {
415 &self.rationale
416 }
417
418 /// Returns the source facts source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
419 pub fn source_facts(&self) -> &[SourceFact] {
420 &self.source_facts
421 }
422
423 /// Returns the labor impact source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
424 pub const fn labor_impact(&self) -> &LaborImpactEstimate {
425 &self.labor_impact
426 }
427
428 /// Returns the required review gates source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
429 pub fn required_review_gates(&self) -> &[policy::ReviewGate] {
430 &self.required_review_gates
431 }
432
433 /// Reports whether the manager daily brief workflow satisfies the is source grounded safety condition.
434 pub fn is_source_grounded(&self) -> bool {
435 !self.source_facts.is_empty()
436 && self
437 .source_facts
438 .iter()
439 .all(SourceFact::has_source_evidence)
440 }
441}
442
443#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
444/// Scoped checkout packet carried by the manager daily brief workflow; it assembles reviewable manager brief packets from deterministic context and agent drafts.
445pub struct ScopedCheckoutPacket {
446 location_id: entities::LocationId,
447 operating_day: operations::operating_day::Date,
448 packet: checkout_completion::Packet,
449}
450
451impl ScopedCheckoutPacket {
452 /// Returns the location id source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
453 pub const fn location_id(&self) -> entities::LocationId {
454 self.location_id
455 }
456
457 /// Returns the operating day source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
458 pub const fn operating_day(&self) -> operations::operating_day::Date {
459 self.operating_day
460 }
461
462 /// Returns the packet source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
463 pub const fn packet(&self) -> &checkout_completion::Packet {
464 &self.packet
465 }
466}
467
468#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
469/// Scoped retention packet carried by the manager daily brief workflow; it assembles reviewable manager brief packets from deterministic context and agent drafts.
470pub struct ScopedRetentionPacket {
471 location_id: entities::LocationId,
472 operating_day: operations::operating_day::Date,
473 packet: crm_retention::Packet,
474}
475
476impl ScopedRetentionPacket {
477 /// Returns the location id source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
478 pub const fn location_id(&self) -> entities::LocationId {
479 self.location_id
480 }
481
482 /// Returns the operating day source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
483 pub const fn operating_day(&self) -> operations::operating_day::Date {
484 self.operating_day
485 }
486
487 /// Returns the packet source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
488 pub const fn packet(&self) -> &crm_retention::Packet {
489 &self.packet
490 }
491}
492
493#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
494/// Input contract for building the workflow packet from source-grounded records.
495pub struct Request {
496 location_id: entities::LocationId,
497 operating_day: operations::operating_day::Date,
498 prepared_for: ManagerBriefPersona,
499 demand_attention_threshold: DemandThresholdUnits,
500 #[builder(default)]
501 service_demand_facts: Vec<analytics::service_demand::Fact>,
502 #[builder(default)]
503 checkout_packets: Vec<ScopedCheckoutPacket>,
504 #[builder(default)]
505 retention_packets: Vec<ScopedRetentionPacket>,
506}
507
508impl Request {
509 /// Returns the location id source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
510 pub const fn location_id(&self) -> entities::LocationId {
511 self.location_id
512 }
513
514 /// Returns the operating day source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
515 pub const fn operating_day(&self) -> operations::operating_day::Date {
516 self.operating_day
517 }
518
519 /// Returns the prepared for source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
520 pub const fn prepared_for(&self) -> ManagerBriefPersona {
521 self.prepared_for
522 }
523
524 /// Returns the demand attention threshold source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
525 pub const fn demand_attention_threshold(&self) -> DemandThresholdUnits {
526 self.demand_attention_threshold
527 }
528
529 /// Returns the service demand facts source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
530 pub fn service_demand_facts(&self) -> &[analytics::service_demand::Fact] {
531 &self.service_demand_facts
532 }
533
534 /// Returns the checkout packets source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
535 pub fn checkout_packets(&self) -> &[ScopedCheckoutPacket] {
536 &self.checkout_packets
537 }
538
539 /// Returns the retention packets source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
540 pub fn retention_packets(&self) -> &[ScopedRetentionPacket] {
541 &self.retention_packets
542 }
543}
544
545#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
546/// Reviewable packet handed to staff or agents with deterministic gates already applied.
547pub struct Packet {
548 location_id: entities::LocationId,
549 operating_day: operations::operating_day::Date,
550 prepared_for: ManagerBriefPersona,
551 actions: Vec<BriefAction>,
552 safe_agent_actions: Vec<SafeAgentAction>,
553 blocked_actions: Vec<BlockedAction>,
554 before_minutes: AggregateLaborMinutes,
555 after_minutes: AggregateLaborMinutes,
556}
557
558impl Packet {
559 /// Returns the location id source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
560 pub const fn location_id(&self) -> entities::LocationId {
561 self.location_id
562 }
563
564 /// Returns the operating day source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
565 pub const fn operating_day(&self) -> operations::operating_day::Date {
566 self.operating_day
567 }
568
569 /// Returns the prepared for source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
570 pub const fn prepared_for(&self) -> ManagerBriefPersona {
571 self.prepared_for
572 }
573
574 /// Returns the actions source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
575 pub fn actions(&self) -> &[BriefAction] {
576 &self.actions
577 }
578
579 /// Returns the safe agent actions source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
580 pub fn safe_agent_actions(&self) -> &[SafeAgentAction] {
581 &self.safe_agent_actions
582 }
583
584 /// Returns the blocked actions source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
585 pub fn blocked_actions(&self) -> &[BlockedAction] {
586 &self.blocked_actions
587 }
588
589 /// Returns the before minutes source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
590 pub const fn before_minutes(&self) -> AggregateLaborMinutes {
591 self.before_minutes
592 }
593
594 /// Returns the after minutes source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
595 pub const fn after_minutes(&self) -> AggregateLaborMinutes {
596 self.after_minutes
597 }
598
599 /// Returns the minutes saved source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
600 pub const fn minutes_saved(&self) -> u16 {
601 self.before_minutes.0.saturating_sub(self.after_minutes.0)
602 }
603
604 /// Returns the all actions are source grounded source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
605 pub fn all_actions_are_source_grounded(&self) -> bool {
606 self.actions.iter().all(BriefAction::is_source_grounded)
607 }
608}
609
610#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
611/// Decision taxonomy for feedback outcome in the manager daily brief workflow; each value carries operational meaning for source-grounded routing and review.
612pub enum FeedbackOutcome {
613 /// Records a completed result so follow-up impact is auditable.
614 Completed,
615 /// Records a deferred result so follow-up impact is auditable.
616 Deferred,
617 /// Records a suppressed by manager result so follow-up impact is auditable.
618 SuppressedByManager,
619 /// Records a source fact was wrong result so follow-up impact is auditable.
620 SourceFactWasWrong,
621}
622
623#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
624/// Outcome record carried by the manager daily brief workflow; it assembles reviewable manager brief packets from deterministic context and agent drafts.
625pub struct OutcomeRecord {
626 action_id: ActionId,
627 recorded_by: entities::ActorRef,
628 outcome: FeedbackOutcome,
629 before_minutes: LaborMinutes,
630 actual_minutes: LaborMinutes,
631 #[builder(default)]
632 source_record_refs: Vec<source::RecordRef>,
633}
634
635impl OutcomeRecord {
636 /// Returns the action id source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
637 pub const fn action_id(&self) -> &ActionId {
638 &self.action_id
639 }
640
641 /// Returns the recorded by source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
642 pub const fn recorded_by(&self) -> &entities::ActorRef {
643 &self.recorded_by
644 }
645
646 /// Returns the outcome source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
647 pub const fn outcome(&self) -> FeedbackOutcome {
648 self.outcome
649 }
650
651 /// Returns the before minutes source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
652 pub const fn before_minutes(&self) -> LaborMinutes {
653 self.before_minutes
654 }
655
656 /// Returns the actual minutes source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
657 pub const fn actual_minutes(&self) -> LaborMinutes {
658 self.actual_minutes
659 }
660
661 /// Returns the actual minutes saved source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
662 pub const fn actual_minutes_saved(&self) -> u16 {
663 self.before_minutes.0.saturating_sub(self.actual_minutes.0)
664 }
665
666 /// Returns the source record refs source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
667 pub fn source_record_refs(&self) -> &[source::RecordRef] {
668 &self.source_record_refs
669 }
670
671 /// Returns the records feedback without external mutation source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
672 pub fn records_feedback_without_external_mutation(&self) -> bool {
673 true
674 }
675
676 /// Returns the blocked actions source evidence carried by this manager daily brief workflow artifact without changing provider, customer, payment, or schedule state.
677 pub fn blocked_actions(&self) -> Vec<BlockedAction> {
678 blocked_actions_for()
679 }
680}
681
682#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
683/// Decision taxonomy for error in the manager daily brief workflow; each value carries operational meaning for source-grounded routing and review.
684pub enum Error {
685 #[error("labor minutes must be greater than zero")]
686 /// Identifies zero labor minutes as the reason the workflow must stop, retry, or request review.
687 ZeroLaborMinutes,
688 #[error("demand threshold units must be greater than zero")]
689 /// Identifies zero demand threshold units as the reason the workflow must stop, retry, or request review.
690 ZeroDemandThresholdUnits,
691}
692
693/// Result type returned by fallible manager daily brief operations.
694pub type Result<T> = std::result::Result<T, Error>;
695
696#[derive(Debug, Clone, Copy, PartialEq, Eq)]
697/// Workflow carried by the manager daily brief workflow; it assembles reviewable manager brief packets from deterministic context and agent drafts.
698pub struct Workflow;
699
700impl Workflow {
701 /// Builds evaluate for the manager daily brief workflow contract from validated source facts while preserving review gates and draft-only side-effect boundaries.
702 pub fn evaluate(request: Request) -> Packet {
703 let mut actions = Vec::new();
704 actions.extend(service_demand_actions(&request));
705 actions.extend(checkout_exception_actions(&request));
706 actions.extend(retention_actions(&request));
707
708 let before_minutes = total_before_minutes(&actions);
709 let after_minutes = total_after_minutes(&actions);
710
711 Packet {
712 location_id: request.location_id,
713 operating_day: request.operating_day,
714 prepared_for: request.prepared_for,
715 actions,
716 safe_agent_actions: vec![
717 SafeAgentAction::SummarizeSourceEvidence,
718 SafeAgentAction::RankManagerActions,
719 SafeAgentAction::DraftInternalTaskForReview,
720 SafeAgentAction::RecordManagerFeedback,
721 SafeAgentAction::EstimateLaborMinutesSaved,
722 ],
723 blocked_actions: blocked_actions_for(),
724 before_minutes,
725 after_minutes,
726 }
727 }
728}
729
730fn service_demand_actions(request: &Request) -> Vec<BriefAction> {
731 request
732 .service_demand_facts
733 .iter()
734 .filter(|fact| service_demand_fact_matches_request_scope(fact, request))
735 .filter(|fact| fact.demand_units().get() >= request.demand_attention_threshold.get())
736 .map(|fact| {
737 let mut source_facts = vec![SourceFact::builder()
738 .kind(SourceFactKind::ServiceDemandForecast)
739 .summary(BriefSummary::try_new("Service demand crosses the manager attention threshold for this operating day.").expect("static brief summary is valid"))
740 .source_record_refs(fact.source_record_refs().to_vec())
741 .build()];
742
743 let mut required_review_gates = Vec::new();
744 if matches!(
745 fact.data_quality_status(),
746 analytics::service_demand::DataQualityStatus::ManagerReviewRequired
747 ) {
748 source_facts.push(SourceFact::builder()
749 .kind(SourceFactKind::SourceDataQualityIssue)
750 .summary(BriefSummary::try_new("Demand fact carries nonblocking source data-quality issues that should stay visible in the brief.").expect("static brief summary is valid"))
751 .source_record_refs(fact.source_record_refs().to_vec())
752 .build());
753 required_review_gates.push(policy::ReviewGate::ManagerApproval);
754 }
755
756 BriefAction::builder()
757 .id(ActionId::try_new(format!(
758 "demand-staffing-{}",
759 fact.id().as_str()
760 ))
761 .expect("fact ids are non-empty"))
762 .kind(BriefActionKind::ReviewDemandAgainstStaffingPlan)
763 .priority(BriefActionPriority::High)
764 .owner_persona(ManagerBriefPersona::GeneralManager)
765 .removed_manual_work(RemovedManualWork::DemandVersusStaffingScan)
766 .rationale(ActionRationale::try_new("Manager starts from a ranked source-grounded staffing risk instead of manually comparing reservation dashboards to the schedule.").expect("static rationale is valid"))
767 .source_facts(source_facts)
768 .labor_impact(LaborImpactEstimate::new(
769 LaborMinutes::try_new(45).expect("static minutes are valid"),
770 LaborMinutes::try_new(15).expect("static minutes are valid"),
771 ))
772 .required_review_gates(required_review_gates)
773 .build()
774 })
775 .collect()
776}
777
778fn service_demand_fact_matches_request_scope(
779 fact: &analytics::service_demand::Fact,
780 request: &Request,
781) -> bool {
782 scoped_packet_matches_request_scope(
783 fact.operating_day().location_id(),
784 fact.operating_day().date(),
785 request,
786 )
787}
788
789fn scoped_packet_matches_request_scope(
790 location_id: entities::LocationId,
791 operating_day: operations::operating_day::Date,
792 request: &Request,
793) -> bool {
794 location_id == request.location_id && operating_day == request.operating_day
795}
796
797fn checkout_exception_actions(request: &Request) -> Vec<BriefAction> {
798 request
799 .checkout_packets
800 .iter()
801 .filter(|scoped| scoped_packet_matches_request_scope(scoped.location_id(), scoped.operating_day(), request))
802 .map(ScopedCheckoutPacket::packet)
803 .filter(|packet| {
804 !matches!(
805 packet.completion_status(),
806 checkout_completion::CompletionStatus::StaffVerifiedCheckout
807 )
808 })
809 .map(|packet| {
810 BriefAction::builder()
811 .id(ActionId::try_new(format!(
812 "checkout-exception-{:?}",
813 packet.reservation_id()
814 ))
815 .expect("formatted reservation ids are non-empty"))
816 .kind(BriefActionKind::ResolveCheckoutException)
817 .priority(BriefActionPriority::High)
818 .owner_persona(ManagerBriefPersona::FrontDeskLead)
819 .removed_manual_work(RemovedManualWork::CheckoutExceptionAudit)
820 .rationale(ActionRationale::try_new("Front desk lead receives the unresolved checkout handoff instead of auditing open reservations one by one.").expect("static rationale is valid"))
821 .source_facts(vec![SourceFact::builder()
822 .kind(SourceFactKind::CheckoutCompletionStatus)
823 .summary(BriefSummary::try_new("Checkout/completion contract says this stay still needs staff or manager review.").expect("static brief summary is valid"))
824 .source_record_refs(vec![source::RecordRef::from_provenance(packet.provenance())])
825 .build()])
826 .labor_impact(LaborImpactEstimate::new(
827 LaborMinutes::try_new(20).expect("static minutes are valid"),
828 LaborMinutes::try_new(8).expect("static minutes are valid"),
829 ))
830 .required_review_gates(packet.required_review_gates().to_vec())
831 .build()
832 })
833 .collect()
834}
835
836fn retention_actions(request: &Request) -> Vec<BriefAction> {
837 request
838 .retention_packets
839 .iter()
840 .filter(|scoped| scoped_packet_matches_request_scope(scoped.location_id(), scoped.operating_day(), request))
841 .map(ScopedRetentionPacket::packet)
842 .filter(|packet| {
843 matches!(
844 packet.eligibility(),
845 crm_retention::FollowUpEligibility::Eligible { .. }
846 )
847 })
848 .map(|packet| {
849 let source_record_refs = packet
850 .review_packet()
851 .staff_evidence()
852 .iter()
853 .map(|evidence| source::RecordRef::from_provenance(evidence.provenance()))
854 .chain(packet.source_record_refs().iter().cloned())
855 .collect::<Vec<_>>();
856
857 BriefAction::builder()
858 .id(ActionId::try_new(format!(
859 "retention-follow-up-{:?}",
860 packet.reservation_id()
861 ))
862 .expect("formatted reservation ids are non-empty"))
863 .kind(BriefActionKind::ApproveRetentionFollowUpDraft)
864 .priority(BriefActionPriority::Medium)
865 .owner_persona(ManagerBriefPersona::FrontDeskLead)
866 .removed_manual_work(RemovedManualWork::RetentionFollowUpQueuePrioritization)
867 .rationale(ActionRationale::try_new("Front desk lead receives eligible source-grounded retention opportunities instead of manually scanning completed stays for follow-up candidates.").expect("static rationale is valid"))
868 .source_facts(vec![SourceFact::builder()
869 .kind(SourceFactKind::RetentionFollowUpEligibility)
870 .summary(BriefSummary::try_new("CRM/retention contract says this stay has an eligible draft-only follow-up opportunity.").expect("static brief summary is valid"))
871 .source_record_refs(source_record_refs)
872 .build()])
873 .labor_impact(LaborImpactEstimate::new(
874 LaborMinutes::try_new(30).expect("static minutes are valid"),
875 LaborMinutes::try_new(10).expect("static minutes are valid"),
876 ))
877 .required_review_gates(packet.required_review_gates().to_vec())
878 .build()
879 })
880 .collect()
881}
882
883fn total_before_minutes(actions: &[BriefAction]) -> AggregateLaborMinutes {
884 AggregateLaborMinutes::new(
885 actions
886 .iter()
887 .map(|action| action.labor_impact.before_minutes.get())
888 .sum::<u16>(),
889 )
890}
891
892fn total_after_minutes(actions: &[BriefAction]) -> AggregateLaborMinutes {
893 AggregateLaborMinutes::new(
894 actions
895 .iter()
896 .map(|action| action.labor_impact.after_minutes.get())
897 .sum::<u16>(),
898 )
899}
900
901fn blocked_actions_for() -> Vec<BlockedAction> {
902 vec![
903 BlockedAction::ChangeStaffSchedule,
904 BlockedAction::MutateProviderOrPmsRecord,
905 BlockedAction::SendCustomerMessage,
906 BlockedAction::MoveRefundDiscountOrPayment,
907 BlockedAction::HideSourceDataQualityIssue,
908 ]
909}