Files
govoplan-notifications/src/govoplan_notifications/backend/manifest.py
T

275 lines
12 KiB
Python

from __future__ import annotations
from pathlib import Path
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER
from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard
from govoplan_core.core.modules import (
DocumentationTopic,
FrontendModule,
FrontendRoute,
MigrationSpec,
ModuleContext,
ModuleInterfaceProvider,
ModuleManifest,
PermissionDefinition,
RoleTemplate,
)
from govoplan_core.core.provider_governance import declared_module_architecture
from govoplan_core.core.notifications import CAPABILITY_NOTIFICATIONS_DISPATCH
from govoplan_core.core.views import ViewSurface
from govoplan_core.db.base import Base
from govoplan_notifications.backend.db import models as notification_models # noqa: F401 - populate Notifications ORM metadata
MODULE_ID = "notifications"
MODULE_NAME = "Notifications"
MODULE_VERSION = "0.1.8"
READ_SCOPE = "notifications:notification:read"
WRITE_SCOPE = "notifications:notification:write"
DISPATCH_SCOPE = "notifications:delivery:dispatch"
ADMIN_SCOPE = "notifications:notification:admin"
def _permission(scope: str, label: str, description: str) -> PermissionDefinition:
module_id, resource, action = scope.split(":", 2)
return PermissionDefinition(
scope=scope,
label=label,
description=description,
category="Notifications",
level="tenant",
module_id=module_id,
resource=resource,
action=action,
)
PERMISSIONS = (
_permission(READ_SCOPE, "View notifications", "Read the authenticated actor's notification inbox and delivery state."),
_permission(WRITE_SCOPE, "Manage notifications", "Create, update, cancel, read, and acknowledge notifications."),
_permission(DISPATCH_SCOPE, "Dispatch notifications", "Run notification delivery attempts and delivery workers."),
_permission(ADMIN_SCOPE, "Administer notifications", "Explicitly read and manage tenant-wide notification delivery state."),
)
ROLE_TEMPLATES = (
RoleTemplate(
slug="notification_manager",
name="Notification manager",
description="Manage and dispatch tenant notifications.",
permissions=(READ_SCOPE, WRITE_SCOPE, DISPATCH_SCOPE),
),
RoleTemplate(
slug="notification_viewer",
name="Notification viewer",
description="Read notification state.",
permissions=(READ_SCOPE,),
),
)
def _tenant_summary(session, tenant_id: str) -> dict[str, int]:
from govoplan_notifications.backend.db.models import NotificationMessage
return {
"notifications": session.query(NotificationMessage).filter(NotificationMessage.tenant_id == tenant_id, NotificationMessage.deleted_at.is_(None)).count(),
"pending_notifications": session.query(NotificationMessage).filter(NotificationMessage.tenant_id == tenant_id, NotificationMessage.status.in_(("pending", "queued", "failed")), NotificationMessage.deleted_at.is_(None)).count(),
}
def _notifications_router(_context: ModuleContext):
from govoplan_notifications.backend.router import router
return router
manifest = ModuleManifest(
id=MODULE_ID,
name=MODULE_NAME,
version=MODULE_VERSION,
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
optional_dependencies=("mail", "tasks", "portal", "workflow_engine", "calendar", "scheduling"),
provides_interfaces=(ModuleInterfaceProvider(name="notifications.dispatch", version="0.1.8"),),
permissions=PERMISSIONS,
role_templates=ROLE_TEMPLATES,
route_factory=_notifications_router,
tenant_summary_providers=(_tenant_summary,),
documentation=(
DocumentationTopic(
id="notifications.center-and-preferences",
title="Use the notification center",
summary="The title-bar badge and notification center collect durable notices that require attention outside an immediate request.",
body="Open the notification center to read, acknowledge, or follow notifications from enabled modules. Preferences control eligible delivery channels and categories. Disabling an optional external channel does not remove the in-product notification unless the originating module's retention policy does so.",
documentation_types=("user",),
audience=("user",),
related_modules=("mail", "calendar", "scheduling", "workflow_engine"),
metadata={
"kind": "reference",
"help_contexts": [
"notifications.route.notifications",
"notifications.page.inbox",
"notifications.page.detail",
"notifications.settings.preferences",
"notifications.widget.summary",
"notifications.state.read-only",
],
"consequence_classes": {
"mark_read": "record the notification as read for the current recipient",
"acknowledge": "record explicit recipient acknowledgement in addition to read state",
"cancel": "stop an eligible notification before provider acceptance without claiming remote recall",
"update_preferences": "replace the current user's badge, channel, digest, and source-muting preferences",
},
},
),
DocumentationTopic(
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.",
documentation_types=("admin",),
audience=("tenant_admin", "operator", "module_admin"),
related_modules=("mail", "audit", "ops"),
metadata={
"kind": "reference",
"help_contexts": [
"notifications.page.delivery",
"notifications.action.dispatch",
"notifications.state.delivery-unavailable",
],
"consequence_classes": {
"dispatch_pending": "attempt delivery for up to the requested number of eligible tenant notifications",
"inspect_attempts": "read sanitized provider, status, timing, and error evidence for the selected notification",
},
},
),
),
frontend=FrontendModule(
module_id=MODULE_ID,
package_name="@govoplan/notifications-webui",
routes=(
FrontendRoute(
path="/notifications",
component="NotificationCenterPage",
required_any=(READ_SCOPE,),
order=59,
surface_id="notifications.route.notifications",
),
),
view_surfaces=(
ViewSurface(
id="notifications.page.inbox",
module_id=MODULE_ID,
kind="section",
label="Notification inbox",
parent_id="notifications.route.notifications",
order=20,
),
ViewSurface(
id="notifications.page.detail",
module_id=MODULE_ID,
kind="section",
label="Notification details",
parent_id="notifications.route.notifications",
order=30,
),
ViewSurface(
id="notifications.page.delivery",
module_id=MODULE_ID,
kind="section",
label="Notification delivery evidence",
parent_id="notifications.page.detail",
order=40,
),
ViewSurface(
id="notifications.action.mark-read",
module_id=MODULE_ID,
kind="action",
label="Mark notification as read",
parent_id="notifications.page.detail",
order=50,
),
ViewSurface(
id="notifications.action.acknowledge",
module_id=MODULE_ID,
kind="action",
label="Acknowledge notification",
parent_id="notifications.page.detail",
order=60,
),
ViewSurface(
id="notifications.action.cancel",
module_id=MODULE_ID,
kind="action",
label="Cancel notification delivery",
parent_id="notifications.page.delivery",
order=70,
),
ViewSurface(
id="notifications.action.dispatch",
module_id=MODULE_ID,
kind="action",
label="Dispatch pending notifications",
parent_id="notifications.page.delivery",
order=80,
),
ViewSurface(
id="notifications.widget.summary",
module_id=MODULE_ID,
kind="section",
label="Notification summary widget",
order=90,
),
ViewSurface(
id="notifications.settings.preferences",
module_id=MODULE_ID,
kind="section",
label="Notification preferences",
order=100,
),
),
),
migration_spec=MigrationSpec(
module_id=MODULE_ID,
metadata=Base.metadata,
script_location=str(Path(__file__).with_name("migrations") / "versions"),
retirement_supported=True,
retirement_provider=drop_table_retirement_provider(
notification_models.NotificationMessage,
notification_models.NotificationDeliveryAttempt,
notification_models.NotificationPreference,
label="Notifications",
),
retirement_notes="Destructive retirement drops notification-owned database tables after the installer captures a database snapshot.",
),
uninstall_guard_providers=(
persistent_table_uninstall_guard(
notification_models.NotificationMessage,
notification_models.NotificationDeliveryAttempt,
notification_models.NotificationPreference,
label="Notifications",
),
),
capability_factories={
CAPABILITY_NOTIFICATIONS_DISPATCH: lambda context: __import__(
"govoplan_notifications.backend.capabilities",
fromlist=["dispatch_capability"],
).dispatch_capability(context),
},
architecture=declared_module_architecture(
layer="communication_participation",
kind="runtime",
maturity="vertical_slice",
documentation_ref="docs/NOTIFICATION_INBOX_BOUNDARY.md",
test_ref="tests/test_notifications.py",
known_limits=("Production email delivery depends on the optional Mail capability and does not provide an independent transport.",),
owned_concepts=("notification", "notification preference", "notification delivery attempt"),
non_owned_concepts=("mail transport", "domain event", "portal message"),
recovery_docs=("docs/EMAIL_DELIVERY.md",),
operations_docs=("docs/EMAIL_DELIVERY.md",),
),
)
def get_manifest() -> ModuleManifest:
return manifest