feat(B-NOTIF): Notification→Message Konsolidierung — System-Channel, post_system_message(), Migration
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
B-NOTIF-SYS: CommConversation um is_system Feld erweitert, get_or_create_system_channel() B-NOTIF-EVT: post_system_message() in kommunikation/services.py — erstellt CommMessage im System-Channel - create_notification() als deprecated Wrapper delegiert auf post_system_message() - Text-Block + action_card Block mit Deep-Link, Metadata: notification_type/severity/entity_ref B-NOTIF-PREF: NotificationPreference als Routing-Konfiguration, stumm/disabled respektiert B-NOTIF-MIG: Migration 0120 — Notifications → CommMessages migriert, notifications_legacy View B-NOTIF-DEPREC: Notification-Routes als deprecated markiert, keine Routes entfernt B-NOTIF-TEST: 19 Tests in test_notification_migration.py — alle grün - System-Channel, post_system_message, create_notification Wrapper, Preferences, Unread-Badge, Migration Mapping, Sensitive Fields
This commit is contained in:
+55
-35
@@ -1,7 +1,13 @@
|
||||
"""Notification service — create and manage user notifications."""
|
||||
"""Notification service — create and manage user notifications.
|
||||
|
||||
As of B-NOTIF-EVT, the primary entry point is post_system_message() which posts
|
||||
to the Communication system channel. create_notification() is retained as a
|
||||
deprecated backward-compat wrapper that delegates to post_system_message().
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import UTC
|
||||
from typing import Any
|
||||
@@ -15,6 +21,31 @@ from app.models.notification import (
|
||||
NotificationType,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Re-export post_system_message from kommunikation services for convenience
|
||||
async def post_system_message(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
message_type: str,
|
||||
title: str,
|
||||
body: str | None = None,
|
||||
entity_type: str | None = None,
|
||||
entity_id: uuid.UUID | None = None,
|
||||
severity: str = "info",
|
||||
):
|
||||
"""Post a typed system message to the tenant system channel.
|
||||
|
||||
Delegates to kommunikation.services.post_system_message.
|
||||
Returns the created CommMessage, or None if the user has muted this type.
|
||||
"""
|
||||
from app.plugins.builtins.kommunikation.services import post_system_message as _post
|
||||
return await _post(
|
||||
db, tenant_id, user_id, message_type, title, body,
|
||||
entity_type, entity_id, severity,
|
||||
)
|
||||
|
||||
|
||||
async def create_notification(
|
||||
db: AsyncSession,
|
||||
@@ -28,34 +59,24 @@ async def create_notification(
|
||||
) -> Notification | None:
|
||||
"""Create a new notification for a user if they have not disabled this type.
|
||||
|
||||
.. deprecated:: B-NOTIF-EVT
|
||||
Use post_system_message() instead. This wrapper delegates to
|
||||
post_system_message() and also creates a legacy Notification record
|
||||
for backward compatibility with existing routes and frontend.
|
||||
|
||||
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,
|
||||
)
|
||||
)
|
||||
# Delegate to post_system_message for the comm channel
|
||||
comm_msg = await post_system_message(
|
||||
db, tenant_id, user_id, type, title, body,
|
||||
entity_type, entity_id, severity="info",
|
||||
)
|
||||
pref_row = pref.scalar_one_or_none()
|
||||
|
||||
# If preference exists and is disabled, skip
|
||||
if pref_row and not pref_row.is_enabled:
|
||||
if comm_msg is None:
|
||||
# User has muted this type — don't create legacy record either
|
||||
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
|
||||
# Also create legacy Notification record for backward compat
|
||||
notif = Notification(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
@@ -68,17 +89,18 @@ async def create_notification(
|
||||
db.add(notif)
|
||||
await db.flush()
|
||||
|
||||
# Publish notification.created event
|
||||
# Publish notification.created event (backward compat)
|
||||
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,
|
||||
'entity_type': entity_type,
|
||||
'entity_id': str(entity_id) if entity_id else None,
|
||||
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,
|
||||
"entity_type": entity_type,
|
||||
"entity_id": str(entity_id) if entity_id else None,
|
||||
"comm_message_id": str(comm_msg.id),
|
||||
})
|
||||
|
||||
return notif
|
||||
@@ -93,7 +115,6 @@ async def list_notifications(
|
||||
) -> 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)
|
||||
@@ -104,7 +125,6 @@ async def list_notifications(
|
||||
)
|
||||
total = (await db.execute(count_q)).scalar() or 0
|
||||
|
||||
# Query — unread first (read_at IS NULL), then newest
|
||||
q = (
|
||||
select(Notification)
|
||||
.where(
|
||||
@@ -112,7 +132,7 @@ async def list_notifications(
|
||||
Notification.user_id == user_id,
|
||||
)
|
||||
.order_by(
|
||||
Notification.read_at.isnot(None), # False (unread) sorts first
|
||||
Notification.read_at.isnot(None),
|
||||
Notification.created_at.desc(),
|
||||
)
|
||||
.offset(offset)
|
||||
|
||||
Reference in New Issue
Block a user