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}