Add institutional provenance and approval gates
This commit is contained in:
@@ -20,6 +20,12 @@ from govoplan_campaign.backend.schemas import (
|
||||
SendCampaignNowRequest,
|
||||
SendCampaignNowResponse,
|
||||
)
|
||||
from govoplan_campaign.backend.approval_gate import (
|
||||
CampaignApprovalGateError,
|
||||
campaign_approval_status,
|
||||
request_campaign_approval,
|
||||
)
|
||||
from govoplan_campaign.backend.approval_schemas import CampaignApprovalRequestInput
|
||||
from govoplan_core.auth import ApiPrincipal, require_any_scope, require_scope
|
||||
from govoplan_core.audit.logging import audit_from_principal
|
||||
from govoplan_campaign.backend.db.models import (
|
||||
@@ -92,14 +98,21 @@ def campaign_delivery_options(
|
||||
_get_campaign_for_principal(session, campaign_id, principal, write=True)
|
||||
_require_permission(principal, "campaigns:recipient:read")
|
||||
try:
|
||||
options = synchronous_send_options(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
campaign_id=campaign_id,
|
||||
version_id=version_id,
|
||||
)
|
||||
version = _get_version_for_tenant(
|
||||
session, str(options["version_id"]), principal.tenant_id
|
||||
)
|
||||
return CampaignDeliveryOptionsResponse(
|
||||
**synchronous_send_options(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
campaign_id=campaign_id,
|
||||
version_id=version_id,
|
||||
),
|
||||
**options,
|
||||
postbox_available=postbox_integration().available,
|
||||
approval_gate=campaign_approval_status(
|
||||
session, tenant_id=principal.tenant_id, version=version
|
||||
),
|
||||
)
|
||||
except QueueingError as exc:
|
||||
raise HTTPException(
|
||||
@@ -107,6 +120,77 @@ def campaign_delivery_options(
|
||||
) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{campaign_id}/versions/{version_id}/approval-request",
|
||||
response_model=dict[str, object],
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def create_campaign_approval_request(
|
||||
campaign_id: str,
|
||||
version_id: str,
|
||||
payload: CampaignApprovalRequestInput,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_scope("campaigns:campaign:review")),
|
||||
) -> dict[str, object]:
|
||||
campaign = _get_campaign_for_principal(session, campaign_id, principal, write=True)
|
||||
_require_permission(principal, "campaigns:recipient:read")
|
||||
if campaign.current_version_id != version_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Approval can only be requested for the current Campaign version.",
|
||||
)
|
||||
version = _get_version_for_tenant(session, version_id, principal.tenant_id)
|
||||
if not version.build_summary:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail="Build Campaign messages before requesting approval.",
|
||||
)
|
||||
try:
|
||||
request = request_campaign_approval(
|
||||
session,
|
||||
principal,
|
||||
campaign=campaign,
|
||||
version=version,
|
||||
title=payload.title,
|
||||
description=payload.description,
|
||||
steps=tuple(step.to_definition() for step in payload.steps),
|
||||
idempotency_key=payload.idempotency_key,
|
||||
template_id=payload.template_id,
|
||||
template_revision=payload.template_revision,
|
||||
unique_actors_across_steps=payload.unique_actors_across_steps,
|
||||
expires_at=payload.expires_at,
|
||||
policy_refs=tuple(payload.policy_refs),
|
||||
)
|
||||
audit_from_principal(
|
||||
session,
|
||||
principal,
|
||||
action="campaign.approval_requested",
|
||||
object_type="campaign_version",
|
||||
object_id=version.id,
|
||||
details={
|
||||
"campaign_id": campaign.id,
|
||||
"approval_request_id": request.id,
|
||||
"approval_request_revision": request.revision,
|
||||
"execution_snapshot_hash": version.execution_snapshot_hash,
|
||||
},
|
||||
commit=True,
|
||||
)
|
||||
return {
|
||||
"request_id": request.id,
|
||||
"request_revision": request.revision,
|
||||
"request_state": request.state,
|
||||
"approval_gate": campaign_approval_status(
|
||||
session, tenant_id=principal.tenant_id, version=version
|
||||
),
|
||||
}
|
||||
except (CampaignApprovalGateError, ExecutionSnapshotError) as exc:
|
||||
session.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
|
||||
@router.post("/{campaign_id}/queue", response_model=QueueCampaignResponse)
|
||||
def queue_campaign(
|
||||
campaign_id: str,
|
||||
|
||||
Reference in New Issue
Block a user