TorbatYar/backend/services/healthcare/app/api/v1/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

234 lines
9.0 KiB
Python

"""Appointment scheduling APIs — Phase 13.1."""
from __future__ import annotations
from datetime import datetime
from uuid import UUID
from fastapi import APIRouter, Depends, Query, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_db, get_pagination, require_tenant
from app.api.permissions import require_permissions
from app.commands.appointments import AppointmentCommands
from app.models.types import AppointmentStatus
from app.permissions.definitions import APPOINTMENTS_MANAGE, APPOINTMENTS_VIEW, SCHEDULES_MANAGE, SCHEDULES_VIEW
from app.queries.appointments import AppointmentQueries
from app.schemas.appointments import (
AppointmentCreate,
AppointmentLifecycleRequest,
AppointmentListResponse,
AppointmentRead,
AppointmentReschedule,
AppointmentTypeCreate,
AppointmentTypeRead,
AvailabilityWindowCreate,
AvailabilityWindowRead,
ScheduleCreate,
ScheduleRead,
WaitingListEntryCreate,
WaitingListEntryRead,
)
from app.specifications.appointments import ALLOWED_SORT_FIELDS, AppointmentListSpec
from shared.pagination import PaginationParams
from shared.security import CurrentUser
schedules_router = APIRouter()
availability_router = APIRouter()
appointment_types_router = APIRouter()
appointments_router = APIRouter()
waiting_list_router = APIRouter()
def _appointment_spec(
status_filter: AppointmentStatus | None = Query(default=None, alias="status"),
doctor_id: UUID | None = Query(default=None),
clinic_id: UUID | None = Query(default=None),
patient_id: UUID | None = Query(default=None),
date_from: datetime | None = Query(default=None),
date_to: datetime | None = Query(default=None),
q: str | None = Query(default=None, max_length=100),
sort_by: str = Query(default="scheduled_start"),
sort_dir: str = Query(default="desc", pattern="^(?i)(asc|desc)$"),
) -> AppointmentListSpec:
if sort_by not in ALLOWED_SORT_FIELDS:
sort_by = "scheduled_start"
return AppointmentListSpec(
status=status_filter,
doctor_id=doctor_id,
clinic_id=clinic_id,
patient_id=patient_id,
date_from=date_from,
date_to=date_to,
q=q,
sort_by=sort_by,
sort_dir=sort_dir.lower(),
)
@schedules_router.post("", response_model=ScheduleRead, status_code=status.HTTP_201_CREATED)
async def create_schedule(
body: ScheduleCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
user: CurrentUser = Depends(require_permissions(SCHEDULES_MANAGE)),
):
return await AppointmentCommands(db).create_schedule(tenant_id, body, actor=user)
@schedules_router.get("", response_model=list[ScheduleRead])
async def list_schedules(
tenant_id: UUID = Depends(require_tenant),
pagination: PaginationParams = Depends(get_pagination),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(require_permissions(SCHEDULES_VIEW)),
):
return await AppointmentQueries(db).list_schedules(
tenant_id, offset=pagination.offset, limit=pagination.page_size
)
@availability_router.post("", response_model=AvailabilityWindowRead, status_code=status.HTTP_201_CREATED)
async def create_availability(
body: AvailabilityWindowCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
user: CurrentUser = Depends(require_permissions(SCHEDULES_MANAGE)),
):
return await AppointmentCommands(db).create_availability(tenant_id, body, actor=user)
@availability_router.get("", response_model=list[AvailabilityWindowRead])
async def list_availability(
tenant_id: UUID = Depends(require_tenant),
pagination: PaginationParams = Depends(get_pagination),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(require_permissions(SCHEDULES_VIEW)),
):
return await AppointmentQueries(db).list_availability(
tenant_id, offset=pagination.offset, limit=pagination.page_size
)
@appointment_types_router.post("", response_model=AppointmentTypeRead, status_code=status.HTTP_201_CREATED)
async def create_appointment_type(
body: AppointmentTypeCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
user: CurrentUser = Depends(require_permissions(APPOINTMENTS_MANAGE)),
):
return await AppointmentCommands(db).create_type(tenant_id, body, actor=user)
@appointment_types_router.get("", response_model=list[AppointmentTypeRead])
async def list_appointment_types(
tenant_id: UUID = Depends(require_tenant),
pagination: PaginationParams = Depends(get_pagination),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(require_permissions(APPOINTMENTS_VIEW)),
):
return await AppointmentQueries(db).list_types(
tenant_id, offset=pagination.offset, limit=pagination.page_size
)
@appointments_router.post("", response_model=AppointmentRead, status_code=status.HTTP_201_CREATED)
async def book_appointment(
body: AppointmentCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
user: CurrentUser = Depends(require_permissions(APPOINTMENTS_MANAGE)),
):
return await AppointmentCommands(db).book(tenant_id, body, actor=user)
@appointments_router.get("", response_model=AppointmentListResponse)
async def list_appointments(
tenant_id: UUID = Depends(require_tenant),
pagination: PaginationParams = Depends(get_pagination),
spec: AppointmentListSpec = Depends(_appointment_spec),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(require_permissions(APPOINTMENTS_VIEW)),
):
items, total = await AppointmentQueries(db).list_appointments(
tenant_id, offset=pagination.offset, limit=pagination.page_size, spec=spec
)
return AppointmentListResponse(
items=items, total=total, page=pagination.page, page_size=pagination.page_size
)
@appointments_router.get("/{appointment_id}", response_model=AppointmentRead)
async def get_appointment(
appointment_id: UUID,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(require_permissions(APPOINTMENTS_VIEW)),
):
return await AppointmentQueries(db).get_appointment(tenant_id, appointment_id)
@appointments_router.post("/{appointment_id}/confirm", response_model=AppointmentRead)
async def confirm_appointment(
appointment_id: UUID,
body: AppointmentLifecycleRequest,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
user: CurrentUser = Depends(require_permissions(APPOINTMENTS_MANAGE)),
):
return await AppointmentCommands(db).confirm(tenant_id, appointment_id, body, actor=user)
@appointments_router.post("/{appointment_id}/cancel", response_model=AppointmentRead)
async def cancel_appointment(
appointment_id: UUID,
body: AppointmentLifecycleRequest,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
user: CurrentUser = Depends(require_permissions(APPOINTMENTS_MANAGE)),
):
return await AppointmentCommands(db).cancel(tenant_id, appointment_id, body, actor=user)
@appointments_router.post("/{appointment_id}/reschedule", response_model=AppointmentRead)
async def reschedule_appointment(
appointment_id: UUID,
body: AppointmentReschedule,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
user: CurrentUser = Depends(require_permissions(APPOINTMENTS_MANAGE)),
):
return await AppointmentCommands(db).reschedule(tenant_id, appointment_id, body, actor=user)
@appointments_router.post("/{appointment_id}/no-show", response_model=AppointmentRead)
async def mark_no_show(
appointment_id: UUID,
body: AppointmentLifecycleRequest,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
user: CurrentUser = Depends(require_permissions(APPOINTMENTS_MANAGE)),
):
return await AppointmentCommands(db).mark_no_show(tenant_id, appointment_id, body, actor=user)
@waiting_list_router.post("", response_model=WaitingListEntryRead, status_code=status.HTTP_201_CREATED)
async def create_waiting_list_entry(
body: WaitingListEntryCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
user: CurrentUser = Depends(require_permissions(APPOINTMENTS_MANAGE)),
):
return await AppointmentCommands(db).create_waiting_list(tenant_id, body, actor=user)
@waiting_list_router.get("", response_model=list[WaitingListEntryRead])
async def list_waiting_list(
tenant_id: UUID = Depends(require_tenant),
pagination: PaginationParams = Depends(get_pagination),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(require_permissions(APPOINTMENTS_VIEW)),
):
return await AppointmentQueries(db).list_waiting_list(
tenant_id, offset=pagination.offset, limit=pagination.page_size
)