feat: clinic and user table change

feat: centralized db error handler
fix: api responses
This commit is contained in:
deepvasoya 2025-05-16 13:24:26 +05:30
parent 2efc09cf20
commit 30f51618fe
18 changed files with 338 additions and 80 deletions

View File

@ -27,8 +27,7 @@ api_router.include_router(
api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
api_router.include_router(s3.router, dependencies=[Depends(auth_required)], api_router.include_router(s3.router, prefix="/s3", tags=["s3"])
prefix="/s3", tags=["s3"])
api_router.include_router(users.router, prefix="/users", tags=["users"], dependencies=[Depends(auth_required)]) api_router.include_router(users.router, prefix="/users", tags=["users"], dependencies=[Depends(auth_required)])

View File

@ -2,12 +2,13 @@ from fastapi import APIRouter
from services.authService import AuthService from services.authService import AuthService
from schemas.CreateSchemas import UserCreate from schemas.CreateSchemas import UserCreate
from schemas.ApiResponse import ApiResponse from schemas.ApiResponse import ApiResponse
from schemas.BaseSchemas import AuthBase
router = APIRouter() router = APIRouter()
@router.post("/login") @router.post("/login")
async def login(email: str, password: str): def login(data: AuthBase):
token = await AuthService().login(email, password) token = AuthService().login(data)
return ApiResponse( return ApiResponse(
data=token, data=token,
message="Login successful" message="Login successful"
@ -15,9 +16,9 @@ async def login(email: str, password: str):
@router.post("/register") @router.post("/register")
async def register(user_data: UserCreate): def register(user_data: UserCreate):
await AuthService().register(user_data) token = AuthService().register(user_data)
return ApiResponse( return ApiResponse(
data="OK", data=token,
message="User registered successfully" message="User registered successfully"
) )

View File

@ -24,13 +24,17 @@ async def get_clinics(
skip: int = DEFAULT_SKIP, limit: int = DEFAULT_LIMIT skip: int = DEFAULT_SKIP, limit: int = DEFAULT_LIMIT
): ):
clinics = ClinicServices().get_clinics(skip, 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}") @router.get("/{clinic_id}")
async def get_clinic(clinic_id: int): async def get_clinic(clinic_id: int):
clinic = ClinicServices().get_clinic_by_id(clinic_id) 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) @router.put("/{clinic_id}", response_model=Clinic)
@ -38,7 +42,7 @@ async def update_clinic(
clinic_id: int, clinic: ClinicUpdate clinic_id: int, clinic: ClinicUpdate
): ):
clinic = ClinicServices().update_clinic(clinic_id, clinic) 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) @router.delete("/{clinic_id}", status_code=status.HTTP_204_NO_CONTENT)

View File

@ -1,15 +1,17 @@
from fastapi import APIRouter, status from fastapi import APIRouter, status
from fastapi import Request from fastapi import Request
from services.s3Service import upload_file as upload_file_service 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.ApiResponse import ApiResponse
from schemas.CreateSchemas import S3Create
router = APIRouter() router = APIRouter()
@router.post("/", status_code=status.HTTP_200_OK) @router.post("/")
async def upload_file(request: Request, folder: S3FolderNameEnum, file_name: str, clinic_id: Optional[str] = None): def upload_file(data:S3Create):
userId = request.state.user["id"] try:
resp = await upload_file_service(userId, folder, file_name, clinic_id) resp = upload_file_service(data.folder, data.file_name)
return ApiResponse(data=resp, message="File uploaded successfully") return ApiResponse(data=resp, message="File uploaded successfully")
except Exception as e:
logger.error(f"Error uploading file: {str(e)}")
raise e

View File

@ -9,12 +9,12 @@ from utils.constants import DEFAULT_LIMIT, DEFAULT_PAGE
router = APIRouter() router = APIRouter()
@router.get("/") @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: if page == 0:
page = 1 page = 1
offset = (page - 1) * limit offset = (page - 1) * limit
user = await UserServices().get_users(limit, offset, search) user = UserServices().get_users(limit, offset, search)
return ApiResponse( return ApiResponse(
data=user, data=user,
@ -22,43 +22,47 @@ async def get_users(limit:int = DEFAULT_LIMIT, page:int = DEFAULT_PAGE, search:s
) )
@router.get("/me") @router.get("/me")
async def get_user(request: Request): def get_user(request: Request):
user_id = request.state.user["id"] try:
user = await UserServices().get_user(user_id) user_id = request.state.user["id"]
return ApiResponse( user = UserServices().get_user(user_id)
data=user, return ApiResponse(
message="User fetched successfully" data=user,
) message="User fetched successfully"
)
except Exception as e:
logger.error(f"Error getting user: {str(e)}")
raise e
@router.get("/{user_id}") @router.get("/{user_id}")
async def get_user(request: Request, user_id: int): def get_user(request: Request, user_id: int):
user = await UserServices().get_user(user_id) user = UserServices().get_user(user_id)
return ApiResponse( return ApiResponse(
data=user, data=user,
message="User fetched successfully" message="User fetched successfully"
) )
@router.delete("/") @router.delete("/")
async def delete_user(request: Request): def delete_user(request: Request):
user_id = request.state.user["id"] user_id = request.state.user["id"]
await UserServices().delete_user(user_id) UserServices().delete_user(user_id)
return ApiResponse( return ApiResponse(
data="OK", data="OK",
message="User deleted successfully" message="User deleted successfully"
) )
@router.put("/") @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_id = request.state.user["id"]
user = await UserServices().update_user(user_id, user_data) user = UserServices().update_user(user_id, user_data)
return ApiResponse( return ApiResponse(
data=user, data=user,
message="User updated successfully" message="User updated successfully"
) )
@router.put("/{user_id}") @router.put("/{user_id}")
async def update_user(request: Request, user_id: int, user_data: UserUpdate): def update_user(request: Request, user_id: int, user_data: UserUpdate):
user = await UserServices().update_user(user_id, user_data) user = UserServices().update_user(user_id, user_data)
return ApiResponse( return ApiResponse(
data=user, data=user,
message="User updated successfully" message="User updated successfully"

129
exceptions/db_exceptions.py Normal file
View File

@ -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__}")

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 sqlalchemy.orm import relationship
from database import Base from database import Base
@ -39,7 +39,9 @@ class Clinics(Base, CustomBase):
general_info = Column(String, nullable=True) general_info = Column(String, nullable=True)
status = Column(Enum(ClinicStatus)) status = Column(Enum(ClinicStatus))
domain = Column(String, nullable=True) # unique for each clinic 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") doctors = relationship("Doctors", back_populates="clinic")
clinicDoctors = relationship("ClinicDoctors", back_populates="clinic") clinicDoctors = relationship("ClinicDoctors", back_populates="clinic")
creator = relationship("Users", back_populates="created_clinics")

View File

@ -14,6 +14,7 @@ class Users(Base, CustomBase):
clinicRole = Column(Enum(ClinicUserRoles), nullable=True) clinicRole = Column(Enum(ClinicUserRoles), nullable=True)
userType = Column(Enum(UserType), nullable=True) userType = Column(Enum(UserType), nullable=True)
profile_pic = Column(String, nullable=True) profile_pic = Column(String, nullable=True)
mobile = Column(String)
# Notification relationships # Notification relationships
sent_notifications = relationship("Notifications", foreign_keys="Notifications.sender_id", back_populates="sender") sent_notifications = relationship("Notifications", foreign_keys="Notifications.sender_id", back_populates="sender")
@ -21,3 +22,6 @@ class Users(Base, CustomBase):
# FCM relationships # FCM relationships
fcm = relationship("Fcm", back_populates="user") fcm = relationship("Fcm", back_populates="user")
# Clinics created by this user
created_clinics = relationship("Clinics", back_populates="creator")

Binary file not shown.

View File

@ -5,11 +5,16 @@ from pydantic import BaseModel, EmailStr
from enums.enums import AppointmentStatus, ClinicDoctorStatus, ClinicDoctorType, ClinicUserRoles, UserType, Integration 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) # Base schemas (shared attributes for create/read operations)
class ClinicBase(BaseModel): class ClinicBase(BaseModel):
name: str name: str
address: Optional[str] = None address: Optional[str] = None
phone: str phone: str
emergency_phone: Optional[str] = None
email: Optional[EmailStr] = None email: Optional[EmailStr] = None
integration: Integration integration: Integration
pms_id: str pms_id: str
@ -31,6 +36,8 @@ class ClinicBase(BaseModel):
voice_model_gender: Optional[str] = None voice_model_gender: Optional[str] = None
scenarios: Optional[str] = None scenarios: Optional[str] = None
general_info: Optional[str] = None general_info: Optional[str] = None
creator_id: Optional[int] = None
fax: Optional[str] = None
class DoctorBase(BaseModel): class DoctorBase(BaseModel):
@ -70,6 +77,7 @@ class UserBase(BaseModel):
password: str password: str
clinicRole: Optional[ClinicUserRoles] = None clinicRole: Optional[ClinicUserRoles] = None
userType: Optional[UserType] = None userType: Optional[UserType] = None
mobile: str
class ClinicDoctorBase(BaseModel): class ClinicDoctorBase(BaseModel):

View File

@ -49,3 +49,9 @@ class CallTranscriptsCreate(CallTranscriptsBase):
class NotificationCreate(NotificationBase): class NotificationCreate(NotificationBase):
pass pass
class S3Create(BaseModel):
folder: str
file_name: str
clinic_id: Optional[str] = None

View File

@ -27,11 +27,14 @@ class UserResponse(UserBase):
create_time: datetime create_time: datetime
update_time: datetime update_time: datetime
password: str = Field(exclude=True) password: str = Field(exclude=True)
created_clinics: Optional[List[Clinic]] = None
class Config: class Config:
orm_mode = True orm_mode = True
from_attributes = True
allow_population_by_field_name = True allow_population_by_field_name = True
class Doctor(DoctorBase): class Doctor(DoctorBase):
id: int id: int
create_time: datetime create_time: datetime

View File

@ -3,19 +3,20 @@ from services.jwtService import create_jwt_token
from services.userServices import UserServices from services.userServices import UserServices
from utils.password_utils import verify_password from utils.password_utils import verify_password
from schemas.CreateSchemas import UserCreate from schemas.CreateSchemas import UserCreate
from schemas.BaseSchemas import AuthBase
from exceptions.unauthorized_exception import UnauthorizedException from exceptions.unauthorized_exception import UnauthorizedException
class AuthService: class AuthService:
def __init__(self): def __init__(self):
self.user_service = UserServices() self.user_service = UserServices()
async def login(self, email, password) -> str: def login(self, data: AuthBase) -> str:
# get user # get user
user = await self.user_service.get_user_by_email(email) user = self.user_service.get_user_by_email(data.email)
# verify password # verify password
if not verify_password(password, user.password): if not verify_password(data.password, user.password):
raise UnauthorizedException("Invalid credentials") raise UnauthorizedException("Invalid credentials")
# remove password from user dict # remove password from user dict
@ -26,6 +27,16 @@ class AuthService:
token = create_jwt_token(user_dict) token = create_jwt_token(user_dict)
return token return token
async def register(self, user_data: UserCreate) -> None: def register(self, user_data: UserCreate):
await self.user_service.create_user(user_data) response = self.user_service.create_user(user_data)
return 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

View File

@ -18,6 +18,10 @@ class ClinicServices:
return clinic_response 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: def get_clinic_by_id(self, clinic_id: int) -> Clinic:
clinic = self.db.query(Clinics).filter(Clinics.id == clinic_id).first() clinic = self.db.query(Clinics).filter(Clinics.id == clinic_id).first()

View File

@ -40,37 +40,27 @@ class S3Service:
def get_s3_service(): def get_s3_service():
return S3Service() return S3Service()
async def upload_file( def upload_file(
user_id: str,
folder: S3FolderNameEnum, folder: S3FolderNameEnum,
file_name: str, file_name: str,
clinic_id: Optional[str] = None
) -> Dict[str, str]: ) -> Dict[str, str]:
""" """
Generate a pre-signed URL for uploading a file to S3. Generate a pre-signed URL for uploading a file to S3.
Args: Args:
user_id: The ID of the user
folder: The folder enum to store the file in folder: The folder enum to store the file in
file_name: The name of the file file_name: The name of the file
clinic_id: Optional design ID for assets
Returns: Returns:
Dict containing the URLs and key information Dict containing the URLs and key information
""" """
s3_service = get_s3_service() 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) timestamp = int(datetime.now().timestamp() * 1000)
if folder == S3FolderNameEnum.PROFILE: if folder == S3FolderNameEnum.PROFILE:
key = f"common/{S3FolderNameEnum.PROFILE.value}/{user_id}/{timestamp}_{file_name}" key = f"common/{timestamp}_{file_name}"
else: else:
key = f"common/{S3FolderNameEnum.ASSETS.value}/clinic/{clinic_id}/{timestamp}_{file_name}" key = f"assets/{timestamp}_{file_name}"
try: try:
put_url = s3_service.s3.generate_presigned_url( put_url = s3_service.s3.generate_presigned_url(

View File

@ -10,16 +10,18 @@ from enums.enums import ClinicStatus, UserType
from schemas.UpdateSchemas import UserUpdate from schemas.UpdateSchemas import UserUpdate
from exceptions.unauthorized_exception import UnauthorizedException from exceptions.unauthorized_exception import UnauthorizedException
from interface.common_response import CommonResponse from interface.common_response import CommonResponse
from exceptions.business_exception import BusinessValidationException
from utils.password_utils import hash_password from utils.password_utils import hash_password
from schemas.CreateSchemas import UserCreate from schemas.CreateSchemas import UserCreate
from exceptions.resource_not_found_exception import ResourceNotFoundException from exceptions.resource_not_found_exception import ResourceNotFoundException
from exceptions.db_exceptions import DBExceptionHandler
class UserServices: class UserServices:
def __init__(self): def __init__(self):
self.db: Session = next(get_db()) self.db: Session = next(get_db())
async def create_user(self, user_data: UserCreate): def create_user(self, user_data: UserCreate):
# Start a transaction # Start a transaction
try: try:
user = user_data.user user = user_data.user
@ -42,23 +44,27 @@ class UserServices:
password=hash_password(user.password), password=hash_password(user.password),
clinicRole=user.clinicRole, clinicRole=user.clinicRole,
userType=user.userType, userType=user.userType,
mobile=user.mobile
) )
# Add user to database but don't commit yet # Add user to database but don't commit yet
self.db.add(new_user) self.db.add(new_user)
self.db.flush() # Flush to get the user ID without committing
# Get clinic data # Get clinic data
clinic = user_data.clinic clinic = user_data.clinic
# cross verify domain, in db # cross verify domain, in db
# Convert to lowercase and keep only alphabetic characters, hyphens, and underscores # Convert to lowercase and keep only alphanumeric characters, hyphens, and underscores
domain = ''.join(char for char in clinic.name.lower() if char.isalpha() or char == '-' or char == '_') 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() existing_clinic = self.db.query(Clinics).filter(Clinics.domain == domain).first()
if existing_clinic: if existing_clinic:
# This will trigger rollback in the exception handler # This will trigger rollback in the exception handler
raise ValidationException("Clinic with same domain already exists") raise ValidationException("Clinic with same domain already exists")
# Create clinic instance # Create clinic instance
new_clinic = Clinics( new_clinic = Clinics(
name=clinic.name, name=clinic.name,
@ -85,8 +91,9 @@ class UserServices:
voice_model_gender=clinic.voice_model_gender, voice_model_gender=clinic.voice_model_gender,
scenarios=clinic.scenarios, scenarios=clinic.scenarios,
general_info=clinic.general_info, general_info=clinic.general_info,
status=ClinicStatus.UNDER_REVIEW, status=ClinicStatus.UNDER_REVIEW, #TODO: change this to PAYMENT_DUE
domain=domain, 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 # Add clinic to database
@ -95,31 +102,49 @@ class UserServices:
# Now commit both user and clinic in a single transaction # Now commit both user and clinic in a single transaction
self.db.commit() self.db.commit()
return return new_user
except Exception as e: except Exception as e:
logger.error(f"Error creating user: {str(e)}") logger.error(f"Error creating user: {str(e)}")
# Rollback the transaction if any error occurs # Rollback the transaction if any error occurs
self.db.rollback() self.db.rollback()
if isinstance(e, ValidationException):
raise ValidationException(e.message) # Use the centralized exception handler
if isinstance(e, ResourceNotFoundException): DBExceptionHandler.handle_exception(e, context="creating user")
raise ResourceNotFoundException(e.message)
raise e
def get_user(self, user_id) -> UserResponse: 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 if not user:
user = self.db.query(Users).filter(Users.id == user_id).first() logger.error("User not found")
raise ResourceNotFoundException("User not found")
if not user:
logger.error("User not found") # First convert the user to a dictionary
raise ResourceNotFoundException("User not found") user_dict = {}
for column in user.__table__.columns:
user_dict = user.__dict__.copy() user_dict[column.name] = getattr(user, column.name)
user_response = UserResponse(**user_dict).model_dump() # Convert created clinics to dictionaries
if user.created_clinics:
return user_response 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): def get_users(self, limit:int, offset:int, search:str):
query = self.db.query(Users) query = self.db.query(Users)
@ -141,7 +166,7 @@ class UserServices:
return response 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() user = self.db.query(Users).filter(Users.email == email.lower()).first()
if not user: if not user: