Skip to main content

domain/boarding/
mod.rs

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