From 30f51618febd8fae2672a2bde8cacf8737054820 Mon Sep 17 00:00:00 2001 From: deepvasoya Date: Fri, 16 May 2025 13:24:26 +0530 Subject: [PATCH] feat: clinic and user table change feat: centralized db error handler fix: api responses --- apis/__init__.py | 3 +- apis/endpoints/auth.py | 11 +- apis/endpoints/clinics.py | 10 +- apis/endpoints/s3.py | 16 ++- apis/endpoints/users.py | 38 +++--- exceptions/db_exceptions.py | 129 ++++++++++++++++++ .../402a9152a6fc_clinic_user_relation.py | 34 +++++ migrations/versions/ad47f4af583e_user.py | 32 +++++ models/Clinics.py | 8 +- models/Users.py | 4 + requirements.txt | Bin 4412 -> 5400 bytes schemas/BaseSchemas.py | 8 ++ schemas/CreateSchemas.py | 6 + schemas/ResponseSchemas.py | 3 + services/authService.py | 23 +++- services/clinicServices.py | 4 + services/s3Service.py | 18 +-- services/userServices.py | 71 ++++++---- 18 files changed, 338 insertions(+), 80 deletions(-) create mode 100644 exceptions/db_exceptions.py create mode 100644 migrations/versions/402a9152a6fc_clinic_user_relation.py create mode 100644 migrations/versions/ad47f4af583e_user.py diff --git a/apis/__init__.py b/apis/__init__.py index 0706c25..815469e 100644 --- a/apis/__init__.py +++ b/apis/__init__.py @@ -27,8 +27,7 @@ api_router.include_router( api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) -api_router.include_router(s3.router, dependencies=[Depends(auth_required)], -prefix="/s3", tags=["s3"]) +api_router.include_router(s3.router, prefix="/s3", tags=["s3"]) api_router.include_router(users.router, prefix="/users", tags=["users"], dependencies=[Depends(auth_required)]) diff --git a/apis/endpoints/auth.py b/apis/endpoints/auth.py index 7717fe6..38f6762 100644 --- a/apis/endpoints/auth.py +++ b/apis/endpoints/auth.py @@ -2,12 +2,13 @@ from fastapi import APIRouter from services.authService import AuthService from schemas.CreateSchemas import UserCreate from schemas.ApiResponse import ApiResponse +from schemas.BaseSchemas import AuthBase router = APIRouter() @router.post("/login") -async def login(email: str, password: str): - token = await AuthService().login(email, password) +def login(data: AuthBase): + token = AuthService().login(data) return ApiResponse( data=token, message="Login successful" @@ -15,9 +16,9 @@ async def login(email: str, password: str): @router.post("/register") -async def register(user_data: UserCreate): - await AuthService().register(user_data) +def register(user_data: UserCreate): + token = AuthService().register(user_data) return ApiResponse( - data="OK", + data=token, message="User registered successfully" ) \ No newline at end of file diff --git a/apis/endpoints/clinics.py b/apis/endpoints/clinics.py index bc7e67e..39b9865 100644 --- a/apis/endpoints/clinics.py +++ b/apis/endpoints/clinics.py @@ -24,13 +24,17 @@ async def get_clinics( skip: int = DEFAULT_SKIP, limit: int = DEFAULT_LIMIT ): clinics = ClinicServices().get_clinics(skip, limit) - return ApiResponse(data=clinics, message="Clinics retrieved successfully", status_code=status.HTTP_200_OK) + return ApiResponse(data=clinics, message="Clinics retrieved successfully" ) +@router.get("/latest-id") +async def get_latest_clinic_id(): + clinic_id = ClinicServices().get_latest_clinic_id() + return ApiResponse(data=clinic_id, message="Latest clinic ID retrieved successfully") @router.get("/{clinic_id}") async def get_clinic(clinic_id: int): clinic = ClinicServices().get_clinic_by_id(clinic_id) - return ApiResponse(data=clinic, message="Clinic retrieved successfully", status_code=status.HTTP_200_OK) + return ApiResponse(data=clinic, message="Clinic retrieved successfully") @router.put("/{clinic_id}", response_model=Clinic) @@ -38,7 +42,7 @@ async def update_clinic( clinic_id: int, clinic: ClinicUpdate ): clinic = ClinicServices().update_clinic(clinic_id, clinic) - return ApiResponse(data=clinic, message="Clinic updated successfully", status_code=status.HTTP_200_OK) + return ApiResponse(data=clinic, message="Clinic updated successfully") @router.delete("/{clinic_id}", status_code=status.HTTP_204_NO_CONTENT) diff --git a/apis/endpoints/s3.py b/apis/endpoints/s3.py index 1732cb5..a9841fd 100644 --- a/apis/endpoints/s3.py +++ b/apis/endpoints/s3.py @@ -1,15 +1,17 @@ from fastapi import APIRouter, status from fastapi import Request from services.s3Service import upload_file as upload_file_service -from enums.enums import S3FolderNameEnum -from typing import Optional from schemas.ApiResponse import ApiResponse +from schemas.CreateSchemas import S3Create router = APIRouter() -@router.post("/", status_code=status.HTTP_200_OK) -async def upload_file(request: Request, folder: S3FolderNameEnum, file_name: str, clinic_id: Optional[str] = None): - userId = request.state.user["id"] - resp = await upload_file_service(userId, folder, file_name, clinic_id) - return ApiResponse(data=resp, message="File uploaded successfully") +@router.post("/") +def upload_file(data:S3Create): + try: + resp = upload_file_service(data.folder, data.file_name) + return ApiResponse(data=resp, message="File uploaded successfully") + except Exception as e: + logger.error(f"Error uploading file: {str(e)}") + raise e diff --git a/apis/endpoints/users.py b/apis/endpoints/users.py index 3079a45..851775e 100644 --- a/apis/endpoints/users.py +++ b/apis/endpoints/users.py @@ -9,12 +9,12 @@ from utils.constants import DEFAULT_LIMIT, DEFAULT_PAGE router = APIRouter() @router.get("/") -async def get_users(limit:int = DEFAULT_LIMIT, page:int = DEFAULT_PAGE, search:str = ""): +def get_users(limit:int = DEFAULT_LIMIT, page:int = DEFAULT_PAGE, search:str = ""): if page == 0: page = 1 offset = (page - 1) * limit - user = await UserServices().get_users(limit, offset, search) + user = UserServices().get_users(limit, offset, search) return ApiResponse( data=user, @@ -22,43 +22,47 @@ async def get_users(limit:int = DEFAULT_LIMIT, page:int = DEFAULT_PAGE, search:s ) @router.get("/me") -async def get_user(request: Request): - user_id = request.state.user["id"] - user = await UserServices().get_user(user_id) - return ApiResponse( - data=user, - message="User fetched successfully" - ) +def get_user(request: Request): + try: + user_id = request.state.user["id"] + user = UserServices().get_user(user_id) + return ApiResponse( + data=user, + message="User fetched successfully" + ) + except Exception as e: + logger.error(f"Error getting user: {str(e)}") + raise e @router.get("/{user_id}") -async def get_user(request: Request, user_id: int): - user = await UserServices().get_user(user_id) +def get_user(request: Request, user_id: int): + user = UserServices().get_user(user_id) return ApiResponse( data=user, message="User fetched successfully" ) @router.delete("/") -async def delete_user(request: Request): +def delete_user(request: Request): user_id = request.state.user["id"] - await UserServices().delete_user(user_id) + UserServices().delete_user(user_id) return ApiResponse( data="OK", message="User deleted successfully" ) @router.put("/") -async def update_user(request: Request, user_data: UserUpdate): +def update_user(request: Request, user_data: UserUpdate): user_id = request.state.user["id"] - user = await UserServices().update_user(user_id, user_data) + user = UserServices().update_user(user_id, user_data) return ApiResponse( data=user, message="User updated successfully" ) @router.put("/{user_id}") -async def update_user(request: Request, user_id: int, user_data: UserUpdate): - user = await UserServices().update_user(user_id, user_data) +def update_user(request: Request, user_id: int, user_data: UserUpdate): + user = UserServices().update_user(user_id, user_data) return ApiResponse( data=user, message="User updated successfully" diff --git a/exceptions/db_exceptions.py b/exceptions/db_exceptions.py new file mode 100644 index 0000000..1e6364d --- /dev/null +++ b/exceptions/db_exceptions.py @@ -0,0 +1,129 @@ +from loguru import logger +from sqlalchemy.exc import SQLAlchemyError +from .business_exception import BusinessValidationException +from .resource_not_found_exception import ResourceNotFoundException +from .validation_exception import ValidationException + +class DBExceptionHandler: + """ + Centralized handler for database exceptions. + This class provides methods to handle and transform database exceptions + into application-specific exceptions with user-friendly messages. + """ + @staticmethod + def _extract_detail_message(e): + """ + Extract the detailed error message from a database exception. + + Args: + e: The exception to extract the message from + + Returns: + str: The detailed error message if found, None otherwise + """ + if hasattr(e, 'args') and e.args and '\nDETAIL:' in str(e.args[0]): + # Extract just the part after 'DETAIL:' + detailed_message = str(e.args[0]).split('\nDETAIL:')[1].strip() + # Clean up any trailing newlines or other characters + detailed_message = detailed_message.split('\n')[0].strip() + return detailed_message + return None + + @staticmethod + def handle_exception(e, context="database operation"): + """ + Handle database exceptions and convert them to application-specific exceptions. + + Args: + e: The exception to handle + context: A string describing the context of the operation (for logging) + + Raises: + BusinessValidationException: With a user-friendly message + ValidationException: If the original exception was a ValidationException + ResourceNotFoundException: If the original exception was a ResourceNotFoundException + """ + logger.error(f"Error during {context}: {str(e)}") + + # Pass through our custom exceptions + if isinstance(e, ValidationException): + raise ValidationException(e.message) + if isinstance(e, ResourceNotFoundException): + raise ResourceNotFoundException(e.message) + if isinstance(e, BusinessValidationException): + raise BusinessValidationException(e.message) + + # Handle SQLAlchemy errors + if isinstance(e, SQLAlchemyError): + # Check for PostgreSQL unique constraint violations + if hasattr(e, '__cause__') and hasattr(e.__cause__, '__class__') and e.__cause__.__class__.__name__ == 'UniqueViolation': + # Try to extract the detailed error message + detailed_message = DBExceptionHandler._extract_detail_message(e) + if detailed_message: + raise BusinessValidationException(detailed_message) + + # Fallback to extracting field from the error if we couldn't get the detailed message + field = None + if 'Key (' in str(e.__cause__) and ')=' in str(e.__cause__): + field = str(e.__cause__).split('Key (')[1].split(')=')[0] + + # Generic message if we couldn't extract a better one + raise BusinessValidationException(f"A record with this {field or 'information'} already exists") + + # Handle foreign key constraint violations + elif hasattr(e, '__cause__') and hasattr(e.__cause__, '__class__') and e.__cause__.__class__.__name__ == 'ForeignKeyViolation': + # Try to extract detailed message + detailed_message = DBExceptionHandler._extract_detail_message(e) + if detailed_message: + raise BusinessValidationException(detailed_message) + raise BusinessValidationException(f"Referenced record does not exist") + + # Handle check constraint violations + elif hasattr(e, '__cause__') and hasattr(e.__cause__, '__class__') and e.__cause__.__class__.__name__ == 'CheckViolation': + # Try to extract detailed message + detailed_message = DBExceptionHandler._extract_detail_message(e) + if detailed_message: + raise BusinessValidationException(detailed_message) + raise BusinessValidationException(f"Invalid data: failed validation check") + + # Handle not null constraint violations + elif hasattr(e, '__cause__') and hasattr(e.__cause__, '__class__') and e.__cause__.__class__.__name__ == 'NotNullViolation': + # Try to extract detailed message + detailed_message = DBExceptionHandler._extract_detail_message(e) + if detailed_message: + raise BusinessValidationException(detailed_message) + + # Fallback to extracting field name + field = None + if 'column "' in str(e.__cause__) and '" violates' in str(e.__cause__): + field = str(e.__cause__).split('column "')[1].split('" violates')[0] + raise BusinessValidationException(f"Required field {field or ''} cannot be empty") + + # Generic SQLAlchemy error + elif hasattr(e, '__cause__') and hasattr(e.__cause__, '__class__') and e.__cause__.__class__.__name__ == 'IntegrityError': + # Try to extract detailed message + detailed_message = DBExceptionHandler._extract_detail_message(e) + if detailed_message: + raise BusinessValidationException(detailed_message) + raise BusinessValidationException(f"Database error: {e.__class__.__name__}") + + # Handle unique constraint violations (redundant with first check, but keeping for safety) + elif hasattr(e, '__cause__') and hasattr(e.__cause__, '__class__') and e.__cause__.__class__.__name__ == 'UniqueViolation': + # Try to extract detailed message + detailed_message = DBExceptionHandler._extract_detail_message(e) + if detailed_message: + raise BusinessValidationException(detailed_message) + raise BusinessValidationException(f"A record with this information already exists") + + # Handle other database errors + elif hasattr(e, '__cause__') and hasattr(e.__cause__, '__class__') and e.__cause__.__class__.__name__ == 'OperationalError': + # Try to extract detailed message + detailed_message = DBExceptionHandler._extract_detail_message(e) + if detailed_message: + raise BusinessValidationException(detailed_message) + raise BusinessValidationException(f"Database error: {e.__class__.__name__}") + + + + # For any other exceptions, provide a generic message + raise BusinessValidationException(f"An error occurred: {e.__class__.__name__}") diff --git a/migrations/versions/402a9152a6fc_clinic_user_relation.py b/migrations/versions/402a9152a6fc_clinic_user_relation.py new file mode 100644 index 0000000..40e42de --- /dev/null +++ b/migrations/versions/402a9152a6fc_clinic_user_relation.py @@ -0,0 +1,34 @@ +"""clinic-user-relation + +Revision ID: 402a9152a6fc +Revises: ac71b9a4b040 +Create Date: 2025-05-15 12:09:28.050689 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '402a9152a6fc' +down_revision: Union[str, None] = 'ac71b9a4b040' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('clinics', sa.Column('creator_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'clinics', 'users', ['creator_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'clinics', type_='foreignkey') + op.drop_column('clinics', 'creator_id') + # ### end Alembic commands ### diff --git a/migrations/versions/ad47f4af583e_user.py b/migrations/versions/ad47f4af583e_user.py new file mode 100644 index 0000000..8d80bee --- /dev/null +++ b/migrations/versions/ad47f4af583e_user.py @@ -0,0 +1,32 @@ +"""user + +Revision ID: ad47f4af583e +Revises: 402a9152a6fc +Create Date: 2025-05-15 16:40:24.114531 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'ad47f4af583e' +down_revision: Union[str, None] = '402a9152a6fc' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('mobile', sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('users', 'mobile') + # ### end Alembic commands ### diff --git a/models/Clinics.py b/models/Clinics.py index e16db9e..bb64abc 100644 --- a/models/Clinics.py +++ b/models/Clinics.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, Integer, String, Boolean +from sqlalchemy import Column, Integer, String, Boolean, ForeignKey from sqlalchemy.orm import relationship from database import Base @@ -39,7 +39,9 @@ class Clinics(Base, CustomBase): general_info = Column(String, nullable=True) status = Column(Enum(ClinicStatus)) domain = Column(String, nullable=True) # unique for each clinic + creator_id = Column(Integer, ForeignKey("users.id"), nullable=True) # Reference to the user who created this clinic - + # Relationships doctors = relationship("Doctors", back_populates="clinic") - clinicDoctors = relationship("ClinicDoctors", back_populates="clinic") \ No newline at end of file + clinicDoctors = relationship("ClinicDoctors", back_populates="clinic") + creator = relationship("Users", back_populates="created_clinics") \ No newline at end of file diff --git a/models/Users.py b/models/Users.py index cc6a5e0..d9c5d32 100644 --- a/models/Users.py +++ b/models/Users.py @@ -14,6 +14,7 @@ class Users(Base, CustomBase): clinicRole = Column(Enum(ClinicUserRoles), nullable=True) userType = Column(Enum(UserType), nullable=True) profile_pic = Column(String, nullable=True) + mobile = Column(String) # Notification relationships sent_notifications = relationship("Notifications", foreign_keys="Notifications.sender_id", back_populates="sender") @@ -21,3 +22,6 @@ class Users(Base, CustomBase): # FCM relationships fcm = relationship("Fcm", back_populates="user") + + # Clinics created by this user + created_clinics = relationship("Clinics", back_populates="creator") diff --git a/requirements.txt b/requirements.txt index 42db48d47d78611e713afa7b83da8d49a02b6263..15de012e8d9bb2497a11b5fc0d1e06400529316e 100644 GIT binary patch delta 807 zcmZuvJxjw-6uqxf(H5;BMI1z`gHWk7N&N(;jt=7JBx!7Iv}vVj5eJdtAE>-VaCUIc zsJQwQ1i?vM)IT6Do_mvM2MI~wopaAQ_uh9uca`sdrW;fxk1FJnqj$!$m+$@a3OUrK zQ#z&&ZQ?1>5?OeQG)5^<8)SoVB$!FdK#T#jg{l_#T*>DatqH@5szclXrw!|WSb3O~ z#i9;E8N?KD`_eq`7kZuzmKuFux9!Sh-<;y(4EQ-1~+jW=IAA7bDV-(@?`*VbIdWt$<)2% z?o^E0$P~8Eqc!wda$Y|tH>NqYgQ1!D#yz1UFx0SLC`1ljgtKK89M*%c65gM!h?-HF<>5##R!Biu!uwCF>(1SH-*LZ5n|$x~MjJOg{GE zB12rto+VjwY{jP4sE=@KoXyaesqGwJq7F94*T9qXZ78xI>6gEk>&NG8)d) z)Q{=)*@B#Rpu>wgi0YZKE>SF*zi&G5BwNRtW<@RJ-5FtdOUN&>U;5uyRJ$P-Wj^C=+F diff --git a/schemas/BaseSchemas.py b/schemas/BaseSchemas.py index 9369929..78134c7 100644 --- a/schemas/BaseSchemas.py +++ b/schemas/BaseSchemas.py @@ -5,11 +5,16 @@ from pydantic import BaseModel, EmailStr from enums.enums import AppointmentStatus, ClinicDoctorStatus, ClinicDoctorType, ClinicUserRoles, UserType, Integration +class AuthBase(BaseModel): + email: EmailStr + password: str + # Base schemas (shared attributes for create/read operations) class ClinicBase(BaseModel): name: str address: Optional[str] = None phone: str + emergency_phone: Optional[str] = None email: Optional[EmailStr] = None integration: Integration pms_id: str @@ -31,6 +36,8 @@ class ClinicBase(BaseModel): voice_model_gender: Optional[str] = None scenarios: Optional[str] = None general_info: Optional[str] = None + creator_id: Optional[int] = None + fax: Optional[str] = None class DoctorBase(BaseModel): @@ -70,6 +77,7 @@ class UserBase(BaseModel): password: str clinicRole: Optional[ClinicUserRoles] = None userType: Optional[UserType] = None + mobile: str class ClinicDoctorBase(BaseModel): diff --git a/schemas/CreateSchemas.py b/schemas/CreateSchemas.py index b6cf670..7697e9f 100644 --- a/schemas/CreateSchemas.py +++ b/schemas/CreateSchemas.py @@ -49,3 +49,9 @@ class CallTranscriptsCreate(CallTranscriptsBase): class NotificationCreate(NotificationBase): pass + + +class S3Create(BaseModel): + folder: str + file_name: str + clinic_id: Optional[str] = None \ No newline at end of file diff --git a/schemas/ResponseSchemas.py b/schemas/ResponseSchemas.py index a272829..07dda9f 100644 --- a/schemas/ResponseSchemas.py +++ b/schemas/ResponseSchemas.py @@ -27,11 +27,14 @@ class UserResponse(UserBase): create_time: datetime update_time: datetime password: str = Field(exclude=True) + created_clinics: Optional[List[Clinic]] = None class Config: orm_mode = True + from_attributes = True allow_population_by_field_name = True + class Doctor(DoctorBase): id: int create_time: datetime diff --git a/services/authService.py b/services/authService.py index a64e129..bb319e3 100644 --- a/services/authService.py +++ b/services/authService.py @@ -3,19 +3,20 @@ from services.jwtService import create_jwt_token from services.userServices import UserServices from utils.password_utils import verify_password from schemas.CreateSchemas import UserCreate +from schemas.BaseSchemas import AuthBase from exceptions.unauthorized_exception import UnauthorizedException class AuthService: def __init__(self): self.user_service = UserServices() - async def login(self, email, password) -> str: + def login(self, data: AuthBase) -> str: # get user - user = await self.user_service.get_user_by_email(email) + user = self.user_service.get_user_by_email(data.email) # verify password - if not verify_password(password, user.password): + if not verify_password(data.password, user.password): raise UnauthorizedException("Invalid credentials") # remove password from user dict @@ -26,6 +27,16 @@ class AuthService: token = create_jwt_token(user_dict) return token - async def register(self, user_data: UserCreate) -> None: - await self.user_service.create_user(user_data) - return \ No newline at end of file + def register(self, user_data: UserCreate): + response = self.user_service.create_user(user_data) + user = { + "id": response.id, + "username": response.username, + "email": response.email, + "clinicRole": response.clinicRole, + "userType": response.userType, + "mobile": response.mobile, + "clinicId": response.created_clinics[0].id + } + token = create_jwt_token(user) + return token \ No newline at end of file diff --git a/services/clinicServices.py b/services/clinicServices.py index 886d0f2..97ae0a9 100644 --- a/services/clinicServices.py +++ b/services/clinicServices.py @@ -18,6 +18,10 @@ class ClinicServices: return clinic_response + def get_latest_clinic_id(self) -> int: + clinic = self.db.query(Clinics).order_by(Clinics.id.desc()).first() + return clinic.id if clinic else 0 + def get_clinic_by_id(self, clinic_id: int) -> Clinic: clinic = self.db.query(Clinics).filter(Clinics.id == clinic_id).first() diff --git a/services/s3Service.py b/services/s3Service.py index ee809da..1a53ead 100644 --- a/services/s3Service.py +++ b/services/s3Service.py @@ -40,37 +40,27 @@ class S3Service: def get_s3_service(): return S3Service() -async def upload_file( - user_id: str, +def upload_file( folder: S3FolderNameEnum, file_name: str, - clinic_id: Optional[str] = None ) -> Dict[str, str]: """ Generate a pre-signed URL for uploading a file to S3. Args: - user_id: The ID of the user folder: The folder enum to store the file in file_name: The name of the file - clinic_id: Optional design ID for assets - Returns: Dict containing the URLs and key information """ s3_service = get_s3_service() - if folder == S3FolderNameEnum.ASSETS and not clinic_id: - raise BusinessValidationException("Clinic id is required!") - - if folder != S3FolderNameEnum.PROFILE and not user_id: - raise BusinessValidationException("User id is required!") - + timestamp = int(datetime.now().timestamp() * 1000) if folder == S3FolderNameEnum.PROFILE: - key = f"common/{S3FolderNameEnum.PROFILE.value}/{user_id}/{timestamp}_{file_name}" + key = f"common/{timestamp}_{file_name}" else: - key = f"common/{S3FolderNameEnum.ASSETS.value}/clinic/{clinic_id}/{timestamp}_{file_name}" + key = f"assets/{timestamp}_{file_name}" try: put_url = s3_service.s3.generate_presigned_url( diff --git a/services/userServices.py b/services/userServices.py index 1020d1a..de5621f 100644 --- a/services/userServices.py +++ b/services/userServices.py @@ -10,16 +10,18 @@ from enums.enums import ClinicStatus, UserType from schemas.UpdateSchemas import UserUpdate from exceptions.unauthorized_exception import UnauthorizedException from interface.common_response import CommonResponse +from exceptions.business_exception import BusinessValidationException from utils.password_utils import hash_password from schemas.CreateSchemas import UserCreate from exceptions.resource_not_found_exception import ResourceNotFoundException +from exceptions.db_exceptions import DBExceptionHandler class UserServices: def __init__(self): self.db: Session = next(get_db()) - async def create_user(self, user_data: UserCreate): + def create_user(self, user_data: UserCreate): # Start a transaction try: user = user_data.user @@ -42,23 +44,27 @@ class UserServices: password=hash_password(user.password), clinicRole=user.clinicRole, userType=user.userType, + mobile=user.mobile ) # Add user to database but don't commit yet self.db.add(new_user) + self.db.flush() # Flush to get the user ID without committing # Get clinic data clinic = user_data.clinic # cross verify domain, in db - # Convert to lowercase and keep only alphabetic characters, hyphens, and underscores - domain = ''.join(char for char in clinic.name.lower() if char.isalpha() or char == '-' or char == '_') + # Convert to lowercase and keep only alphanumeric characters, hyphens, and underscores + domain = ''.join(char for char in clinic.name.lower() if char.isalnum() or char == '-' or char == '_') existing_clinic = self.db.query(Clinics).filter(Clinics.domain == domain).first() if existing_clinic: # This will trigger rollback in the exception handler raise ValidationException("Clinic with same domain already exists") + + # Create clinic instance new_clinic = Clinics( name=clinic.name, @@ -85,8 +91,9 @@ class UserServices: voice_model_gender=clinic.voice_model_gender, scenarios=clinic.scenarios, general_info=clinic.general_info, - status=ClinicStatus.UNDER_REVIEW, + status=ClinicStatus.UNDER_REVIEW, #TODO: change this to PAYMENT_DUE domain=domain, + creator_id=new_user.id, # Set the creator_id to link the clinic to the user who created it ) # Add clinic to database @@ -95,31 +102,49 @@ class UserServices: # Now commit both user and clinic in a single transaction self.db.commit() - return + return new_user except Exception as e: logger.error(f"Error creating user: {str(e)}") # Rollback the transaction if any error occurs self.db.rollback() - if isinstance(e, ValidationException): - raise ValidationException(e.message) - if isinstance(e, ResourceNotFoundException): - raise ResourceNotFoundException(e.message) - raise e + + # Use the centralized exception handler + DBExceptionHandler.handle_exception(e, context="creating user") def get_user(self, user_id) -> UserResponse: + try: + # Query the user by ID and explicitly load the created clinics relationship + from sqlalchemy.orm import joinedload + user = self.db.query(Users).options(joinedload(Users.created_clinics)).filter(Users.id == user_id).first() - # Query the user by ID - user = self.db.query(Users).filter(Users.id == user_id).first() - - if not user: - logger.error("User not found") - raise ResourceNotFoundException("User not found") - - user_dict = user.__dict__.copy() - - user_response = UserResponse(**user_dict).model_dump() - - return user_response + if not user: + logger.error("User not found") + raise ResourceNotFoundException("User not found") + + # First convert the user to a dictionary + user_dict = {} + for column in user.__table__.columns: + user_dict[column.name] = getattr(user, column.name) + + # Convert created clinics to dictionaries + if user.created_clinics: + clinics_list = [] + for clinic in user.created_clinics: + clinic_dict = {} + for column in clinic.__table__.columns: + clinic_dict[column.name] = getattr(clinic, column.name) + clinics_list.append(clinic_dict) + user_dict['created_clinics'] = clinics_list + + # Create the user response + user_response = UserResponse.model_validate(user_dict) + + # Return the response as a dictionary + return user_response.model_dump() + except Exception as e: + # Use the centralized exception handler + from twillio.exceptions.db_exceptions import DBExceptionHandler + DBExceptionHandler.handle_exception(e, context="getting user") def get_users(self, limit:int, offset:int, search:str): query = self.db.query(Users) @@ -141,7 +166,7 @@ class UserServices: return response - async def get_user_by_email(self, email: str) -> UserResponse: + def get_user_by_email(self, email: str) -> UserResponse: user = self.db.query(Users).filter(Users.email == email.lower()).first() if not user: