Spaces:
Sleeping
Sleeping
| """ | |
| 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, | |
| ) | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| ) | |
| 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 | |