Skip to main content

app/
booking_triage.rs

1//! Booking triage rules for deterministic review before agent drafting.
2//!
3//! ## Operator summary
4//!
5//! Staff use booking triage to decide which reservation queue owns the next action: ready for
6//! staff approval, missing information, vaccine/document review, special care/behavior/payment
7//! review, waitlist/availability review, or failed-safe data cleanup. The workflow reduces labor
8//! by assembling source-backed reservation, pet-profile, policy, deposit, and hard-stop evidence
9//! into one staff packet before any agent drafts customer-safe language.
10//!
11//! Booking triage is not allowed to confirm or reject a booking, promise availability, hold or
12//! release capacity, assign rooms or play groups, mutate provider/PMS records, send customer
13//! messages, clear vaccine/care/behavior exceptions, or move payment/deposit money. Provider/PMS
14//! lifecycle state, approved location policy, verified vaccine/document facts, trusted payment
15//! records, staff approvals, and source snapshots remain authoritative. Review gates protect pets,
16//! customers, and staff whenever facts are missing, stale, conflicting, sensitive, payment-related,
17//! or require manager/care/medical/behavior/customer-message approval.
18//!
19//! The typestate request machine models the safe sequence for triage evidence:
20//! intake, pet profile attachment, reservation fact attachment, deterministic
21//! review, and staff-ready handoff. The machine's macro helper pages are a
22//! `statum` implementation detail; this module documents the operational rules
23//! here and on the source state variants so external readers understand that the
24//! `Request`/state APIs emitted by the macro enforce evidence order rather than granting live
25//! booking authority.
26//! ```
27//! use app::booking_triage as triage;
28//!
29//! let vaccine_review = triage::rule::ReviewFinding::builder()
30//!     .rule_id(triage::rule::Id::VaccineRequirements)
31//!     .failure_code(triage::FailureCode::MissingOrUnverifiedVaccine)
32//!     .readiness_bucket(triage::ReadinessBucket::VaccinePending)
33//!     .human_approval_required(triage::ApprovalGate::MedicalDocumentReview)
34//!     .evidence_refs(vec![triage::EvidenceRef::try_new(
35//!         "gingr:reservation:fixture-123:vaccine-expired",
36//!     )?])
37//!     .build();
38//!
39//! let deterministic = triage::DeterministicResult::evaluate(vec![
40//!     triage::rule::Evaluation::needs_human_approval(vaccine_review),
41//! ]);
42//!
43//! assert_eq!(deterministic.recommended_status(), triage::ReadinessBucket::VaccinePending);
44//! assert!(deterministic.requires(triage::ApprovalGate::MedicalDocumentReview));
45//! assert_eq!(
46//!     deterministic.staff_decision_boundary(),
47//!     triage::StaffDecisionBoundary::ReviewPacketOnly,
48//! );
49//! assert!(deterministic.blocked_actions().contains(&triage::BlockedAction::ConfirmBooking));
50//! assert!(deterministic.blocked_actions().contains(&triage::BlockedAction::SendCustomerMessage));
51//! assert!(deterministic.blocked_actions().contains(&triage::BlockedAction::MutateProviderRecord));
52//!
53//! let packet = triage::StaffEvaluationPacket::new(
54//!     triage::Reservation::try_new("reservation-fixture-123")?,
55//!     deterministic,
56//! );
57//! let draft = triage::ConfirmationDraft::new(
58//!     triage::CustomerMessageDraft::try_new("We can draft this only after staff review.")?,
59//! );
60//!
61//! assert_eq!(
62//!     packet.try_with_confirmation_draft(draft).unwrap_err(),
63//!     triage::ConfirmationDraftError::DeterministicGateNotReadyForDraft,
64//! );
65//! # Ok::<(), Box<dyn std::error::Error>>(())
66//! ```
67use nutype::nutype;
68use serde::{Deserialize, Serialize};
69use statum::{machine, state, transition};
70
71use domain::entities::reservation as reservation_entity;
72use domain::{entities, pet};
73
74#[nutype(
75    sanitize(trim),
76    validate(not_empty, len_char_max = 80),
77    derive(
78        Debug,
79        Clone,
80        PartialEq,
81        Eq,
82        PartialOrd,
83        Ord,
84        Hash,
85        Serialize,
86        Deserialize
87    )
88)]
89pub struct Reservation(String);
90
91#[nutype(
92    sanitize(trim),
93    validate(not_empty, len_char_max = 160),
94    derive(
95        Debug,
96        Clone,
97        PartialEq,
98        Eq,
99        PartialOrd,
100        Ord,
101        Hash,
102        Serialize,
103        Deserialize
104    )
105)]
106pub struct PolicySnapshot(String);
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109/// Classifies pet profile completeness values that drive the booking-readiness workflow.
110pub enum PetProfileCompleteness {
111    /// Routes booking triage work flagged as complete to the right queue, review gate, or agent packet.
112    Complete,
113    /// Routes booking triage work flagged as missing required fields to the right queue, review gate, or agent packet.
114    MissingRequiredFields,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118/// Pet profile used by the booking-readiness workflow; it keeps booking work grounded in deterministic policy evidence before any agent draft reaches staff.
119pub struct PetProfile {
120    /// Name preserved as evidence for audit, review, or agent context.
121    pub name: pet::Name,
122    /// Completeness preserved as evidence for audit, review, or agent context.
123    pub completeness: PetProfileCompleteness,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127/// Policy attached data used by the booking-readiness workflow; it keeps booking work grounded in deterministic policy evidence before any agent draft reaches staff.
128pub struct PolicyAttachedData {
129    /// Pet profile preserved as evidence for audit, review, or agent context.
130    pub pet_profile: PetProfile,
131    /// Policy snapshot preserved as evidence for audit, review, or agent context.
132    pub policy_snapshot: PolicySnapshot,
133}
134
135mod request_typestate {
136    #![allow(missing_docs)]
137
138    use super::*;
139
140    /// Typestate markers for booking-triage request progress.
141    ///
142    /// The variants record which source-backed prerequisites are present before
143    /// staff or an agent can evaluate booking readiness. The surrounding module
144    /// allows missing docs only for undocumented public helper items generated by
145    /// `statum` from this documented source enum.
146    #[state]
147    #[derive(Debug, Clone, PartialEq, Eq)]
148    pub enum RequestState {
149        /// The intake exists, but no pet profile evidence has been attached yet.
150        Intake,
151        /// A source-backed pet profile has been attached for policy checks.
152        PetProfileAttached(PetProfile),
153        /// A policy snapshot has been attached alongside the pet profile.
154        PolicyAttached(PolicyAttachedData),
155        /// All deterministic inputs required for policy decisioning are present.
156        ReadyForPolicyDecision(PolicyAttachedData),
157    }
158
159    /// Typestate request machine for booking-triage intake, policy attachment, and decisioning.
160    ///
161    /// The state-specific request types emitted by the macro enforce the ordering of evidence
162    /// attachment in code: intake first, then pet profile evidence, then policy
163    /// evidence, and only then a packet ready for deterministic staff review. The
164    /// machine stores source facts but does not confirm bookings, send customer
165    /// messages, or mutate a provider/PMS record.
166    #[machine]
167    #[derive(Debug, Clone, PartialEq, Eq)]
168    pub struct Request<RequestState> {
169        /// Source reservation label or identifier that the typed request evaluates.
170        pub(super) reservation: Reservation,
171    }
172
173    #[transition]
174    impl Request<Intake> {
175        /// Attaches pet profile evidence before the request can move to policy decisioning.
176        pub fn attach_pet_profile(
177            self,
178            name: pet::Name,
179            completeness: PetProfileCompleteness,
180        ) -> Request<PetProfileAttached> {
181            self.transition_with(PetProfile { name, completeness })
182        }
183    }
184
185    #[transition]
186    impl Request<PetProfileAttached> {
187        /// Attaches policy snapshot evidence before the request can move to policy decisioning.
188        pub fn attach_policy_snapshot(
189            self,
190            policy_snapshot: PolicySnapshot,
191        ) -> Request<PolicyAttached> {
192            let pet_profile = self.state_data.clone();
193            self.transition_with(PolicyAttachedData {
194                pet_profile,
195                policy_snapshot,
196            })
197        }
198    }
199
200    #[transition]
201    impl Request<PolicyAttached> {
202        /// Marks the packet as ready for policy decision once required evidence has been attached.
203        pub fn mark_ready_for_policy_decision(self) -> Request<ReadyForPolicyDecision> {
204            let ready_data = self.state_data.clone();
205            self.transition_with(ready_data)
206        }
207    }
208}
209
210pub use request_typestate::{
211    Intake, PetProfileAttached, PolicyAttached, ReadyForPolicyDecision, Request, RequestState,
212    RequestStateTrait,
213};
214
215impl<S: RequestStateTrait> Request<S> {
216    /// Returns the reservation identifier this booking-readiness packet is evaluating.
217    pub fn reservation(&self) -> &Reservation {
218        &self.reservation
219    }
220}
221
222#[nutype(
223    sanitize(trim),
224    validate(not_empty, len_char_max = 180),
225    derive(
226        Debug,
227        Clone,
228        PartialEq,
229        Eq,
230        PartialOrd,
231        Ord,
232        Hash,
233        Serialize,
234        Deserialize
235    )
236)]
237pub struct EvidenceRef(String);
238
239#[nutype(
240    sanitize(trim),
241    validate(not_empty, len_char_max = 1000),
242    derive(
243        Debug,
244        Clone,
245        PartialEq,
246        Eq,
247        PartialOrd,
248        Ord,
249        Hash,
250        Serialize,
251        Deserialize
252    )
253)]
254pub struct RecommendationText(String);
255
256#[nutype(
257    sanitize(trim),
258    validate(not_empty, len_char_max = 1200),
259    derive(
260        Debug,
261        Clone,
262        PartialEq,
263        Eq,
264        PartialOrd,
265        Ord,
266        Hash,
267        Serialize,
268        Deserialize
269    )
270)]
271pub struct CustomerMessageDraft(String);
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
274/// Deterministic booking status bucket used to prioritize staff review.
275pub enum ReadinessBucket {
276    /// Prioritizes reservations that are ready for staff approval for staff triage queues.
277    ReadyForStaffApproval,
278    /// Prioritizes reservations that are missing info for staff triage queues.
279    MissingInfo,
280    /// Prioritizes reservations that are vaccine pending for staff triage queues.
281    VaccinePending,
282    /// Prioritizes reservations that are special review for staff triage queues.
283    SpecialReview,
284    /// Prioritizes reservations that are waitlisted for staff triage queues.
285    Waitlisted,
286    /// Prioritizes reservations that are offered for staff triage queues.
287    Offered,
288    /// Prioritizes reservations that are confirmed for staff triage queues.
289    Confirmed,
290    /// Prioritizes reservations that are rejected for staff triage queues.
291    Rejected,
292    /// Prioritizes reservations that are failed safely for staff triage queues.
293    FailedSafely,
294}
295
296impl ReadinessBucket {
297    const fn priority(self) -> u8 {
298        match self {
299            Self::Rejected => 95,
300            Self::FailedSafely => 90,
301            Self::SpecialReview => 80,
302            Self::VaccinePending => 70,
303            Self::MissingInfo => 60,
304            Self::Waitlisted => 50,
305            Self::Offered => 40,
306            Self::Confirmed => 30,
307            Self::ReadyForStaffApproval => 10,
308        }
309    }
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
313/// Human approval checkpoints that must clear before the workflow can advance.
314pub enum ApprovalGate {
315    /// Requires none before staff can rely on the packet for the next workflow step.
316    None,
317    /// Requires staff approval before staff can rely on the packet for the next workflow step.
318    StaffApproval,
319    /// Requires manager approval before staff can rely on the packet for the next workflow step.
320    ManagerApproval,
321    /// Requires medical document review before staff can rely on the packet for the next workflow step.
322    MedicalDocumentReview,
323    /// Requires behavior review before staff can rely on the packet for the next workflow step.
324    BehaviorReview,
325    /// Requires care team approval before staff can rely on the packet for the next workflow step.
326    CareTeamApproval,
327    /// Requires payment manager approval before staff can rely on the packet for the next workflow step.
328    PaymentManagerApproval,
329    /// Requires customer message approval before staff can rely on the packet for the next workflow step.
330    CustomerMessageApproval,
331    /// Requires confirmed booking automation before staff can rely on the packet for the next workflow step.
332    ConfirmedBookingAutomation,
333    /// Requires rejection approval before staff can rely on the packet for the next workflow step.
334    RejectionApproval,
335}
336
337#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
338/// Classifies failure code values that drive the booking-readiness workflow.
339pub enum FailureCode {
340    /// Identifies missing required input as the reason the workflow must stop, retry, or request review.
341    MissingRequiredInput,
342    /// Identifies stale snapshot as the reason the workflow must stop, retry, or request review.
343    StaleSnapshot,
344    /// Identifies conflicting source as the reason the workflow must stop, retry, or request review.
345    ConflictingSource,
346    /// Identifies unmapped provider value as the reason the workflow must stop, retry, or request review.
347    UnmappedProviderValue,
348    /// Identifies missing policy as the reason the workflow must stop, retry, or request review.
349    MissingPolicy,
350    /// Identifies capacity unavailable as the reason the workflow must stop, retry, or request review.
351    CapacityUnavailable,
352    /// Identifies policy hard stop as the reason the workflow must stop, retry, or request review.
353    PolicyHardStop,
354    /// Identifies missing or unverified vaccine as the reason the workflow must stop, retry, or request review.
355    MissingOrUnverifiedVaccine,
356    /// Identifies deposit not satisfied as the reason the workflow must stop, retry, or request review.
357    DepositNotSatisfied,
358    /// Identifies behavior exception requires review as the reason the workflow must stop, retry, or request review.
359    BehaviorExceptionRequiresReview,
360    /// Identifies special care requires review as the reason the workflow must stop, retry, or request review.
361    SpecialCareRequiresReview,
362}
363
364#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
365/// Review-safe agent tasks allowed to save staff time without crossing mutation or send gates.
366pub enum SafeAgentAction {
367    /// Allows agents to evidence summary for staff review without mutating records or contacting customers.
368    EvidenceSummary,
369    /// Allows agents to internal task draft for staff review without mutating records or contacting customers.
370    InternalTaskDraft,
371    /// Allows agents to manager packet draft for staff review without mutating records or contacting customers.
372    ManagerPacketDraft,
373    /// Allows agents to customer safe script draft for staff review without mutating records or contacting customers.
374    CustomerSafeScriptDraft,
375    /// Allows agents to missing info request draft for staff review without mutating records or contacting customers.
376    MissingInfoRequestDraft,
377}
378
379#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
380/// Actions the agent must never perform without a human/operator system of record.
381pub enum BlockedAction {
382    /// Blocks agents from confirm booking until staff or the system of record performs the action.
383    ConfirmBooking,
384    /// Blocks agents from reject request until staff or the system of record performs the action.
385    RejectRequest,
386    /// Blocks agents from accept special care until staff or the system of record performs the action.
387    AcceptSpecialCare,
388    /// Blocks agents from approve behavior exception until staff or the system of record performs the action.
389    ApproveBehaviorException,
390    /// Blocks agents from mutate provider record until staff or the system of record performs the action.
391    MutateProviderRecord,
392    /// Blocks agents from send customer message until staff or the system of record performs the action.
393    SendCustomerMessage,
394    /// Blocks agents from move payment until staff or the system of record performs the action.
395    MovePayment,
396}
397
398#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
399/// How far the packet may advance before a staff decision is required.
400pub enum StaffDecisionBoundary {
401    /// Limits the packet to draft confirmation allowed so agents stay inside the approved handoff gate.
402    DraftConfirmationAllowed,
403    /// Limits the packet to review packet only so agents stay inside the approved handoff gate.
404    ReviewPacketOnly,
405}
406
407#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
408/// Classifies confirmation draft error values that drive the booking-readiness workflow.
409pub enum ConfirmationDraftError {
410    /// Identifies deterministic gate not ready for draft as the reason the workflow must stop, retry, or request review.
411    DeterministicGateNotReadyForDraft,
412}
413
414/// Deterministic booking rules that explain readiness findings and safe agent actions.
415pub mod rule {
416    use bon::Builder;
417    use serde::{Deserialize, Serialize};
418
419    use super::{ApprovalGate, EvidenceRef, FailureCode, ReadinessBucket, SafeAgentAction};
420
421    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
422    /// Classifies id values that drive the booking-readiness workflow.
423    pub enum Id {
424        /// Routes booking triage work flagged as date range and service supported to the right queue, review gate, or agent packet.
425        DateRangeAndServiceSupported,
426        /// Routes booking triage work flagged as accommodation availability to the right queue, review gate, or agent packet.
427        AccommodationAvailability,
428        /// Routes booking triage work flagged as size capacity room or group fit to the right queue, review gate, or agent packet.
429        SizeCapacityRoomOrGroupFit,
430        /// Routes booking triage work flagged as service capacity and addons to the right queue, review gate, or agent packet.
431        ServiceCapacityAndAddons,
432        /// Routes booking triage work flagged as vaccine requirements to the right queue, review gate, or agent packet.
433        VaccineRequirements,
434        /// Routes booking triage work flagged as vaccine pending handling to the right queue, review gate, or agent packet.
435        VaccinePendingHandling,
436        /// Routes booking triage work flagged as deposit and pricing requirements to the right queue, review gate, or agent packet.
437        DepositAndPricingRequirements,
438        /// Routes booking triage work flagged as holiday blackout minimum stay to the right queue, review gate, or agent packet.
439        HolidayBlackoutMinimumStay,
440        /// Routes booking triage work flagged as staff coverage constraints to the right queue, review gate, or agent packet.
441        StaffCoverageConstraints,
442        /// Routes booking triage work flagged as behavior restrictions to the right queue, review gate, or agent packet.
443        BehaviorRestrictions,
444        /// Routes booking triage work flagged as anxiety aggression exception handling to the right queue, review gate, or agent packet.
445        AnxietyAggressionExceptionHandling,
446        /// Routes booking triage work flagged as medication special care limits to the right queue, review gate, or agent packet.
447        MedicationSpecialCareLimits,
448        /// Routes booking triage work flagged as multi pet constraints to the right queue, review gate, or agent packet.
449        MultiPetConstraints,
450        /// Routes booking triage work flagged as late pickup checkout impact to the right queue, review gate, or agent packet.
451        LatePickupCheckoutImpact,
452    }
453
454    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
455    /// Classifies decision values that drive the booking-readiness workflow.
456    pub enum Decision {
457        /// Routes booking triage work flagged as pass to the right queue, review gate, or agent packet.
458        Pass,
459        /// Routes booking triage work flagged as hard block to the right queue, review gate, or agent packet.
460        HardBlock,
461        /// Routes booking triage work flagged as needs human approval to the right queue, review gate, or agent packet.
462        NeedsHumanApproval,
463        /// Routes booking triage work flagged as unknown to the right queue, review gate, or agent packet.
464        Unknown,
465        /// Routes booking triage work flagged as not applicable to the right queue, review gate, or agent packet.
466        NotApplicable,
467    }
468
469    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
470    /// Review finding used by the booking-readiness workflow; it keeps booking work grounded in deterministic policy evidence before any agent draft reaches staff.
471    pub struct ReviewFinding {
472        /// Rule id preserved as evidence for audit, review, or agent context.
473        pub rule_id: Id,
474        /// Failure code preserved as evidence for audit, review, or agent context.
475        pub failure_code: FailureCode,
476        /// Readiness bucket preserved as evidence for audit, review, or agent context.
477        pub readiness_bucket: ReadinessBucket,
478        /// Human approval required preserved as evidence for audit, review, or agent context.
479        pub human_approval_required: ApprovalGate,
480        #[builder(default)]
481        /// Evidence refs preserved as evidence for audit, review, or agent context.
482        pub evidence_refs: Vec<EvidenceRef>,
483    }
484
485    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
486    /// Evaluation used by the booking-readiness workflow; it keeps booking work grounded in deterministic policy evidence before any agent draft reaches staff.
487    pub struct Evaluation {
488        /// Rule id preserved as evidence for audit, review, or agent context.
489        pub rule_id: Id,
490        /// Decision preserved as evidence for audit, review, or agent context.
491        pub decision: Decision,
492        /// Readiness bucket preserved as evidence for audit, review, or agent context.
493        pub readiness_bucket: ReadinessBucket,
494        /// Evidence refs preserved as evidence for audit, review, or agent context.
495        pub evidence_refs: Vec<EvidenceRef>,
496        /// Failure code preserved as evidence for audit, review, or agent context.
497        pub failure_code: Option<FailureCode>,
498        /// Human approval required preserved as evidence for audit, review, or agent context.
499        pub human_approval_required: ApprovalGate,
500        /// Safe agent actions preserved as evidence for audit, review, or agent context.
501        pub safe_agent_actions: Vec<SafeAgentAction>,
502    }
503
504    impl Evaluation {
505        /// Builds or derives pass data for the booking-readiness workflow's reviewed decision model.
506        pub fn pass(rule_id: Id, evidence_refs: Vec<EvidenceRef>) -> Self {
507            Self {
508                rule_id,
509                decision: Decision::Pass,
510                readiness_bucket: ReadinessBucket::ReadyForStaffApproval,
511                evidence_refs,
512                failure_code: None,
513                human_approval_required: ApprovalGate::None,
514                safe_agent_actions: vec![SafeAgentAction::EvidenceSummary],
515            }
516        }
517
518        /// Builds or derives unknown data for the booking-readiness workflow's reviewed decision model.
519        pub fn unknown(finding: ReviewFinding) -> Self {
520            Self::blocked_or_review(finding, Decision::Unknown)
521        }
522
523        /// Builds or derives needs human approval data for the booking-readiness workflow's reviewed decision model.
524        pub fn needs_human_approval(finding: ReviewFinding) -> Self {
525            Self::blocked_or_review(finding, Decision::NeedsHumanApproval)
526        }
527
528        /// Builds or derives hard block data for the booking-readiness workflow's reviewed decision model.
529        pub fn hard_block(finding: ReviewFinding) -> Self {
530            Self::blocked_or_review(finding, Decision::HardBlock)
531        }
532
533        fn blocked_or_review(finding: ReviewFinding, decision: Decision) -> Self {
534            Self {
535                rule_id: finding.rule_id,
536                decision,
537                readiness_bucket: finding.readiness_bucket,
538                evidence_refs: finding.evidence_refs,
539                failure_code: Some(finding.failure_code),
540                human_approval_required: finding.human_approval_required,
541                safe_agent_actions: vec![
542                    SafeAgentAction::EvidenceSummary,
543                    SafeAgentAction::InternalTaskDraft,
544                    SafeAgentAction::ManagerPacketDraft,
545                ],
546            }
547        }
548    }
549}
550
551#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
552/// Deterministic result used by the booking-readiness workflow; it keeps booking work grounded in deterministic policy evidence before any agent draft reaches staff.
553pub struct DeterministicResult {
554    rule_evaluations: Vec<rule::Evaluation>,
555    recommended_status: ReadinessBucket,
556    approval_gates: Vec<ApprovalGate>,
557    blocked_actions: Vec<BlockedAction>,
558}
559
560impl DeterministicResult {
561    /// Builds or derives evaluate data for the booking-readiness workflow's reviewed decision model.
562    pub fn evaluate(rule_evaluations: Vec<rule::Evaluation>) -> Self {
563        let recommended_status = rule_evaluations
564            .iter()
565            .map(|rule| rule.readiness_bucket)
566            .max_by_key(|status| status.priority())
567            .unwrap_or(ReadinessBucket::MissingInfo);
568
569        let mut approval_gates: Vec<ApprovalGate> = rule_evaluations
570            .iter()
571            .map(|rule| rule.human_approval_required)
572            .filter(|gate| *gate != ApprovalGate::None)
573            .collect();
574        approval_gates.sort_unstable();
575        approval_gates.dedup();
576
577        let mut blocked_actions = vec![
578            BlockedAction::ConfirmBooking,
579            BlockedAction::RejectRequest,
580            BlockedAction::MutateProviderRecord,
581            BlockedAction::SendCustomerMessage,
582        ];
583        if approval_gates.contains(&ApprovalGate::BehaviorReview) {
584            blocked_actions.push(BlockedAction::ApproveBehaviorException);
585        }
586        if approval_gates.contains(&ApprovalGate::CareTeamApproval) {
587            blocked_actions.push(BlockedAction::AcceptSpecialCare);
588        }
589        if approval_gates.contains(&ApprovalGate::PaymentManagerApproval) {
590            blocked_actions.push(BlockedAction::MovePayment);
591        }
592        blocked_actions.sort_unstable();
593        blocked_actions.dedup();
594
595        Self {
596            rule_evaluations,
597            recommended_status,
598            approval_gates,
599            blocked_actions,
600        }
601    }
602
603    /// Returns the recommended status value kept on this booking-readiness workflow object for staff review and agent context.
604    pub const fn recommended_status(&self) -> ReadinessBucket {
605        self.recommended_status
606    }
607
608    /// Reports whether the booking-readiness workflow satisfies the requires safety condition.
609    pub fn requires(&self, gate: ApprovalGate) -> bool {
610        self.approval_gates.contains(&gate)
611    }
612
613    /// Returns the blocked actions value kept on this booking-readiness workflow object for staff review and agent context.
614    pub fn blocked_actions(&self) -> &[BlockedAction] {
615        &self.blocked_actions
616    }
617
618    /// Returns the rule evaluations value kept on this booking-readiness workflow object for staff review and agent context.
619    pub fn rule_evaluations(&self) -> &[rule::Evaluation] {
620        &self.rule_evaluations
621    }
622
623    /// Returns the staff may confirm without human gate value kept on this booking-readiness workflow object for staff review and agent context.
624    pub fn staff_may_confirm_without_human_gate(&self) -> bool {
625        matches!(
626            self.recommended_status,
627            ReadinessBucket::ReadyForStaffApproval
628        ) && self.approval_gates.is_empty()
629    }
630
631    /// Returns the staff decision gate value kept on this booking-readiness workflow object for staff review and agent context.
632    pub const fn staff_decision_boundary(&self) -> StaffDecisionBoundary {
633        match self.recommended_status {
634            ReadinessBucket::ReadyForStaffApproval | ReadinessBucket::Offered => {
635                StaffDecisionBoundary::DraftConfirmationAllowed
636            }
637            _ => StaffDecisionBoundary::ReviewPacketOnly,
638        }
639    }
640}
641
642#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
643/// Classifies agent recommended action values that drive the booking-readiness workflow.
644pub enum AgentRecommendedAction {
645    /// Routes booking triage work flagged as draft confirmation for staff approval to the right queue, review gate, or agent packet.
646    DraftConfirmationForStaffApproval,
647    /// Routes booking triage work flagged as draft missing info request to the right queue, review gate, or agent packet.
648    DraftMissingInfoRequest,
649    /// Routes booking triage work flagged as draft review packet to the right queue, review gate, or agent packet.
650    DraftReviewPacket,
651}
652
653#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
654/// Ai recommendation used by the booking-readiness workflow; it keeps booking work grounded in deterministic policy evidence before any agent draft reaches staff.
655pub struct AiRecommendation {
656    recommended_action: AgentRecommendedAction,
657    rationale: RecommendationText,
658}
659
660impl AiRecommendation {
661    /// Builds the booking-triage service around a read-only reservation evidence repository.
662    pub const fn new(
663        recommended_action: AgentRecommendedAction,
664        rationale: RecommendationText,
665    ) -> Self {
666        Self {
667            recommended_action,
668            rationale,
669        }
670    }
671
672    /// Builds or derives recommend staff confirmation data for the booking-readiness workflow's reviewed decision model.
673    pub const fn recommend_staff_confirmation(rationale: RecommendationText) -> Self {
674        Self::new(
675            AgentRecommendedAction::DraftConfirmationForStaffApproval,
676            rationale,
677        )
678    }
679
680    /// Returns the recommended action value kept on this booking-readiness workflow object for staff review and agent context.
681    pub const fn recommended_action(&self) -> AgentRecommendedAction {
682        self.recommended_action
683    }
684
685    /// Returns the rationale value kept on this booking-readiness workflow object for staff review and agent context.
686    pub const fn rationale(&self) -> &RecommendationText {
687        &self.rationale
688    }
689}
690
691#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
692/// Confirmation draft used by the booking-readiness workflow; it keeps booking work grounded in deterministic policy evidence before any agent draft reaches staff.
693pub struct ConfirmationDraft {
694    body: CustomerMessageDraft,
695    approval_gate: ApprovalGate,
696}
697
698impl ConfirmationDraft {
699    /// Builds the booking-triage service around a read-only reservation evidence repository.
700    pub const fn new(body: CustomerMessageDraft) -> Self {
701        Self {
702            body,
703            approval_gate: ApprovalGate::CustomerMessageApproval,
704        }
705    }
706
707    /// Returns the body value kept on this booking-readiness workflow object for staff review and agent context.
708    pub const fn body(&self) -> &CustomerMessageDraft {
709        &self.body
710    }
711
712    /// Returns the approval gate value kept on this booking-readiness workflow object for staff review and agent context.
713    pub const fn approval_gate(&self) -> ApprovalGate {
714        self.approval_gate
715    }
716}
717
718#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
719/// Classifies audit event draft values that drive the booking-readiness workflow.
720pub enum AuditEventDraft {
721    /// Routes booking triage work flagged as policy decision recorded to the right queue, review gate, or agent packet.
722    PolicyDecisionRecorded,
723    /// Routes booking triage work flagged as reservation status suggested to the right queue, review gate, or agent packet.
724    ReservationStatusSuggested,
725    /// Routes booking triage work flagged as confirmation draft generated to the right queue, review gate, or agent packet.
726    ConfirmationDraftGenerated,
727    /// Routes booking triage work flagged as message approval requested to the right queue, review gate, or agent packet.
728    MessageApprovalRequested,
729}
730
731#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
732/// Staff evaluation packet used by the booking-readiness workflow; it keeps booking work grounded in deterministic policy evidence before any agent draft reaches staff.
733pub struct StaffEvaluationPacket {
734    reservation: Reservation,
735    deterministic_result: DeterministicResult,
736    ai_recommendation: Option<AiRecommendation>,
737    confirmation_draft: Option<ConfirmationDraft>,
738    audit_event_drafts: Vec<AuditEventDraft>,
739}
740
741impl StaffEvaluationPacket {
742    /// Builds the booking-triage service around a read-only reservation evidence repository.
743    pub fn new(reservation: Reservation, deterministic_result: DeterministicResult) -> Self {
744        Self {
745            reservation,
746            deterministic_result,
747            ai_recommendation: None,
748            confirmation_draft: None,
749            audit_event_drafts: vec![AuditEventDraft::PolicyDecisionRecorded],
750        }
751    }
752
753    /// Returns the with ai recommendation value kept on this booking-readiness workflow object for staff review and agent context.
754    pub fn with_ai_recommendation(mut self, ai_recommendation: AiRecommendation) -> Self {
755        self.ai_recommendation = Some(ai_recommendation);
756        self.audit_event_drafts
757            .push(AuditEventDraft::ReservationStatusSuggested);
758        self.dedup_audit_event_drafts();
759        self
760    }
761
762    /// Returns the with confirmation draft value kept on this booking-readiness workflow object for staff review and agent context.
763    pub fn with_confirmation_draft(mut self, confirmation_draft: ConfirmationDraft) -> Self {
764        self = self
765            .try_with_confirmation_draft(confirmation_draft)
766            .expect("confirmation drafts require ready/offered deterministic gates");
767        self
768    }
769
770    /// Attempts to advance the booking-readiness workflow while preserving deterministic safety gates.
771    pub fn try_with_confirmation_draft(
772        mut self,
773        confirmation_draft: ConfirmationDraft,
774    ) -> core::result::Result<Self, ConfirmationDraftError> {
775        if self.deterministic_result.staff_decision_boundary()
776            != StaffDecisionBoundary::DraftConfirmationAllowed
777        {
778            return Err(ConfirmationDraftError::DeterministicGateNotReadyForDraft);
779        }
780        self.confirmation_draft = Some(confirmation_draft);
781        self.audit_event_drafts
782            .push(AuditEventDraft::ConfirmationDraftGenerated);
783        self.audit_event_drafts
784            .push(AuditEventDraft::MessageApprovalRequested);
785        self.dedup_audit_event_drafts();
786        Ok(self)
787    }
788
789    /// Returns the reservation value kept on this booking-readiness workflow object for staff review and agent context.
790    pub const fn reservation(&self) -> &Reservation {
791        &self.reservation
792    }
793
794    /// Returns the deterministic result value kept on this booking-readiness workflow object for staff review and agent context.
795    pub const fn deterministic_result(&self) -> &DeterministicResult {
796        &self.deterministic_result
797    }
798
799    /// Returns the ai recommendation value kept on this booking-readiness workflow object for staff review and agent context.
800    pub fn ai_recommendation(&self) -> &AiRecommendation {
801        self.ai_recommendation
802            .as_ref()
803            .expect("staff evaluation packet should include an AI recommendation")
804    }
805
806    /// Returns the confirmation draft value kept on this booking-readiness workflow object for staff review and agent context.
807    pub fn confirmation_draft(&self) -> &ConfirmationDraft {
808        self.confirmation_draft
809            .as_ref()
810            .expect("staff evaluation packet should include a confirmation draft")
811    }
812
813    /// Returns the audit event drafts value kept on this booking-readiness workflow object for staff review and agent context.
814    pub fn audit_event_drafts(&self) -> &[AuditEventDraft] {
815        &self.audit_event_drafts
816    }
817
818    /// Returns the suggested status value kept on this booking-readiness workflow object for staff review and agent context.
819    pub const fn suggested_status(&self) -> reservation_entity::Status {
820        match self.deterministic_result.recommended_status {
821            ReadinessBucket::ReadyForStaffApproval => reservation_entity::Status::Offered,
822            ReadinessBucket::MissingInfo => reservation_entity::Status::MissingInfo,
823            ReadinessBucket::VaccinePending => reservation_entity::Status::VaccinePending,
824            ReadinessBucket::SpecialReview => reservation_entity::Status::SpecialReview,
825            ReadinessBucket::Waitlisted => reservation_entity::Status::Waitlisted,
826            ReadinessBucket::Offered => reservation_entity::Status::Offered,
827            ReadinessBucket::Confirmed => reservation_entity::Status::Offered,
828            ReadinessBucket::Rejected => reservation_entity::Status::SpecialReview,
829            ReadinessBucket::FailedSafely => reservation_entity::Status::SpecialReview,
830        }
831    }
832
833    fn dedup_audit_event_drafts(&mut self) {
834        self.audit_event_drafts.sort_unstable();
835        self.audit_event_drafts.dedup();
836    }
837}
838
839#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
840/// Classifies error values that drive the booking-readiness workflow.
841pub enum Error {
842    #[error("booking triage reservation repository could not load requested reservation")]
843    /// Identifies reservation not found as the reason the workflow must stop, retry, or request review.
844    ReservationNotFound,
845}
846
847/// Shared app result type used across the booking triage gate.
848pub type AppResult<T> = core::result::Result<T, Error>;
849
850/// Reservation identifiers used by booking-triage packets and review evidence.
851pub mod reservation {
852    use super::entities;
853
854    /// Read-only reservation repository used to retrieve source facts for booking triage evaluation.
855    pub trait Repository {
856        /// Fetches the reservation source record by id without confirming, cancelling, messaging, or mutating provider state.
857        fn get(&self, id: entities::reservation::Id) -> Option<entities::Reservation>;
858    }
859}
860
861#[derive(Debug, Clone)]
862/// Service used by the booking-readiness workflow; it keeps booking work grounded in deterministic policy evidence before any agent draft reaches staff.
863pub struct Service<R> {
864    reservations: R,
865}
866
867impl<R> Service<R>
868where
869    R: reservation::Repository,
870{
871    /// Builds the booking-triage service around a read-only reservation evidence repository.
872    pub const fn new(reservations: R) -> Self {
873        Self { reservations }
874    }
875
876    /// Evaluates one reservation into a staff review packet using deterministic policy gates before any agent draft is allowed.
877    pub fn evaluate(&self, id: entities::reservation::Id) -> AppResult<StaffEvaluationPacket> {
878        let reservation = self
879            .reservations
880            .get(id)
881            .ok_or(Error::ReservationNotFound)?;
882        let deterministic_result =
883            DeterministicResult::evaluate(evaluate_reservation(&reservation));
884        Ok(StaffEvaluationPacket::new(
885            Reservation::try_new(reservation.id.0.to_string())
886                .expect("uuid reservation id should be a non-empty app reservation label"),
887            deterministic_result,
888        ))
889    }
890}
891
892fn evaluate_reservation(reservation: &entities::Reservation) -> Vec<rule::Evaluation> {
893    if reservation.hard_stops.is_empty() && reservation.deposit_is_satisfied() {
894        return vec![rule::Evaluation::pass(
895            rule::Id::DateRangeAndServiceSupported,
896            vec![
897                EvidenceRef::try_new("reservation:requested-without-hard-stops")
898                    .expect("static evidence ref is valid"),
899            ],
900        )];
901    }
902
903    let mut evaluations = Vec::new();
904    for hard_stop in &reservation.hard_stops {
905        evaluations.push(evaluate_hard_stop(hard_stop));
906    }
907    if !reservation.deposit_is_satisfied() {
908        evaluations.push(rule::Evaluation::needs_human_approval(review_finding(
909            rule::Id::DepositAndPricingRequirements,
910            FailureCode::DepositNotSatisfied,
911            ReadinessBucket::SpecialReview,
912            ApprovalGate::PaymentManagerApproval,
913            "deposit:missing-or-unverified",
914        )));
915    }
916    evaluations
917}
918
919trait ReservationDepositReadiness {
920    fn deposit_is_satisfied(&self) -> bool;
921}
922
923impl ReservationDepositReadiness for entities::Reservation {
924    fn deposit_is_satisfied(&self) -> bool {
925        self.deposit.as_ref().is_some_and(|deposit| {
926            matches!(
927                deposit.status(),
928                domain::payment::DepositStatus::Paid
929                    | domain::payment::DepositStatus::NotRequired
930                    | domain::payment::DepositStatus::WaivedByManager
931            )
932        })
933    }
934}
935
936fn evaluate_hard_stop(hard_stop: &entities::HardStop) -> rule::Evaluation {
937    match hard_stop {
938        entities::HardStop::MissingRequiredVaccine(_) => {
939            rule::Evaluation::needs_human_approval(review_finding(
940                rule::Id::VaccineRequirements,
941                FailureCode::MissingOrUnverifiedVaccine,
942                ReadinessBucket::VaccinePending,
943                ApprovalGate::MedicalDocumentReview,
944                "vaccine:missing-required",
945            ))
946        }
947        entities::HardStop::IneligibleForGroupPlay(_)
948        | entities::HardStop::BehaviorReviewRequired => {
949            rule::Evaluation::needs_human_approval(review_finding(
950                rule::Id::BehaviorRestrictions,
951                FailureCode::BehaviorExceptionRequiresReview,
952                ReadinessBucket::SpecialReview,
953                ApprovalGate::BehaviorReview,
954                "behavior:review-required",
955            ))
956        }
957        entities::HardStop::MedicalOrMedicationReviewRequired => {
958            rule::Evaluation::needs_human_approval(review_finding(
959                rule::Id::MedicationSpecialCareLimits,
960                FailureCode::SpecialCareRequiresReview,
961                ReadinessBucket::SpecialReview,
962                ApprovalGate::CareTeamApproval,
963                "care:medical-or-medication-review-required",
964            ))
965        }
966        entities::HardStop::DepositRequired => {
967            rule::Evaluation::needs_human_approval(review_finding(
968                rule::Id::DepositAndPricingRequirements,
969                FailureCode::DepositNotSatisfied,
970                ReadinessBucket::SpecialReview,
971                ApprovalGate::PaymentManagerApproval,
972                "deposit:required",
973            ))
974        }
975        entities::HardStop::InHeat | entities::HardStop::AgeBelowMinimumWeeks(_) => {
976            rule::Evaluation::hard_block(review_finding(
977                rule::Id::DateRangeAndServiceSupported,
978                FailureCode::PolicyHardStop,
979                ReadinessBucket::Rejected,
980                ApprovalGate::ManagerApproval,
981                "policy:hard-stop",
982            ))
983        }
984    }
985}
986
987fn review_finding(
988    rule_id: rule::Id,
989    failure_code: FailureCode,
990    readiness_bucket: ReadinessBucket,
991    human_approval_required: ApprovalGate,
992    evidence_ref: &'static str,
993) -> rule::ReviewFinding {
994    rule::ReviewFinding::builder()
995        .rule_id(rule_id)
996        .failure_code(failure_code)
997        .readiness_bucket(readiness_bucket)
998        .human_approval_required(human_approval_required)
999        .evidence_refs(vec![
1000            EvidenceRef::try_new(evidence_ref).expect("static evidence ref is valid"),
1001        ])
1002        .build()
1003}