#!/usr/bin/env python3 """Generate healthcare phases 13.2-13.7 implementation files.""" from __future__ import annotations import textwrap from pathlib import Path ROOT = Path(__file__).resolve().parents[1] FILES: dict[str, str] = {} def add(path: str, content: str) -> None: FILES[path] = textwrap.dedent(content).lstrip() + "\n" # ============================================================================= # Phase 13.2 — Doctor Panel # ============================================================================= add( "app/schemas/doctor_panel.py", '''\ """Doctor panel DTOs — Phase 13.2.""" from __future__ import annotations from datetime import datetime from uuid import UUID from pydantic import BaseModel, ConfigDict, Field from app.models.types import VisitSessionStatus from app.schemas.common import ORMBase from app.schemas.appointments import AppointmentRead _FORBID = ConfigDict(extra="forbid") class VisitSessionCreate(BaseModel): model_config = _FORBID clinic_id: UUID doctor_id: UUID patient_id: UUID appointment_id: UUID code: str = Field(max_length=50) queue_position: int = Field(default=100, ge=1, le=10000) metadata_json: dict | None = None class VisitSessionActionRequest(BaseModel): model_config = _FORBID version: int = Field(ge=1) reason: str | None = Field(default=None, max_length=500) class VisitSessionRead(ORMBase): id: UUID tenant_id: UUID clinic_id: UUID doctor_id: UUID patient_id: UUID appointment_id: UUID code: str status: VisitSessionStatus queue_position: int started_at: datetime | None finished_at: datetime | None metadata_json: dict | None version: int is_deleted: bool created_at: datetime updated_at: datetime class VisitNoteCreate(BaseModel): model_config = _FORBID visit_session_id: UUID doctor_id: UUID code: str = Field(max_length=50) title: str = Field(max_length=255) body: str | None = None storage_file_ref: str | None = Field(default=None, max_length=255) metadata_json: dict | None = None class VisitNoteUpdate(BaseModel): model_config = _FORBID title: str | None = Field(default=None, max_length=255) body: str | None = None storage_file_ref: str | None = Field(default=None, max_length=255) version: int = Field(ge=1) metadata_json: dict | None = None class VisitNoteRead(ORMBase): id: UUID tenant_id: UUID visit_session_id: UUID doctor_id: UUID code: str title: str body: str | None storage_file_ref: str | None metadata_json: dict | None version: int is_deleted: bool created_at: datetime updated_at: datetime class DoctorDashboardRead(BaseModel): doctor_id: UUID queued_count: int in_progress_count: int completed_today_count: int upcoming_appointments: list[AppointmentRead] class DoctorQueueRead(BaseModel): doctor_id: UUID items: list[VisitSessionRead] ''', ) add( "app/validators/doctor_panel.py", '''\ """Doctor panel validators — Phase 13.2.""" from __future__ import annotations from shared.exceptions import AppError from app.models.types import VisitSessionAction, VisitSessionStatus ALLOWED: dict[VisitSessionAction, set[VisitSessionStatus]] = { VisitSessionAction.CALL_NEXT: {VisitSessionStatus.QUEUED}, VisitSessionAction.START: {VisitSessionStatus.QUEUED}, VisitSessionAction.FINISH: {VisitSessionStatus.IN_PROGRESS}, VisitSessionAction.DEFER: {VisitSessionStatus.QUEUED, VisitSessionStatus.IN_PROGRESS}, } TARGET: dict[VisitSessionAction, VisitSessionStatus] = { VisitSessionAction.CALL_NEXT: VisitSessionStatus.QUEUED, VisitSessionAction.START: VisitSessionStatus.IN_PROGRESS, VisitSessionAction.FINISH: VisitSessionStatus.COMPLETED, VisitSessionAction.DEFER: VisitSessionStatus.DEFERRED, } TERMINAL = frozenset({VisitSessionStatus.COMPLETED, VisitSessionStatus.CANCELLED}) def ensure_visit_session_transition(*, action: VisitSessionAction, current: VisitSessionStatus) -> None: if current in TERMINAL: raise AppError("جلسه ویزیت در وضعیت پایانی است", status_code=409, error_code="visit_session_terminal") allowed = ALLOWED.get(action, set()) if current not in allowed: raise AppError( "انتقال وضعیت جلسه ویزیت مجاز نیست", status_code=409, error_code="invalid_visit_session_transition", details={"from_status": current.value, "action": action.value}, ) def target_status_for(action: VisitSessionAction) -> VisitSessionStatus: return TARGET[action] ''', ) add( "app/policies/doctor_panel.py", '''\ """Visit session lifecycle policies — Phase 13.2.""" from __future__ import annotations from app.models.types import VisitSessionAction, VisitSessionStatus from app.validators.doctor_panel import ensure_visit_session_transition, target_status_for class VisitSessionPolicy: def ensure_transition(self, *, action: VisitSessionAction, current: VisitSessionStatus) -> VisitSessionStatus: ensure_visit_session_transition(action=action, current=current) return target_status_for(action) ''', ) add( "app/specifications/doctor_panel.py", '''\ """Doctor panel list specifications — Phase 13.2.""" from __future__ import annotations from dataclasses import dataclass from uuid import UUID from app.models.types import VisitSessionStatus ALLOWED_SORT_FIELDS = frozenset({"queue_position", "created_at", "started_at"}) @dataclass class VisitSessionListSpec: doctor_id: UUID | None = None clinic_id: UUID | None = None status: VisitSessionStatus | None = None sort_by: str = "queue_position" sort_dir: str = "asc" ''', ) add( "app/repositories/doctor_panel.py", '''\ """Doctor panel repositories — Phase 13.2.""" from __future__ import annotations from datetime import datetime, timezone from uuid import UUID from sqlalchemy import and_, func, select from app.models.doctor_panel import VisitNote, VisitSession from app.models.types import VisitSessionStatus from app.repositories.base import TenantBaseRepository from app.specifications.doctor_panel import VisitSessionListSpec class VisitSessionRepository(TenantBaseRepository[VisitSession]): model = VisitSession async def get_by_code(self, tenant_id: UUID, code: str) -> VisitSession | None: stmt = select(VisitSession).where( VisitSession.tenant_id == tenant_id, VisitSession.code == code, VisitSession.is_deleted.is_(False), ) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def get_by_appointment(self, tenant_id: UUID, appointment_id: UUID) -> VisitSession | None: stmt = select(VisitSession).where( VisitSession.tenant_id == tenant_id, VisitSession.appointment_id == appointment_id, VisitSession.is_deleted.is_(False), ) result = await self.session.execute(stmt) return result.scalar_one_or_none() def _filter_clauses(self, tenant_id: UUID, spec: VisitSessionListSpec | None): clauses = [VisitSession.tenant_id == tenant_id, VisitSession.is_deleted.is_(False)] if spec is None: return clauses if spec.doctor_id is not None: clauses.append(VisitSession.doctor_id == spec.doctor_id) if spec.clinic_id is not None: clauses.append(VisitSession.clinic_id == spec.clinic_id) if spec.status is not None: clauses.append(VisitSession.status == spec.status) return clauses async def list_filtered(self, tenant_id: UUID, *, offset: int = 0, limit: int = 20, spec: VisitSessionListSpec | None = None): clauses = self._filter_clauses(tenant_id, spec) sort_col = getattr(VisitSession, spec.sort_by if spec else "queue_position", VisitSession.queue_position) order = sort_col.asc() if spec and spec.sort_dir == "asc" else sort_col.desc() stmt = select(VisitSession).where(and_(*clauses)).order_by(order).offset(offset).limit(limit) result = await self.session.execute(stmt) return result.scalars().all() async def count_by_status(self, tenant_id: UUID, *, doctor_id: UUID, status: VisitSessionStatus) -> int: stmt = select(func.count()).select_from(VisitSession).where( VisitSession.tenant_id == tenant_id, VisitSession.doctor_id == doctor_id, VisitSession.status == status, VisitSession.is_deleted.is_(False), ) result = await self.session.execute(stmt) return int(result.scalar_one()) async def count_completed_today(self, tenant_id: UUID, *, doctor_id: UUID) -> int: today = datetime.now(timezone.utc).date() stmt = select(func.count()).select_from(VisitSession).where( VisitSession.tenant_id == tenant_id, VisitSession.doctor_id == doctor_id, VisitSession.status == VisitSessionStatus.COMPLETED, VisitSession.is_deleted.is_(False), func.date(VisitSession.finished_at) == today, ) result = await self.session.execute(stmt) return int(result.scalar_one()) class VisitNoteRepository(TenantBaseRepository[VisitNote]): model = VisitNote async def get_by_code(self, tenant_id: UUID, code: str) -> VisitNote | None: stmt = select(VisitNote).where( VisitNote.tenant_id == tenant_id, VisitNote.code == code, VisitNote.is_deleted.is_(False), ) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def list_by_session(self, tenant_id: UUID, visit_session_id: UUID): stmt = select(VisitNote).where( VisitNote.tenant_id == tenant_id, VisitNote.visit_session_id == visit_session_id, VisitNote.is_deleted.is_(False), ) result = await self.session.execute(stmt) return result.scalars().all() ''', ) if __name__ == "__main__": for rel, content in FILES.items(): path = ROOT / rel path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content, encoding="utf-8") print(f"wrote {rel}")