feat: fcm apis

feat: push notification api
This commit is contained in:
deepvasoya 2025-05-14 17:13:11 +05:30
parent 287b6e5761
commit 2efc09cf20
11 changed files with 243 additions and 2 deletions

View File

@ -5,7 +5,7 @@ from fastapi.security import HTTPBearer
# Import the security scheme # Import the security scheme
bearer_scheme = HTTPBearer(scheme_name="Bearer Authentication") bearer_scheme = HTTPBearer(scheme_name="Bearer Authentication")
from .endpoints import clinics, doctors, calender, appointments, patients, admin, auth, s3, users, clinicDoctor, dashboard, call_transcripts from .endpoints import clinics, doctors, calender, appointments, patients, admin, auth, s3, users, clinicDoctor, dashboard, call_transcripts, notifications
api_router = APIRouter() api_router = APIRouter()
# api_router.include_router(twilio.router, prefix="/twilio") # api_router.include_router(twilio.router, prefix="/twilio")
@ -37,3 +37,5 @@ api_router.include_router(clinicDoctor.router, prefix="/clinic-doctors", tags=["
api_router.include_router(dashboard.router, prefix="/dashboard", tags=["dashboard"], dependencies=[Depends(auth_required)]) api_router.include_router(dashboard.router, prefix="/dashboard", tags=["dashboard"], dependencies=[Depends(auth_required)])
api_router.include_router(call_transcripts.router, prefix="/call-transcripts", tags=["call-transcripts"], dependencies=[Depends(auth_required)]) api_router.include_router(call_transcripts.router, prefix="/call-transcripts", tags=["call-transcripts"], dependencies=[Depends(auth_required)])
api_router.include_router(notifications.router, prefix="/notifications", tags=["notifications"], dependencies=[Depends(auth_required)])

View File

@ -0,0 +1,37 @@
from fastapi import APIRouter
from utils.constants import DEFAULT_LIMIT, DEFAULT_PAGE
from services.notificationServices import NotificationServices
from schemas.ApiResponse import ApiResponse
from fastapi import Request
router = APIRouter()
@router.get("/")
def get_notifications(request: Request, limit: int = DEFAULT_LIMIT, page: int = DEFAULT_PAGE):
if page <0:
page = 1
offset = (page - 1) * limit
notifications = NotificationServices().getNotifications(request.state.user["id"], limit, offset)
return ApiResponse(data=notifications, message="Notifications retrieved successfully")
@router.delete("/")
def delete_notification(notification_id: int):
NotificationServices().deleteNotification(notification_id)
return ApiResponse(data="OK", message="Notification deleted successfully")
@router.put("/")
def update_notification_status(notification_id: int):
NotificationServices().updateNotificationStatus(notification_id)
return ApiResponse(data="OK", message="Notification status updated successfully")
@router.post("/")
def send_notification(title: str, message: str, sender_id: int, receiver_id: int):
NotificationServices().createNotification(title, message, sender_id, receiver_id)
return ApiResponse(data="OK", message="Notification sent successfully")
@router.post("/fcm")
def send_fcm_notification(req: Request, token: str):
NotificationServices().createOrUpdateFCMToken(req.state.user["id"], token)
return ApiResponse(data="OK", message="FCM Notification sent successfully")

View File

@ -0,0 +1,51 @@
"""notification table
Revision ID: ac71b9a4b040
Revises: 0ce7107c1910
Create Date: 2025-05-14 16:14:23.750891
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'ac71b9a4b040'
down_revision: Union[str, None] = '0ce7107c1910'
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.create_table('notifications',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(), nullable=True),
sa.Column('message', sa.String(), nullable=True),
sa.Column('is_read', sa.Boolean(), nullable=True),
sa.Column('sender_id', sa.Integer(), nullable=False),
sa.Column('receiver_id', sa.Integer(), nullable=False),
sa.Column('create_time', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('update_time', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['receiver_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['sender_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_notifications_id'), 'notifications', ['id'], unique=False)
op.create_index(op.f('ix_notifications_receiver_id'), 'notifications', ['receiver_id'], unique=False)
op.create_index(op.f('ix_notifications_sender_id'), 'notifications', ['sender_id'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_notifications_sender_id'), table_name='notifications')
op.drop_index(op.f('ix_notifications_receiver_id'), table_name='notifications')
op.drop_index(op.f('ix_notifications_id'), table_name='notifications')
op.drop_table('notifications')
# ### end Alembic commands ###

15
models/Fcm.py Normal file
View File

@ -0,0 +1,15 @@
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import relationship
from sqlalchemy import ForeignKey
from database import Base
from .CustomBase import CustomBase
class Fcm(Base, CustomBase):
__tablename__ = "fcm"
id = Column(Integer, primary_key=True, index=True)
token = Column(String)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
user = relationship("Users", back_populates="fcm")

19
models/Notifications.py Normal file
View File

@ -0,0 +1,19 @@
from sqlalchemy import Boolean, Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from database import Base
from .CustomBase import CustomBase
class Notifications(Base, CustomBase):
__tablename__ = "notifications"
id = Column(Integer, primary_key=True, index=True)
title = Column(String)
message = Column(String)
is_read = Column(Boolean, default=False)
sender_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
sender = relationship("Users", foreign_keys=[sender_id], back_populates="sent_notifications")
receiver_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
receiver = relationship("Users", foreign_keys=[receiver_id], back_populates="received_notifications")

View File

@ -3,6 +3,7 @@ from database import Base
from sqlalchemy import Enum from sqlalchemy import Enum
from enums.enums import ClinicUserRoles, UserType from enums.enums import ClinicUserRoles, UserType
from models.CustomBase import CustomBase from models.CustomBase import CustomBase
from sqlalchemy.orm import relationship
class Users(Base, CustomBase): class Users(Base, CustomBase):
__tablename__ = "users" __tablename__ = "users"
@ -13,3 +14,10 @@ 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)
# Notification relationships
sent_notifications = relationship("Notifications", foreign_keys="Notifications.sender_id", back_populates="sender")
received_notifications = relationship("Notifications", foreign_keys="Notifications.receiver_id", back_populates="receiver")
# FCM relationships
fcm = relationship("Fcm", back_populates="user")

View File

@ -7,6 +7,9 @@ from .Calendar import Calenders
from .AppointmentRelations import AppointmentRelations from .AppointmentRelations import AppointmentRelations
from .MasterAppointmentTypes import MasterAppointmentTypes from .MasterAppointmentTypes import MasterAppointmentTypes
from .ClinicDoctors import ClinicDoctors from .ClinicDoctors import ClinicDoctors
from .Notifications import Notifications
from .CallTranscripts import CallTranscripts
from .Fcm import Fcm
__all__ = [ __all__ = [
"Users", "Users",
@ -18,4 +21,7 @@ __all__ = [
"AppointmentRelations", "AppointmentRelations",
"MasterAppointmentTypes", "MasterAppointmentTypes",
"ClinicDoctors", "ClinicDoctors",
"Notifications",
"CallTranscripts",
"Fcm",
] ]

View File

@ -84,4 +84,12 @@ class CallTranscriptsBase(BaseModel):
patient_number:str patient_number:str
call_duration:str call_duration:str
call_received_time:str call_received_time:str
transcript_key_id:str transcript_key_id:str
class NotificationBase(BaseModel):
title: str
message: str
is_read: bool
sender_id: int
receiver_id: int

View File

@ -45,3 +45,7 @@ class ClinicDoctorCreate(ClinicDoctorBase):
class CallTranscriptsCreate(CallTranscriptsBase): class CallTranscriptsCreate(CallTranscriptsBase):
pass pass
class NotificationCreate(NotificationBase):
pass

View File

@ -150,5 +150,14 @@ class CallTranscriptsResponse(CallTranscriptsBase):
create_time: datetime create_time: datetime
update_time: datetime update_time: datetime
class Config:
orm_mode = True
class NotificationResponse(NotificationBase):
id: int
create_time: datetime
update_time: datetime
class Config: class Config:
orm_mode = True orm_mode = True

View File

@ -0,0 +1,82 @@
from logging import Logger
from database import get_db
from sqlalchemy.orm import Session
from firebase_admin import messaging
from models import Notifications, Users, Fcm
from interface.common_response import CommonResponse
from schemas.ResponseSchemas import NotificationResponse
from exceptions.resource_not_found_exception import ResourceNotFoundException
class NotificationServices:
def __init__(self):
self.db:Session = next(get_db())
def createNotification(self, title, message, sender_id, receiver_id):
# validate sender and receiver
sender = self.db.query(Users).filter(Users.id == sender_id).first()
receiver = self.db.query(Users).filter(Users.id == receiver_id).first()
if not sender or not receiver:
raise ResourceNotFoundException("Sender or receiver not found")
notification = Notifications(title=title, message=message, sender_id=sender_id, receiver_id=receiver_id)
self.db.add(notification)
self.db.commit()
return True
def getNotifications(self, user_id, limit: int, offset: int):
notifications = self.db.query(Notifications).filter(Notifications.receiver_id == user_id).limit(limit).offset(offset).all()
total = self.db.query(Notifications).filter(Notifications.receiver_id == user_id).count()
response = CommonResponse(data=[NotificationResponse(**notification.__dict__.copy()) for notification in notifications], total=total)
return response
def deleteNotification(self, notification_id):
notification = self.db.query(Notifications).filter(Notifications.id == notification_id).first()
if not notification:
raise ResourceNotFoundException("Notification not found")
self.db.delete(notification)
self.db.commit()
return True
def updateNotificationStatus(self, notification_id):
notification = self.db.query(Notifications).filter(Notifications.id == notification_id).first()
if not notification:
raise ResourceNotFoundException("Notification not found")
notification.is_read = not notification.is_read
self.db.commit()
return True
def createOrUpdateFCMToken(self,user_id,token):
fcm = self.db.query(Fcm).filter(Fcm.token == token).first()
if fcm is None:
fcm = Fcm(user_id=user_id, token=token)
self.db.add(fcm)
self.db.commit()
return True
fcm.token = token
self.db.commit()
return True
def sendPushNotification(self, token:str, payload):
try:
message = messaging.Message(
notification=messaging.Notification(
title=payload["title"],
body=payload["body"],
),
data={},
token=token,
)
response = messaging.send(message)
return response
except:
Logger.error("Failed to send push notification")