Skip to main content

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}