Files
Aegis/backend/app/services/notification_service.py
Kitos bfce1a8a0e
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
refactor(core): introduce Unit of Work and remove commits from services
- Add UnitOfWork context manager in domain/unit_of_work.py with commit/rollback/flush API and auto-rollback on exception

- Remove all db.commit() from test_workflow_service (8 calls), notification_service (4 calls), status_service (1 call)

- Services now only stage changes via db.add/db.flush; caller owns the transaction boundary

- Update routers/tests.py: wrap 9 workflow endpoints in UnitOfWork context managers

- Update routers/notifications.py: wrap mark_as_read and mark_all_as_read in UnitOfWork
2026-02-18 12:51:55 +01:00

179 lines
5.6 KiB
Python

"""Notification service — create, read, and manage in-app notifications.
Provides helpers for generating notifications automatically when test
state changes occur, plus CRUD for the notifications API.
Functions in this module stage changes via ``db.add()`` / ``db.flush()``
but do **not** commit. The caller is responsible for committing.
"""
import uuid
from datetime import datetime, timedelta
from sqlalchemy.orm import Session
from sqlalchemy import func
from app.models.notification import Notification
from app.models.user import User
# ---------------------------------------------------------------------------
# Core CRUD
# ---------------------------------------------------------------------------
def create_notification(
db: Session,
user_id: uuid.UUID,
type: str,
title: str,
message: str | None = None,
entity_type: str | None = None,
entity_id: uuid.UUID | None = None,
) -> Notification:
"""Create a single notification for a user."""
notif = Notification(
user_id=user_id,
type=type,
title=title,
message=message,
entity_type=entity_type,
entity_id=entity_id,
)
db.add(notif)
db.flush()
return notif
def mark_as_read(db: Session, notification_id: uuid.UUID, user_id: uuid.UUID) -> bool:
"""Mark a single notification as read. Returns True if updated."""
notif = (
db.query(Notification)
.filter(Notification.id == notification_id, Notification.user_id == user_id)
.first()
)
if notif is None:
return False
notif.read = True
return True
def mark_all_as_read(db: Session, user_id: uuid.UUID) -> int:
"""Mark all unread notifications for a user as read. Returns count updated."""
count = (
db.query(Notification)
.filter(Notification.user_id == user_id, Notification.read == False) # noqa: E712
.update({"read": True})
)
return count
def get_unread_count(db: Session, user_id: uuid.UUID) -> int:
"""Return the number of unread notifications for a user."""
return (
db.query(func.count(Notification.id))
.filter(Notification.user_id == user_id, Notification.read == False) # noqa: E712
.scalar()
) or 0
def cleanup_old_notifications(db: Session, days: int = 90) -> int:
"""Delete read notifications older than *days*. Returns count deleted."""
cutoff = datetime.utcnow() - timedelta(days=days)
count = (
db.query(Notification)
.filter(
Notification.read == True, # noqa: E712
Notification.created_at < cutoff,
)
.delete()
)
return count
# ---------------------------------------------------------------------------
# Automatic notification dispatchers
# ---------------------------------------------------------------------------
def notify_test_state_change(db: Session, test, new_state: str) -> None:
"""Dispatch notifications based on a test's new state.
Called by the workflow service after each state transition.
Rules:
- red_executing -> notify creator (confirmation)
- blue_evaluating -> notify all blue_tech users
- in_review -> notify red_lead and blue_lead users
- rejected -> notify creator
- validated -> notify creator
"""
test_name = test.name
test_id = test.id
creator_id = test.created_by
if new_state == "red_executing" and creator_id:
create_notification(
db,
user_id=creator_id,
type="test_state_changed",
title="Test execution started",
message=f'Your test "{test_name}" has moved to execution phase.',
entity_type="test",
entity_id=test_id,
)
elif new_state == "blue_evaluating":
# Notify all blue_tech users
blue_users = db.query(User).filter(User.role == "blue_tech", User.is_active == True).all() # noqa: E712
for user in blue_users:
create_notification(
db,
user_id=user.id,
type="test_assigned",
title="New test ready for blue evaluation",
message=f'Test "{test_name}" needs blue team evaluation.',
entity_type="test",
entity_id=test_id,
)
elif new_state == "in_review":
# Notify red_lead and blue_lead users
managers = (
db.query(User)
.filter(User.role.in_(["red_lead", "blue_lead"]), User.is_active == True) # noqa: E712
.all()
)
for user in managers:
create_notification(
db,
user_id=user.id,
type="validation_needed",
title="Test ready for validation",
message=f'Test "{test_name}" is awaiting your review.',
entity_type="test",
entity_id=test_id,
)
elif new_state == "rejected" and creator_id:
create_notification(
db,
user_id=creator_id,
type="test_rejected",
title="Test rejected",
message=f'Your test "{test_name}" has been rejected. Please review and resubmit.',
entity_type="test",
entity_id=test_id,
)
elif new_state == "validated" and creator_id:
create_notification(
db,
user_id=creator_id,
type="test_validated",
title="Test validated",
message=f'Your test "{test_name}" has been validated successfully.',
entity_type="test",
entity_id=test_id,
)