1use chrono::{DateTime, Utc};
2use domain::{entities, policy, source};
3use nutype::nutype;
4use serde::{Deserialize, Serialize};
5
6#[nutype(
7 sanitize(trim),
8 validate(not_empty, len_char_max = 1200),
9 derive(
10 Debug,
11 Clone,
12 PartialEq,
13 Eq,
14 PartialOrd,
15 Ord,
16 Hash,
17 Serialize,
18 Deserialize
19 )
20)]
21pub struct CareSummary(String);
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
24pub enum BelongingsStatus {
26 ReturnedToCustomer,
28 NeedsStaffFollowUp,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
33pub enum DepartureNotesReview {
35 StaffReviewed,
37 ManagerReviewRequired,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
42pub enum CompletionStatus {
44 StaffVerifiedCheckout,
46 NeedsStaffHandoffReview,
48 SourceNotCheckedOut,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
53pub enum SafeAgentAction {
55 SummarizeCheckoutEvidence,
57 CreateInternalHandoffTask,
59 DraftRetentionFollowUpForReview,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
64pub enum BlockedAction {
66 SuggestCheckedOutStatus,
68 SendCustomerMessage,
70 MutateProviderOrPmsRecord,
72 MoveRefundDiscountOrPayment,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
77pub enum AuditEventDraft {
79 SourceCheckoutObserved,
81 StaffHandoffRecorded,
84 StaffHandoffReviewRequested,
86 CheckoutCompletionSuggested,
88 CustomerMessageApprovalRequested,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
93pub struct StaffHandoff {
95 completed_by: entities::ActorRef,
96 completed_at: DateTime<Utc>,
97 belongings_status: BelongingsStatus,
98 care_summary: CareSummary,
99 departure_notes_review: DepartureNotesReview,
100}
101
102impl StaffHandoff {
103 pub const fn completed_by(&self) -> &entities::ActorRef {
105 &self.completed_by
106 }
107
108 pub const fn completed_at(&self) -> DateTime<Utc> {
110 self.completed_at
111 }
112
113 pub const fn belongings_status(&self) -> BelongingsStatus {
115 self.belongings_status
116 }
117
118 pub const fn care_summary(&self) -> &CareSummary {
120 &self.care_summary
121 }
122
123 pub const fn departure_notes_review(&self) -> DepartureNotesReview {
125 self.departure_notes_review
126 }
127
128 const fn is_resolved_for_checkout_completion(&self) -> bool {
129 matches!(self.belongings_status, BelongingsStatus::ReturnedToCustomer)
130 && matches!(
131 self.departure_notes_review,
132 DepartureNotesReview::StaffReviewed
133 )
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
138pub struct Request {
140 reservation_id: entities::reservation::Id,
141 source_provenance: source::Provenance,
142 observed_source_status: source::reservation::Status,
143 staff_handoff: StaffHandoff,
144}
145
146impl Request {
147 pub const fn reservation_id(&self) -> entities::reservation::Id {
149 self.reservation_id
150 }
151
152 pub const fn source_provenance(&self) -> &source::Provenance {
154 &self.source_provenance
155 }
156
157 pub fn observed_source_status(&self) -> source::reservation::Status {
159 self.observed_source_status.clone()
160 }
161
162 pub const fn staff_handoff(&self) -> &StaffHandoff {
164 &self.staff_handoff
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
169pub struct Packet {
171 reservation_id: entities::reservation::Id,
172 provenance: source::Provenance,
173 staff_handoff: StaffHandoff,
174 completion_status: CompletionStatus,
175 suggested_reservation_status: Option<entities::reservation::Status>,
176 required_review_gates: Vec<policy::ReviewGate>,
177 safe_agent_actions: Vec<SafeAgentAction>,
178 blocked_actions: Vec<BlockedAction>,
179 audit_event_drafts: Vec<AuditEventDraft>,
180}
181
182impl Packet {
183 pub const fn reservation_id(&self) -> entities::reservation::Id {
185 self.reservation_id
186 }
187
188 pub const fn provenance(&self) -> &source::Provenance {
190 &self.provenance
191 }
192
193 pub const fn staff_handoff(&self) -> &StaffHandoff {
195 &self.staff_handoff
196 }
197
198 pub const fn completion_status(&self) -> CompletionStatus {
200 self.completion_status
201 }
202
203 pub fn suggested_reservation_status(&self) -> Option<entities::reservation::Status> {
205 self.suggested_reservation_status.clone()
206 }
207
208 pub fn required_review_gates(&self) -> &[policy::ReviewGate] {
210 &self.required_review_gates
211 }
212
213 pub fn safe_agent_actions(&self) -> &[SafeAgentAction] {
215 &self.safe_agent_actions
216 }
217
218 pub fn blocked_actions(&self) -> &[BlockedAction] {
220 &self.blocked_actions
221 }
222
223 pub fn audit_event_drafts(&self) -> &[AuditEventDraft] {
225 &self.audit_event_drafts
226 }
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
230pub struct Workflow;
232
233impl Workflow {
234 pub fn evaluate(request: Request) -> Packet {
236 let completion_status = completion_status_for(&request);
237 let suggested_reservation_status = match completion_status {
238 CompletionStatus::StaffVerifiedCheckout => {
239 Some(entities::reservation::Status::CheckedOut)
240 }
241 CompletionStatus::NeedsStaffHandoffReview | CompletionStatus::SourceNotCheckedOut => {
242 None
243 }
244 };
245 let required_review_gates = required_review_gates_for(completion_status);
246 let safe_agent_actions = safe_agent_actions_for(completion_status);
247 let blocked_actions = blocked_actions_for(completion_status);
248 let audit_event_drafts = audit_event_drafts_for(completion_status);
249
250 Packet {
251 reservation_id: request.reservation_id,
252 provenance: request.source_provenance,
253 staff_handoff: request.staff_handoff,
254 completion_status,
255 suggested_reservation_status,
256 required_review_gates,
257 safe_agent_actions,
258 blocked_actions,
259 audit_event_drafts,
260 }
261 }
262}
263
264fn completion_status_for(request: &Request) -> CompletionStatus {
265 if !matches!(
266 request.observed_source_status,
267 source::reservation::Status::CheckedOut
268 ) {
269 return CompletionStatus::SourceNotCheckedOut;
270 }
271
272 if request.staff_handoff.is_resolved_for_checkout_completion() {
273 CompletionStatus::StaffVerifiedCheckout
274 } else {
275 CompletionStatus::NeedsStaffHandoffReview
276 }
277}
278
279fn required_review_gates_for(completion_status: CompletionStatus) -> Vec<policy::ReviewGate> {
280 match completion_status {
281 CompletionStatus::StaffVerifiedCheckout => {
282 vec![policy::ReviewGate::CustomerMessageApproval]
283 }
284 CompletionStatus::NeedsStaffHandoffReview | CompletionStatus::SourceNotCheckedOut => {
285 vec![policy::ReviewGate::ManagerApproval]
286 }
287 }
288}
289
290fn safe_agent_actions_for(completion_status: CompletionStatus) -> Vec<SafeAgentAction> {
291 let mut actions = vec![
292 SafeAgentAction::SummarizeCheckoutEvidence,
293 SafeAgentAction::CreateInternalHandoffTask,
294 ];
295 if matches!(completion_status, CompletionStatus::StaffVerifiedCheckout) {
296 actions.push(SafeAgentAction::DraftRetentionFollowUpForReview);
297 }
298 actions
299}
300
301fn blocked_actions_for(completion_status: CompletionStatus) -> Vec<BlockedAction> {
302 let mut blocked_actions = vec![
303 BlockedAction::SendCustomerMessage,
304 BlockedAction::MutateProviderOrPmsRecord,
305 BlockedAction::MoveRefundDiscountOrPayment,
306 ];
307 if !matches!(completion_status, CompletionStatus::StaffVerifiedCheckout) {
308 blocked_actions.push(BlockedAction::SuggestCheckedOutStatus);
309 }
310 blocked_actions.sort_unstable();
311 blocked_actions.dedup();
312 blocked_actions
313}
314
315fn audit_event_drafts_for(completion_status: CompletionStatus) -> Vec<AuditEventDraft> {
316 let mut drafts = vec![AuditEventDraft::StaffHandoffRecorded];
317 match completion_status {
318 CompletionStatus::StaffVerifiedCheckout => {
319 drafts.push(AuditEventDraft::SourceCheckoutObserved);
320 drafts.push(AuditEventDraft::CheckoutCompletionSuggested);
321 drafts.push(AuditEventDraft::CustomerMessageApprovalRequested);
322 }
323 CompletionStatus::NeedsStaffHandoffReview => {
324 drafts.push(AuditEventDraft::SourceCheckoutObserved);
325 drafts.push(AuditEventDraft::StaffHandoffReviewRequested);
326 }
327 CompletionStatus::SourceNotCheckedOut => {
328 drafts.push(AuditEventDraft::StaffHandoffReviewRequested);
329 }
330 }
331 drafts.sort_unstable();
332 drafts.dedup();
333 drafts
334}