Skip to main content

domain/boarding/
capacity.rs

1//! Boarding capacity decisions for room/suite availability.
2//!
3//! Capacity examples use semantic accommodation paths so a labor-saving agent can explain whether
4//! the front desk should confirm, waitlist, or route an exception for manager review:
5//!
6//! ```
7//! use domain::{boarding, entities};
8//! use uuid::Uuid;
9//!
10//! let luxury_suite = boarding::capacity::SegmentCounts::builder()
11//!     .accommodation(boarding::accommodation::Kind::LuxuryDogSuite)
12//!     .total(boarding::capacity::RoomCount::try_new(10).unwrap())
13//!     .occupied(boarding::capacity::RoomCount::try_new(10).unwrap())
14//!     .build();
15//! let snapshot = boarding::capacity::Snapshot::new(vec![
16//!     boarding::capacity::NightlySegmentSnapshot::from_counts(luxury_suite),
17//! ])
18//! .unwrap();
19//! let request = boarding::capacity::Request::new(
20//!     entities::LocationId(Uuid::nil()),
21//!     entities::Species::Dog,
22//!     boarding::accommodation::Preference::Specific(boarding::accommodation::Kind::LuxuryDogSuite),
23//! );
24//!
25//! assert_eq!(
26//!     boarding::capacity::Policy.evaluate(&request, &snapshot),
27//!     boarding::capacity::Decision::Waitlist {
28//!         reason: boarding::capacity::WaitlistReason::EligibleSegmentFull,
29//!     },
30//! );
31//! ```
32
33use super::*;
34use crate::policy;
35use bon::Builder;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
38/// Non-negative count of rooms in a boarding accommodation segment.
39pub struct RoomCount(u16);
40
41impl RoomCount {
42    /// Promotes a source-system room count into the boarding capacity domain.
43    pub const fn try_new(value: u16) -> std::result::Result<Self, RoomCountError> {
44        Ok(Self(value))
45    }
46
47    /// Returns the raw room count for source adapters, reports, and serialization.
48    pub const fn get(self) -> u16 {
49        self.0
50    }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
54/// Validation errors for room-count promotion from boundary data.
55pub enum RoomCountError {}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Builder)]
58/// Builder-facing source counts for one accommodation segment on a boarding night.
59pub struct SegmentCounts {
60    /// Accommodation segment these counts describe.
61    pub accommodation: accommodation::Kind,
62    total: RoomCount,
63    occupied: RoomCount,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
67/// Immutable nightly capacity snapshot for one accommodation segment.
68pub struct NightlySegmentSnapshot {
69    /// Accommodation segment these counts describe.
70    pub accommodation: accommodation::Kind,
71    total: RoomCount,
72    occupied: RoomCount,
73}
74
75impl NightlySegmentSnapshot {
76    /// Freezes builder-provided segment counts into a nightly snapshot used by capacity policy.
77    pub const fn from_counts(counts: SegmentCounts) -> Self {
78        Self {
79            accommodation: counts.accommodation,
80            total: counts.total,
81            occupied: counts.occupied,
82        }
83    }
84
85    /// Returns total rooms known for this accommodation segment.
86    pub const fn total(&self) -> RoomCount {
87        self.total
88    }
89
90    /// Returns occupied rooms already committed for this accommodation segment.
91    pub const fn occupied(&self) -> RoomCount {
92        self.occupied
93    }
94
95    /// Returns remaining rooms after committed occupancy, saturating at zero for dirty data.
96    pub const fn available_rooms(&self) -> RoomCount {
97        RoomCount(self.total.get().saturating_sub(self.occupied.get()))
98    }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102/// Point-in-time boarding inventory evidence used to make confirm/waitlist/deny decisions.
103pub struct Snapshot {
104    segments: Vec<NightlySegmentSnapshot>,
105}
106
107impl Snapshot {
108    /// Creates a capacity snapshot from one or more nightly accommodation segments.
109    pub fn new(segments: Vec<NightlySegmentSnapshot>) -> std::result::Result<Self, SnapshotError> {
110        if segments.is_empty() {
111            return Err(SnapshotError::EmptyInventory);
112        }
113        Ok(Self { segments })
114    }
115
116    /// Returns the source-derived accommodation segments considered by capacity policy.
117    pub fn segments(&self) -> &[NightlySegmentSnapshot] {
118        &self.segments
119    }
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
123/// Snapshot validation errors that prevent safe capacity automation.
124pub enum SnapshotError {
125    #[error("boarding capacity snapshot requires at least one accommodation segment")]
126    /// No inventory segments were available, so automation must not infer availability.
127    EmptyInventory,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131/// Boarding capacity request for a location, species, and accommodation preference.
132pub struct Request {
133    /// Resort location whose room inventory is the authority for this check.
134    pub location_id: LocationId,
135    /// Pet species used to reject incompatible room types before availability is promised.
136    pub species: crate::entities::Species,
137    /// Accommodation preference requested by the guest or staff workflow.
138    pub accommodation: accommodation::Preference,
139}
140
141impl Request {
142    /// Creates a capacity request from already-identified location, species, and preference values.
143    pub const fn new(
144        location_id: LocationId,
145        species: crate::entities::Species,
146        accommodation: accommodation::Preference,
147    ) -> Self {
148        Self {
149            location_id,
150            species,
151            accommodation,
152        }
153    }
154}
155
156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
157/// Capacity outcome an agent may present to staff when handling a boarding request.
158pub enum Decision {
159    /// A compatible accommodation segment has at least one available room.
160    Available {
161        /// Accommodation that can be offered from the available source inventory.
162        accommodation: accommodation::Kind,
163    },
164    /// Compatible accommodation exists but is currently full, so staff should route to waitlist.
165    Waitlist {
166        /// Source-grounded reason for the waitlist or denial outcome.
167        reason: WaitlistReason,
168    },
169    /// The request cannot be confirmed from the supplied source evidence and requires a review gate.
170    Deny {
171        /// Source-grounded reason for the waitlist or denial outcome.
172        reason: DenialReason,
173        /// Human approval gate required before overriding the denied capacity decision.
174        review_gate: policy::ReviewGate,
175    },
176}
177
178impl Decision {
179    /// Returns the human review gate required before staff override a denied capacity decision.
180    pub fn required_review_gate(&self) -> Option<policy::ReviewGate> {
181        match self {
182            Self::Deny { review_gate, .. } => Some(review_gate.clone()),
183            Self::Available { .. } | Self::Waitlist { .. } => None,
184        }
185    }
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
189/// Reasons boarding capacity policy must deny confirmation from available evidence.
190pub enum DenialReason {
191    /// Requested accommodation type does not support the pet species.
192    SpeciesAccommodationMismatch,
193    /// No source inventory segment matches the requested compatible accommodation kinds.
194    NoEligibleSegment,
195    /// Local policy data required for the capacity check was unavailable.
196    PolicyUnavailable,
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
200/// Reasons a boarding request should be waitlisted instead of confirmed.
201pub enum WaitlistReason {
202    /// The room type is valid for the pet, but all matching rooms are occupied.
203    EligibleSegmentFull,
204}
205
206#[derive(Debug, Clone, Default)]
207/// Deterministic boarding capacity policy that does not invent inventory.
208pub struct Policy;
209
210impl Policy {
211    /// Evaluates a boarding request against source-derived inventory and returns confirm, waitlist, or denial evidence.
212    pub fn evaluate(&self, request: &Request, snapshot: &Snapshot) -> Decision {
213        let mut compatible_but_full = false;
214
215        for wanted in request.accommodation.acceptable_kinds() {
216            if !wanted.supports_species(&request.species) {
217                return Decision::Deny {
218                    reason: DenialReason::SpeciesAccommodationMismatch,
219                    review_gate: policy::ReviewGate::ManagerApproval,
220                };
221            }
222
223            for segment in snapshot.segments() {
224                if segment.accommodation == *wanted {
225                    if segment.available_rooms().get() > 0 {
226                        return Decision::Available {
227                            accommodation: *wanted,
228                        };
229                    }
230                    compatible_but_full = true;
231                }
232            }
233        }
234
235        if compatible_but_full {
236            Decision::Waitlist {
237                reason: WaitlistReason::EligibleSegmentFull,
238            }
239        } else {
240            Decision::Deny {
241                reason: DenialReason::NoEligibleSegment,
242                review_gate: policy::ReviewGate::ManagerApproval,
243            }
244        }
245    }
246}