Skip to main content

app/
checkout_completion.rs

1//! Checkout-completion workflow rules for staff departure handoff and closeout review.
2//!
3//! ## Operator summary
4//!
5//! Staff use this workflow to decide whether a departure belongs in the verified-checkout queue,
6//! the staff-handoff-review queue, or the source-status-reconciliation queue. It compares the
7//! source reservation status with front-desk handoff evidence such as belongings return, care
8//! summary, and departure-note review so operators do not manually audit every checkout record
9//! across the PMS, care notes, and follow-up queues.
10//!
11//! The workflow can reduce labor by summarizing checkout evidence, creating an internal handoff
12//! task, drafting retention follow-up for review, and producing audit-event drafts. It is not
13//! allowed to close a live PMS/provider record, send a customer message, apply a checkout status
14//! without staff/source agreement, release capacity, waive/discount/refund, collect payment, or
15//! move money. Payment and closeout surfaces remain review queues, not autonomous execution.
16//!
17//! Source facts remain authoritative in their own systems: `domain::source::Provenance` and
18//! `domain::source::reservation::Status` for observed provider state, staff-submitted handoff
19//! evidence for belongings and departure notes, `domain::entities::reservation::Status` for the
20//! normalized lifecycle suggestion, and approved payment/ledger records for balances, refunds,
21//! discounts, or waivers. Review gates protect pets, customers, and staff by requiring manager
22//! approval when source or handoff evidence is incomplete and customer-message approval before any
23//! departure or retention copy leaves draft form.
24
25use chrono::{DateTime, Utc};
26use domain::{entities, policy, source};
27use nutype::nutype;
28use serde::{Deserialize, Serialize};
29
30#[nutype(
31    sanitize(trim),
32    validate(not_empty, len_char_max = 1200),
33    derive(
34        Debug,
35        Clone,
36        PartialEq,
37        Eq,
38        PartialOrd,
39        Ord,
40        Hash,
41        Serialize,
42        Deserialize
43    )
44)]
45pub struct CareSummary(String);
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
48/// Decision choices for belongings status in the checkout completion workflow; each value routes reviewed source facts to the right queue, draft, or staff gate.
49pub enum BelongingsStatus {
50    /// Routes the item to returned to customer for staff queueing, review, and downstream agent context.
51    ReturnedToCustomer,
52    /// Routes the item to needs staff follow up for staff queueing, review, and downstream agent context.
53    NeedsStaffFollowUp,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
57/// Decision choices for departure notes review in the checkout completion workflow; each value routes reviewed source facts to the right queue, draft, or staff gate.
58pub enum DepartureNotesReview {
59    /// Selects staff reviewed for the checkout completion decision model so the app can choose a review, evidence, or draft path without taking live action.
60    StaffReviewed,
61    /// Selects manager review required for the checkout completion decision model so the app can choose a review, evidence, or draft path without taking live action.
62    ManagerReviewRequired,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
66/// Decision choices for completion status in the checkout completion workflow; each value routes reviewed source facts to the right queue, draft, or staff gate.
67pub enum CompletionStatus {
68    /// Routes the item to staff verified checkout for staff queueing, review, and downstream agent context.
69    StaffVerifiedCheckout,
70    /// Routes the item to needs staff handoff review for staff queueing, review, and downstream agent context.
71    NeedsStaffHandoffReview,
72    /// Routes the item to source not checked out for staff queueing, review, and downstream agent context.
73    SourceNotCheckedOut,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
77/// Review-safe agent tasks allowed to save staff time without crossing mutation or send gates.
78pub enum SafeAgentAction {
79    /// Allows agents to summarize checkout evidence for staff review without mutating records or contacting customers.
80    SummarizeCheckoutEvidence,
81    /// Allows agents to create internal handoff task for staff review without mutating records or contacting customers.
82    CreateInternalHandoffTask,
83    /// Allows agents to draft retention follow up for review for staff review without mutating records or contacting customers.
84    DraftRetentionFollowUpForReview,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
88/// Actions the agent must never perform without a human/operator system of record.
89pub enum BlockedAction {
90    /// Blocks agents from suggest checked out status until staff or the system of record performs the action.
91    SuggestCheckedOutStatus,
92    /// Blocks agents from send customer message until staff or the system of record performs the action.
93    SendCustomerMessage,
94    /// Blocks agents from mutate provider or pms record until staff or the system of record performs the action.
95    MutateProviderOrPmsRecord,
96    /// Blocks agents from move refund discount or payment until staff or the system of record performs the action.
97    MoveRefundDiscountOrPayment,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
101/// Decision choices for audit event draft in the checkout completion workflow; each value routes reviewed source facts to the right queue, draft, or staff gate.
102pub enum AuditEventDraft {
103    /// Selects source checkout observed for the checkout completion decision model so the app can choose a review, evidence, or draft path without taking live action.
104    SourceCheckoutObserved,
105    /// Records the staff-submitted handoff payload as received, even when the source status prevents
106    /// treating it as checkout-completion evidence.
107    StaffHandoffRecorded,
108    /// Selects staff handoff review requested for the checkout completion decision model so the app can choose a review, evidence, or draft path without taking live action.
109    StaffHandoffReviewRequested,
110    /// Selects checkout completion suggested for the checkout completion decision model so the app can choose a review, evidence, or draft path without taking live action.
111    CheckoutCompletionSuggested,
112    /// Selects customer message approval requested for the checkout completion decision model so the app can choose a review, evidence, or draft path without taking live action.
113    CustomerMessageApprovalRequested,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
117/// Staff handoff used by the checkout completion workflow; it keeps checkout tasks, payment exceptions, and handoff notes explicit for staff review.
118pub struct StaffHandoff {
119    completed_by: entities::ActorRef,
120    completed_at: DateTime<Utc>,
121    belongings_status: BelongingsStatus,
122    care_summary: CareSummary,
123    departure_notes_review: DepartureNotesReview,
124}
125
126impl StaffHandoff {
127    /// Returns the completed by evidence available to checkout completion review while leaving provider, customer, payment, and schedule systems unchanged.
128    pub const fn completed_by(&self) -> &entities::ActorRef {
129        &self.completed_by
130    }
131
132    /// Returns the completed at evidence available to checkout completion review while leaving provider, customer, payment, and schedule systems unchanged.
133    pub const fn completed_at(&self) -> DateTime<Utc> {
134        self.completed_at
135    }
136
137    /// Returns the belongings status evidence available to checkout completion review while leaving provider, customer, payment, and schedule systems unchanged.
138    pub const fn belongings_status(&self) -> BelongingsStatus {
139        self.belongings_status
140    }
141
142    /// Returns the care summary evidence available to checkout completion review while leaving provider, customer, payment, and schedule systems unchanged.
143    pub const fn care_summary(&self) -> &CareSummary {
144        &self.care_summary
145    }
146
147    /// Returns the departure notes review evidence available to checkout completion review while leaving provider, customer, payment, and schedule systems unchanged.
148    pub const fn departure_notes_review(&self) -> DepartureNotesReview {
149        self.departure_notes_review
150    }
151
152    const fn is_resolved_for_checkout_completion(&self) -> bool {
153        matches!(self.belongings_status, BelongingsStatus::ReturnedToCustomer)
154            && matches!(
155                self.departure_notes_review,
156                DepartureNotesReview::StaffReviewed
157            )
158    }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
162/// Input rules for building the workflow packet from source-grounded records.
163pub struct Request {
164    reservation_id: entities::reservation::Id,
165    source_provenance: source::Provenance,
166    observed_source_status: source::reservation::Status,
167    staff_handoff: StaffHandoff,
168}
169
170impl Request {
171    /// Returns the reservation id evidence available to checkout completion review while leaving provider, customer, payment, and schedule systems unchanged.
172    pub const fn reservation_id(&self) -> entities::reservation::Id {
173        self.reservation_id
174    }
175
176    /// Returns the source provenance evidence available to checkout completion review while leaving provider, customer, payment, and schedule systems unchanged.
177    pub const fn source_provenance(&self) -> &source::Provenance {
178        &self.source_provenance
179    }
180
181    /// Returns the observed source status evidence available to checkout completion review while leaving provider, customer, payment, and schedule systems unchanged.
182    pub fn observed_source_status(&self) -> source::reservation::Status {
183        self.observed_source_status.clone()
184    }
185
186    /// Returns the staff handoff evidence available to checkout completion review while leaving provider, customer, payment, and schedule systems unchanged.
187    pub const fn staff_handoff(&self) -> &StaffHandoff {
188        &self.staff_handoff
189    }
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193/// Reviewable packet handed to staff or agents with deterministic gates already applied.
194pub struct Packet {
195    reservation_id: entities::reservation::Id,
196    provenance: source::Provenance,
197    staff_handoff: StaffHandoff,
198    completion_status: CompletionStatus,
199    suggested_reservation_status: Option<entities::reservation::Status>,
200    required_review_gates: Vec<policy::ReviewGate>,
201    safe_agent_actions: Vec<SafeAgentAction>,
202    blocked_actions: Vec<BlockedAction>,
203    audit_event_drafts: Vec<AuditEventDraft>,
204}
205
206impl Packet {
207    /// Returns the reservation id evidence available to checkout completion review while leaving provider, customer, payment, and schedule systems unchanged.
208    pub const fn reservation_id(&self) -> entities::reservation::Id {
209        self.reservation_id
210    }
211
212    /// Returns the provenance evidence available to checkout completion review while leaving provider, customer, payment, and schedule systems unchanged.
213    pub const fn provenance(&self) -> &source::Provenance {
214        &self.provenance
215    }
216
217    /// Returns the staff handoff evidence available to checkout completion review while leaving provider, customer, payment, and schedule systems unchanged.
218    pub const fn staff_handoff(&self) -> &StaffHandoff {
219        &self.staff_handoff
220    }
221
222    /// Returns the completion status evidence available to checkout completion review while leaving provider, customer, payment, and schedule systems unchanged.
223    pub const fn completion_status(&self) -> CompletionStatus {
224        self.completion_status
225    }
226
227    /// Returns the suggested reservation status evidence available to checkout completion review while leaving provider, customer, payment, and schedule systems unchanged.
228    pub fn suggested_reservation_status(&self) -> Option<entities::reservation::Status> {
229        self.suggested_reservation_status.clone()
230    }
231
232    /// Returns the required review gates evidence available to checkout completion review while leaving provider, customer, payment, and schedule systems unchanged.
233    pub fn required_review_gates(&self) -> &[policy::ReviewGate] {
234        &self.required_review_gates
235    }
236
237    /// Returns the safe agent actions evidence available to checkout completion review while leaving provider, customer, payment, and schedule systems unchanged.
238    pub fn safe_agent_actions(&self) -> &[SafeAgentAction] {
239        &self.safe_agent_actions
240    }
241
242    /// Returns the blocked actions evidence available to checkout completion review while leaving provider, customer, payment, and schedule systems unchanged.
243    pub fn blocked_actions(&self) -> &[BlockedAction] {
244        &self.blocked_actions
245    }
246
247    /// Returns the audit event drafts evidence available to checkout completion review while leaving provider, customer, payment, and schedule systems unchanged.
248    pub fn audit_event_drafts(&self) -> &[AuditEventDraft] {
249        &self.audit_event_drafts
250    }
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254/// Workflow used by the checkout completion workflow; it keeps checkout tasks, payment exceptions, and handoff notes explicit for staff review.
255pub struct Workflow;
256
257impl Workflow {
258    /// Builds the evaluate result for the checkout completion workflow from reviewed source facts while preserving human review gates and draft-only side effects.
259    pub fn evaluate(request: Request) -> Packet {
260        let completion_status = completion_status_for(&request);
261        let suggested_reservation_status = match completion_status {
262            CompletionStatus::StaffVerifiedCheckout => {
263                Some(entities::reservation::Status::CheckedOut)
264            }
265            CompletionStatus::NeedsStaffHandoffReview | CompletionStatus::SourceNotCheckedOut => {
266                None
267            }
268        };
269        let required_review_gates = required_review_gates_for(completion_status);
270        let safe_agent_actions = safe_agent_actions_for(completion_status);
271        let blocked_actions = blocked_actions_for(completion_status);
272        let audit_event_drafts = audit_event_drafts_for(completion_status);
273
274        Packet {
275            reservation_id: request.reservation_id,
276            provenance: request.source_provenance,
277            staff_handoff: request.staff_handoff,
278            completion_status,
279            suggested_reservation_status,
280            required_review_gates,
281            safe_agent_actions,
282            blocked_actions,
283            audit_event_drafts,
284        }
285    }
286}
287
288fn completion_status_for(request: &Request) -> CompletionStatus {
289    if !matches!(
290        request.observed_source_status,
291        source::reservation::Status::CheckedOut
292    ) {
293        return CompletionStatus::SourceNotCheckedOut;
294    }
295
296    if request.staff_handoff.is_resolved_for_checkout_completion() {
297        CompletionStatus::StaffVerifiedCheckout
298    } else {
299        CompletionStatus::NeedsStaffHandoffReview
300    }
301}
302
303fn required_review_gates_for(completion_status: CompletionStatus) -> Vec<policy::ReviewGate> {
304    match completion_status {
305        CompletionStatus::StaffVerifiedCheckout => {
306            vec![policy::ReviewGate::CustomerMessageApproval]
307        }
308        CompletionStatus::NeedsStaffHandoffReview | CompletionStatus::SourceNotCheckedOut => {
309            vec![policy::ReviewGate::ManagerApproval]
310        }
311    }
312}
313
314fn safe_agent_actions_for(completion_status: CompletionStatus) -> Vec<SafeAgentAction> {
315    let mut actions = vec![
316        SafeAgentAction::SummarizeCheckoutEvidence,
317        SafeAgentAction::CreateInternalHandoffTask,
318    ];
319    if matches!(completion_status, CompletionStatus::StaffVerifiedCheckout) {
320        actions.push(SafeAgentAction::DraftRetentionFollowUpForReview);
321    }
322    actions
323}
324
325fn blocked_actions_for(completion_status: CompletionStatus) -> Vec<BlockedAction> {
326    let mut blocked_actions = vec![
327        BlockedAction::SendCustomerMessage,
328        BlockedAction::MutateProviderOrPmsRecord,
329        BlockedAction::MoveRefundDiscountOrPayment,
330    ];
331    if !matches!(completion_status, CompletionStatus::StaffVerifiedCheckout) {
332        blocked_actions.push(BlockedAction::SuggestCheckedOutStatus);
333    }
334    blocked_actions.sort_unstable();
335    blocked_actions.dedup();
336    blocked_actions
337}
338
339fn audit_event_drafts_for(completion_status: CompletionStatus) -> Vec<AuditEventDraft> {
340    let mut drafts = vec![AuditEventDraft::StaffHandoffRecorded];
341    match completion_status {
342        CompletionStatus::StaffVerifiedCheckout => {
343            drafts.push(AuditEventDraft::SourceCheckoutObserved);
344            drafts.push(AuditEventDraft::CheckoutCompletionSuggested);
345            drafts.push(AuditEventDraft::CustomerMessageApprovalRequested);
346        }
347        CompletionStatus::NeedsStaffHandoffReview => {
348            drafts.push(AuditEventDraft::SourceCheckoutObserved);
349            drafts.push(AuditEventDraft::StaffHandoffReviewRequested);
350        }
351        CompletionStatus::SourceNotCheckedOut => {
352            drafts.push(AuditEventDraft::StaffHandoffReviewRequested);
353        }
354    }
355    drafts.sort_unstable();
356    drafts.dedup();
357    drafts
358}