Skip to main content

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}