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.campaigns import ( CAPABILITY_CAMPAIGNS_ACCESS, CAPABILITY_CAMPAIGNS_DELIVERY_TASKS, CAPABILITY_CAMPAIGNS_MAIL_POLICY_CONTEXT, CAPABILITY_CAMPAIGNS_POLICY_CONTEXT, CAPABILITY_CAMPAIGNS_RETENTION, ) from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard from govoplan_core.core.modules import ( FrontendModule, MigrationSpec, ModuleContext, ModuleInterfaceProvider, ModuleInterfaceRequirement, ModuleManifest, NavItem, PermissionDefinition, RoleTemplate, ) from govoplan_core.db.base import Base from govoplan_campaign.backend.change_tracking import register_campaign_change_tracking from govoplan_campaign.backend.db import models as campaign_models # noqa: F401 - populate Campaign ORM metadata register_campaign_change_tracking() def _permission(scope: str, label: str, description: str, category: str) -> PermissionDefinition: module_id, resource, action = scope.split(":", 2) return PermissionDefinition( scope=scope, label=label, description=description, category=category, level="tenant", module_id=module_id, resource=resource, action=action, ) PERMISSIONS = ( _permission("campaigns:campaign:read", "View campaigns", "Open campaign metadata, versions and permitted message summaries.", "Campaigns"), _permission("campaigns:campaign:create", "Create campaigns", "Create new campaigns in an accessible tenant scope.", "Campaigns"), _permission("campaigns:campaign:update", "Edit campaigns", "Edit current working campaign versions.", "Campaigns"), _permission("campaigns:campaign:copy", "Copy campaigns", "Create campaigns or working versions from existing campaigns.", "Campaigns"), _permission("campaigns:campaign:archive", "Archive campaigns", "Archive campaigns without destroying audit evidence.", "Campaigns"), _permission("campaigns:campaign:delete", "Delete campaigns", "Delete draft-only campaigns where retention policy allows it.", "Campaigns"), _permission("campaigns:campaign:share", "Share campaigns", "Grant or revoke explicit campaign access.", "Campaigns"), _permission("campaigns:campaign:validate", "Validate campaigns", "Run technical validation and manage validation locks.", "Campaigns"), _permission("campaigns:campaign:build", "Build campaigns", "Build exact messages and attachment evidence.", "Campaigns"), _permission("campaigns:campaign:review", "Approve campaign review", "Approve or reject built messages and review conditions.", "Campaigns"), _permission("campaigns:campaign:send_test", "Mock-send campaigns", "Use mock delivery and verification tools.", "Campaigns"), _permission("campaigns:campaign:queue", "Queue campaigns", "Place approved executions into the delivery queue.", "Campaigns"), _permission("campaigns:campaign:control", "Control delivery", "Pause, resume or cancel queued and sending jobs.", "Campaigns"), _permission("campaigns:campaign:send", "Send campaigns", "Start real SMTP delivery.", "Campaigns"), _permission("campaigns:campaign:retry", "Retry delivery", "Retry failed or unattempted delivery jobs.", "Campaigns"), _permission("campaigns:campaign:reconcile", "Reconcile delivery", "Resolve outcome-unknown SMTP attempts after inspection.", "Campaigns"), _permission("campaigns:recipient:read", "View recipients", "Read recipient lists and recipient-specific campaign data.", "Recipients"), _permission("campaigns:recipient:write", "Edit recipients", "Create and edit recipient rows and field values.", "Recipients"), _permission("campaigns:recipient:import", "Import recipients", "Bulk-import recipient lists.", "Recipients"), _permission("campaigns:recipient:export", "Export recipients", "Export recipient data and reports.", "Recipients"), _permission("campaigns:report:read", "View reports", "View campaign delivery reports and aggregate outcomes.", "Reports"), _permission("campaigns:report:export", "Export reports", "Download detailed campaign reports.", "Reports"), _permission("campaigns:report:send", "Send reports", "Email campaign reports to configured recipients.", "Reports"), ) ROLE_TEMPLATES = ( RoleTemplate( slug="campaign_manager", name="Campaign manager", description="Prepare, validate and build campaigns without approving real delivery.", permissions=( "campaigns:campaign:read", "campaigns:campaign:create", "campaigns:campaign:update", "campaigns:campaign:copy", "campaigns:campaign:validate", "campaigns:campaign:build", "campaigns:recipient:read", "campaigns:recipient:write", "campaigns:recipient:import", "campaigns:report:read", ), ), RoleTemplate( slug="campaign_reviewer", name="Campaign reviewer", description="Inspect and approve prepared campaign messages.", permissions=( "campaigns:campaign:read", "campaigns:campaign:validate", "campaigns:campaign:review", "campaigns:recipient:read", "campaigns:report:read", ), ), RoleTemplate( slug="campaign_sender", name="Campaign sender", description="Queue, test, control, send and reconcile prepared campaigns.", permissions=( "campaigns:campaign:read", "campaigns:campaign:send_test", "campaigns:campaign:queue", "campaigns:campaign:control", "campaigns:campaign:send", "campaigns:campaign:retry", "campaigns:campaign:reconcile", "campaigns:recipient:read", "campaigns:report:read", "campaigns:report:send", ), ), ) def _tenant_summary(session, tenant_id: str) -> dict[str, int]: from govoplan_campaign.backend.db.models import Campaign return {"campaigns": session.query(Campaign).filter(Campaign.tenant_id == tenant_id).count()} def _campaigns_router(context: ModuleContext): from govoplan_campaign.backend.runtime import configure_runtime configure_runtime(registry=context.registry, settings=context.settings) from fastapi import APIRouter from govoplan_campaign.backend.router import router from govoplan_campaign.backend.schema_router import router as schema_router aggregate = APIRouter() aggregate.include_router(router) aggregate.include_router(schema_router) return aggregate manifest = ModuleManifest( id="campaigns", name="Campaigns", version="0.1.6", required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR), optional_dependencies=("files", "mail"), provides_interfaces=( ModuleInterfaceProvider(name="campaigns.access", version="0.1.6"), ModuleInterfaceProvider(name="campaigns.delivery_tasks", version="0.1.6"), ModuleInterfaceProvider(name="campaigns.mail_policy_context", version="0.1.6"), ModuleInterfaceProvider(name="campaigns.policy_context", version="0.1.6"), ModuleInterfaceProvider(name="campaigns.retention", version="0.1.6"), ), requires_interfaces=( ModuleInterfaceRequirement( name="files.campaign_attachments", version_min="0.1.0", version_max_exclusive="0.2.0", optional=True, ), ModuleInterfaceRequirement( name="mail.campaign_delivery", version_min="0.1.0", version_max_exclusive="0.2.0", optional=True, ), ), permissions=PERMISSIONS, route_factory=_campaigns_router, role_templates=ROLE_TEMPLATES, tenant_summary_providers=(_tenant_summary,), nav_items=( NavItem(path="/campaigns", label="Campaigns", icon="campaign", required_any=("campaigns:campaign:read",), order=20), NavItem( path="/operator", label="Operator Queue", icon="activity", required_any=( "campaigns:campaign:queue", "campaigns:campaign:retry", "campaigns:campaign:reconcile", "campaigns:campaign:control", "campaigns:campaign:send", ), order=30, ), NavItem(path="/reports", label="Reports", icon="reports", required_any=("campaigns:report:read",), order=70), ), frontend=FrontendModule( module_id="campaigns", package_name="@govoplan/campaign-webui", nav_items=( NavItem(path="/campaigns", label="Campaigns", icon="campaign", required_any=("campaigns:campaign:read",), order=20), NavItem( path="/operator", label="Operator Queue", icon="activity", required_any=( "campaigns:campaign:queue", "campaigns:campaign:retry", "campaigns:campaign:reconcile", "campaigns:campaign:control", "campaigns:campaign:send", ), order=30, ), NavItem(path="/reports", label="Reports", icon="reports", required_any=("campaigns:report:read",), order=70), NavItem(path="/address-book", label="Address Book", icon="users", order=80), NavItem(path="/templates", label="Templates", icon="form", order=90), ), ), migration_spec=MigrationSpec( module_id="campaigns", metadata=Base.metadata, script_location=str(Path(__file__).with_name("migrations") / "versions"), retirement_supported=True, retirement_provider=drop_table_retirement_provider( campaign_models.Campaign, campaign_models.CampaignShare, campaign_models.RecipientImportMappingProfile, campaign_models.CampaignVersion, campaign_models.CampaignJob, campaign_models.CampaignIssue, campaign_models.AttachmentBlob, campaign_models.AttachmentInstance, campaign_models.SendAttempt, campaign_models.ImapAppendAttempt, label="Campaigns", ), retirement_notes="Destructive retirement drops campaign-owned database tables after the installer captures a database snapshot.", ), uninstall_guard_providers=( persistent_table_uninstall_guard( campaign_models.Campaign, campaign_models.CampaignShare, campaign_models.RecipientImportMappingProfile, campaign_models.CampaignVersion, campaign_models.CampaignJob, campaign_models.CampaignIssue, campaign_models.AttachmentBlob, campaign_models.AttachmentInstance, campaign_models.SendAttempt, campaign_models.ImapAppendAttempt, label="Campaigns", ), ), capability_factories={ CAPABILITY_CAMPAIGNS_ACCESS: lambda context: __import__( "govoplan_campaign.backend.capabilities", fromlist=["access_capability"], ).access_capability(context), CAPABILITY_CAMPAIGNS_DELIVERY_TASKS: lambda context: __import__( "govoplan_campaign.backend.capabilities", fromlist=["delivery_tasks_capability"], ).delivery_tasks_capability(context), CAPABILITY_CAMPAIGNS_MAIL_POLICY_CONTEXT: lambda context: __import__( "govoplan_campaign.backend.capabilities", fromlist=["mail_policy_context_capability"], ).mail_policy_context_capability(context), CAPABILITY_CAMPAIGNS_POLICY_CONTEXT: lambda context: __import__( "govoplan_campaign.backend.capabilities", fromlist=["policy_context_capability"], ).policy_context_capability(context), CAPABILITY_CAMPAIGNS_RETENTION: lambda context: __import__( "govoplan_campaign.backend.capabilities", fromlist=["retention_capability"], ).retention_capability(context), }, ) def get_manifest() -> ModuleManifest: return manifest