Files
leocrm/app/core/notifications.py
T
Agent Zero 6e7e39d101 fix: Event-bus workflow trigger, RBAC on all routes, search provider, events, cron jobs
Critical fixes:
- Event Bus → Workflow auto-trigger: wildcard subscription starts workflows on matching events
- Kommunikation routes: require_permission on all 30+ endpoints (comm:read/write/delete/manage)
- Permissions routes: require_permission('permissions:admin') on all management endpoints
- CompanySearchProvider registered in auto_register_providers()

Medium fixes:
- system_notif events: 10 event_bus.publish() calls added (lead.created, contact.created/updated,
  task.created/overdue, mail.received, user.created, workflow.completed, notification.created, backup.*)
- Cron jobs: backup_check (daily), search_index_check (daily), workflow_timeout (5min) registered
- AI tool permission: call_crm_api now requires 'ai:write' permission
- New file: automation/jobs.py with backup_check and search_index_check functions
2026-07-25 03:22:55 +02:00

179 lines
4.7 KiB
Python

"""Notification service — create and manage user notifications."""
from __future__ import annotations
import uuid
from datetime import UTC
from typing import Any
from sqlalchemy import and_, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.notification import (
Notification,
NotificationPreference,
NotificationType,
)
async def create_notification(
db: AsyncSession,
tenant_id: uuid.UUID,
user_id: uuid.UUID,
type: str,
title: str,
body: str | None = None,
) -> Notification | None:
"""Create a new notification for a user if they have not disabled this type.
Returns None if the user has opted out of this notification type.
"""
# Check user preference
pref = await db.execute(
select(NotificationPreference).where(
and_(
NotificationPreference.user_id == user_id,
NotificationPreference.type_key == type,
NotificationPreference.tenant_id == tenant_id,
)
)
)
pref_row = pref.scalar_one_or_none()
# If preference exists and is disabled, skip
if pref_row and not pref_row.is_enabled:
return None
# If no preference, check if type is enabled by default
if not pref_row:
type_def = await db.execute(
select(NotificationType).where(NotificationType.type_key == type)
)
type_row = type_def.scalar_one_or_none()
if type_row and not type_row.is_enabled_by_default:
return None
# Create notification
notif = Notification(
tenant_id=tenant_id,
user_id=user_id,
type=type,
title=title,
body=body,
)
db.add(notif)
await db.flush()
# Publish notification.created event
from app.core.event_bus import get_event_bus
event_bus = get_event_bus()
await event_bus.publish('notification.created', {
'notification_id': str(notif.id),
'tenant_id': str(tenant_id),
'user_id': str(user_id),
'type': type,
'title': title,
})
return notif
async def list_notifications(
db: AsyncSession,
tenant_id: uuid.UUID,
user_id: uuid.UUID,
page: int = 1,
page_size: int = 25,
) -> dict[str, Any]:
"""List notifications for a user, unread first, then by created_at desc."""
offset = (page - 1) * page_size
# Count total
count_q = (
select(func.count())
.select_from(Notification)
.where(
Notification.tenant_id == tenant_id,
Notification.user_id == user_id,
)
)
total = (await db.execute(count_q)).scalar() or 0
# Query — unread first (read_at IS NULL), then newest
q = (
select(Notification)
.where(
Notification.tenant_id == tenant_id,
Notification.user_id == user_id,
)
.order_by(
Notification.read_at.isnot(None), # False (unread) sorts first
Notification.created_at.desc(),
)
.offset(offset)
.limit(page_size)
)
result = await db.execute(q)
items = result.scalars().all()
return {
"items": [_notification_to_dict(n) for n in items],
"total": total,
"page": page,
"page_size": page_size,
}
async def mark_notification_read(
db: AsyncSession,
tenant_id: uuid.UUID,
user_id: uuid.UUID,
notification_id: uuid.UUID,
) -> Notification | None:
"""Mark a notification as read."""
from datetime import datetime
q = (
update(Notification)
.where(
Notification.id == notification_id,
Notification.tenant_id == tenant_id,
Notification.user_id == user_id,
)
.values(read_at=datetime.now(UTC))
.returning(Notification)
)
result = await db.execute(q)
row = result.scalar_one_or_none()
return row
async def get_unread_count(
db: AsyncSession,
tenant_id: uuid.UUID,
user_id: uuid.UUID,
) -> int:
"""Get unread notification count for a user."""
q = (
select(func.count())
.select_from(Notification)
.where(
Notification.tenant_id == tenant_id,
Notification.user_id == user_id,
Notification.read_at.is_(None),
)
)
result = await db.execute(q)
return result.scalar() or 0
def _notification_to_dict(n: Notification) -> dict[str, Any]:
"""Convert a notification to a dict."""
return {
"id": str(n.id),
"type": n.type,
"title": n.title,
"body": n.body,
"read_at": n.read_at.isoformat() if n.read_at else None,
"created_at": n.created_at.isoformat() if n.created_at else None,
}