Files
nimbusflow/backend/repositories/schedule.py

258 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# myapp/repositories/schedule.py
# ------------------------------------------------------------
# Persistence layer for the ``Schedule`` model.
# ------------------------------------------------------------
from __future__ import annotations
from typing import Any, List, Optional, Sequence
from backend.db import BaseRepository
from backend.models import Schedule as ScheduleModel
from backend.models import ScheduleStatus
class ScheduleRepository(BaseRepository[ScheduleModel]):
"""Dataaccess object for the ``Schedules`` table."""
# ------------------------------------------------------------------
# Tablelevel constants change them in one place if the schema evolves.
# ------------------------------------------------------------------
_TABLE = "Schedules"
_PK = "ScheduleId"
# ------------------------------------------------------------------
# CRUD helpers
# ------------------------------------------------------------------
def create(
self,
*,
service_id: int,
member_id: int,
status: ScheduleStatus = ScheduleStatus.PENDING,
reason: Optional[str] = None,
scheduled_at: Optional[Any] = None,
expires_at: Optional[Any] = None,
) -> ScheduleModel:
"""
Insert a brandnew schedule row.
Parameters
----------
service_id, member_id : int
FK references.
status : ScheduleStatus
Desired initial status (PENDING, DECLINED, …).
reason : str | None
Stored in ``DeclineReason`` when the status is ``DECLINED``.
scheduled_at, expires_at : datetimecompatible | None
``scheduled_at`` defaults to SQLites ``CURRENT_TIMESTAMP``.
"""
# Handle timestamp - use actual datetime if not provided
import datetime
if scheduled_at is None:
scheduled_at = datetime.datetime.now(datetime.UTC).isoformat()
schedule = ScheduleModel(
ScheduleId=-1, # placeholder will be replaced
ServiceId=service_id,
MemberId=member_id,
Status=status.value,
ScheduledAt=scheduled_at,
AcceptedAt=None,
DeclinedAt=None,
ExpiresAt=expires_at,
DeclineReason=reason if status == ScheduleStatus.DECLINED else None,
)
return self._insert(self._TABLE, schedule, self._PK)
def get_by_id(self, schedule_id: int) -> Optional[ScheduleModel]:
"""Fetch a schedule by its primary key."""
sql = f"SELECT * FROM {self._TABLE} WHERE {self._PK} = ?"
row = self.db.fetchone(sql, (schedule_id,))
return ScheduleModel.from_row(row) if row else None
def list_all(self) -> List[ScheduleModel]:
"""Return every schedule row."""
return self._select_all(self._TABLE, ScheduleModel)
# ------------------------------------------------------------------
# Helper used by the SchedulingService to locate an existing row.
# ------------------------------------------------------------------
def get_one(self, *, member_id: int, service_id: int) -> Optional[ScheduleModel]:
"""
Return the *first* schedule (any status) for the supplied
``member_id`` / ``service_id`` pair, or ``None`` if none exists.
"""
sql = f"""
SELECT *
FROM {self._TABLE}
WHERE MemberId = ?
AND ServiceId = ?
LIMIT 1
"""
row = self.db.fetchone(sql, (member_id, service_id))
return ScheduleModel.from_row(row) if row else None
# ------------------------------------------------------------------
# Generic statuschange helper (used for “decline” and similar ops).
# ------------------------------------------------------------------
def update_status(
self,
*,
schedule_id: int,
new_status: ScheduleStatus,
reason: Optional[str] = None,
) -> None:
"""
Switch a schedules status and optionally store a reason.
If ``new_status`` is ``DECLINED`` the ``DeclineReason`` column is
populated; otherwise it is cleared.
"""
# Build the SET clause dynamically we only touch the columns we need.
set_clause = "Status = ?, DeclinedAt = NULL, DeclineReason = NULL"
params: list[Any] = [new_status.value]
if new_status == ScheduleStatus.DECLINED:
set_clause = "Status = ?, DeclinedAt = datetime('now'), DeclineReason = ?"
params.extend([reason])
params.append(schedule_id) # WHERE clause param
sql = f"""
UPDATE {self._TABLE}
SET {set_clause}
WHERE {self._PK} = ?
"""
self.db.execute(sql, tuple(params))
# ------------------------------------------------------------------
# Query helpers used by the scheduling service
# ------------------------------------------------------------------
def has_any(
self,
member_id: int,
service_id: int,
statuses: Sequence[ScheduleStatus],
) -> bool:
"""True if a schedule exists for the pair with any of the given statuses."""
if not statuses:
return False
placeholders = ",".join("?" for _ in statuses)
sql = f"""
SELECT 1
FROM {self._TABLE}
WHERE MemberId = ?
AND ServiceId = ?
AND Status IN ({placeholders})
LIMIT 1
"""
params = (member_id, service_id, *[s.value for s in statuses])
row = self.db.fetchone(sql, params)
return row is not None
# ------------------------------------------------------------------
# Statustransition helpers (accept / decline) kept for completeness.
# ------------------------------------------------------------------
def mark_accepted(
self,
schedule_id: int,
accepted_at: Optional[Any] = None,
) -> None:
if accepted_at is None:
sql = f"""
UPDATE {self._TABLE}
SET Status = ?,
AcceptedAt = datetime('now'),
DeclinedAt = NULL,
DeclineReason = NULL
WHERE {self._PK} = ?
"""
self.db.execute(sql, (ScheduleStatus.ACCEPTED.value, schedule_id))
else:
sql = f"""
UPDATE {self._TABLE}
SET Status = ?,
AcceptedAt = ?,
DeclinedAt = NULL,
DeclineReason = NULL
WHERE {self._PK} = ?
"""
self.db.execute(sql, (ScheduleStatus.ACCEPTED.value, accepted_at, schedule_id))
def mark_declined(
self,
schedule_id: int,
declined_at: Optional[Any] = None,
decline_reason: Optional[str] = None,
) -> None:
if declined_at is None:
sql = f"""
UPDATE {self._TABLE}
SET Status = ?,
DeclinedAt = datetime('now'),
DeclineReason = ?
WHERE {self._PK} = ?
"""
self.db.execute(sql, (ScheduleStatus.DECLINED.value, decline_reason, schedule_id))
else:
sql = f"""
UPDATE {self._TABLE}
SET Status = ?,
DeclinedAt = ?,
DeclineReason = ?
WHERE {self._PK} = ?
"""
self.db.execute(sql, (ScheduleStatus.DECLINED.value, declined_at, decline_reason, schedule_id))
# ------------------------------------------------------------------
# Sameday helper used by the scheduling service
# ------------------------------------------------------------------
def has_schedule_on_date(self, member_id: int, service_date: str) -> bool:
"""
Return ``True`` if any *active* schedule (pending or accepted) exists for
``member_id`` on the calendar day ``service_date`` (format YYYYMMDD).
This abstracts the "a member can only be actively scheduled once per day"
rule so the service layer does not need to know the underlying
table layout. Declined schedules do not count as blocking.
"""
sql = f"""
SELECT 1
FROM {self._TABLE} AS s
JOIN Services AS sv ON s.ServiceId = sv.ServiceId
WHERE s.MemberId = ?
AND sv.ServiceDate = ?
AND s.Status IN (?, ?)
LIMIT 1
"""
row = self.db.fetchone(sql, (member_id, service_date, ScheduleStatus.PENDING.value, ScheduleStatus.ACCEPTED.value))
return row is not None
# ------------------------------------------------------------------
# Miscellaneous convenience queries
# ------------------------------------------------------------------
def get_pending_for_service(self, service_id: int) -> List[ScheduleModel]:
"""All PENDING schedules for a given service."""
sql = f"""
SELECT *
FROM {self._TABLE}
WHERE ServiceId = ?
AND Status = ?
"""
rows = self.db.fetchall(sql, (service_id, ScheduleStatus.PENDING.value))
return [ScheduleModel.from_row(r) for r in rows]
def delete_schedule(self, schedule_id: int) -> bool:
"""
Delete a schedule by ID.
Returns:
bool: True if a schedule was deleted, False if not found
"""
sql = f"DELETE FROM {self._TABLE} WHERE {self._PK} = ?"
cursor = self.db.execute(sql, (schedule_id,))
return cursor.rowcount > 0