diff --git a/src/govoplan_notifications/backend/capabilities.py b/src/govoplan_notifications/backend/capabilities.py index e73865a..94b9bac 100644 --- a/src/govoplan_notifications/backend/capabilities.py +++ b/src/govoplan_notifications/backend/capabilities.py @@ -12,6 +12,17 @@ class SqlNotificationDispatchProvider(NotificationDispatchProvider): self._settings = context.settings self._registry = context.registry + def tenant_id_for_notification( + self, + session: object, + *, + notification_id: str, + ) -> str | None: + from govoplan_notifications.backend.db.models import NotificationMessage + + notification = session.get(NotificationMessage, notification_id) # type: ignore[attr-defined] + return notification.tenant_id if notification is not None else None + def enqueue_notification( self, session: object, diff --git a/src/govoplan_notifications/backend/manifest.py b/src/govoplan_notifications/backend/manifest.py index ce5ab9a..f13effc 100644 --- a/src/govoplan_notifications/backend/manifest.py +++ b/src/govoplan_notifications/backend/manifest.py @@ -125,7 +125,7 @@ manifest = ModuleManifest( id="notifications.delivery-operations", title="Operate notification delivery", summary="Notifications persists message intent and bounded per-channel attempts before workers dispatch optional delivery channels.", - body="Producing modules emit notifications through the dispatch capability and do not own delivery credentials. In-product delivery is the baseline. Production email delivery is available only through an enabled Mail capability; file delivery remains development-only. Operators can inspect pending and failed attempts and retry only outcomes that are safe to repeat.", + body="Producing modules emit notifications through the dispatch capability and do not own delivery credentials. In-product delivery is the baseline. Production email delivery is available only through an enabled Mail capability; file delivery remains development-only. Operators can inspect pending and failed attempts and retry only outcomes that are safe to repeat. Tenant module entitlement is checked before enqueue and again before worker delivery; disabling Notifications preserves accepted messages and exposes an operator action instead of silently consuming them.", documentation_types=("admin",), audience=("tenant_admin", "operator", "module_admin"), related_modules=("mail", "audit", "ops"), diff --git a/tests/test_notifications.py b/tests/test_notifications.py index 4829995..044959d 100644 --- a/tests/test_notifications.py +++ b/tests/test_notifications.py @@ -467,6 +467,19 @@ class NotificationServiceTests(unittest.TestCase): ) self.assertEqual(payload["source_module"], "scheduling") self.assertEqual(payload["status"], "pending") + self.assertEqual( + "tenant-1", + capability.tenant_id_for_notification( + session, + notification_id=str(payload["id"]), + ), + ) + self.assertIsNone( + capability.tenant_id_for_notification( + session, + notification_id="missing", + ) + ) def test_summary_counts_unread_active_notifications(self) -> None: with self.Session() as session: