Skip to main content

app/
booking_triage.rs

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