feat: clinic and user table change
feat: centralized db error handler fix: api responses
This commit is contained in:
parent
2efc09cf20
commit
30f51618fe
|
|
@ -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)])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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__}")
|
||||||
|
|
@ -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 ###
|
||||||
|
|
@ -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 ###
|
||||||
|
|
@ -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")
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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:
|
# First convert the user to a dictionary
|
||||||
logger.error("User not found")
|
user_dict = {}
|
||||||
raise ResourceNotFoundException("User not found")
|
for column in user.__table__.columns:
|
||||||
|
user_dict[column.name] = getattr(user, column.name)
|
||||||
|
|
||||||
user_dict = user.__dict__.copy()
|
# 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
|
||||||
|
|
||||||
user_response = UserResponse(**user_dict).model_dump()
|
# Create the user response
|
||||||
|
user_response = UserResponse.model_validate(user_dict)
|
||||||
|
|
||||||
return user_response
|
# 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:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue