158 lines
6.1 KiB
Python
158 lines
6.1 KiB
Python
# myapp/repositories/service_availability.py
|
||
# ------------------------------------------------------------
|
||
# Persistence layer for the ServiceAvailability table.
|
||
# ------------------------------------------------------------
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import List, Optional, Sequence, Any
|
||
|
||
from backend.db import BaseRepository
|
||
from backend.models import ServiceAvailability as ServiceAvailabilityModel
|
||
|
||
|
||
class ServiceAvailabilityRepository(BaseRepository[ServiceAvailabilityModel]):
|
||
"""
|
||
CRUD + query helpers for the ``ServiceAvailability`` table.
|
||
|
||
The table records which members are allowed to receive which
|
||
service‑type slots (e.g. “9 AM”, “11 AM”, “6 PM”). All SQL is
|
||
parameterised to stay safe from injection attacks.
|
||
"""
|
||
|
||
# ------------------------------------------------------------------
|
||
# Table‑level constants – change them in one place if the schema evolves.
|
||
# ------------------------------------------------------------------
|
||
_TABLE = "ServiceAvailability"
|
||
_PK = "ServiceAvailabilityId"
|
||
|
||
# ------------------------------------------------------------------
|
||
# Basic CRUD helpers
|
||
# ------------------------------------------------------------------
|
||
def create(
|
||
self,
|
||
member_id: int,
|
||
service_type_id: int,
|
||
) -> ServiceAvailabilityModel:
|
||
"""
|
||
Insert a new availability row.
|
||
|
||
The ``UNIQUE (MemberId, ServiceTypeId)`` constraint guarantees
|
||
idempotency – if the pair already exists SQLite will raise an
|
||
``IntegrityError``. To make the operation truly idempotent we
|
||
first check for an existing row and return it unchanged.
|
||
"""
|
||
existing = self.get(member_id, service_type_id)
|
||
if existing:
|
||
return existing
|
||
|
||
avail = ServiceAvailabilityModel(
|
||
ServiceAvailabilityId=-1, # placeholder – will be overwritten
|
||
MemberId=member_id,
|
||
ServiceTypeId=service_type_id,
|
||
)
|
||
return self._insert(self._TABLE, avail, self._PK)
|
||
|
||
def get(
|
||
self,
|
||
member_id: int,
|
||
service_type_id: int,
|
||
) -> Optional[ServiceAvailabilityModel]:
|
||
"""
|
||
Retrieve a single availability record for the given member /
|
||
service‑type pair, or ``None`` if it does not exist.
|
||
"""
|
||
sql = f"""
|
||
SELECT *
|
||
FROM {self._TABLE}
|
||
WHERE MemberId = ?
|
||
AND ServiceTypeId = ?
|
||
"""
|
||
row = self.db.fetchone(sql, (member_id, service_type_id))
|
||
return ServiceAvailabilityModel.from_row(row) if row else None
|
||
|
||
def delete(self, availability_id: int) -> None:
|
||
"""
|
||
Hard‑delete an availability row by its primary key.
|
||
Use with care – most callers will prefer ``revoke`` (by member &
|
||
service type) which is a bit more expressive.
|
||
"""
|
||
sql = f"DELETE FROM {self._TABLE} WHERE {self._PK} = ?"
|
||
self.db.execute(sql, (availability_id,))
|
||
|
||
# ------------------------------------------------------------------
|
||
# Convenience “grant / revoke” helpers (the most common ops)
|
||
# ------------------------------------------------------------------
|
||
def grant(self, member_id: int, service_type_id: int) -> ServiceAvailabilityModel:
|
||
"""
|
||
Public API to give a member permission for a particular service slot.
|
||
Internally delegates to ``create`` which already handles the
|
||
idempotent‑check.
|
||
"""
|
||
return self.create(member_id, service_type_id)
|
||
|
||
def revoke(self, member_id: int, service_type_id: int) -> None:
|
||
"""
|
||
Remove a member’s permission for a particular service slot.
|
||
"""
|
||
sql = f"""
|
||
DELETE FROM {self._TABLE}
|
||
WHERE MemberId = ?
|
||
AND ServiceTypeId = ?
|
||
"""
|
||
self.db.execute(sql, (member_id, service_type_id))
|
||
|
||
# ------------------------------------------------------------------
|
||
# Query helpers used by the scheduling service
|
||
# ------------------------------------------------------------------
|
||
def list_by_member(self, member_id: int) -> List[ServiceAvailabilityModel]:
|
||
"""
|
||
Return every ``ServiceAvailability`` row that belongs to the given
|
||
member. Handy for building a member’s personal “available slots”
|
||
view.
|
||
"""
|
||
sql = f"""
|
||
SELECT *
|
||
FROM {self._TABLE}
|
||
WHERE MemberId = ?
|
||
"""
|
||
rows = self.db.fetchall(sql, (member_id,))
|
||
return [ServiceAvailabilityModel.from_row(r) for r in rows]
|
||
|
||
def list_by_service_type(self, service_type_id: int) -> List[ServiceAvailabilityModel]:
|
||
"""
|
||
Return all members that are allowed to receive the given service type.
|
||
"""
|
||
sql = f"""
|
||
SELECT *
|
||
FROM {self._TABLE}
|
||
WHERE ServiceTypeId = ?
|
||
"""
|
||
rows = self.db.fetchall(sql, (service_type_id,))
|
||
return [ServiceAvailabilityModel.from_row(r) for r in rows]
|
||
|
||
def list_all(self) -> List[ServiceAvailabilityModel]:
|
||
"""
|
||
Return every row in the table – useful for admin dashboards or
|
||
bulk‑export scripts.
|
||
"""
|
||
return self._select_all(self._TABLE, ServiceAvailabilityModel)
|
||
|
||
# ------------------------------------------------------------------
|
||
# Helper for the round‑robin scheduler
|
||
# ------------------------------------------------------------------
|
||
def members_for_type(self, service_type_id: int) -> List[int]:
|
||
"""
|
||
Return a flat list of ``MemberId`` values that are eligible for the
|
||
supplied ``service_type_id``. The scheduling service can then
|
||
intersect this list with the pool of members that have the correct
|
||
classification, activity flag, etc.
|
||
"""
|
||
sql = f"""
|
||
SELECT MemberId
|
||
FROM {self._TABLE}
|
||
WHERE ServiceTypeId = ?
|
||
"""
|
||
rows = self.db.fetchall(sql, (service_type_id,))
|
||
# ``rows`` is a sequence of sqlite3.Row objects; each row acts like a dict.
|
||
return [row["MemberId"] for row in rows] |