from __future__ import annotations from govoplan_core.core.modules import DocumentationCondition, DocumentationContext, DocumentationLink, DocumentationTopic from govoplan_campaign.backend.delivery_policy import ( CampaignDeliveryPolicyError, effective_synchronous_send_policy, ) _CAMPAIGN_USER_SCOPES = ( "campaigns:campaign:read", "campaigns:campaign:create", "campaigns:campaign:update", "campaigns:campaign:copy", "campaigns:campaign:archive", "campaigns:campaign:delete", "campaigns:campaign:share", "campaigns:campaign:validate", "campaigns:campaign:build", "campaigns:campaign:review", "campaigns:campaign:send_test", "campaigns:campaign:queue", "campaigns:campaign:control", "campaigns:campaign:send", "campaigns:campaign:retry", "campaigns:campaign:reconcile", "campaigns:recipient:read", "campaigns:recipient:write", "campaigns:recipient:import", "campaigns:recipient:export", "campaigns:report:read", "campaigns:report:export", "campaigns:report:send", ) _CAMPAIGN_HELP_CONTEXTS = ( "campaigns.list", "campaign.overview", ) _FILES_INTEGRATION = "files.campaign_attachments" _MAIL_INTEGRATION = "mail.campaign_delivery" _ADDRESSES_LOOKUP_INTEGRATION = "addresses.lookup" _ADDRESSES_SOURCE_INTEGRATION = "addresses.recipient_source" _NOTIFICATIONS_INTEGRATION = "notifications.dispatch" def _workflow_topic( *, topic_id: str, title: str, summary: str, body: str, order: int, audience: tuple[str, ...], required_scopes: tuple[str, ...], route: str, screen: str, help_contexts: tuple[str, ...], prerequisites: tuple[str, ...], steps: tuple[str, ...], outcome: str, verification: str, related_topic_ids: tuple[str, ...] = (), required_modules: tuple[str, ...] = ("campaigns",), required_capabilities: tuple[str, ...] = (), links: tuple[DocumentationLink, ...] = (DocumentationLink(label="Campaigns", href="/campaigns", kind="runtime"),), related_modules: tuple[str, ...] = (), limitations: tuple[str, ...] = (), ) -> DocumentationTopic: metadata: dict[str, object] = { "kind": "workflow", "route": route, "screen": screen, "help_contexts": list(help_contexts), "prerequisites": list(prerequisites), "steps": list(steps), "outcome": outcome, "verification": verification, "related_topic_ids": list(related_topic_ids), } if limitations: metadata["limitations"] = list(limitations) return DocumentationTopic( id=topic_id, title=title, summary=summary, body=body, layer="configured", documentation_types=("user",), audience=audience, order=order, conditions=( DocumentationCondition( required_modules=required_modules, required_capabilities=required_capabilities, required_scopes=required_scopes, ), ), links=links, related_modules=related_modules, unlocks=(outcome,), source_module_id="campaigns", metadata=metadata, ) CAMPAIGN_USER_DOCUMENTATION = ( _workflow_topic( topic_id="campaigns.workflow.create-campaign", title="Create a campaign", summary="Start a governed campaign as an editable draft and complete its purpose and ownership before adding delivery data.", body="A new campaign starts with one editable working version. Creating it does not grant access to Mail profiles, managed files, address sources, or delivery actions; those remain separately authorized.", order=30, audience=("campaign_manager", "campaign_author"), required_scopes=("campaigns:campaign:read", "campaigns:campaign:create"), route="/campaigns", screen="Campaigns", help_contexts=("campaigns.list",), prerequisites=("You may create campaigns in the active tenant.",), steps=( "Open Campaigns and select New campaign.", "Use the creation wizard to enter a clear name, identifier, and purpose.", "Open the new campaign and confirm its owner before adding recipient or delivery data.", "Continue through the preparation sections and save the editable working version.", ), outcome="An owned campaign draft with an editable working version.", verification="The Campaign overview shows the new campaign as a draft and identifies its current working version.", related_topic_ids=("campaigns.workflow.prepare-validate-and-build", "campaigns.workflow.import-recipients"), ), _workflow_topic( topic_id="campaigns.workflow.create-editable-successor", title="Create an editable successor", summary="Continue work after a permanent or delivery-final lock without rewriting the preserved version.", body="Create editable copy means creating the campaign's next working version. Validation locks and temporary user locks are removed in place instead; they must not create parallel drafts.", order=31, audience=("campaign_manager", "campaign_author"), required_scopes=("campaigns:campaign:read", "campaigns:campaign:copy", "campaigns:recipient:read"), route="/campaigns/{campaign_id}", screen="Campaign workspace", help_contexts=("campaign.overview", "campaign.audit"), prerequisites=( "You have write access to the selected campaign.", "Its current version is permanently user-locked or delivery-final.", ), steps=( "Open the locked current version and review the reason it is read-only.", "Select Create editable copy in the locked-version notice.", "If the notice instead offers an unlock action, unlock that validation or temporary user lock rather than creating a successor.", "Review the new working version, reselect any Mail profile when requested, and validate and build again before delivery.", ), outcome="A new editable working version while the source version and its evidence remain unchanged.", verification="The Campaign overview identifies a new current version number and the earlier version remains in history.", related_topic_ids=("campaigns.workflow.prepare-validate-and-build", "campaigns.mail-profile-user-journey"), ), _workflow_topic( topic_id="campaigns.workflow.import-recipients", title="Import recipients into a campaign", summary="Turn a text, CSV, or spreadsheet table into reviewed campaign-local recipient rows with source provenance.", body="Import copies valid rows into the editable campaign version. Invalid rows stay visible during preview instead of disappearing, and later changes to the source file do not silently change the saved campaign.", order=33, audience=("campaign_manager", "campaign_author"), required_scopes=( "campaigns:campaign:read", "campaigns:campaign:update", "campaigns:recipient:read", "campaigns:recipient:write", "campaigns:recipient:import", ), route="/campaigns/{campaign_id}/recipients", screen="Recipient data", help_contexts=("campaign.recipients", "campaign.recipient-data"), prerequisites=( "The current campaign version is editable and you have write access to it.", "The source is tabular and contains only data needed for this campaign.", ), steps=( "Open Recipient data and select Import.", "Upload or paste the table, then confirm its encoding, sheet, header rows, and separator where applicable.", "Map address and campaign fields, choose append or replace, and review every invalid or excluded row.", "Select Import valid rows, inspect the resulting recipient table, and save the campaign page.", ), outcome="A campaign-local recipient snapshot with mapping and source evidence.", verification="Reopen Recipient data, confirm the expected row count and addressing, then validate before building messages.", related_topic_ids=("campaigns.workflow.import-address-source", "campaigns.workflow.prepare-validate-and-build"), ), _workflow_topic( topic_id="campaigns.workflow.import-address-source", title="Import a recipient source from Addresses", summary="Copy a permitted reusable address book or list into the campaign as a traceable versioned snapshot.", body="Campaign does not follow the address source live. It records the selected source revision and warns when a newer source revision is available, so re-import is always an explicit author action.", order=32, audience=("campaign_manager", "campaign_author"), required_modules=("campaigns", "addresses"), required_capabilities=(_ADDRESSES_SOURCE_INTEGRATION,), required_scopes=( "campaigns:campaign:read", "campaigns:campaign:update", "campaigns:recipient:read", "campaigns:recipient:write", "campaigns:recipient:import", ), route="/campaigns/{campaign_id}/recipients", screen="Recipient data", help_contexts=("campaign.recipients", "campaign.recipient-data"), prerequisites=( "Addresses offers at least one source visible to you.", "The current campaign version is editable and you have write access to it.", ), steps=( "Open Recipient data and select Import address book/list.", "Choose the permitted source and review the returned snapshot and its revision.", "Choose append or replace, import the snapshot, inspect the recipient rows, and save.", "When a stale-source warning appears later, compare the source and re-import deliberately if the campaign should use the new revision.", ), outcome="A campaign-local recipient snapshot whose Addresses source and revision can be traced.", verification="Recipient data records the source provenance, and a later source revision does not alter the saved rows until you re-import it.", related_topic_ids=("campaigns.workflow.import-recipients", "campaigns.workflow.prepare-validate-and-build"), related_modules=("addresses",), ), _workflow_topic( topic_id="campaigns.workflow.use-managed-attachments", title="Use managed files as campaign attachments", summary="Select governed file versions, preview rule matches, and preserve exactly which files were used for the campaign build.", body="Managed attachments remain owned by Files. Campaign stores governed references and frozen build evidence; it does not copy Files administration authority or accept arbitrary server paths.", order=34, audience=("campaign_manager", "campaign_author"), required_modules=("campaigns", "files"), required_capabilities=(_FILES_INTEGRATION,), required_scopes=( "campaigns:campaign:read", "campaigns:campaign:update", "campaigns:campaign:validate", "campaigns:recipient:read", "files:file:read", "files:file:share", ), route="/campaigns/{campaign_id}/files", screen="Attachments", help_contexts=("campaign.attachments",), prerequisites=( "The current campaign version is editable and you have write access to it.", "The required file versions exist in Files and may be shared with this campaign.", ), steps=( "Open Attachments and choose managed files or patterns from Files.", "Define campaign-wide and recipient-specific rules, including unmatched-file and archive behavior.", "Preview the matches and link the exact managed versions to the campaign.", "Save, validate, and build, then inspect the resolved attachment evidence in Review and send.", ), outcome="Governed file versions linked to the campaign and frozen into the exact build evidence.", verification="Confirm the expected filenames, paths, matches, and link state in the Campaign UI. Exact managed versions and checksums remain retained in build evidence for authorized supporting tools.", related_topic_ids=("campaigns.workflow.prepare-validate-and-build",), links=( DocumentationLink(label="Campaigns", href="/campaigns", kind="runtime"), DocumentationLink(label="Files", href="/files", kind="runtime"), ), related_modules=("files",), ), _workflow_topic( topic_id="campaigns.workflow.queue-delivery", title="Queue a campaign for controlled delivery", summary="Place an exact reviewed build into the worker queue while preserving recipient-level state and retry protection.", body="Queueing is a controlled state change, not proof of delivery. Ordinary batches should use background workers, and accepted or outcome-unknown effects remain protected from blind retry.", order=35, audience=("campaign_sender", "campaign_operator"), required_modules=("campaigns", "mail"), required_capabilities=(_MAIL_INTEGRATION,), required_scopes=( "campaigns:campaign:read", "campaigns:campaign:queue", "campaigns:recipient:read", "mail:profile:use", ), route="/campaigns/{campaign_id}/review", screen="Review and send", help_contexts=(), prerequisites=( "The selected version is validated, locked, built, and reviewed.", "Its Mail profile remains available and authorized for the current campaign context.", "You have write access to the selected campaign.", ), steps=( "Open Review and send and confirm the selected version, build, recipients, warnings, and review completion.", "Select Queue for workers and confirm the exact durable execution that will be committed.", "Remain on Review and send or leave and return later; both views read the same durable job states.", "Use the delivery controls to pause, resume, cancel unsent work, or retry eligible failures without requeueing accepted or uncertain outcomes.", ), outcome="Eligible jobs queued with an auditable execution snapshot and protected delivery state.", verification="The Report shows the selected version's jobs as queued or progressing, without changing any accepted job back to retryable.", related_topic_ids=("campaigns.workflow.complete-review", "campaigns.workflow.retry-and-reconcile"), links=( DocumentationLink(label="Campaigns", href="/campaigns", kind="runtime"), DocumentationLink(label="Campaign operator queue", href="/operator", kind="runtime"), ), related_modules=("mail", "notifications"), ), _workflow_topic( topic_id="campaigns.workflow.send-small-controlled-run", title="Send a campaign immediately", summary="Run the eligible jobs synchronously only after deliberately confirming that the reviewed campaign is small enough for an interactive request.", body="Send now is protected by an effective deployment/tenant recipient-job maximum. The server counts the exact persisted eligible build, rejects an oversized or empty run before SMTP, and preflights every message and the Mail profile revision before the first provider effect.", order=36, audience=("campaign_sender", "campaign_operator"), required_modules=("campaigns", "mail"), required_capabilities=(_MAIL_INTEGRATION,), required_scopes=( "campaigns:campaign:read", "campaigns:campaign:send", "campaigns:recipient:read", "mail:profile:use", ), route="/campaigns/{campaign_id}/review", screen="Review and send", help_contexts=("campaign.review-send",), prerequisites=( "The selected version is validated, locked, built, and reviewed.", "The exact eligible recipient-job count is within the effective limit shown on Review and send.", "Its Mail profile remains available and authorized for the current campaign context.", ), steps=( "Open Review and send and inspect the exact build and delivery readiness checks.", "Review the effective synchronous limit and exact eligible count; use Queue for workers when Send now is unavailable or for an ordinary batch.", "Select Send now and confirm the protected delivery action.", "Open Report and inspect each resulting delivery state before attempting any recovery action.", ), outcome="A synchronous real delivery run with recipient-level attempt and outcome evidence.", verification="The Report distinguishes accepted, failed, unattempted, cancelled, and outcome-unknown jobs and retains their attempt history.", related_topic_ids=("campaigns.workflow.queue-delivery", "campaigns.workflow.retry-and-reconcile"), related_modules=("mail",), ), _workflow_topic( topic_id="campaigns.workflow.view-aggregate-delivery-report", title="Review aggregate campaign outcomes", summary="Inspect privacy-protected Campaign totals without receiving recipient rows, message content, delivery diagnostics, or export authority.", body="The aggregate Reports view exposes only approved campaign-level business outcomes. Positive cells below the configured threshold are suppressed together with a complementary value or the denominator when needed, so totals cannot be subtracted to recover a small group.", order=37, audience=("campaign_aggregate_reader", "campaign_reader", "campaign_manager"), required_scopes=("campaigns:report:read",), route="/reports", screen="Reports", help_contexts=("campaign.report",), prerequisites=("The campaign is owned by or explicitly shared with you, or you hold tenant-wide authority.",), steps=( "Open Reports and select a campaign available to you.", "Read the stated denominator before comparing accepted, failed, unknown, active, excluded, cancelled, and unattempted outcomes.", "Treat Suppressed as an intentional privacy boundary rather than zero or missing data.", "Request separately authorized recipient-level access only when the task genuinely requires individual evidence.", ), outcome="A business-level Campaign outcome view with small-group and recipient privacy preserved.", verification="No row, address, message, attachment, diagnostic, filter, drill-down, or export action is available from the aggregate view.", related_topic_ids=("campaigns.workflow.view-delivery-report",), links=(DocumentationLink(label="Aggregate Campaign reports", href="/reports", kind="runtime"),), ), _workflow_topic( topic_id="campaigns.workflow.view-delivery-report", title="Review campaign delivery details", summary="Inspect delivery totals and recipient-level job evidence in the current Campaign Report UI.", body="The recipient-aware Campaign Report requires campaign-read, report-read, and recipient-read authority. Infrastructure diagnostics remain separately authorized, and the server checks every direct detail route independently of the interface.", order=38, audience=("campaign_reader", "campaign_manager", "campaign_reviewer", "campaign_sender"), required_scopes=("campaigns:campaign:read", "campaigns:report:read", "campaigns:recipient:read"), route="/campaigns/{campaign_id}/report", screen="Campaign Report", help_contexts=("campaign.report",), prerequisites=("You may read the selected campaign and its report.",), steps=( "Open the campaign and select Report.", "Review totals for queued, accepted, failed, cancelled, excluded/skipped, unattempted, and outcome-unknown jobs.", "Inspect the authorized recipient-level rows and attempt history.", "Treat outcome-unknown jobs as unresolved and hand them to an authorized operator for evidence-backed reconciliation.", ), outcome="A recipient-aware view of delivery progress and outcomes with diagnostics still separately protected.", verification="The report totals and job rows match the selected campaign version, and infrastructure diagnostics are absent unless separately authorized.", related_topic_ids=("campaigns.workflow.view-aggregate-delivery-report", "campaigns.workflow.retry-and-reconcile", "campaigns.workflow.export-delivery-report"), ), _workflow_topic( topic_id="campaigns.workflow.export-delivery-report", title="Export recipient delivery results", summary="Download an authorized CSV snapshot of recipient-level campaign delivery results for controlled downstream use.", body="A report export contains personal and delivery evidence. Store, transmit, retain, and delete it according to the campaign's purpose and the applicable export and retention policy.", order=39, audience=("campaign_report_exporter", "campaign_auditor"), required_scopes=( "campaigns:campaign:read", "campaigns:report:read", "campaigns:report:export", "campaigns:recipient:read", "campaigns:recipient:export", ), route="/campaigns/{campaign_id}/report", screen="Campaign Report", help_contexts=("campaign.report", "campaign.audit"), prerequisites=( "You may export both campaign reports and recipient data.", "The export has an approved purpose and destination.", ), steps=( "Open the campaign Report and select the intended version.", "Confirm that recipient-level export is necessary for the task.", "Select Download CSV and store the result only in the approved location.", "Verify the selected version and row counts, then apply the required retention or deletion rule to the downloaded copy.", ), outcome="A point-in-time CSV export of authorized campaign job results.", verification="The downloaded file identifies the intended campaign/version and contains business evidence without credentials or ordinary-reader infrastructure details.", related_topic_ids=("campaigns.workflow.view-delivery-report",), ), _workflow_topic( topic_id="campaigns.workflow.share-campaign", title="Share a campaign", summary="Grant a user or group explicit view or write access to one campaign without widening their platform permissions.", body="A share can only narrow access to the selected campaign within the recipient's existing role. It never grants Mail profile use, Files authority, tenant-wide recipient access, or a missing Campaign action.", order=39, audience=("campaign_owner", "campaign_access_manager"), required_scopes=("campaigns:campaign:read", "campaigns:campaign:share"), route="/campaigns/{campaign_id}/global-settings", screen="Ownership and sharing", help_contexts=("campaign.overview", "campaign.global-settings"), prerequisites=( "You have write access to the selected campaign.", "The target user or group already has an appropriate Campaign role for the intended actions.", ), steps=( "Open Global settings and find Ownership and sharing.", "Select Share, choose a user or group, and choose Can view or Can edit and operate.", "Save the share and verify the target appears in the sharing table with the intended level.", "Use the row action to revoke the explicit share when it is no longer needed.", ), outcome="Explicit campaign access for the selected subject, bounded by that subject's existing permissions.", verification="The sharing table shows the active target and level; revocation removes its explicit access while preserving the audit record.", related_topic_ids=("campaigns.reference.composition-assurance",), ), _workflow_topic( topic_id="campaigns.workflow.archive-campaign", title="Archive a campaign", summary="Remove a completed campaign from active work while preserving its versions, outcomes, and audit evidence.", body="Archive only after queued, sending, and outcome-unknown work has been resolved. Archiving preserves evidence and is the correct lifecycle action for any campaign with build, lock, or delivery history.", order=40, audience=("campaign_owner", "campaign_records_manager"), required_scopes=("campaigns:campaign:read", "campaigns:campaign:archive"), route="/campaigns/{campaign_id}", screen="Campaign lifecycle", help_contexts=("campaign.overview", "campaign.report", "campaign.audit"), prerequisites=( "You have write access to the selected campaign.", "No delivery job is queued, sending, or awaiting uncertain-outcome reconciliation.", ), steps=( "Open Report and resolve every active or outcome-unknown delivery state.", "Confirm that the campaign should leave active work while its evidence remains retained.", "Invoke the authorized Archive action from a supporting client.", "Reopen or query the campaign and confirm its state is Archived.", ), outcome="An archived campaign whose versions, reports, and audit evidence remain preserved.", verification="The campaign state is Archived and its report remains available. Ask an authorized audit reader to verify the platform audit event.", related_topic_ids=("campaigns.workflow.view-delivery-report", "campaigns.workflow.delete-untouched-draft"), limitations=( "The current Campaign Web UI does not yet expose the archive action; use an authorized supporting client or API.", "The Campaign-local Audit page is not integrated yet; audit verification uses the platform audit surface or API.", ), ), _workflow_topic( topic_id="campaigns.workflow.delete-untouched-draft", title="Delete an untouched campaign draft", summary="Immediately remove a draft that has no protected build, lock, publication, snapshot, or delivery evidence.", body="Deletion is deliberately narrower than archive. It marks an eligible draft deleted and emits an audit record; it cannot erase a campaign that already carries evidence that must be retained.", order=41, audience=("campaign_owner", "campaign_records_manager"), required_scopes=("campaigns:campaign:read", "campaigns:campaign:delete"), route="/campaigns/{campaign_id}", screen="Campaign lifecycle", help_contexts=("campaign.overview", "campaign.audit"), prerequisites=( "You have write access to the selected campaign.", "The campaign is still a draft and has no jobs, locks, published version, or execution snapshot.", ), steps=( "Confirm that the draft is not needed and contains no evidence that should be retained.", "Invoke the authorized Delete action from a supporting client and confirm the destructive action.", "If deletion is refused because protected evidence exists, archive the campaign after resolving any active delivery state.", "Verify that the deleted draft no longer appears in active Campaigns and that the audit event exists.", ), outcome="An eligible untouched draft removed from active Campaigns with attributable deletion evidence.", verification="The draft is no longer returned as an active campaign. Ask an authorized audit reader to verify who deleted it and when through the platform audit surface.", related_topic_ids=("campaigns.workflow.archive-campaign",), limitations=( "The current Campaign Web UI does not yet expose the delete action; use an authorized supporting client or API.", "The Campaign-local Audit page is not integrated yet; audit verification uses the platform audit surface or API.", ), ), ) def documentation_topics(context: DocumentationContext) -> tuple[DocumentationTopic, ...]: """Describe the current Campaign composition without resolving module data. The provider deliberately uses only the actor's permission evaluator and the registry's declared contracts. Resource access, policy decisions, and campaign state remain authoritative at the point where an action is used. """ if context.documentation_type != "user": return () principal = context.principal if principal is None or not _has_any_scope(principal, _CAMPAIGN_USER_SCOPES): return () mail_available = _integration_available(context.registry, _MAIL_INTEGRATION) current_configuration = list(_actor_capabilities(principal, mail_available=mail_available)) integration_configuration, integration_limitations = _integration_summary(context.registry, principal) current_configuration.extend(integration_configuration) delivery_configuration, delivery_limitations = _delivery_policy_summary(context) current_configuration.extend(delivery_configuration) limitations = [ "Access to a particular campaign still depends on its owner or sharing rules and on the campaign's current state.", "Profile, file, and address pickers re-evaluate the actual resources visible in the selected campaign; this overview does not claim that an eligible item currently exists.", *integration_limitations, *delivery_limitations, ] return ( DocumentationTopic( id="campaigns.current-composition", title="Your Campaign role and installed composition", summary="This guide separates actions authorized by your role from optional services connected to Campaign in this installation.", body=_composition_body(current_configuration, limitations), layer="configured", documentation_types=("user",), audience=("campaign_user",), order=29, conditions=( DocumentationCondition( required_modules=("campaigns",), any_scopes=_CAMPAIGN_USER_SCOPES, ), ), links=(DocumentationLink(label="Campaigns", href="/campaigns", kind="runtime"),), related_modules=("mail", "files", "addresses", "notifications"), source_module_id="campaigns", metadata={ "kind": "reference", "route": "/campaigns", "screen": "Campaigns", "help_contexts": list(_CAMPAIGN_HELP_CONTEXTS), "current_configuration": current_configuration, "limitations": limitations, }, ), ) def _actor_capabilities(principal: object, *, mail_available: bool) -> tuple[str, ...]: capabilities: list[str] = [] _append_if(capabilities, principal, ("campaigns:campaign:read",), "Open campaigns shared with you and inspect their business state.") _append_if(capabilities, principal, ("campaigns:campaign:create",), "Create new campaigns.") _append_if(capabilities, principal, ("campaigns:campaign:update",), "Edit eligible working campaign versions.") _append_if(capabilities, principal, ("campaigns:campaign:copy",), "Create an editable successor from an eligible existing version.") _append_if(capabilities, principal, ("campaigns:recipient:read",), "Inspect recipients and recipient-specific campaign data.") _append_if(capabilities, principal, ("campaigns:recipient:write",), "Add and edit recipient rows.") _append_if(capabilities, principal, ("campaigns:recipient:import",), "Import recipient snapshots.") _append_if(capabilities, principal, ("campaigns:campaign:validate",), "Validate campaign inputs and resolve blocking issues.") _append_if(capabilities, principal, ("campaigns:campaign:build",), "Build exact recipient messages for review.") _append_if(capabilities, principal, ("campaigns:campaign:review",), "Record review completion for an exact build.") if mail_available: _append_if(capabilities, principal, ("campaigns:campaign:send_test",), "Run authorized delivery verification tools.") _append_if(capabilities, principal, ("campaigns:campaign:queue",), "Queue an eligible reviewed campaign for controlled delivery.") _append_if(capabilities, principal, ("campaigns:campaign:control",), "Pause, resume, or cancel eligible delivery jobs.") if mail_available: _append_if(capabilities, principal, ("campaigns:campaign:send",), "Start an eligible real delivery run.") _append_if(capabilities, principal, ("campaigns:campaign:retry",), "Retry delivery jobs whose recorded state permits a retry.") _append_if(capabilities, principal, ("campaigns:campaign:reconcile",), "Reconcile delivery effects whose outcome is uncertain.") _append_if(capabilities, principal, ("campaigns:report:read",), "View authorized campaign delivery reports.") _append_if( capabilities, principal, ("campaigns:report:export", "campaigns:recipient:export"), "Export authorized delivery and recipient report data.", require_all=True, ) _append_if(capabilities, principal, ("campaigns:campaign:share",), "Manage explicit access to eligible campaigns.") _append_if(capabilities, principal, ("campaigns:campaign:archive",), "Archive eligible campaigns while retaining their evidence.") _append_if(capabilities, principal, ("campaigns:campaign:delete",), "Delete eligible untouched drafts.") if mail_available: _append_if( capabilities, principal, ("campaigns:report:send", "campaigns:report:read", "mail:profile:use"), "Send an authorized campaign report through a Mail profile.", require_all=True, ) return tuple(capabilities) def _integration_summary(registry: object, principal: object) -> tuple[tuple[str, ...], tuple[str, ...]]: configured: list[str] = [] limitations: list[str] = [] mail_available = _integration_available(registry, _MAIL_INTEGRATION) can_select_mail_profile = _has_all_scopes( principal, ("campaigns:campaign:read", "campaigns:campaign:update", "mail:profile:use"), ) can_deliver_with_mail = _has_scope(principal, "mail:profile:use") and _has_any_scope( principal, ("campaigns:campaign:queue", "campaigns:campaign:send", "campaigns:campaign:retry"), ) if mail_available and can_select_mail_profile: configured.append("Installed composition: Campaign can open Mail's actor-filtered profile picker while you edit; eligible profiles are resolved in the selected campaign context.") elif mail_available and can_deliver_with_mail: configured.append("Installed composition: Mail-backed Campaign delivery is connected, and the selected profile is re-authorized for each delivery action.") elif mail_available: limitations.append("Mail delivery is configured, but selecting a Mail profile is not included in your current tasks.") else: limitations.append("Mail-backed Campaign delivery is not available in the current composition.") files_available = _integration_available(registry, _FILES_INTEGRATION) can_preview_files = _has_all_scopes( principal, ("campaigns:campaign:read", "campaigns:recipient:read", "files:file:read"), ) can_link_files = _has_all_scopes( principal, ( "campaigns:campaign:read", "campaigns:campaign:update", "campaigns:campaign:validate", "campaigns:recipient:read", "files:file:read", "files:file:share", ), ) if files_available and can_link_files: configured.append("Installed composition: Managed file versions can be selected, previewed, and linked as governed campaign attachments when the selected campaign and files pass their resource checks.") elif files_available and can_preview_files: configured.append("Installed composition: Managed campaign attachments can be previewed; linking a version requires campaign-update, validation, and attachment-sharing authority.") elif files_available: limitations.append("Managed attachments are configured, but they are not included in your current Campaign tasks.") else: limitations.append("Managed file attachments are not available in the current composition; campaign-owned uploads remain separate.") lookup_available = _integration_available(registry, _ADDRESSES_LOOKUP_INTEGRATION) source_available = _integration_available(registry, _ADDRESSES_SOURCE_INTEGRATION) if lookup_available and _has_scope(principal, "campaigns:recipient:read"): configured.append("Installed composition: Address records can be looked up while preparing recipients.") elif lookup_available: limitations.append("Address lookup is configured, but recipient inspection is not included in your current Campaign tasks.") else: limitations.append("Connected address lookup is not available in the current composition.") can_import_source = _has_all_scopes( principal, ( "campaigns:campaign:read", "campaigns:campaign:update", "campaigns:recipient:read", "campaigns:recipient:write", "campaigns:recipient:import", ), ) if source_available and can_import_source: configured.append("Installed composition: The actor-filtered Addresses source picker can copy a selected source into a campaign as a traceable recipient snapshot.") elif source_available: limitations.append("Connected recipient sources are configured, but source import is not included in your current Campaign tasks.") else: limitations.append("Connected recipient-source snapshots are not available; direct campaign imports remain available when authorized.") if _integration_available(registry, _NOTIFICATIONS_INTEGRATION): configured.append("Installed composition: In-app notifications can report important Campaign delivery status changes.") else: limitations.append("Automatic in-app Campaign status notifications are not configured.") return tuple(configured), tuple(limitations) def _delivery_policy_summary(context: DocumentationContext) -> tuple[tuple[str, ...], tuple[str, ...]]: session = context.session tenant_id = str(getattr(context.principal, "tenant_id", "") or "").strip() if session is None or not tenant_id: return (), ("The effective synchronous Campaign limit could not be resolved for this documentation request.",) try: policy = effective_synchronous_send_policy(session, tenant_id=tenant_id) # type: ignore[arg-type] except CampaignDeliveryPolicyError as exc: return (), (f"Synchronous Campaign delivery is disabled until its policy configuration is corrected: {exc}",) return ( ( "Delivery policy: Send now is limited to " f"{policy.max_recipient_jobs} eligible recipient job(s) for this tenant. " "The exact built count and Queue for workers alternative are shown before confirmation." ), ), () def _append_if( target: list[str], principal: object, scopes: tuple[str, ...], text: str, *, require_all: bool = False, ) -> None: allowed = _has_all_scopes(principal, scopes) if require_all else _has_any_scope(principal, scopes) if allowed: target.append(f"Role authorization: {text}") def _has_scope(principal: object, scope: str) -> bool: checker = getattr(principal, "has", None) if not callable(checker): return False try: return bool(checker(scope)) except Exception: return False def _has_any_scope(principal: object, scopes: tuple[str, ...]) -> bool: return any(_has_scope(principal, scope) for scope in scopes) def _has_all_scopes(principal: object, scopes: tuple[str, ...]) -> bool: return all(_has_scope(principal, scope) for scope in scopes) def _integration_available(registry: object, interface_name: str) -> bool: return _registry_has_interface(registry, interface_name) and _registry_has_capability(registry, interface_name) def _registry_has_interface(registry: object, interface_name: str) -> bool: manifests_provider = getattr(registry, "manifests", None) if not callable(manifests_provider): return False try: manifests = tuple(manifests_provider()) except Exception: return False return any( getattr(provider, "name", None) == interface_name for manifest in manifests for provider in (getattr(manifest, "provides_interfaces", ()) or ()) ) def _registry_has_capability(registry: object, capability_name: str) -> bool: capability_checker = getattr(registry, "has_capability", None) if not callable(capability_checker): return False try: return bool(capability_checker(capability_name)) except Exception: return False def _composition_body(current_configuration: list[str], limitations: list[str]) -> str: del current_configuration, limitations return "The facts below are calculated for the current actor and installed module contracts. They do not bypass campaign ownership, sharing, state, or operation-time resource and policy checks."