domain/boarding/mod.rs
1//! Boarding service-line rules for capacity, stay policy, care handoffs, and upsells.
2//!
3//! # Operator summary
4//!
5//! Boarding supports the queue where front-desk staff and managers decide whether an overnight
6//! stay can be confirmed, waitlisted, held for care review, or turned into a safe follow-up offer.
7//! The useful labor reduction is deterministic triage: capacity snapshots, care profiles, deposit
8//! status, cancellation notice, housekeeping cadence, handoff checklists, and exit-bath evidence
9//! become typed decisions instead of scattered note reading and ad hoc manager pings.
10//!
11//! This module is not permission to automate live boarding operations. It does not book or cancel
12//! reservations, mutate room inventory, collect deposits, issue refunds, send customer messages,
13//! make medical judgments, or override staff. Source systems and promoted domain facts remain
14//! authoritative: room counts and reservation status from the provider/read model, payment/deposit
15//! evidence from `domain::payment`, care instructions and medication-review requirements from the
16//! pet care profile, and policy values captured in the boarding location rules.
17//!
18//! Review gates protect pets, customers, and staff at the unsafe edges: manager approval for
19//! denied or exception capacity decisions, medical-document review for missing feeding or medication
20//! ambiguity, refund/deposit exception review for payment edge cases, and customer-message approval
21//! before any upsell recommendation becomes customer-facing.
22//!
23//! Next step: start with the location ruleset to see the policy, then open the child module that
24//! matches the business question: `capacity` for confirm/waitlist decisions, `accommodation`
25//! for room fit, `deposit` for payment gates, `care` for feeding or medication handoff,
26//! `housekeeping` for stay execution, or `upsell` for review-gated add-on recommendations.
27//!
28//! The rest of the module documents the externally visible boarding rules that labor-saving agents
29//! may use when drafting staff packets, manager briefs, and customer-response recommendations.
30
31use bon::Builder;
32use serde::{Deserialize, Deserializer, Serialize};
33
34use crate::entities::{LocationId, PetId};
35use crate::money;
36
37macro_rules! positive_scalar {
38 ($name:ident, $primitive:ty, $error:ident, $message:literal) => {
39 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
40 /// Positive boarding quantity used where zero would erase a real labor or stay requirement.
41 pub struct $name($primitive);
42
43 impl $name {
44 /// Rejects impossible boarding values before they affect capacity, minimum-stay, deposit, service-window, or checkout calculations.
45 pub const fn try_new(value: $primitive) -> std::result::Result<Self, $error> {
46 if value == 0 {
47 return Err($error::Zero);
48 }
49 Ok(Self(value))
50 }
51
52 /// Returns the boarding number used by capacity, service-window, minimum-stay, deposit, or checkout calculations.
53 pub const fn get(self) -> $primitive {
54 self.0
55 }
56 }
57
58 impl<'de> Deserialize<'de> for $name {
59 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
60 where
61 D: Deserializer<'de>,
62 {
63 Self::try_new(<$primitive>::deserialize(deserializer)?)
64 .map_err(serde::de::Error::custom)
65 }
66 }
67
68 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
69 /// Validation failure returned when a required positive boarding scalar is zero.
70 pub enum $error {
71 #[error($message)]
72 /// Rejects zero where the pet-resort workflow requires a positive quantity.
73 Zero,
74 }
75 };
76}
77
78positive_scalar!(
79 RoomInventory,
80 u16,
81 RoomInventoryError,
82 "boarding room inventory requires at least one room"
83);
84positive_scalar!(
85 StayNights,
86 u16,
87 StayNightsError,
88 "boarding minimum stay requires at least one night"
89);
90positive_scalar!(
91 NoticeHours,
92 u16,
93 NoticeHoursError,
94 "boarding cancellation notice requires at least one hour"
95);
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
98/// Hour within a resort service day used for boarding arrival and departure windows.
99pub struct HourOfDay(u8);
100
101impl HourOfDay {
102 /// Rejects impossible boarding values before they affect capacity, minimum-stay, deposit, service-window, or checkout calculations.
103 pub const fn try_new(value: u8) -> std::result::Result<Self, HourOfDayError> {
104 if value > 23 {
105 return Err(HourOfDayError::OutsideClockDay);
106 }
107 Ok(Self(value))
108 }
109
110 /// Returns the boarding number used by capacity, service-window, minimum-stay, deposit, or checkout calculations.
111 pub const fn get(self) -> u8 {
112 self.0
113 }
114}
115
116impl<'de> Deserialize<'de> for HourOfDay {
117 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
118 where
119 D: Deserializer<'de>,
120 {
121 Self::try_new(u8::deserialize(deserializer)?).map_err(serde::de::Error::custom)
122 }
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
126/// Validation errors for boarding service-window hours.
127pub enum HourOfDayError {
128 #[error("boarding service-window hour must be between 0 and 23")]
129 /// Hour was outside the 0–23 clock range and cannot define a service window.
130 OutsideClockDay,
131}
132
133/// Accommodation policy for room/suite fit, species safety, capacity, and booking recommendations.
134pub mod accommodation;
135
136/// Room and suite capacity policy for confirm, waitlist, and denial decisions.
137pub mod capacity;
138
139/// Deposit readiness policy for boarding confirmation gates.
140pub mod deposit;
141
142/// Care policy for feeding, medication review, and kennel-staff handoff decisions during a stay.
143pub mod care;
144
145/// Upsell policy for review-gated boarding add-ons and checkout recommendations.
146pub mod upsell;
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
149/// Coarse availability status used in boarding rules and manager briefs.
150pub enum RoomAvailability {
151 /// Rooms are generally available for this reservation path.
152 Open,
153 /// Inventory is constrained and staff should treat capacity as a labor/care watch item.
154 Limited,
155 /// New reservations should be routed to waitlist unless a manager approves otherwise.
156 WaitlistOnly,
157 /// Reservations should not be accepted from this reservation path.
158 Closed,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
162/// Capacity posture for boarding rules, pairing inventory with availability status.
163pub struct CapacityPlan {
164 room_inventory: RoomInventory,
165 /// Staff-facing availability status derived from resort capacity evidence.
166 pub availability: RoomAvailability,
167}
168
169impl CapacityPlan {
170 /// Creates the boarding value from validated domain parts without re-reading source systems.
171 pub const fn new(room_inventory: RoomInventory, availability: RoomAvailability) -> Self {
172 Self {
173 room_inventory,
174 availability,
175 }
176 }
177 /// Returns the inventory count represented by this capacity plan.
178 pub const fn room_inventory(&self) -> RoomInventory {
179 self.room_inventory
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
184/// Check-in or check-out window that constrains front-desk staffing and guest promises.
185pub struct ServiceWindow {
186 start: HourOfDay,
187 end: HourOfDay,
188}
189
190impl ServiceWindow {
191 /// Creates the boarding value from validated domain parts without re-reading source systems.
192 pub const fn new(
193 start: HourOfDay,
194 end: HourOfDay,
195 ) -> std::result::Result<Self, ServiceWindowError> {
196 if start.get() >= end.get() {
197 return Err(ServiceWindowError::EndMustFollowStart);
198 }
199 Ok(Self { start, end })
200 }
201 /// Returns the inclusive start hour staff may use for this service window.
202 pub const fn start(&self) -> HourOfDay {
203 self.start
204 }
205 /// Returns the exclusive end hour after which this service window is closed.
206 pub const fn end(&self) -> HourOfDay {
207 self.end
208 }
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
212/// Validation errors for boarding arrival or departure windows.
213pub enum ServiceWindowError {
214 #[error("boarding service window end must follow start")]
215 /// The end hour did not follow the start hour, so the window cannot be offered.
216 EndMustFollowStart,
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
220/// Deposit rule used to determine whether a boarding reservation can be confirmed.
221pub enum DepositRule {
222 /// Reservation path is secured without a deposit requirement.
223 NotRequired,
224 /// Required deposit amount sourced from policy or booking evidence.
225 Required {
226 /// Money amount staff must collect or have waived before this deposit rule is satisfied.
227 amount: money::Money,
228 },
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
232/// Payment timing that controls when staff must collect boarding charges or deposits.
233pub enum PaymentTiming {
234 /// Payment is required before the reservation is considered secured.
235 DueAtBooking,
236 /// Payment is collected when the pet arrives for the stay.
237 DueAtCheckIn,
238 /// Payment can be collected during departure checkout.
239 DueAtCheckout,
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
243/// Optional boarding-adjacent services that may appear in staff offer recommendations.
244pub enum Upsell {
245 /// Bath offered before departure from boarding.
246 ExitBath,
247 /// Training add-on that can be bundled with a boarding stay after staff review.
248 TrainingSession,
249 /// Additional play or enrichment add-on during the stay.
250 EnrichmentPlay,
251 /// Premium comfort add-on for the boarding room or suite.
252 PremiumBedding,
253}
254
255/// Housekeeping policies for boarded pets and room turns.
256pub mod housekeeping;
257
258/// Check-in/check-out windows and staff handoff requirements.
259pub mod handoff;
260
261/// Minimum-stay rules for holidays, multi-pet buffers, and standard stays.
262pub mod minimum_stay;
263
264/// Cancellation notice and penalty rules for boarding reservations.
265pub mod cancellation;
266
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
268/// Boarding service-line ruleset combining capacity, stay, payment, handoff, and upsell policy.
269pub struct Contract {
270 /// Capacity posture staff and automation must honor before confirming stays.
271 pub capacity: CapacityPlan,
272 /// Guest arrival window used for front-desk staffing and check-in promises.
273 pub arrival_window: ServiceWindow,
274 /// Guest departure window used for checkout staffing and Pawgress/report timing.
275 pub departure_window: ServiceWindow,
276 /// Minimum-stay rule for standard, holiday, or multi-pet boarding demand.
277 pub minimum_stay: minimum_stay::Policy,
278 /// Cancellation policy that governs notice, deposit forfeiture, and manager review.
279 pub cancellation: cancellation::Policy,
280 /// Deposit requirement used before staff or automation treats the booking as secured.
281 pub deposit: DepositRule,
282 /// Payment timing that constrains collection workflow and front-desk labor.
283 pub payment: PaymentTiming,
284 /// Room-cleaning cadence that feeds labor planning for the stay.
285 pub housekeeping: housekeeping::Cadence,
286 /// Staff handoff checklist required at arrival, medication review, or departure.
287 pub handoff: handoff::Requirement,
288 #[builder(default)]
289 /// Optional services that can be offered only through the review-gated recommendation flow.
290 pub upsells: Vec<Upsell>,
291}
292
293impl Contract {
294 /// Reports whether these boarding rules require deposit collection before confirmation.
295 pub fn requires_deposit_collection(&self) -> bool {
296 matches!(self.deposit, DepositRule::Required { .. })
297 }
298 /// Builds the baseline PetSuites-style boarding rules used by examples and tests.
299 pub fn standard_petsuites() -> Self {
300 Self::builder()
301 .capacity(CapacityPlan::new(
302 RoomInventory::try_new(1).unwrap(),
303 RoomAvailability::Limited,
304 ))
305 .arrival_window(
306 ServiceWindow::new(
307 HourOfDay::try_new(7).unwrap(),
308 HourOfDay::try_new(18).unwrap(),
309 )
310 .unwrap(),
311 )
312 .departure_window(
313 ServiceWindow::new(
314 HourOfDay::try_new(7).unwrap(),
315 HourOfDay::try_new(12).unwrap(),
316 )
317 .unwrap(),
318 )
319 .minimum_stay(minimum_stay::Policy::new(
320 StayNights::try_new(1).unwrap(),
321 minimum_stay::Reason::StandardPolicy,
322 ))
323 .cancellation(cancellation::Policy::new(
324 NoticeHours::try_new(24).unwrap(),
325 cancellation::Penalty::ForfeitDeposit,
326 ))
327 .deposit(DepositRule::Required {
328 amount: money::Money::new(
329 money::MinorUnits::try_new(1).unwrap(),
330 money::Currency::Usd,
331 ),
332 })
333 .payment(PaymentTiming::DueAtCheckout)
334 .housekeeping(housekeeping::Cadence::DailyRoomReset)
335 .handoff(handoff::Requirement::ArrivalCareReview)
336 .upsells(vec![Upsell::ExitBath, Upsell::TrainingSession])
337 .build()
338 }
339}