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}