From 2efc09cf201bf1b9215c20c807962e63c95b1113 Mon Sep 17 00:00:00 2001 From: deepvasoya Date: Wed, 14 May 2025 17:13:11 +0530 Subject: [PATCH] feat: fcm apis feat: push notification api --- apis/__init__.py | 4 +- apis/endpoints/notifications.py | 37 +++++++++ .../ac71b9a4b040_notification_table.py | 51 ++++++++++++ models/Fcm.py | 15 ++++ models/Notifications.py | 19 +++++ models/Users.py | 8 ++ models/__init__.py | 6 ++ schemas/BaseSchemas.py | 10 ++- schemas/CreateSchemas.py | 4 + schemas/ResponseSchemas.py | 9 ++ services/notificationServices.py | 82 +++++++++++++++++++ 11 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 apis/endpoints/notifications.py create mode 100644 migrations/versions/ac71b9a4b040_notification_table.py create mode 100644 models/Fcm.py create mode 100644 models/Notifications.py create mode 100644 services/notificationServices.py diff --git a/apis/__init__.py b/apis/__init__.py index 4ebcc8c..0706c25 100644 --- a/apis/__init__.py +++ b/apis/__init__.py @@ -5,7 +5,7 @@ from fastapi.security import HTTPBearer # Import the security scheme 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.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(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)]) diff --git a/apis/endpoints/notifications.py b/apis/endpoints/notifications.py new file mode 100644 index 0000000..a90213e --- /dev/null +++ b/apis/endpoints/notifications.py @@ -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") \ No newline at end of file diff --git a/migrations/versions/ac71b9a4b040_notification_table.py b/migrations/versions/ac71b9a4b040_notification_table.py new file mode 100644 index 0000000..eeedc69 --- /dev/null +++ b/migrations/versions/ac71b9a4b040_notification_table.py @@ -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 ### diff --git a/models/Fcm.py b/models/Fcm.py new file mode 100644 index 0000000..e607615 --- /dev/null +++ b/models/Fcm.py @@ -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") diff --git a/models/Notifications.py b/models/Notifications.py new file mode 100644 index 0000000..fc5b906 --- /dev/null +++ b/models/Notifications.py @@ -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") \ No newline at end of file diff --git a/models/Users.py b/models/Users.py index 31a0f51..cc6a5e0 100644 --- a/models/Users.py +++ b/models/Users.py @@ -3,6 +3,7 @@ from database import Base from sqlalchemy import Enum from enums.enums import ClinicUserRoles, UserType from models.CustomBase import CustomBase +from sqlalchemy.orm import relationship class Users(Base, CustomBase): __tablename__ = "users" @@ -13,3 +14,10 @@ class Users(Base, CustomBase): clinicRole = Column(Enum(ClinicUserRoles), nullable=True) userType = Column(Enum(UserType), 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") diff --git a/models/__init__.py b/models/__init__.py index 2edd73e..7f9c5f9 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -7,6 +7,9 @@ from .Calendar import Calenders from .AppointmentRelations import AppointmentRelations from .MasterAppointmentTypes import MasterAppointmentTypes from .ClinicDoctors import ClinicDoctors +from .Notifications import Notifications +from .CallTranscripts import CallTranscripts +from .Fcm import Fcm __all__ = [ "Users", @@ -18,4 +21,7 @@ __all__ = [ "AppointmentRelations", "MasterAppointmentTypes", "ClinicDoctors", + "Notifications", + "CallTranscripts", + "Fcm", ] diff --git a/schemas/BaseSchemas.py b/schemas/BaseSchemas.py index c129327..9369929 100644 --- a/schemas/BaseSchemas.py +++ b/schemas/BaseSchemas.py @@ -84,4 +84,12 @@ class CallTranscriptsBase(BaseModel): patient_number:str call_duration:str call_received_time:str - transcript_key_id:str \ No newline at end of file + transcript_key_id:str + + +class NotificationBase(BaseModel): + title: str + message: str + is_read: bool + sender_id: int + receiver_id: int diff --git a/schemas/CreateSchemas.py b/schemas/CreateSchemas.py index 021fa7b..b6cf670 100644 --- a/schemas/CreateSchemas.py +++ b/schemas/CreateSchemas.py @@ -45,3 +45,7 @@ class ClinicDoctorCreate(ClinicDoctorBase): class CallTranscriptsCreate(CallTranscriptsBase): pass + + +class NotificationCreate(NotificationBase): + pass diff --git a/schemas/ResponseSchemas.py b/schemas/ResponseSchemas.py index b0814d9..a272829 100644 --- a/schemas/ResponseSchemas.py +++ b/schemas/ResponseSchemas.py @@ -150,5 +150,14 @@ class CallTranscriptsResponse(CallTranscriptsBase): create_time: datetime update_time: datetime + class Config: + orm_mode = True + + +class NotificationResponse(NotificationBase): + id: int + create_time: datetime + update_time: datetime + class Config: orm_mode = True \ No newline at end of file diff --git a/services/notificationServices.py b/services/notificationServices.py new file mode 100644 index 0000000..0919c1e --- /dev/null +++ b/services/notificationServices.py @@ -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") \ No newline at end of file