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}