feat: orchestrate accountable Campaign work
Module Package Release / publish-packages (push) Successful in 13s

This commit is contained in:
2026-08-22 02:14:35 +02:00
parent 4f52f010ee
commit 1bd24f9b5b
17 changed files with 1605 additions and 26 deletions
@@ -43,6 +43,13 @@ from govoplan_core.core.idm import (
CAPABILITY_IDM_FUNCTION_ASSIGNMENTS,
IdmFunctionAssignmentDirectory,
)
from govoplan_core.core.events import (
EventActorRef,
EventObjectRef,
EventTenantRef,
PlatformEvent,
emit_platform_event,
)
from govoplan_core.core.notifications import (
NotificationDispatchRequest,
notification_dispatch_provider,
@@ -127,7 +134,10 @@ def list_campaign_work_assignments(
campaign = _get_campaign_for_principal(session, campaign_id, principal)
_require_permission(principal, "campaigns:campaign:read")
statuses = tuple(dict.fromkeys(item.strip() for item in assignment_status if item.strip()))
if any(item not in {"open", "in_progress", "completed", "cancelled"} for item in statuses):
if any(
item not in {"open", "in_progress", "completed", "rejected", "cancelled"}
for item in statuses
):
raise HTTPException(status_code=422, detail="Unsupported assignment status filter.")
query = session.query(CampaignWorkAssignment).filter(
CampaignWorkAssignment.tenant_id == principal.tenant_id,
@@ -288,12 +298,20 @@ def transition_campaign_work_assignment(
if payload.action == "cancel":
raise HTTPException(status_code=403, detail="Only an assignment manager may cancel work.")
_require_revision(assignment, payload.expected_revision)
target_status = {"start": "in_progress", "complete": "completed", "cancel": "cancelled"}[payload.action]
target_status = {
"accept": "in_progress",
"start": "in_progress",
"complete": "completed",
"reject": "rejected",
"cancel": "cancelled",
}[payload.action]
if assignment.status == target_status:
return _assignment_response(assignment)
allowed = {
"accept": {"open"},
"start": {"open"},
"complete": {"open", "in_progress"},
"reject": {"open", "in_progress"},
"cancel": {"open", "in_progress"},
}
if assignment.status not in allowed[payload.action]:
@@ -306,7 +324,13 @@ def transition_campaign_work_assignment(
session,
assignment=assignment,
principal=principal,
event_kind={"start": "started", "complete": "completed", "cancel": "cancelled"}[payload.action],
event_kind={
"accept": "accepted",
"start": "started",
"complete": "completed",
"reject": "rejected",
"cancel": "cancelled",
}[payload.action],
details={"reason": payload.reason},
)
_notify_assignment(session, campaign=campaign, assignment=assignment, event_kind=payload.action)
@@ -697,6 +721,51 @@ def _record_event(
)
session.add(event)
session.flush()
emit_platform_event(
session,
PlatformEvent(
type="campaign.work.changed",
module_id="campaigns",
event_id=event.id,
occurred_at=event.created_at,
correlation_id=assignment.orchestration_correlation_id,
causation_id=assignment.workflow_step_id,
actor=EventActorRef(
type="account",
id=principal.account_id,
label=_actor_label(principal),
),
tenant=EventTenantRef(id=assignment.tenant_id),
subject=EventObjectRef(
type="campaign_work_assignment",
id=assignment.id,
),
resource=EventObjectRef(
type="campaign",
id=assignment.campaign_id,
),
classification="internal",
payload={
"campaign_id": assignment.campaign_id,
"campaign_version_id": assignment.campaign_version_id,
"assignment_id": assignment.id,
"assignment_revision": assignment.resource_revision,
"assignment_ref": (
"campaign-work-assignment:"
f"{assignment.id}:r{assignment.resource_revision}"
),
"outcome": event_kind,
"status": assignment.status,
"assignee_type": assignment.assignee_type,
"assignee_id": assignment.assignee_id,
"action_url": (
f"/campaigns/{assignment.campaign_id}/work"
f"?assignment={assignment.id}"
),
},
),
registry=get_registry(),
)
return event