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.dataflows import CAPABILITY_DATAFLOW_DATASET_OUTPUT from govoplan_core.core.contact_points import ( CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION, ) from govoplan_core.core.distribution_lists import ( CAPABILITY_DISTRIBUTION_LIST_EXPAND, CAPABILITY_DISTRIBUTION_LIST_SOURCE, CAPABILITY_DISTRIBUTION_LIST_WRITER, CAPABILITY_POLICY_DISTRIBUTION_CHANNELS, CAPABILITY_RECIPIENT_CHANNEL_FACTS, ) from govoplan_core.core.identity import ( CAPABILITY_IDENTITY_DIRECTORY, CAPABILITY_IDENTITY_SEARCH, ) from govoplan_core.core.idm import ( CAPABILITY_IDM_FUNCTION_ASSIGNMENTS, CAPABILITY_IDM_RELATIONSHIPS, ) 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, ModuleInterfaceRequirement, ModuleManifest, NavItem, PermissionDefinition, RoleTemplate, ) from govoplan_core.core.provider_governance import declared_module_architecture from govoplan_core.core.organizations import CAPABILITY_ORGANIZATION_DIRECTORY from govoplan_core.core.views import ViewSurface from govoplan_core.db.base import Base from govoplan_dist_lists.backend.db import models as dist_list_models MODULE_ID = "dist_lists" MODULE_NAME = "Distribution Lists" MODULE_VERSION = "0.1.14" READ_SCOPE = "dist_lists:list:read" WRITE_SCOPE = "dist_lists:list:write" ADMIN_SCOPE = "dist_lists:list: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=MODULE_NAME, level="tenant", module_id=module_id, resource=resource, action=action, ) PERMISSIONS = ( _permission(READ_SCOPE, "View distribution lists", "Read operational distribution lists and expansion previews."), _permission(WRITE_SCOPE, "Manage distribution lists", "Create and edit operational distribution lists."), _permission(ADMIN_SCOPE, "Administer distribution lists", "Configure distribution-list policies and provider integrations."), ) ROLE_TEMPLATES = ( RoleTemplate( slug="distribution_list_manager", name="Distribution list manager", description="Create, manage, preview, and reuse operational distribution lists.", permissions=(READ_SCOPE, WRITE_SCOPE), ), ) DOCUMENTATION = ( DocumentationTopic( id=f"{MODULE_ID}.boundary", title="Distribution list boundary", summary="Distribution lists model operational Verteiler separately from address-book lists and workflow Umlauf execution.", body=( "Distribution Lists owns reusable mixed recipient definitions and expansion snapshots. " "Address lists remain in the addresses module and represent address-domain groupings only. " "Workflow and Tasks own Umlauf execution state, ordering, deadlines, escalation, and completion." ), layer="available", documentation_types=("admin", "user"), audience=("operator", "module_admin", "product_owner"), related_modules=("addresses", "campaigns", "mail", "postbox", "notifications", "scheduling", "poll", "workflow_engine", "tasks"), metadata={"seed": True}, ), DocumentationTopic( id=f"{MODULE_ID}.address-contact-resolution", title="Address contact-point resolution", summary="Resolve address contacts and lists into purpose-aware email and postal candidates.", body=( "When Addresses is enabled, Distribution Lists uses its versioned contact-point contract " "to resolve communication purpose, requested channels, address purpose, fallback, locale, " "quality, preference, consent, and suppression. Expansion previews explain rejected points. " "Frozen Distribution List snapshots retain the exact rendered email or postal target, contact-" "point ID, source revision and fingerprint, and decision provenance. If the new contract is " "unavailable, older Addresses lookup and email-source capabilities remain a compatibility fallback." ), layer="available", documentation_types=("admin", "user"), audience=("operator", "module_admin", "product_owner"), related_modules=("addresses", "campaigns", "policy"), metadata={"seed": True}, ), DocumentationTopic( id=f"{MODULE_ID}.idm-group-resolution", title="Effective IDM group audiences", summary="Typed IDM groups expand into explainable, effective-dated identity recipients.", body=( "When IDM is enabled, an IDM group entry resolves through the idm.relationships " "capability at the expansion effective time. Effective identities with linked " "accounts become internal-mail candidates. Future, expired, revoked, inactive, " "and account-less relationships remain in exclusion evidence with source revisions " "and provenance. Without IDM, the provider is reported as unavailable and local or " "other provider-backed lists continue to work." ), layer="available", documentation_types=("admin", "user"), audience=("operator", "module_admin", "product_owner"), related_modules=("identity", "idm"), metadata={"seed": True}, ), ) def _router(_context: ModuleContext): from govoplan_dist_lists.backend.router import router return router def _capability(context: ModuleContext): from govoplan_dist_lists.backend.capabilities import capability return capability(context) def _tenant_summary(session, tenant_id: str) -> dict[str, int]: return { "distribution_lists": ( session.query(dist_list_models.DistributionList) .filter( dist_list_models.DistributionList.tenant_id == tenant_id, dist_list_models.DistributionList.deleted_at.is_(None), ) .count() ), "distribution_list_snapshots": ( session.query(dist_list_models.DistributionListSnapshot) .filter(dist_list_models.DistributionListSnapshot.tenant_id == tenant_id) .count() ), } manifest = ModuleManifest( id=MODULE_ID, name=MODULE_NAME, version=MODULE_VERSION, dependencies=(), optional_dependencies=( "addresses", "access", "identity", "idm", "organizations", "datasources", "connectors", "dataflow", "policy", "campaigns", "templates", "reporting", "mail", "postbox", "notifications", "scheduling", "poll", "workflow_engine", "tasks", ), optional_capabilities=( CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_IDENTITY_DIRECTORY, CAPABILITY_IDENTITY_SEARCH, CAPABILITY_IDM_FUNCTION_ASSIGNMENTS, CAPABILITY_IDM_RELATIONSHIPS, CAPABILITY_ORGANIZATION_DIRECTORY, CAPABILITY_DATAFLOW_DATASET_OUTPUT, CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION, CAPABILITY_RECIPIENT_CHANNEL_FACTS, CAPABILITY_POLICY_DISTRIBUTION_CHANNELS, ), provides_interfaces=( ModuleInterfaceProvider( name=CAPABILITY_DISTRIBUTION_LIST_SOURCE, version=MODULE_VERSION, ), ModuleInterfaceProvider( name=CAPABILITY_DISTRIBUTION_LIST_EXPAND, version=MODULE_VERSION, ), ModuleInterfaceProvider( name=CAPABILITY_DISTRIBUTION_LIST_WRITER, version=MODULE_VERSION, ), ), requires_interfaces=( ModuleInterfaceRequirement( name=CAPABILITY_ADDRESSES_CONTACT_POINT_RESOLUTION, version_min="1.0.0", version_max_exclusive="2.0.0", optional=True, ), ModuleInterfaceRequirement( name=CAPABILITY_RECIPIENT_CHANNEL_FACTS, version_min="0.1.0", version_max_exclusive="0.2.0", optional=True, ), ModuleInterfaceRequirement( name=CAPABILITY_IDM_RELATIONSHIPS, version_min="1.0.0", version_max_exclusive="2.0.0", optional=True, ), ), permissions=PERMISSIONS, role_templates=ROLE_TEMPLATES, nav_items=( NavItem( path="/distribution-lists", label="Distribution Lists", icon="list-tree", required_any=(READ_SCOPE, WRITE_SCOPE, ADMIN_SCOPE), order=74, ), ), frontend=FrontendModule( module_id=MODULE_ID, package_name="@govoplan/dist-lists-webui", routes=( FrontendRoute( path="/distribution-lists", component="DistributionListsPage", required_any=(READ_SCOPE, WRITE_SCOPE, ADMIN_SCOPE), order=74, ), ), nav_items=( NavItem( path="/distribution-lists", label="Distribution Lists", icon="list-tree", required_any=(READ_SCOPE, WRITE_SCOPE, ADMIN_SCOPE), order=74, ), ), view_surfaces=( ViewSurface( id="dist_lists.page", module_id=MODULE_ID, kind="route", label="Distribution Lists", order=74, ), ViewSurface( id="dist_lists.editor", module_id=MODULE_ID, kind="section", label="Distribution-list editor", order=10, ), ViewSurface( id="dist_lists.preview", module_id=MODULE_ID, kind="section", label="Distribution-list expansion preview", order=20, ), ViewSurface( id="dist_lists.picker", module_id=MODULE_ID, kind="action", label="Distribution-list picker", order=30, ), ), ), route_factory=_router, capability_factories={ CAPABILITY_DISTRIBUTION_LIST_SOURCE: _capability, CAPABILITY_DISTRIBUTION_LIST_EXPAND: _capability, CAPABILITY_DISTRIBUTION_LIST_WRITER: _capability, }, tenant_summary_providers=(_tenant_summary,), 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( dist_list_models.DistributionListSnapshot, dist_list_models.DistributionListEntry, dist_list_models.DistributionListRevision, dist_list_models.DistributionList, label=MODULE_NAME, ), retirement_notes=( "Destructive retirement drops distribution-list definitions, immutable revisions, " "and frozen expansion evidence after the installer captures a database snapshot." ), ), uninstall_guard_providers=( persistent_table_uninstall_guard( dist_list_models.DistributionList, dist_list_models.DistributionListRevision, dist_list_models.DistributionListEntry, dist_list_models.DistributionListSnapshot, label=MODULE_NAME, ), ), documentation=DOCUMENTATION, architecture=declared_module_architecture( layer="communication_participation", kind="domain", maturity="vertical_slice", documentation_ref="docs/DISTRIBUTION_LISTS_ARCHITECTURE.md", test_ref="tests/test_service.py", known_limits=("Portal self-service and every AdreMa migration path are not yet implemented.",), supported_authority_modes=("native_authoritative", "external_mirror"), owned_concepts=("distribution list", "distribution-list revision", "frozen expansion"), non_owned_concepts=("contact point", "campaign recipient snapshot", "identity", "dataflow output"), recovery_docs=("docs/DISTRIBUTION_LISTS_ARCHITECTURE.md",), security_docs=("docs/DISTRIBUTION_LISTS_ARCHITECTURE.md",), operations_docs=("README.md",), ), ) def get_manifest() -> ModuleManifest: return manifest