diff --git a/skills/_lib/lumina_skills/domain.py b/skills/_lib/lumina_skills/domain.py index 8eb9d62..4657961 100644 --- a/skills/_lib/lumina_skills/domain.py +++ b/skills/_lib/lumina_skills/domain.py @@ -41,6 +41,21 @@ class Appointment: # for fixture convenience. needs_confirmation: bool = False + def __post_init__(self) -> None: + """Validate that end_time is after start_time.""" + if self.end_time < self.start_time: + raise ValueError( + f"end_time ({self.end_time}) must be >= start_time ({self.start_time}) " + f"for appointment {self.appointment_id}" + ) + + def __repr__(self) -> str: + return ( + f"Appointment(id={self.appointment_id!r}, " + f"{self.start_time:%H:%M}-{self.end_time:%H:%M}, " + f"{self.client_name!r}, {self.status.value})" + ) + def duration_minutes(self) -> int: """Appointment duration in whole minutes.""" delta = self.end_time - self.start_time @@ -65,6 +80,12 @@ class Gap: # The appointment immediately after this gap (if any). following_appointment_id: Optional[str] = None + def __repr__(self) -> str: + return ( + f"Gap({self.start_time:%H:%M}-{self.end_time:%H:%M}, " + f"{self.duration_minutes}min, {self.staff_name!r})" + ) + @dataclass(frozen=True) class DayBoard: @@ -83,6 +104,23 @@ class DayBoard: total_booked_minutes: int = 0 total_gap_minutes: int = 0 + def __repr__(self) -> str: + return ( + f"DayBoard({self.date}, {self.salon_name!r}, " + f"{len(self.appointments)} appts, {len(self.gaps)} gaps)" + ) + + @property + def utilization_rate(self) -> float: + """Fraction of the day that is booked (0.0–1.0). + + Returns 0.0 when there is no total time to compute against. + """ + total = self.total_booked_minutes + self.total_gap_minutes + if total == 0: + return 0.0 + return self.total_booked_minutes / total + def to_dict(self) -> dict: """Serialize to a plain dict for JSON output.""" return { @@ -110,6 +148,8 @@ class DayBoard: "end": g.end_time.isoformat(), "duration_minutes": g.duration_minutes, "staff": g.staff_name, + "preceding_appointment_id": g.preceding_appointment_id, + "following_appointment_id": g.following_appointment_id, } for g in self.gaps ], @@ -118,4 +158,5 @@ class DayBoard: ], "total_booked_minutes": self.total_booked_minutes, "total_gap_minutes": self.total_gap_minutes, + "utilization_rate": round(self.utilization_rate, 4), }