feat(campaign): add privacy-safe aggregate reports

This commit is contained in:
2026-07-22 08:44:12 +02:00
parent 21f3014ac5
commit 06125cc0e8
5 changed files with 1043 additions and 4 deletions

View File

@@ -106,6 +106,14 @@ from govoplan_campaign.backend.db.models import (
)
from govoplan_core.db.session import get_session
from govoplan_campaign.backend.reports.campaigns import CampaignReportError, generate_campaign_report, generate_jobs_csv
from govoplan_campaign.backend.report_privacy_policy import CampaignReportPrivacyPolicyError
from govoplan_campaign.backend.reports.aggregate import (
AggregateCampaignReport,
AggregateCampaignReportError,
AggregateReportCampaignList,
aggregate_report_campaign_item,
generate_aggregate_campaign_report,
)
from govoplan_campaign.backend.response_security import (
public_campaign_payload,
public_delivery_result_message,
@@ -962,6 +970,52 @@ def delete_recipient_import_mapping_profile(
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.get("/aggregate-reports", response_model=AggregateReportCampaignList)
def list_aggregate_campaign_reports(
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_scope("campaigns:report:read")),
):
"""List only the business metadata needed to select an aggregate report."""
campaigns = (
_campaign_query_for_principal(session, principal)
.order_by(Campaign.updated_at.desc())
.all()
)
return AggregateReportCampaignList(
campaigns=[aggregate_report_campaign_item(campaign) for campaign in campaigns]
)
@router.get("/aggregate-reports/{campaign_id}", response_model=AggregateCampaignReport)
def aggregate_campaign_report(
campaign_id: str,
version_id: str | None = None,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_scope("campaigns:report:read")),
):
"""Return the privacy-safe aggregate projection without recipient detail."""
_get_campaign_for_principal(session, campaign_id, principal)
try:
return generate_aggregate_campaign_report(
session,
tenant_id=principal.tenant_id,
campaign_id=campaign_id,
version_id=version_id,
)
except AggregateCampaignReportError as exc:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Campaign report not found",
) from exc
except CampaignReportPrivacyPolicyError as exc:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Campaign report privacy policy is invalid",
) from exc
@router.get("/{campaign_id}/address-lookup", response_model=CampaignAddressLookupResponse)
def lookup_campaign_addresses(
campaign_id: str,
@@ -2937,6 +2991,7 @@ def get_job_diagnostics(
"""Return infrastructure details only to campaign operators/admins."""
_get_campaign_for_principal(session, campaign_id, principal)
_require_permission(principal, "campaigns:recipient:read")
campaign = _get_campaign_for_tenant(session, campaign_id, principal.tenant_id)
job = session.get(CampaignJob, job_id)
if not job or job.campaign_id != campaign.id or job.tenant_id != principal.tenant_id:
@@ -2992,8 +3047,7 @@ def campaign_report(
principal: ApiPrincipal = Depends(require_scope("campaigns:report:read")),
):
_get_campaign_for_principal(session, campaign_id, principal)
if include_jobs:
_require_permission(principal, "campaigns:recipient:read")
_require_permission(principal, "campaigns:recipient:read")
"""Return the full JSON report for one campaign."""
try:
@@ -3048,6 +3102,7 @@ def email_campaign_report(
principal: ApiPrincipal = Depends(require_scope("campaigns:report:send")),
):
campaign = _get_campaign_for_principal(session, campaign_id, principal)
_require_permission(principal, "campaigns:recipient:export")
selected_version_id = payload.version_id or campaign.current_version_id
selected_version = session.get(CampaignVersion, selected_version_id) if selected_version_id else None
if selected_version is not None and selected_version.campaign_id == campaign.id:
@@ -3055,8 +3110,6 @@ def email_campaign_report(
principal,
selected_version.raw_json if isinstance(selected_version.raw_json, dict) else {},
)
if payload.include_jobs or payload.attach_jobs_csv:
_require_permission(principal, "campaigns:recipient:export")
"""Generate a campaign report and send it to one or more email addresses."""
try: