""" app/api/notifications.py ------------------------ Mobile API endpoint for polling in-app notifications. GET /api/v1/notifications Returns unread notifications for the authenticated user. Accepts ?since= to fetch only notifications created after that datetime — used by the iPad poller to avoid re-delivering already- seen alerts. Returns a maximum of 50 notifications per call. PATCH /api/v1/notifications/mark-read Marks a list of notification IDs as read. Request JSON: { "ids": [1, 2, 3] } """ import logging from datetime import datetime from flask import Blueprint, request, g from app import db from app.models.notification import Notification from app.api.errors import api_ok, api_error from app.api.decorators import jwt_required logger = logging.getLogger(__name__) bp = Blueprint('api_notifications', __name__) _INSPECTOR_RELEVANT = { 'issue_assigned', 'issue_reassigned', 'issue_unassigned', 'issue_status', 'issue_comment', 'issue_follow_update', 'verification_requested', 'sla_alert', } def _parse_since(value): """Parse ?since= ISO 8601 string; return None on failure.""" if not value: return None for fmt in ('%Y-%m-%dT%H:%M:%S', '%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%d'): try: return datetime.strptime(value, fmt) except (ValueError, TypeError): pass return None @bp.route('/notifications', methods=['GET']) @jwt_required def list_notifications(): """ Return unread notifications for the authenticated user. Query parameters ---------------- since ISO 8601 datetime Only return notifications created after this time. Omit for the initial fetch (returns last 50). limit int (default 50, max 50) Response 200 ------------ { "ok": true, "data": { "notifications": [ { "id": 12, "title": "Issue #7 Assigned to You", "body": "...", "event_type": "issue_assigned", "issue_id": 7, "created_at": "2026-05-15T09:30:00" }, ... ], "count": 2 } } """ user = g.api_user since = _parse_since(request.args.get('since')) limit = min(int(request.args.get('limit', 50)), 50) query = Notification.query.filter_by( user_id=user.id, is_read=False, ) if since: query = query.filter(Notification.created_at > since) notifications = ( query .order_by(Notification.created_at.asc()) .limit(limit) .all() ) payload = [ { 'id': n.id, 'title': n.title, 'body': n.body, 'event_type': n.event_type, 'issue_id': n.issue_id, 'created_at': n.created_at.strftime('%Y-%m-%dT%H:%M:%S'), } for n in notifications ] logger.info('API NOTIFICATIONS | list | user=%s | since=%s | count=%d', user.username, since, len(payload)) return api_ok({'notifications': payload, 'count': len(payload)}) @bp.route('/notifications/mark-read', methods=['PATCH']) @jwt_required def mark_read(): """ Mark a list of notification IDs as read. Request JSON: { "ids": [1, 2, 3] } Response 200 ------------ { "ok": true, "data": { "marked": 3 } } """ user = g.api_user data = request.get_json(silent=True) or {} ids = data.get('ids') or [] if not isinstance(ids, list): return api_error('ids must be a list', 400) if ids: updated = ( Notification.query .filter( Notification.id.in_(ids), Notification.user_id == user.id, # never touch another user's records ) .all() ) for n in updated: n.is_read = True db.session.commit() count = len(updated) else: count = 0 logger.info('API NOTIFICATIONS | mark_read | user=%s | count=%d', user.username, count) return api_ok({'marked': count})