swiftops-backend / src /app /api /v1 /notifications.py
kamau1's picture
refactor(filters): migrate remaining endpoints (customers, notifications, inventory) to unified Query-based filter parsing system
5ef066f
"""
Notifications API - User notification management
Endpoints for:
- GET /notifications - List user notifications with filters
- GET /notifications/stats - Get notification statistics
- GET /notifications/{id} - Get single notification
- PUT /notifications/{id}/read - Mark notification as read
- PUT /notifications/mark-all-read - Mark multiple/all as read
- DELETE /notifications/{id} - Delete notification
"""
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from typing import Optional
from uuid import UUID
from app.api.deps import get_db, get_current_user
from app.models.user import User
from app.models.notification import Notification
from app.services.notification_service import NotificationService
from app.schemas.notification import (
NotificationListResponse,
NotificationStatsResponse,
NotificationDetail,
NotificationMarkRead,
NotificationMarkReadResponse,
NotificationStatus,
NotificationType,
NotificationSourceType,
NotificationChannel
)
from app.schemas.filters import NotificationFilters
from typing import List
import logging
router = APIRouter()
logger = logging.getLogger(__name__)
def parse_notification_filters(
project_id: Optional[UUID] = Query(None),
notification_type: Optional[str] = Query(None),
source_type: Optional[str] = Query(None),
status: Optional[str] = Query(None),
channel: Optional[str] = Query(None),
is_read: Optional[bool] = Query(None),
search: Optional[str] = Query(None),
sort_by: Optional[str] = Query(None),
sort_order: str = Query("desc"),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
from_date: Optional[str] = Query(None),
to_date: Optional[str] = Query(None),
) -> NotificationFilters:
"""Parse and convert query parameters to NotificationFilters"""
def parse_csv(value: Optional[str]) -> Optional[List[str]]:
if value is None:
return None
return [item.strip() for item in value.split(',') if item.strip()]
return NotificationFilters(
project_id=project_id,
notification_type=parse_csv(notification_type),
source_type=parse_csv(source_type),
status=parse_csv(status),
channel=parse_csv(channel),
is_read=is_read,
search=search,
sort_by=sort_by,
sort_order=sort_order,
page=page,
page_size=page_size,
from_date=None,
to_date=None,
)
@router.get(
"",
response_model=NotificationListResponse,
summary="List user notifications"
)
def list_notifications(
filters: NotificationFilters = Depends(parse_notification_filters),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Get user's notifications with optional filters.
**Filters:**
- `status`: pending, sent, delivered, failed, read
- `notification_type`: assignment, status_change, approval, etc.
- `source_type`: ticket, project, expense, system, etc.
- `channel`: in_app, email, whatsapp, sms, push
- `is_read`: true (read only), false (unread only)
- `project_id`: filter by project (scopes notifications to specific project)
**Returns:**
- Paginated list of notifications
- Total count
- Has more flag
**Example:**
```
GET /notifications?is_read=false&project_id=xxx&page=1&page_size=20
```
"""
service = NotificationService()
result = service.get_user_notifications(
db=db,
user_id=current_user.id,
filters=filters
)
return result
@router.get(
"/stats",
response_model=NotificationStatsResponse,
summary="Get notification statistics"
)
def get_notification_stats(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Get notification statistics for current user.
**Returns:**
- Total notifications
- Unread count (for badge display)
- Read count
- Failed count
- Breakdown by type
- Breakdown by channel
**Example Response:**
```json
{
"total": 45,
"unread": 12,
"read": 30,
"failed": 3,
"by_type": {
"assignment": 15,
"status_change": 20,
"approval": 10
},
"by_channel": {
"in_app": 40,
"email": 5
}
}
```
"""
service = NotificationService()
stats = service.get_notification_stats(
db=db,
user_id=current_user.id
)
return stats
@router.get(
"/{notification_id}",
response_model=NotificationDetail,
summary="Get notification details"
)
def get_notification(
notification_id: UUID,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Get detailed information about a specific notification.
**Authorization:** User can only view their own notifications
**Returns:**
- Full notification details
- Delivery status
- Metadata (action URLs, etc.)
"""
notification = db.query(Notification).filter(
Notification.id == notification_id,
Notification.user_id == current_user.id,
Notification.deleted_at.is_(None)
).first()
if not notification:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found"
)
# Convert to response model
response = NotificationDetail.from_orm(notification)
response.is_read = notification.is_read
response.is_sent = notification.is_sent
return response
@router.put(
"/{notification_id}/read",
response_model=NotificationDetail,
summary="Mark notification as read"
)
def mark_notification_as_read(
notification_id: UUID,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Mark a single notification as read.
**Authorization:** User can only mark their own notifications
**Effects:**
- Sets read_at timestamp
- Updates status to 'read'
- Decrements unread count
**Returns:**
- Updated notification
"""
service = NotificationService()
notification = service.mark_as_read(
db=db,
notification_id=notification_id,
user_id=current_user.id
)
if not notification:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found"
)
# Convert to response model
response = NotificationDetail.from_orm(notification)
response.is_read = notification.is_read
response.is_sent = notification.is_sent
return response
@router.put(
"/mark-all-read",
response_model=NotificationMarkReadResponse,
summary="Mark multiple notifications as read"
)
def mark_all_notifications_as_read(
data: NotificationMarkRead,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Mark multiple notifications as read.
**Request Body:**
- `notification_ids`: Optional list of specific IDs to mark as read
- If `notification_ids` is null/empty, marks ALL unread notifications as read
**Use Cases:**
- Mark all unread: `{"notification_ids": null}`
- Mark specific: `{"notification_ids": ["uuid1", "uuid2"]}`
**Returns:**
- Count of notifications marked as read
- Success message
**Example:**
```json
{
"notification_ids": null
}
```
**Response:**
```json
{
"marked_count": 15,
"message": "Marked 15 notifications as read"
}
```
"""
service = NotificationService()
count = service.mark_all_as_read(
db=db,
user_id=current_user.id,
notification_ids=data.notification_ids
)
message = f"Marked {count} notification{'s' if count != 1 else ''} as read"
return NotificationMarkReadResponse(
marked_count=count,
message=message
)
@router.delete(
"/{notification_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete notification"
)
def delete_notification(
notification_id: UUID,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
Delete a notification.
**Authorization:** User can only delete their own notifications
**Effects:**
- Permanently removes notification from database
- Cannot be undone
**Returns:**
- 204 No Content on success
- 404 if notification not found
"""
service = NotificationService()
deleted = service.delete_notification(
db=db,
notification_id=notification_id,
user_id=current_user.id
)
if not deleted:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found"
)
logger.info(f"User {current_user.id} deleted notification {notification_id}")
return None