feat: clinic approval flow

This commit is contained in:
deepvasoya 2025-05-19 19:03:58 +05:30
parent 205e423b56
commit a00c3884c4
18 changed files with 396 additions and 52 deletions

View File

@ -11,7 +11,7 @@ from .endpoints import clinics, doctors, calender, appointments, patients, admin
api_router = APIRouter()
# api_router.include_router(twilio.router, prefix="/twilio")
api_router.include_router(clinics.router, prefix="/clinics", tags=["clinics"])
api_router.include_router(clinics.router, prefix="/clinics", tags=["clinics"], dependencies=[Depends(auth_required)])
api_router.include_router(doctors.router, prefix="/doctors", tags=["doctors"])

View File

@ -1,8 +1,13 @@
from fastapi import APIRouter, status
from fastapi import APIRouter, Request, status
from services.clinicServices import ClinicServices
from schemas.UpdateSchemas import ClinicStatusUpdate
from schemas.ApiResponse import ApiResponse
router = APIRouter()
@router.get("/", status_code=status.HTTP_200_OK)
def get_admin():
return {"message": "Admin"}
@router.put("/clinic/status")
def update_clinic_status(req:Request, data: ClinicStatusUpdate):
response = ClinicServices().update_clinic_status(req.state.user, data.clinic_id, data.status)
return ApiResponse(data=response, message="Clinic status updated successfully")

View File

