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