improve domain.py: add validation, repr, missing serialization, utilization rate
- Add __post_init__ validation so Appointment rejects negative durations - Add __repr__ to Appointment, Gap, and DayBoard for readable debugging - Fix DayBoard.to_dict to serialize preceding_appointment_id and following_appointment_id on Gap (were previously dropped) - Add utilization_rate property (booked / total) with 0.0 guard
This commit is contained in:
@@ -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),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user