TorbatYar/backend/services/healthcare/app/repositories/appointments.py
Mortezakoohjani 625275e55b feat(healthcare): add backend service and finalize frontend build
Add the Healthcare platform backend (phases 13.0–13.7) with Alembic migration revision fix, production deploy script, validation reports, shared UI exports, and loyalty list page client directives so Next.js build succeeds.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 11:38:31 +03:30

154 lines
5.8 KiB
Python

"""Appointment repositories — Phase 13.1."""
from __future__ import annotations
from datetime import datetime
from uuid import UUID
from sqlalchemy import and_, func, or_, select
from app.models.appointments import (
Appointment,
AppointmentType,
AvailabilityWindow,
Schedule,
WaitingListEntry,
)
from app.models.types import AppointmentStatus
from app.repositories.base import TenantBaseRepository
from app.specifications.appointments import AppointmentListSpec
class ScheduleRepository(TenantBaseRepository[Schedule]):
model = Schedule
async def get_by_code(self, tenant_id: UUID, code: str) -> Schedule | None:
stmt = select(Schedule).where(
Schedule.tenant_id == tenant_id,
Schedule.code == code,
Schedule.is_deleted.is_(False),
)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
class AvailabilityWindowRepository(TenantBaseRepository[AvailabilityWindow]):
model = AvailabilityWindow
async def get_by_code(self, tenant_id: UUID, code: str) -> AvailabilityWindow | None:
stmt = select(AvailabilityWindow).where(
AvailabilityWindow.tenant_id == tenant_id,
AvailabilityWindow.code == code,
AvailabilityWindow.is_deleted.is_(False),
)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
class AppointmentTypeRepository(TenantBaseRepository[AppointmentType]):
model = AppointmentType
async def get_by_code(self, tenant_id: UUID, code: str) -> AppointmentType | None:
stmt = select(AppointmentType).where(
AppointmentType.tenant_id == tenant_id,
AppointmentType.code == code,
AppointmentType.is_deleted.is_(False),
)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
class AppointmentRepository(TenantBaseRepository[Appointment]):
model = Appointment
async def get_by_code(self, tenant_id: UUID, code: str) -> Appointment | None:
stmt = select(Appointment).where(
Appointment.tenant_id == tenant_id,
Appointment.code == code,
Appointment.is_deleted.is_(False),
)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
async def find_conflicts(
self,
tenant_id: UUID,
*,
doctor_id: UUID,
scheduled_start: datetime,
scheduled_end: datetime,
exclude_id: UUID | None = None,
) -> list[Appointment]:
clauses = [
Appointment.tenant_id == tenant_id,
Appointment.doctor_id == doctor_id,
Appointment.is_deleted.is_(False),
Appointment.status.not_in(
[AppointmentStatus.CANCELLED, AppointmentStatus.NO_SHOW]
),
Appointment.scheduled_start < scheduled_end,
Appointment.scheduled_end > scheduled_start,
]
if exclude_id is not None:
clauses.append(Appointment.id != exclude_id)
stmt = select(Appointment).where(and_(*clauses))
result = await self.session.execute(stmt)
return list(result.scalars().all())
def _filter_clauses(self, tenant_id: UUID, spec: AppointmentListSpec | None):
clauses = [
Appointment.tenant_id == tenant_id,
Appointment.is_deleted.is_(False),
]
if spec is None:
return clauses
if spec.status is not None:
clauses.append(Appointment.status == spec.status)
if spec.doctor_id is not None:
clauses.append(Appointment.doctor_id == spec.doctor_id)
if spec.clinic_id is not None:
clauses.append(Appointment.clinic_id == spec.clinic_id)
if spec.patient_id is not None:
clauses.append(Appointment.patient_id == spec.patient_id)
if spec.date_from is not None:
clauses.append(Appointment.scheduled_start >= spec.date_from)
if spec.date_to is not None:
clauses.append(Appointment.scheduled_start <= spec.date_to)
if spec.q:
pattern = f"%{spec.q}%"
clauses.append(or_(Appointment.code.ilike(pattern), Appointment.notes.ilike(pattern)))
return clauses
async def list_filtered(
self,
tenant_id: UUID,
*,
offset: int = 0,
limit: int = 20,
spec: AppointmentListSpec | None = None,
):
clauses = self._filter_clauses(tenant_id, spec)
sort_col = getattr(Appointment, spec.sort_by if spec else "scheduled_start", Appointment.scheduled_start)
order = sort_col.asc() if spec and spec.sort_dir == "asc" else sort_col.desc()
stmt = select(Appointment).where(and_(*clauses)).order_by(order).offset(offset).limit(limit)
result = await self.session.execute(stmt)
return result.scalars().all()
async def count_filtered(self, tenant_id: UUID, *, spec: AppointmentListSpec | None = None) -> int:
clauses = self._filter_clauses(tenant_id, spec)
stmt = select(func.count()).select_from(Appointment).where(and_(*clauses))
result = await self.session.execute(stmt)
return int(result.scalar_one())
class WaitingListEntryRepository(TenantBaseRepository[WaitingListEntry]):
model = WaitingListEntry
async def get_by_code(self, tenant_id: UUID, code: str) -> WaitingListEntry | None:
stmt = select(WaitingListEntry).where(
WaitingListEntry.tenant_id == tenant_id,
WaitingListEntry.code == code,
WaitingListEntry.is_deleted.is_(False),
)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()