From 36c439ba0e7cb080d30560e5e7eec69027195e86 Mon Sep 17 00:00:00 2001 From: deepvasoya Date: Mon, 26 May 2025 12:11:55 +0530 Subject: [PATCH] feat: clinic doc api --- apis/endpoints/clinicDoctor.py | 27 ++- models/ClinicDoctors.py | 11 +- schemas/BaseSchemas.py | 1 - schemas/CreateSchemas.py | 6 +- schemas/ResponseSchemas.py | 23 +-- schemas/UpdateSchemas.py | 7 +- services/clinicDoctorsServices.py | 266 +++++++++++++++++++++++++----- utils/constants.py | 4 +- 8 files changed, 273 insertions(+), 72 deletions(-) diff --git a/apis/endpoints/clinicDoctor.py b/apis/endpoints/clinicDoctor.py index 62a387d..fd3c1c6 100644 --- a/apis/endpoints/clinicDoctor.py +++ b/apis/endpoints/clinicDoctor.py @@ -3,23 +3,34 @@ from schemas.ApiResponse import ApiResponse from schemas.CreateSchemas import ClinicDoctorCreate from schemas.UpdateSchemas import ClinicDoctorUpdate from services.clinicDoctorsServices import ClinicDoctorsServices +from fastapi import Request +from utils.constants import DEFAULT_ORDER, DEFAULT_ORDER_BY, DEFAULT_PAGE, DEFAULT_LIMIT router = APIRouter() @router.get("/") -async def get_clinic_doctors(): - clinic_doctors = await ClinicDoctorsServices().get_clinic_doctors() +async def get_clinic_doctors( + limit:int= DEFAULT_LIMIT, + page:int = DEFAULT_PAGE, + search:str = "", + sort_by:str = DEFAULT_ORDER, + sort_order:str = DEFAULT_ORDER_BY +): + if page < 1: + page = 1 + offset = (page - 1) * limit + clinic_doctors = await ClinicDoctorsServices().get_clinic_doctors(limit, offset, search, sort_by, sort_order) return ApiResponse(data=clinic_doctors, message="Clinic doctors retrieved successfully") @router.post("/") -async def create_clinic_doctor(clinic_doctor: ClinicDoctorCreate): - clinic_doctor = await ClinicDoctorsServices().create_clinic_doctor(clinic_doctor) - return ApiResponse(data=clinic_doctor, message="Clinic doctor created successfully") +async def create_clinic_doctor(req:Request, clinic_doctor: ClinicDoctorCreate): + await ClinicDoctorsServices().create_clinic_doctor(req.state.user, clinic_doctor) + return ApiResponse(data="OK", message="Clinic doctor created successfully") @router.put("/{clinic_doctor_id}") -async def update_clinic_doctor(clinic_doctor_id: int, clinic_doctor: ClinicDoctorUpdate): - clinic_doctor = await ClinicDoctorsServices().update_clinic_doctor(clinic_doctor_id, clinic_doctor) - return ApiResponse(data=clinic_doctor, message="Clinic doctor updated successfully") +async def update_clinic_doctor(req:Request, clinic_doctor_id: int, clinic_doctor: ClinicDoctorUpdate): + await ClinicDoctorsServices().update_clinic_doctor(req.state.user, clinic_doctor_id, clinic_doctor) + return ApiResponse(data="OK", message="Clinic doctor updated successfully") @router.delete("/{clinic_doctor_id}") async def delete_clinic_doctor(clinic_doctor_id: int): diff --git a/models/ClinicDoctors.py b/models/ClinicDoctors.py index fe58273..0b563e7 100644 --- a/models/ClinicDoctors.py +++ b/models/ClinicDoctors.py @@ -1,9 +1,10 @@ -from sqlalchemy import Column, Enum, Integer, String, ForeignKey,Table +from sqlalchemy import Column, Enum, Integer, String, ForeignKey, Table from database import Base from enums.enums import ClinicDoctorType, ClinicDoctorStatus from .CustomBase import CustomBase from sqlalchemy.orm import relationship + class ClinicDoctors(Base, CustomBase): __tablename__ = "clinic_doctors" @@ -12,7 +13,11 @@ class ClinicDoctors(Base, CustomBase): role = Column(Enum(ClinicDoctorType)) status = Column(Enum(ClinicDoctorStatus)) - appointmentRelations = relationship("AppointmentRelations", back_populates="clinicDoctors") + appointmentRelations = relationship( + "AppointmentRelations", + back_populates="clinicDoctors", + cascade="all, delete-orphan", + passive_deletes=True, + ) clinic_id = Column(Integer, ForeignKey("clinics.id")) clinic = relationship("Clinics", back_populates="clinicDoctors") - \ No newline at end of file diff --git a/schemas/BaseSchemas.py b/schemas/BaseSchemas.py index e354117..1d98bb4 100644 --- a/schemas/BaseSchemas.py +++ b/schemas/BaseSchemas.py @@ -116,7 +116,6 @@ class ClinicDoctorBase(BaseModel): name: str role: ClinicDoctorType status: ClinicDoctorStatus - clinic_id: int class CallTranscriptsBase(BaseModel): diff --git a/schemas/CreateSchemas.py b/schemas/CreateSchemas.py index a9d4362..344f472 100644 --- a/schemas/CreateSchemas.py +++ b/schemas/CreateSchemas.py @@ -54,8 +54,10 @@ class UserCreate(BaseModel): clinic: ClinicBase -class ClinicDoctorCreate(ClinicDoctorBase): - pass +class ClinicDoctorCreate(BaseModel): + name: str + role: ClinicDoctorType + appointmentTypes: list[int] class CallTranscriptsCreate(CallTranscriptsBase): diff --git a/schemas/ResponseSchemas.py b/schemas/ResponseSchemas.py index 395c4e9..ee29d0e 100644 --- a/schemas/ResponseSchemas.py +++ b/schemas/ResponseSchemas.py @@ -5,6 +5,7 @@ from enums.enums import ClinicStatus from .BaseSchemas import * from pydantic import Field + # Response schemas (used for API responses) class Clinic(ClinicBase): id: int @@ -151,15 +152,6 @@ class AppointmentDetailed(AppointmentSchema): patient: Patient -class ClinicDoctorResponse(ClinicDoctorBase): - id: int - create_time: datetime - update_time: datetime - - class Config: - orm_mode = True - - class CallTranscriptsResponse(CallTranscriptsBase): id: int create_time: datetime @@ -187,6 +179,18 @@ class MasterAppointmentTypeResponse(MasterAppointmentTypeBase): orm_mode = True +class ClinicDoctorResponse(ClinicDoctorBase): + id: int + create_time: datetime + update_time: datetime + appointmentTypes: Optional[List[MasterAppointmentTypeResponse]] = [] + + class Config: + orm_mode = True + from_attributes = True + allow_population_by_field_name = True + + class ClinicOfferResponse(ClinicOffersBase): id: int create_time: datetime @@ -194,4 +198,3 @@ class ClinicOfferResponse(ClinicOffersBase): class Config: orm_mode = True - \ No newline at end of file diff --git a/schemas/UpdateSchemas.py b/schemas/UpdateSchemas.py index 105620d..ab855c3 100644 --- a/schemas/UpdateSchemas.py +++ b/schemas/UpdateSchemas.py @@ -75,5 +75,8 @@ class UserUpdate(BaseModel): password: Optional[str] = None -class ClinicDoctorUpdate(ClinicDoctorBase): - pass +class ClinicDoctorUpdate(BaseModel): + name: Optional[str] = None + role: Optional[ClinicDoctorType] = None + status: Optional[ClinicDoctorStatus] = None + appointmentTypes: Optional[list[int]] = None diff --git a/services/clinicDoctorsServices.py b/services/clinicDoctorsServices.py index c15c6ce..f59b221 100644 --- a/services/clinicDoctorsServices.py +++ b/services/clinicDoctorsServices.py @@ -1,79 +1,257 @@ +from loguru import logger from schemas.CreateSchemas import ClinicDoctorCreate from schemas.UpdateSchemas import ClinicDoctorUpdate -from schemas.ResponseSchemas import ClinicDoctorResponse +from schemas.ResponseSchemas import ClinicDoctorResponse, MasterAppointmentTypeResponse from database import get_db from models import ClinicDoctors -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, joinedload, selectinload from services.clinicServices import ClinicServices from exceptions import ResourceNotFoundException from interface.common_response import CommonResponse -from sqlalchemy import func -from enums.enums import ClinicDoctorStatus +from sqlalchemy import func, or_, cast, String +from enums.enums import ClinicDoctorStatus, UserType +from models import MasterAppointmentTypes, AppointmentRelations +from utils.constants import DEFAULT_ORDER, DEFAULT_ORDER_BY + class ClinicDoctorsServices: def __init__(self): self.db: Session = next(get_db()) self.clinic_services = ClinicServices() + self.logger = logger - async def create_clinic_doctor(self, clinic_doctor: ClinicDoctorCreate) -> ClinicDoctorResponse: + async def create_clinic_doctor( + self, user, clinic_doctor: ClinicDoctorCreate + ) -> ClinicDoctorResponse: + try: - # check if clinic exists - self.clinic_services.get_clinic_by_id(clinic_doctor.clinic_id) + if user["userType"] != UserType.CLINIC_ADMIN: + self.logger.error("user is not clinic admin") + raise ResourceNotFoundException( + "You are not authorized to perform this action" + ) - clinic_doctor = ClinicDoctors(**clinic_doctor.model_dump()) - self.db.add(clinic_doctor) - self.db.commit() - self.db.refresh(clinic_doctor) - return ClinicDoctorResponse(**clinic_doctor.__dict__.copy()) + if not user["created_clinics"][0]["id"]: + self.logger.error("user has no clinics") + raise ResourceNotFoundException( + "You are not authorized to perform this action" + ) - async def update_clinic_doctor(self, clinic_doctor_id: int, clinic_doctor_data: ClinicDoctorUpdate) -> ClinicDoctorResponse: - # check if clinic doctor exists - clinic_doctor = self.db.query(ClinicDoctors).filter(ClinicDoctors.id == clinic_doctor_id).first() - if clinic_doctor is None: - raise ResourceNotFoundException("Clinic doctor not found") + # verify all appointment types exist in a single query + if clinic_doctor.appointmentTypes: + existing_types = { + t.id + for t in self.db.query(MasterAppointmentTypes.id) + .filter( + MasterAppointmentTypes.id.in_(clinic_doctor.appointmentTypes) + ) + .all() + } + missing_types = set(clinic_doctor.appointmentTypes) - existing_types + if missing_types: + raise ResourceNotFoundException( + f"Appointment types not found: {', '.join(map(str, missing_types))}" + ) - if clinic_doctor_data.clinic_id != clinic_doctor.clinic_id: # check if clinic exists - self.clinic_services.get_clinic_by_id(clinic_doctor_data.clinic_id) + await self.clinic_services.get_clinic_by_id(user["created_clinics"][0]["id"]) - # Update the existing object with new values - update_data = clinic_doctor_data.model_dump(exclude_unset=True) - for key, value in update_data.items(): - setattr(clinic_doctor, key, value) - - self.db.add(clinic_doctor) - self.db.commit() - self.db.refresh(clinic_doctor) - return ClinicDoctorResponse(**clinic_doctor.__dict__.copy()) + # exclude appointmentTypes from clinic_doctor + clinic_doctor_db = ClinicDoctors( + name=clinic_doctor.name, + clinic_id=user["created_clinics"][0]["id"], + role=clinic_doctor.role, + status=ClinicDoctorStatus.ACTIVE, + ) + self.db.add(clinic_doctor_db) + self.db.flush() + # create clinic doctor appointment types + for appointment_type_id in clinic_doctor.appointmentTypes: + clinic_doctor_appointment_type = AppointmentRelations( + clinic_doctor_id=clinic_doctor_db.id, + appointment_type_id=appointment_type_id, + ) + self.db.add(clinic_doctor_appointment_type) + + self.db.commit() + + return + except Exception as e: + self.logger.error(e) + self.db.rollback() + raise e + + async def update_clinic_doctor( + self, user, clinic_doctor_id: int, clinic_doctor_data: ClinicDoctorUpdate + ) -> ClinicDoctorResponse: + + try: + + if user["userType"] != UserType.CLINIC_ADMIN: + self.logger.error("user is not clinic admin") + raise ResourceNotFoundException( + "You are not authorized to perform this action" + ) + + if not user["created_clinics"][0]["id"]: + self.logger.error("user has no clinics") + raise ResourceNotFoundException( + "You are not authorized to perform this action" + ) + + # verify all appointment types exist in a single query + if clinic_doctor_data.appointmentTypes: + existing_types = { + t.id + for t in self.db.query(MasterAppointmentTypes.id) + .filter( + MasterAppointmentTypes.id.in_( + clinic_doctor_data.appointmentTypes + ) + ) + .all() + } + + missing_types = set(clinic_doctor_data.appointmentTypes) - existing_types + if missing_types: + raise ResourceNotFoundException( + f"Appointment types not found: {', '.join(map(str, missing_types))}" + ) + + # check if clinic doctor exists + clinic_doctor = ( + self.db.query(ClinicDoctors) + .filter(ClinicDoctors.id == clinic_doctor_id) + .first() + ) + + if clinic_doctor is None: + raise ResourceNotFoundException("Clinic doctor not found") + + + # Update the existing object with new values + update_data = clinic_doctor_data.model_dump(exclude_unset=True) + + for key, value in update_data.items(): + setattr(clinic_doctor, key, value) + + self.db.add(clinic_doctor) + + # delete existing clinic doctor appointment types + self.db.query(AppointmentRelations).filter( + AppointmentRelations.clinic_doctor_id == clinic_doctor_id + ).delete() + + # create clinic doctor appointment types + for appointment_type_id in clinic_doctor_data.appointmentTypes: + clinic_doctor_appointment_type = AppointmentRelations( + clinic_doctor_id=clinic_doctor_id, + appointment_type_id=appointment_type_id, + ) + self.db.add(clinic_doctor_appointment_type) + + self.db.commit() + return + except Exception as e: + self.db.rollback() + raise e async def delete_clinic_doctor(self, clinic_doctor_id: int): - clinic_doctor = self.db.query(ClinicDoctors).filter(ClinicDoctors.id == clinic_doctor_id).first() + clinic_doctor = ( + self.db.query(ClinicDoctors) + .filter(ClinicDoctors.id == clinic_doctor_id) + .first() + ) self.db.delete(clinic_doctor) self.db.commit() async def get_doctor_status_count(self): - + # Query to count doctors by status - status_counts = self.db.query( - ClinicDoctors.status, - func.count(ClinicDoctors.id).label('count') - ).group_by(ClinicDoctors.status).all() - + status_counts = ( + self.db.query( + ClinicDoctors.status, func.count(ClinicDoctors.id).label("count") + ) + .group_by(ClinicDoctors.status) + .all() + ) + # Initialize result dictionary with all possible statuses set to 0 result = {status.value: 0 for status in ClinicDoctorStatus} - + # Update with actual counts from the query for status, count in status_counts: result[status.value] = count - + return result - - async def get_clinic_doctors(self): - clinic_doctors = self.db.query(ClinicDoctors).all() - total = self.db.query(ClinicDoctors).count() + async def get_clinic_doctors(self, limit: int, offset: int, search: str = "", sort_by: str = DEFAULT_ORDER, sort_order: str = DEFAULT_ORDER_BY): + try: + clinic_doctors_query = ( + self.db.query(ClinicDoctors) + .options( + selectinload(ClinicDoctors.appointmentRelations) + .selectinload(AppointmentRelations.masterAppointmentTypes) + ) + .order_by( + getattr(ClinicDoctors, sort_by).desc() + if sort_order == "desc" + else getattr(ClinicDoctors, sort_by).asc() + ) + ) - response = CommonResponse(data=[ClinicDoctorResponse(**clinic_doctor.__dict__.copy()) for clinic_doctor in clinic_doctors], total=total) + total = self.db.query(ClinicDoctors).count() - return response - \ No newline at end of file + if search: + clinic_doctors_query = clinic_doctors_query.filter( + or_( + ClinicDoctors.name.ilike(f"%{search}%"), + cast(ClinicDoctors.role, String).ilike(f"%{search}%"), + ClinicDoctors.appointmentRelations.any( + AppointmentRelations.masterAppointmentTypes.has( + MasterAppointmentTypes.type.ilike(f"%{search}%") + ) + ) + ) + ) + total = clinic_doctors_query.count() + + clinic_doctors = clinic_doctors_query.limit(limit).offset(offset).all() + + # Build response data manually to include appointment types + response_data = [] + for clinic_doctor in clinic_doctors: + # Extract appointment types from the relationships + appointment_types = [] + for relation in clinic_doctor.appointmentRelations: + if relation.masterAppointmentTypes: + appointment_types.append( + MasterAppointmentTypeResponse( + id=relation.masterAppointmentTypes.id, + type=relation.masterAppointmentTypes.type, + create_time=relation.masterAppointmentTypes.create_time, + update_time=relation.masterAppointmentTypes.update_time + ) + ) + + # Create the clinic doctor response + clinic_doctor_data = ClinicDoctorResponse( + id=clinic_doctor.id, + name=clinic_doctor.name, + role=clinic_doctor.role, + status=clinic_doctor.status, + create_time=clinic_doctor.create_time, + update_time=clinic_doctor.update_time, + appointmentTypes=appointment_types + ) + response_data.append(clinic_doctor_data) + + response = CommonResponse( + data=response_data, + total=total, + ) + + return response + except Exception as e: + self.logger.error(e) + raise e diff --git a/utils/constants.py b/utils/constants.py index 3a4c080..0d7ed67 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -6,8 +6,8 @@ dotenv.load_dotenv() DEFAULT_SKIP = 0 DEFAULT_PAGE = 1 DEFAULT_LIMIT = 10 -DEFAULT_ORDER_BY = "id" -DEFAULT_ORDER = "desc" +DEFAULT_ORDER = "id" +DEFAULT_ORDER_BY = "desc" # jwt JWT_ALGORITHM = os.getenv("JWT_ALGORITHM")