Skip to main content

domain/daycare/
attendance.rs

1//! Daycare recurring-attendance materialization for predictable front-desk work queues.
2//!
3//! ```
4//! use chrono::{NaiveDate, Weekday};
5//! use domain::daycare;
6//!
7//! let recurrence = daycare::attendance::Recurrence::new(
8//!     daycare::attendance::DateRange::new(
9//!         NaiveDate::from_ymd_opt(2026, 6, 15).unwrap(),
10//!         NaiveDate::from_ymd_opt(2026, 6, 19).unwrap(),
11//!     )
12//!     .unwrap(),
13//!     daycare::attendance::Days::try_new(vec![Weekday::Mon, Weekday::Wed, Weekday::Fri]).unwrap(),
14//! );
15//!
16//! let visits = daycare::attendance::Materializer.materialize(&recurrence, &[
17//!     NaiveDate::from_ymd_opt(2026, 6, 17).unwrap(),
18//! ]);
19//! assert_eq!(visits.len(), 2);
20//! ```
21
22use super::*;
23use chrono::Datelike;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26/// Inclusive date range for recurring daycare attendance materialization.
27pub struct DateRange {
28    start: NaiveDate,
29    end: NaiveDate,
30}
31
32impl DateRange {
33    /// Creates an attendance date range, rejecting ranges whose end precedes the start.
34    pub fn new(start: NaiveDate, end: NaiveDate) -> std::result::Result<Self, DateRangeError> {
35        if end < start {
36            return Err(DateRangeError::EndBeforeStart);
37        }
38        Ok(Self { start, end })
39    }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
43/// Validation errors for recurring daycare attendance date ranges.
44pub enum DateRangeError {
45    #[error("daycare attendance recurrence end date must not precede start date")]
46    /// The recurrence end date was earlier than the start date.
47    EndBeforeStart,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51/// Weekdays on which a recurring daycare package or reservation should materialize visits.
52pub struct Days(Vec<chrono::Weekday>);
53
54impl Days {
55    /// Creates a non-empty weekday set for recurring daycare attendance.
56    pub fn try_new(days: Vec<chrono::Weekday>) -> std::result::Result<Self, DaysError> {
57        if days.is_empty() {
58            return Err(DaysError::Empty);
59        }
60        Ok(Self(days))
61    }
62
63    /// Reports whether the recurrence includes the supplied weekday.
64    pub fn contains(&self, day: chrono::Weekday) -> bool {
65        self.0.contains(&day)
66    }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
70/// Validation errors for recurring daycare attendance weekdays.
71pub enum DaysError {
72    #[error("daycare attendance recurrence requires at least one weekday")]
73    /// No weekdays were supplied, so no attendance could be materialized.
74    Empty,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78/// Recurring daycare attendance rule used to pre-build front-desk work queues.
79pub struct Recurrence {
80    /// Inclusive date window in which visits may be materialized.
81    pub date_range: DateRange,
82    /// Weekdays that should generate visits inside the date range.
83    pub days: Days,
84}
85
86impl Recurrence {
87    /// Creates an attendance date range, rejecting ranges whose end precedes the start.
88    pub const fn new(date_range: DateRange, days: Days) -> Self {
89        Self { date_range, days }
90    }
91}
92
93#[derive(Debug, Clone, Default)]
94/// Service that expands recurrence rules into concrete daycare visit dates.
95pub struct Materializer;
96
97impl Materializer {
98    /// Materializes concrete visit dates while excluding source-system exceptions and closures.
99    pub fn materialize(&self, recurrence: &Recurrence, exceptions: &[NaiveDate]) -> Vec<NaiveDate> {
100        let mut dates = Vec::new();
101        let mut current = recurrence.date_range.start;
102        while current <= recurrence.date_range.end {
103            if recurrence.days.contains(current.weekday()) && !exceptions.contains(&current) {
104                dates.push(current);
105            }
106            current = current
107                .succ_opt()
108                .expect("bounded date range should have next date");
109        }
110        dates
111    }
112}