Skip to main content

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}