from __future__ import annotations import logging from fastapi import APIRouter, Depends, HTTPException, Response, status from sqlalchemy.orm import Session from govoplan_campaign.backend.schemas import ( ReportEmailRequest, ReportEmailResponse, ) from govoplan_core.auth import ApiPrincipal, has_scope, require_scope from govoplan_core.audit.logging import audit_from_principal from govoplan_campaign.backend.db.models import ( CampaignVersion, ) 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.reports.emailing import ( CampaignReportEmailError, send_campaign_report_email, ) from govoplan_campaign.backend.integrations import ( MailDeliveryCommandError, MailProfileError, SmtpConfigurationError, SmtpSendError, mail_integration, ) from govoplan_campaign.backend.route_support import ( _get_campaign_for_principal, _require_mail_profile_use_if_needed, _require_permission, ) router = APIRouter(prefix="/campaigns", tags=["campaigns"]) logger = logging.getLogger(__name__) def _enqueue_mail_command() -> None: try: from govoplan_core.celery_app import celery from govoplan_core.settings import settings if settings.celery_enabled: celery.send_task( "govoplan.mail.dispatch_outbox", args=[None, 25], queue="mail", ) except Exception: logger.warning( "Mail delivery command is durable but immediate worker wake-up failed", exc_info=True, ) @router.get("/{campaign_id}/summary") def campaign_summary( campaign_id: str, version_id: str | None = None, include_jobs: bool = False, session: Session = Depends(get_session), principal: ApiPrincipal = Depends(require_scope("campaigns:campaign:read")), ): """Return dashboard-friendly campaign status counters and summaries.""" _get_campaign_for_principal(session, campaign_id, principal) if include_jobs: _require_permission(principal, "campaigns:recipient:read") try: return generate_campaign_report( session, tenant_id=principal.tenant_id, campaign_id=campaign_id, version_id=version_id, include_jobs=include_jobs, include_recent_failures=include_jobs, include_diagnostics=has_scope(principal, "campaigns:diagnostic:read"), ) except CampaignReportError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(exc) ) from exc @router.get("/{campaign_id}/report") def campaign_report( campaign_id: str, version_id: str | None = None, include_jobs: bool = False, session: Session = Depends(get_session), principal: ApiPrincipal = Depends(require_scope("campaigns:report:read")), ): """Return the recipient-level JSON report for one campaign.""" _get_campaign_for_principal(session, campaign_id, principal) _require_permission(principal, "campaigns:recipient:read") try: return generate_campaign_report( session, tenant_id=principal.tenant_id, campaign_id=campaign_id, version_id=version_id, include_jobs=include_jobs, include_recent_failures=include_jobs, include_diagnostics=has_scope(principal, "campaigns:diagnostic:read"), ) except CampaignReportError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(exc) ) from exc @router.get("/{campaign_id}/report/jobs.csv") def campaign_jobs_csv( campaign_id: str, version_id: str | None = None, session: Session = Depends(get_session), principal: ApiPrincipal = Depends(require_scope("campaigns:report:export")), ): """Export per-job campaign status as CSV.""" _get_campaign_for_principal(session, campaign_id, principal) _require_permission(principal, "campaigns:recipient:export") try: csv_text = generate_jobs_csv( session, tenant_id=principal.tenant_id, campaign_id=campaign_id, version_id=version_id, include_diagnostics=has_scope(principal, "campaigns:diagnostic:read"), ) except CampaignReportError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(exc) ) from exc return Response( content=csv_text, media_type="text/csv; charset=utf-8", headers={ "Content-Disposition": f'attachment; filename="campaign-{campaign_id}-jobs.csv"' }, ) @router.post("/{campaign_id}/report/email", response_model=ReportEmailResponse) def email_campaign_report( campaign_id: str, payload: ReportEmailRequest, session: Session = Depends(get_session), principal: ApiPrincipal = Depends(require_scope("campaigns:report:send")), ): """Generate a campaign report and send it to one or more email addresses.""" 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: _require_mail_profile_use_if_needed( principal, selected_version.raw_json if isinstance(selected_version.raw_json, dict) else {}, ) try: result = send_campaign_report_email( session, tenant_id=principal.tenant_id, campaign_id=campaign_id, version_id=payload.version_id, to=payload.to, include_jobs=payload.include_jobs, attach_jobs_csv=payload.attach_jobs_csv, attach_report_json=payload.attach_report_json, dry_run=payload.dry_run, idempotency_key=payload.idempotency_key, created_by_user_id=principal.user.id, ) audit_from_principal( session, principal, action="report.email_requested" if not payload.dry_run else "report.email_dry_run", object_type="campaign", object_id=campaign_id, details=result.audit_dict(), commit=True, ) if not payload.dry_run: _enqueue_mail_command() return ReportEmailResponse(result=result.as_dict()) except CampaignReportError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(exc) ) from exc except ( CampaignReportEmailError, MailProfileError, MailDeliveryCommandError, SmtpConfigurationError, SmtpSendError, ) as exc: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc) ) from exc except Exception as exc: logger.error("Campaign report email failed with an unexpected internal error") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Campaign report email could not be completed.", ) from exc @router.get( "/{campaign_id}/report/email/{command_id}", response_model=ReportEmailResponse, ) def campaign_report_email_status( campaign_id: str, command_id: str, session: Session = Depends(get_session), principal: ApiPrincipal = Depends(require_scope("campaigns:report:read")), ): _get_campaign_for_principal(session, campaign_id, principal) try: result = mail_integration().delivery_command_summary( session, tenant_id=principal.tenant_id, command_id=command_id, ) except MailDeliveryCommandError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(exc), ) from exc if ( result.get("source_module") != "campaigns" or result.get("source_resource_type") != "campaign" or result.get("source_resource_id") != campaign_id ): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Campaign report delivery not found", ) return ReportEmailResponse(result=result)