@ -1,29 +1,33 @@
from typing import List
from fastapi import APIRouter, HTTPException, status
# database
from database import get_db
from typing import List, Literal, Union
from fastapi import APIRouter, status, Request
# schemas
from schemas.ResponseSchemas import Clinic
from schemas.UpdateSchemas import ClinicUpdate
from models.Clinics import Clinics
# services
from services.clinicServices import ClinicServices
# Constants
from schemas.ApiResponse import ApiResponse
from utils.constants import DEFAULT_SKIP, DEFAULT_LIMIT
from interface.common_response import CommonResponse
from utils.constants import DEFAULT_PAGE, DEFAULT_SKIP, DEFAULT_LIMIT
router = APIRouter()
@router.get("/", response_model=List[Clinic])
@router.get("/")
async def get_clinics(
skip: int = DEFAULT_SKIP, limit: int = DEFAULT_LIMIT
req:Request,
page: int = DEFAULT_PAGE,
limit: int = DEFAULT_LIMIT,
filter_type: Union[Literal["UNREGISTERED"], Literal["REGISTERED"]] = "UNREGISTERED",
search:str = ""
):
clinics = ClinicServices().get_clinics(skip, limit)
if page < 1:
page = 1
offset = (page - 1) * limit
clinics = ClinicServices().get_clinics(req.state.user, limit, offset, filter_type, search)
return ApiResponse(data=clinics, message="Clinics retrieved successfully" )
@router.get("/latest-id")
@ -31,26 +35,26 @@ 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("/verified-files/{clinic_id}")
async def get_verified_files(clinic_id: int):
clinic = ClinicServices().get_clinic_by_id(clinic_id)
return ApiResponse(data=clinic, message="Clinic 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")
@router.put("/{clinic_id}", response_model=Clinic)
@router.put("/{clinic_id}")
async def update_clinic(
req:Request,
clinic_id: int, clinic: ClinicUpdate
):
clinic = ClinicServices().update_clinic(clinic_id, clinic)
clinic = ClinicServices().update_clinic(req.state.user, clinic_id, clinic)
return ApiResponse(data=clinic, message="Clinic updated successfully")
@router.delete("/{clinic_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_clinic(clinic_id: int):
db_clinic = db.query(Clinics).where(Clinics.id == clinic_id).first()
if db_clinic is None:
raise HTTPException(status_code=404, detail="Clinic not found")
db.delete(db_clinic)
db.commit()
return None
ClinicServices().delete_clinic(clinic_id)
return ApiResponse(message="Clinic deleted successfully")

View File

@ -2,6 +2,7 @@ from fastapi import APIRouter, Request
from services.dashboardService import DashboardService
from schemas.ApiResponse import ApiResponse
from enums.enums import UserType
from schemas.CreateSchemas import SignupPricingMasterCreate
router = APIRouter()
@ -9,3 +10,15 @@ router = APIRouter()
async def get_clinic_doctor_status_count(req:Request):
counts = DashboardService().get_dashboard_counts(isSuperAdmin=req.state.user["userType"] == UserType.SUPER_ADMIN)
return ApiResponse(data=counts, message="Counts fetched successfully")
@router.post("/signup-pricing-master")
async def update_signup_pricing_master(req:Request, signup_pricing_master:SignupPricingMasterCreate):
user = req.state.user
response = DashboardService().update_signup_pricing_master(user, signup_pricing_master)
return ApiResponse(data=response, message="Signup pricing master updated successfully")
@router.get("/signup-pricing-master")
async def get_signup_pricing_master():
pricing = DashboardService().get_signup_pricing_master()
return ApiResponse(data=pricing, message="Signup pricing master fetched successfully")

View File

@ -0,0 +1,32 @@
"""file-verification-table
Revision ID: 497238c0338d
Revises: ad47f4af583e
Create Date: 2025-05-19 16:34:54.211429
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '497238c0338d'
down_revision: Union[str, None] = 'ad47f4af583e'
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('clinic_file_verifications', sa.Column('rejection_reason', sa.String(length=255), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('clinic_file_verifications', 'rejection_reason')
# ### end Alembic commands ###

View File

@ -0,0 +1,32 @@
"""file-verification-table
Revision ID: ec157808ef2a
Revises: 497238c0338d
Create Date: 2025-05-19 17:16:52.137111
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'ec157808ef2a'
down_revision: Union[str, None] = '497238c0338d'
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('clinic_file_verifications', sa.Column('logo_is_verified', sa.Boolean(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('clinic_file_verifications', 'logo_is_verified')
# ### end Alembic commands ###

View File

@ -0,0 +1,18 @@
from database import Base
from sqlalchemy import Column, Integer, Boolean, ForeignKey, String
from .CustomBase import CustomBase
from sqlalchemy.orm import relationship
class ClinicFileVerifications(Base, CustomBase):
__tablename__ = "clinic_file_verifications"
id = Column(Integer, primary_key=True, index=True)
clinic_id = Column(Integer, ForeignKey("clinics.id"), nullable=False)
logo_is_verified = Column(Boolean, default=False)
abn_doc_is_verified = Column(Boolean, default=False)
contract_doc_is_verified = Column(Boolean, default=False)
last_changed_by = Column(Integer, ForeignKey("users.id"), nullable=False)
rejection_reason = Column(String(255), nullable=True)
clinic = relationship("Clinics", back_populates="clinic_file_verifications")
last_changed_by_user = relationship("Users", back_populates="clinic_file_verifications")

View File

@ -44,4 +44,5 @@ class Clinics(Base, CustomBase):
# Relationships
doctors = relationship("Doctors", back_populates="clinic")
clinicDoctors = relationship("ClinicDoctors", back_populates="clinic")
creator = relationship("Users", back_populates="created_clinics")
creator = relationship("Users", back_populates="created_clinics")
clinic_file_verifications = relationship("ClinicFileVerifications", back_populates="clinic")

View File

@ -0,0 +1,11 @@
from sqlalchemy import Column, Integer, Numeric
from database import Base
from .CustomBase import CustomBase
class SignupPricingMaster(Base, CustomBase):
__tablename__ = "signup_pricing_master"
id = Column(Integer, primary_key=True, index=True)
setup_fees = Column(Numeric(precision=10, scale=2))
subscription_fees = Column(Numeric(precision=10, scale=2))
per_call_charges = Column(Numeric(precision=10, scale=2))

View File

@ -25,3 +25,4 @@ class Users(Base, CustomBase):
# Clinics created by this user
created_clinics = relationship("Clinics", back_populates="creator")
clinic_file_verifications = relationship("ClinicFileVerifications", back_populates="last_changed_by_user")

View File

@ -11,6 +11,8 @@ from .Notifications import Notifications
from .CallTranscripts import CallTranscripts
from .Fcm import Fcm
from .BlockedEmail import BlockedEmail
from .SignupPricingMaster import SignupPricingMaster
from .ClinicFileVerifications import ClinicFileVerifications
__all__ = [
"Users",
@ -26,4 +28,6 @@ __all__ = [
"CallTranscripts",
"Fcm",
"BlockedEmail",
"SignupPricingMaster",
"ClinicFileVerifications"
]

View File

@ -13,6 +13,17 @@ class SNSBase(BaseModel):
Message: str
class ClinicFileVerificationBase(BaseModel):
abn_doc_is_verified: Optional[bool] = None
contract_doc_is_verified: Optional[bool] = None
logo_is_verified: Optional[bool] = None
last_changed_by: Optional[int] = None
class SignupPricingMasterBase(BaseModel):
setup_fees: Optional[float] = None
subscription_fees: Optional[float] = None
per_call_charges: Optional[float] = None
class AuthBase(BaseModel):
email: EmailStr

View File

@ -25,6 +25,10 @@ class CalendarCreate(CalendarBase):
pass
class SignupPricingMasterCreate(SignupPricingMasterBase):
pass
class AppointmentCreateWithNames(BaseModel):
doctor_name: str
patient_name: str

View File

@ -1,5 +1,7 @@
from datetime import datetime
from typing import List
from typing import Any, List, Optional
from enums.enums import ClinicStatus
from .BaseSchemas import *
from pydantic import Field
@ -8,6 +10,7 @@ class Clinic(ClinicBase):
id: int
create_time: datetime
update_time: datetime
status: ClinicStatus
class Config:
orm_mode = True
@ -22,6 +25,15 @@ class ClinicDoctorResponse(ClinicDoctorBase):
orm_mode = True
class SignupPricingMasterResponse(SignupPricingMasterBase):
id: int
create_time: datetime
update_time: datetime
class Config:
orm_mode = True
class UserResponse(UserBase):
id: int
create_time: datetime

View File

@ -6,7 +6,6 @@ class ClinicUpdate(BaseModel):
name: Optional[str] = None
address: Optional[str] = None
phone: Optional[str] = None
status: Optional[ClinicStatus] = None
integration: Optional[Integration] = None
pms_id: Optional[str] = None
practice_name: Optional[str] = None
@ -29,6 +28,14 @@ class ClinicUpdate(BaseModel):
general_info: Optional[str] = None
class ClinicStatusUpdate(BaseModel):
clinic_id: int
status: ClinicStatus
rejection_reason: Optional[str] = None
class SignupPricingMasterUpdate(SignupPricingMasterBase):
pass
class DoctorUpdate(BaseModel):
name: Optional[str] = None
age: Optional[int] = None

View File

@ -1,51 +1,145 @@
from database import get_db
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, joinedload
from models import Clinics
from schemas.UpdateSchemas import ClinicUpdate
from schemas.UpdateSchemas import ClinicStatusUpdate, ClinicUpdate
from schemas.ResponseSchemas import Clinic
from typing import List
from typing import List, Literal, Union
from exceptions import ResourceNotFoundException
from enums.enums import ClinicStatus
from enums.enums import ClinicStatus, UserType
from exceptions.unauthorized_exception import UnauthorizedException
from interface.common_response import CommonResponse
from sqlalchemy import or_,func, case
from services.s3Service import get_signed_url
from models import ClinicFileVerifications
from schemas.BaseSchemas import ClinicFileVerificationBase
class ClinicServices:
def __init__(self):
self.db: Session = next(get_db())
def get_clinics(self, limit:int, offset:int) -> List[Clinic]:
clinics = self.db.query(Clinics).limit(limit).offset(offset).all()
def get_clinics(self, user, limit:int, offset:int, filter_type: Union[Literal["UNREGISTERED"], Literal["REGISTERED"]] = "UNREGISTERED", search:str = ""):
if user["userType"] != UserType.SUPER_ADMIN:
raise UnauthorizedException("You are not authorized to get clinics")
clinics_query = self.db.query(Clinics)
if filter_type == "UNREGISTERED":
clinics_query = clinics_query.filter(Clinics.status != ClinicStatus.ACTIVE)
elif filter_type == "REGISTERED":
clinics_query = clinics_query.filter(Clinics.status == ClinicStatus.ACTIVE)
if search:
clinics_query = clinics_query.filter(
or_(
Clinics.name.contains(search),
Clinics.email.contains(search),
Clinics.phone.contains(search),
Clinics.address.contains(search)
)
)
clinics = clinics_query.limit(limit).offset(offset).all()
# Get all counts in a single optimized query
from sqlalchemy import text
count_query = text("""
SELECT
COUNT(*) as total,
COUNT(CASE WHEN status = 'ACTIVE' THEN 1 END) as active,
COUNT(CASE WHEN status = 'REJECTED' THEN 1 END) as rejected
FROM clinics
""")
result = self.db.execute(count_query).first()
totalClinics = result.total or 0
totalRegisteredClinics = result.active or 0
totalRejectedClinics = result.rejected or 0
clinic_response = [Clinic(**clinic.__dict__.copy()) for clinic in clinics]
return clinic_response
for clinic in clinic_response:
clinic.logo = get_signed_url(clinic.logo) if clinic.logo else None
clinic_response_with_total = {
"clinics": clinic_response,
"totalRegisteredClinics": totalRegisteredClinics,
"totalRejectedClinics": totalRejectedClinics,
}
response = CommonResponse(data=clinic_response_with_total, total=totalClinics)
return 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):
try:
clinic = self.db.query(Clinics).options(joinedload(Clinics.creator)).filter(Clinics.id == clinic_id).first()
if clinic is None:
raise ResourceNotFoundException("Clinic not found")
clinic_response = Clinic(**clinic.__dict__.copy())
clinic_response.logo = get_signed_url(clinic_response.logo) if clinic_response.logo else None
clinic_response.abn_doc = get_signed_url(clinic_response.abn_doc) if clinic_response.abn_doc else None
clinic_response.contract_doc = get_signed_url(clinic_response.contract_doc) if clinic_response.contract_doc else None
clinic_resp = {
"clinic": clinic_response,
"creator": {
"name": clinic.creator.username,
"email": clinic.creator.email,
"phone": clinic.creator.mobile,
"designation": clinic.creator.clinicRole
},
"clinic_files": self.get_clinic_files(clinic_id)
}
return clinic_resp
except Exception as e:
raise Exception(e)
def get_clinic_files(self, clinic_id: int):
clinic_files = self.db.query(ClinicFileVerifications).filter(ClinicFileVerifications.clinic_id == clinic_id).first()
if clinic_files is None:
raise ResourceNotFoundException("Clinic not found")
response = ClinicFileVerificationBase(**clinic_files.__dict__.copy())
return response
def update_clinic(self, user, clinic_id: int, clinic_data: ClinicUpdate):
clinic = self.db.query(Clinics).filter(Clinics.id == clinic_id).first()
if clinic is None:
raise ResourceNotFoundException("Clinic not found")
clinic_response = Clinic(**clinic.__dict__.copy())
return clinic_response
def update_clinic(self, clinic_id: int, clinic_data: ClinicUpdate):
clinic = self.db.query(Clinics).filter(Clinics.id == clinic_id).first()
if clinic is None:
raise ResourceNotFoundException("Clinic not found")
if clinic.creator_id != user["id"]:
raise UnauthorizedException("You are not authorized to update this clinic")
update_data = clinic_data.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(clinic, key, value)
self.db.add(clinic)
self.db.commit()
self.db.refresh(clinic)
return Clinic(**clinic.__dict__.copy())
clinic_response = Clinic(**clinic.__dict__.copy())
clinic_response.logo = get_signed_url(clinic_response.logo) if clinic_response.logo else None
return clinic_response
def delete_clinic(self, clinic_id: int):
clinic = self.db.query(Clinics).filter(Clinics.id == clinic_id).first()
@ -94,4 +188,29 @@ class ClinicServices:
if key:
counts[key] = count
return counts
return counts
def update_clinic_status(self, user, clinic_id: int, status: ClinicStatus):
if user["userType"] != UserType.SUPER_ADMIN:
raise UnauthorizedException("You are not authorized to update clinic status")
clinic = self.db.query(Clinics).filter(Clinics.id == clinic_id).first()
if clinic is None:
raise ResourceNotFoundException("Clinic not found")
clinic.status = status
self.db.add(clinic)
self.db.commit()
self.db.refresh(clinic)
clinic_response = Clinic(**clinic.__dict__.copy())
# if rejected then email to clinic creator
if clinic.status == ClinicStatus.REJECTED:
pass
return clinic_response

View File

@ -1,17 +1,74 @@
from database import get_db
from services.clinicDoctorsServices import ClinicDoctorsServices
from services.clinicServices import ClinicServices
from schemas.BaseSchemas import SignupPricingMasterBase
from schemas.ResponseSchemas import SignupPricingMasterResponse
from models.SignupPricingMaster import SignupPricingMaster
from exceptions import UnauthorizedException
from enums.enums import UserType
from exceptions import ResourceNotFoundException
class DashboardService:
def __init__(self):
self.db = next(get_db())
self.clinicDoctorsServices = ClinicDoctorsServices()
self.clinicServices = ClinicServices()
def get_dashboard_counts(self, isSuperAdmin:bool):
def get_dashboard_counts(self, isSuperAdmin: bool):
if isSuperAdmin:
clinicCounts = self.clinicServices.get_clinic_count()
return clinicCounts
else:
clinicDoctorsCount = self.clinicDoctorsServices.get_doctor_status_count()
return clinicDoctorsCount
return clinicDoctorsCount
def update_signup_pricing_master(
self, user, pricing_data: SignupPricingMasterBase
):
if user["userType"] != UserType.SUPER_ADMIN:
raise UnauthorizedException(
"You are not authorized to update signup pricing master"
)
existing_pricing = self.db.query(SignupPricingMaster).first()
if existing_pricing is None:
# Create new record
new_pricing = SignupPricingMaster(
**pricing_data.model_dump()
)
self.db.add(new_pricing)
self.db.commit()
self.db.refresh(new_pricing)
response = SignupPricingMasterResponse(
**new_pricing.__dict__.copy()
)
return response
else:
# Update existing record with values from the request
existing_pricing.setup_fees = pricing_data.setup_fees
existing_pricing.subscription_fees = pricing_data.subscription_fees
existing_pricing.per_call_charges = pricing_data.per_call_charges
self.db.commit()
self.db.refresh(existing_pricing)
response = SignupPricingMasterResponse(
**existing_pricing.__dict__.copy()
)
return response
def get_signup_pricing_master(self):
signup_pricing_master = self.db.query(SignupPricingMaster).first()
if signup_pricing_master is None:
raise ResourceNotFoundException("Signup pricing master not found")
response = SignupPricingMasterResponse(
**signup_pricing_master.__dict__.copy()
)
return response

View File

@ -11,10 +11,12 @@ from schemas.UpdateSchemas import UserUpdate
from exceptions.unauthorized_exception import UnauthorizedException
from interface.common_response import CommonResponse
from exceptions.business_exception import BusinessValidationException
from models import ClinicFileVerifications
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
from sqlalchemy.orm import joinedload
class UserServices:
@ -98,7 +100,19 @@ class UserServices:
# Add clinic to database
self.db.add(new_clinic)
self.db.flush()
# Create clinic files
clinic_files = ClinicFileVerifications(
clinic_id=new_clinic.id,
abn_doc_is_verified=False,
contract_doc_is_verified=False,
last_changed_by=new_user.id
)
# Add clinic files to database
self.db.add(clinic_files)
# Now commit both user and clinic in a single transaction
self.db.commit()
@ -114,7 +128,6 @@ class UserServices:
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()
if not user: