diff --git a/apis/__init__.py b/apis/__init__.py index e5e0c55..21bacbb 100644 --- a/apis/__init__.py +++ b/apis/__init__.py @@ -1,7 +1,11 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Security from middleware.auth_dependency import auth_required +from fastapi.security import HTTPBearer -from .endpoints import clinics, doctors, calender, appointments, patients, admin, auth +# Import the security scheme +bearer_scheme = HTTPBearer(scheme_name="Bearer Authentication") + +from .endpoints import clinics, doctors, calender, appointments, patients, admin, auth, s3 api_router = APIRouter() # api_router.include_router(twilio.router, prefix="/twilio") @@ -10,5 +14,10 @@ api_router.include_router(doctors.router, prefix="/doctors", tags=["doctors"]) api_router.include_router(calender.router, prefix="/calender", tags=["calender"]) api_router.include_router(appointments.router, prefix="/appointments", tags=["appointments"]) api_router.include_router(patients.router, prefix="/patients", tags=["patients"]) -api_router.include_router(admin.router, prefix="/admin", dependencies=[Depends(auth_required)], tags=["admin"]) +api_router.include_router( + admin.router, + prefix="/admin", + dependencies=[Depends(auth_required)], + tags=["admin"]) api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) +api_router.include_router(s3.router, dependencies=[Depends(auth_required)], prefix="/s3", tags=["s3"]) \ No newline at end of file diff --git a/apis/endpoints/auth.py b/apis/endpoints/auth.py index 9cf728f..81a967c 100644 --- a/apis/endpoints/auth.py +++ b/apis/endpoints/auth.py @@ -7,17 +7,17 @@ router = APIRouter() @router.post("/login") async def login(email: str, password: str): - response = await AuthService().login(email, password) + token = await AuthService().login(email, password) return ApiResponse( - data=response, + data=token, message="Login successful" ) @router.post("/register") async def register(user_data: UserCreate): - response = await AuthService().register(user_data) + await AuthService().register(user_data) return ApiResponse( - data=response, + data="OK", message="User registered successfully" ) diff --git a/apis/endpoints/s3.py b/apis/endpoints/s3.py new file mode 100644 index 0000000..1732cb5 --- /dev/null +++ b/apis/endpoints/s3.py @@ -0,0 +1,15 @@ +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 + +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") diff --git a/enums/enums.py b/enums/enums.py index f71d749..dc912a1 100644 --- a/enums/enums.py +++ b/enums/enums.py @@ -7,12 +7,16 @@ class AppointmentStatus(Enum): CANCELLED = "cancelled" COMPLETED = "completed" +class Integration(Enum): + BP = "bp" + MEDICAL_DIRECTOR = "medical_director" + class ClinicStatus(Enum): ACTIVE = "active" INACTIVE = "inactive" UNDER_REVIEW = "under_review" - REQUESTED_DOCTOR = "requested_doctor" + REQUESTED_DOC = "requested_doc" REJECTED = "rejected" PAYMENT_DUE = "payment_due" @@ -35,3 +39,7 @@ class UserType(Enum): class Integration(Enum): BP = "bp" MEDICAL_DIRECTOR = "medical_director" + +class S3FolderNameEnum(str, Enum): + PROFILE = "profile" + ASSETS = "assets" \ No newline at end of file diff --git a/main.py b/main.py index a1528be..3c35300 100644 --- a/main.py +++ b/main.py @@ -1,19 +1,13 @@ import dotenv -from fastapi import FastAPI +from fastapi import FastAPI, Security from contextlib import asynccontextmanager import logging from fastapi.middleware.cors import CORSMiddleware +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials # db from database import Base, engine -# IMPORTANT: Import all models to register them with SQLAlchemy -# from models.Clinics import Clinics -# from models.Doctors import Doctors -# from models.Patients import Patients -# from models.Appointments import Appointments -# from models.Calendar import Calenders - # routers from apis import api_router @@ -43,8 +37,25 @@ async def lifespan(app: FastAPI): logger.info("Stopping application") +# Define the security scheme +bearer_scheme = HTTPBearer(scheme_name="Bearer Authentication") + app = FastAPI( lifespan=lifespan, + title="Twillio Voice API", + description="API for Twillio Voice application", + version="1.0.0", + # Define security scheme for Swagger UI + openapi_tags=[ + {"name": "admin", "description": "Admin operations requiring authentication"}, + {"name": "auth", "description": "Authentication operations"}, + {"name": "clinics", "description": "Operations with clinics"}, + {"name": "doctors", "description": "Operations with doctors"}, + {"name": "calender", "description": "Calendar operations"}, + {"name": "appointments", "description": "Operations with appointments"}, + {"name": "patients", "description": "Operations with patients"} + ], + swagger_ui_parameters={"defaultModelsExpandDepth": -1} ) # CORS middleware diff --git a/middleware/auth_dependency.py b/middleware/auth_dependency.py index 7e15a2b..87a36f9 100644 --- a/middleware/auth_dependency.py +++ b/middleware/auth_dependency.py @@ -18,7 +18,7 @@ async def auth_required(request: Request ,credentials: HTTPAuthorizationCredenti raise HTTPException(status_code=401, detail="Invalid authentication token") # Get user from database - user = UserServices().get_user(payload["user_id"]) + user = UserServices().get_user(payload["id"]) # set user to request state request.state.user = user diff --git a/migrations/versions/827c736d4aeb_updated_clinic_enum_status.py b/migrations/versions/827c736d4aeb_updated_clinic_enum_status.py new file mode 100644 index 0000000..cf7b986 --- /dev/null +++ b/migrations/versions/827c736d4aeb_updated_clinic_enum_status.py @@ -0,0 +1,32 @@ +"""updated clinic enum status + +Revision ID: 827c736d4aeb +Revises: 928001a9d80f +Create Date: 2025-05-12 15:36:42.117900 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '827c736d4aeb' +down_revision: Union[str, None] = '928001a9d80f' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # Update the enum type to replace 'requested_doctor' with 'requested_doc' + op.execute("ALTER TYPE clinicstatus RENAME VALUE 'REQUESTED_DOCTOR' TO 'REQUESTED_DOC'") + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # Revert the enum type change back to 'requested_doctor' from 'requested_doc' + op.execute("ALTER TYPE clinicstatus RENAME VALUE 'REQUESTED_DOC' TO 'REQUESTED_DOCTOR'") + # ### end Alembic commands ### diff --git a/migrations/versions/928001a9d80f_updated_clinic.py b/migrations/versions/928001a9d80f_updated_clinic.py new file mode 100644 index 0000000..5911934 --- /dev/null +++ b/migrations/versions/928001a9d80f_updated_clinic.py @@ -0,0 +1,110 @@ +"""updated clinic + +Revision ID: 928001a9d80f +Revises: +Create Date: 2025-05-12 14:19:15.351582 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '928001a9d80f' +down_revision: Union[str, None] = None +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('appointments', sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True)) + op.add_column('calenders', sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True)) + op.add_column('clinics', sa.Column('emergency_phone', sa.String(), nullable=True)) + op.add_column('clinics', sa.Column('fax', sa.String(), nullable=True)) + # Create the integration enum type first + integration_type = sa.Enum('BP', 'MEDICAL_DIRECTOR', name='integration') + integration_type.create(op.get_bind()) + op.add_column('clinics', sa.Column('integration', integration_type, nullable=True)) + op.add_column('clinics', sa.Column('pms_id', sa.String(), nullable=True)) + op.add_column('clinics', sa.Column('practice_name', sa.String(), nullable=True)) + op.add_column('clinics', sa.Column('logo', sa.String(), nullable=True)) + op.add_column('clinics', sa.Column('country', sa.String(), nullable=True)) + op.add_column('clinics', sa.Column('postal_code', sa.String(), nullable=True)) + op.add_column('clinics', sa.Column('city', sa.String(), nullable=True)) + op.add_column('clinics', sa.Column('state', sa.String(), nullable=True)) + op.add_column('clinics', sa.Column('abn_doc', sa.String(), nullable=True)) + op.add_column('clinics', sa.Column('abn_number', sa.String(), nullable=True)) + op.add_column('clinics', sa.Column('contract_doc', sa.String(), nullable=True)) + op.add_column('clinics', sa.Column('clinic_phone', sa.String(), nullable=True)) + op.add_column('clinics', sa.Column('is_clinic_phone_enabled', sa.Boolean(), nullable=True)) + op.add_column('clinics', sa.Column('other_info', sa.String(), nullable=True)) + op.add_column('clinics', sa.Column('greeting_msg', sa.String(), nullable=True)) + op.add_column('clinics', sa.Column('voice_model', sa.String(), nullable=True)) + op.add_column('clinics', sa.Column('voice_model_provider', sa.String(), nullable=True)) + op.add_column('clinics', sa.Column('voice_model_gender', sa.String(), nullable=True)) + op.add_column('clinics', sa.Column('scenarios', sa.String(), nullable=True)) + op.add_column('clinics', sa.Column('general_info', sa.String(), nullable=True)) + # Create the clinicstatus enum type first + clinic_status_type = sa.Enum('ACTIVE', 'INACTIVE', 'UNDER_REVIEW', 'REQUESTED_DOCTOR', 'REJECTED', 'PAYMENT_DUE', name='clinicstatus') + clinic_status_type.create(op.get_bind()) + op.add_column('clinics', sa.Column('status', clinic_status_type, nullable=True)) + op.add_column('clinics', sa.Column('domain', sa.String(), nullable=True)) + op.add_column('clinics', sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True)) + op.create_unique_constraint(None, 'clinics', ['emergency_phone']) + op.add_column('doctors', sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True)) + op.add_column('patients', sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True)) + op.add_column('users', sa.Column('profile_pic', sa.String(), nullable=True)) + op.add_column('users', sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('users', 'deleted_at') + op.drop_column('users', 'profile_pic') + op.drop_column('patients', 'deleted_at') + op.drop_column('doctors', 'deleted_at') + op.drop_constraint(None, 'clinics', type_='unique') + op.drop_column('clinics', 'deleted_at') + op.drop_column('clinics', 'domain') + + # Drop the status column first + op.drop_column('clinics', 'status') + # Then drop the enum type + sa.Enum(name='clinicstatus').drop(op.get_bind()) + + op.drop_column('clinics', 'general_info') + op.drop_column('clinics', 'scenarios') + op.drop_column('clinics', 'voice_model_gender') + op.drop_column('clinics', 'voice_model_provider') + op.drop_column('clinics', 'voice_model') + op.drop_column('clinics', 'greeting_msg') + op.drop_column('clinics', 'other_info') + op.drop_column('clinics', 'is_clinic_phone_enabled') + op.drop_column('clinics', 'clinic_phone') + op.drop_column('clinics', 'contract_doc') + op.drop_column('clinics', 'abn_number') + op.drop_column('clinics', 'abn_doc') + op.drop_column('clinics', 'state') + op.drop_column('clinics', 'city') + op.drop_column('clinics', 'postal_code') + op.drop_column('clinics', 'country') + op.drop_column('clinics', 'logo') + op.drop_column('clinics', 'practice_name') + op.drop_column('clinics', 'pms_id') + + # Drop the integration column first + op.drop_column('clinics', 'integration') + # Then drop the enum type + sa.Enum(name='integration').drop(op.get_bind()) + + op.drop_column('clinics', 'fax') + op.drop_column('clinics', 'emergency_phone') + op.drop_column('calenders', 'deleted_at') + op.drop_column('appointments', 'deleted_at') + # ### end Alembic commands ### diff --git a/models/Clinics.py b/models/Clinics.py index e1e28ff..7fb1599 100644 --- a/models/Clinics.py +++ b/models/Clinics.py @@ -1,7 +1,9 @@ -from sqlalchemy import Column, Integer, String +from sqlalchemy import Column, Integer, String, Boolean from sqlalchemy.orm import relationship from database import Base +from enums.enums import Integration, ClinicStatus +from sqlalchemy import Enum from .CustomBase import CustomBase @@ -11,7 +13,32 @@ class Clinics(Base, CustomBase): id = Column(Integer, primary_key=True, index=True) name = Column(String) address = Column(String, nullable=True) - phone = Column(String, unique=True, index=True) + phone = Column(String, unique=True, index=True) # clinic phone + emergency_phone = Column(String, unique=True, nullable=True) email = Column(String, unique=True, index=True, nullable=True) + fax = Column(String, nullable=True) + integration = Column(Enum(Integration)) + pms_id = Column(String, nullable=True) + practice_name = Column(String, nullable=True) + logo = Column(String, nullable=True) + country = Column(String, nullable=True) + postal_code = Column(String, nullable=True) + city = Column(String, nullable=True) + state = Column(String, nullable=True) + abn_doc = Column(String, nullable=True) + abn_number = Column(String, nullable=True) + contract_doc = Column(String, nullable=True) + clinic_phone = Column(String, nullable=True) # AI Receptionist Phone + is_clinic_phone_enabled = Column(Boolean, default=False) + other_info = Column(String, nullable=True) + greeting_msg = Column(String, nullable=True) + voice_model = Column(String, nullable=True) + voice_model_provider = Column(String, nullable=True) + voice_model_gender = Column(String, nullable=True) + scenarios = Column(String, nullable=True) + general_info = Column(String, nullable=True) + status = Column(Enum(ClinicStatus)) + domain = Column(String, nullable=True) # unique for each clinic + doctors = relationship("Doctors", back_populates="clinic") diff --git a/models/Users.py b/models/Users.py index 853567e..31a0f51 100644 --- a/models/Users.py +++ b/models/Users.py @@ -12,3 +12,4 @@ class Users(Base, CustomBase): password = Column(String) clinicRole = Column(Enum(ClinicUserRoles), nullable=True) userType = Column(Enum(UserType), nullable=True) + profile_pic = Column(String, nullable=True) diff --git a/schemas/BaseSchemas.py b/schemas/BaseSchemas.py index a9916af..d82dbf2 100644 --- a/schemas/BaseSchemas.py +++ b/schemas/BaseSchemas.py @@ -2,7 +2,7 @@ from datetime import datetime from typing import List, Optional from pydantic import BaseModel, EmailStr -from enums.enums import AppointmentStatus, ClinicUserRoles, UserType +from enums.enums import AppointmentStatus, ClinicUserRoles, UserType, Integration # Base schemas (shared attributes for create/read operations) @@ -11,6 +11,26 @@ class ClinicBase(BaseModel): address: Optional[str] = None phone: str email: Optional[EmailStr] = None + integration: Integration + pms_id: str + practice_name: str + logo: Optional[str] = None + country: Optional[str] = None + postal_code: Optional[str] = None + city: Optional[str] = None + state: Optional[str] = None + abn_doc: Optional[str] = None + abn_number: Optional[str] = None + contract_doc: Optional[str] = None + clinic_phone: Optional[str] = None + is_clinic_phone_enabled: Optional[bool] = None + other_info: Optional[str] = None + greeting_msg: Optional[str] = None + voice_model: Optional[str] = None + voice_model_provider: Optional[str] = None + voice_model_gender: Optional[str] = None + scenarios: Optional[str] = None + general_info: Optional[str] = None class DoctorBase(BaseModel): diff --git a/schemas/CreateSchemas.py b/schemas/CreateSchemas.py index 22c9a10..dde4b53 100644 --- a/schemas/CreateSchemas.py +++ b/schemas/CreateSchemas.py @@ -32,5 +32,8 @@ class AppointmentCreateWithNames(BaseModel): status: AppointmentStatus = AppointmentStatus.CONFIRMED -class UserCreate(UserBase): - pass +class UserCreate(BaseModel): + # User data sent from frontend + user: UserBase + # Clinic data sent from frontend + clinic: ClinicBase diff --git a/services/authService.py b/services/authService.py index 127b8b0..e8b97f1 100644 --- a/services/authService.py +++ b/services/authService.py @@ -26,7 +26,6 @@ class AuthService: token = create_jwt_token(user_dict) return token - async def register(self, user_data: UserCreate) -> str: - user = await self.user_service.create_user(user_data) - token = create_jwt_token(user) - return token \ No newline at end of file + async def register(self, user_data: UserCreate) -> None: + await self.user_service.create_user(user_data) + return \ No newline at end of file diff --git a/services/s3Service.py b/services/s3Service.py new file mode 100644 index 0000000..b8664d1 --- /dev/null +++ b/services/s3Service.py @@ -0,0 +1,149 @@ +from enum import Enum +from typing import Optional, Dict, Any +import os +from datetime import datetime +from urllib.parse import urlparse + +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError +from fastapi import HTTPException +from pydantic_settings import BaseSettings +from enums.enums import S3FolderNameEnum +from exceptions.business_exception import BusinessValidationException + + +class Settings(BaseSettings): + AWS_REGION: str + AWS_ACCESS_KEY: str + AWS_SECRET_KEY: str + AWS_BUCKET_NAME: str + AWS_S3_EXPIRES: int = 60 * 60 # Default 1 hour + + class Config: + env_file = ".env" + extra = "ignore" # Allow extra fields from environment + +class S3Service: + def __init__(self): + self.settings = Settings() + self.bucket_name = self.settings.AWS_BUCKET_NAME + self.s3 = boto3.client( + 's3', + region_name=self.settings.AWS_REGION, + aws_access_key_id=self.settings.AWS_ACCESS_KEY, + aws_secret_access_key=self.settings.AWS_SECRET_KEY, + config=Config(signature_version='s3v4') + ) + + +def get_s3_service(): + return S3Service() + +async def upload_file( + user_id: str, + 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}" + else: + key = f"common/{S3FolderNameEnum.ASSETS.value}/clinic/{clinic_id}/{timestamp}_{file_name}" + + try: + put_url = s3_service.s3.generate_presigned_url( + ClientMethod='put_object', + Params={ + 'Bucket': s3_service.bucket_name, + 'Key': key, + }, + ExpiresIn=s3_service.settings.AWS_S3_EXPIRES + ) + + get_url = s3_service.s3.generate_presigned_url( + ClientMethod='get_object', + Params={ + 'Bucket': s3_service.bucket_name, + 'Key': key, + }, + ExpiresIn=s3_service.settings.AWS_S3_EXPIRES + ) + + url = urlparse(put_url) + + return { + "api_url": put_url, + "key": key, + "location": f"{url.scheme}://{url.netloc}/{key}", + "get_url": get_url, + } + except ClientError as e: + print(f"Error generating pre-signed URL: {e}") + raise BusinessValidationException(str(e)) + +async def get_signed_url(key: str) -> str: + """ + Generate a pre-signed URL for retrieving a file from S3. + + Args: + key: The key of the file in S3 + + Returns: + The pre-signed URL for getting the object + """ + s3_service = get_s3_service() + try: + url = s3_service.s3.generate_presigned_url( + ClientMethod='get_object', + Params={ + 'Bucket': s3_service.bucket_name, + 'Key': key, + }, + ExpiresIn=3600 # 1 hour + ) + return url + except ClientError as e: + print(f"Error in get_signed_url: {e}") + raise BusinessValidationException(str(e)) + +def get_file_key(url: str) -> str: + """ + Extract the file key from a URL or return the key if already provided. + + Args: + url: The URL or key + + Returns: + The file key + """ + try: + if not url.startswith("http://") and not url.startswith("https://"): + return url + + parsed_url = urlparse(url) + return parsed_url.path.lstrip('/') + except Exception as e: + print(f"Error in get_file_key: {e}") + raise BusinessValidationException(str(e)) \ No newline at end of file diff --git a/services/userServices.py b/services/userServices.py index 4e65c3c..dbe1020 100644 --- a/services/userServices.py +++ b/services/userServices.py @@ -1,12 +1,12 @@ from loguru import logger -from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session -from sqlalchemy import or_ from database import get_db from models.Users import Users from exceptions.validation_exception import ValidationException from schemas.ResponseSchemas import UserResponse +from models import Clinics +from enums.enums import ClinicStatus from utils.password_utils import hash_password from schemas.CreateSchemas import UserCreate from exceptions.resource_not_found_exception import ResourceNotFoundException @@ -17,11 +17,13 @@ class UserServices: self.db: Session = next(get_db()) async def create_user(self, user_data: UserCreate): + # Start a transaction try: + user = user_data.user # Check if user with same username or email exists existing_user = ( self.db.query(Users) - .filter(Users.email == user_data.email.lower()) + .filter(Users.email == user.email.lower()) .first() ) @@ -32,29 +34,72 @@ class UserServices: # Create a new user instance new_user = Users( - username=user_data.username, - email=user_data.email.lower(), - password=hash_password(user_data.password), - clinicRole=user_data.clinicRole, - userType=user_data.userType, + username=user.username, + email=user.email.lower(), + password=hash_password(user.password), + clinicRole=user.clinicRole, + userType=user.userType, ) - # Add to database and commit + # Add user to database but don't commit yet self.db.add(new_user) + + # 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 == '_') + 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, + address=clinic.address, + phone=clinic.phone, + email=clinic.email, + integration=clinic.integration, + pms_id=clinic.pms_id, + practice_name=clinic.practice_name, + logo=clinic.logo, + country=clinic.country, + postal_code=clinic.postal_code, + city=clinic.city, + state=clinic.state, + abn_doc=clinic.abn_doc, + abn_number=clinic.abn_number, + contract_doc=clinic.contract_doc, + clinic_phone=clinic.clinic_phone, + is_clinic_phone_enabled=clinic.is_clinic_phone_enabled, + other_info=clinic.other_info, + greeting_msg=clinic.greeting_msg, + voice_model=clinic.voice_model, + voice_model_provider=clinic.voice_model_provider, + voice_model_gender=clinic.voice_model_gender, + scenarios=clinic.scenarios, + general_info=clinic.general_info, + status=ClinicStatus.UNDER_REVIEW, + domain=domain, + ) + + # Add clinic to database + self.db.add(new_clinic) + + # Now commit both user and clinic in a single transaction self.db.commit() - self.db.refresh(new_user) - user_dict = new_user.__dict__.copy() - - user_response = UserResponse(**user_dict).model_dump() - - return user_response + return except Exception as e: - logger.error("Error creating user", e) + logger.error(f"Error creating user: {str(e)}") + # Rollback the transaction if any error occurs self.db.rollback() - if e.__class__ == ValidationException: + if isinstance(e, ValidationException): raise ValidationException(e.message) - if e.__class__ == ResourceNotFoundException: + if isinstance(e, ResourceNotFoundException): raise ResourceNotFoundException(e.message) raise e