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}