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>
234 lines
9.0 KiB
Python
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
|
|
)
|