feat(poll): enforce auditable lifecycle transitions

This commit is contained in:
2026-07-20 18:18:20 +02:00
parent f4d5cac40d
commit 65e2d1fd65
14 changed files with 1161 additions and 68 deletions

View File

@@ -8,6 +8,7 @@ from pydantic import BaseModel, ConfigDict, Field, model_validator
PollKind = Literal["single_choice", "multiple_choice", "yes_no", "yes_no_maybe", "ranked_choice", "availability"]
PollStatus = Literal["draft", "open", "closed", "decided", "archived"]
PollTransitionAction = Literal["open", "draft", "close", "decide", "archive", "unarchive"]
PollVisibility = Literal["private", "tenant", "public", "unlisted"]
PollResultVisibility = Literal["organizer", "after_response", "after_close", "public"]
AvailabilityValue = Literal["available", "maybe", "unavailable"]
@@ -85,6 +86,13 @@ class PollOptionResponse(BaseModel):
metadata: dict[str, Any] = Field(default_factory=dict)
class PollTransitionAvailabilityResponse(BaseModel):
action: PollTransitionAction
target_status: PollStatus | None = None
available: bool
reason: str | None = None
class PollResponseItem(BaseModel):
id: str
respondent_id: str | None = None
@@ -120,11 +128,13 @@ class PollResponse(BaseModel):
closed_at: datetime | None = None
decided_at: datetime | None = None
decided_option_id: str | None = None
archived_from_status: str | None = None
created_by_user_id: str | None = None
created_at: datetime
updated_at: datetime
metadata: dict[str, Any] = Field(default_factory=dict)
options: list[PollOptionResponse] = Field(default_factory=list)
lifecycle_actions: list[PollTransitionAvailabilityResponse] = Field(default_factory=list)
class PollListResponse(BaseModel):
@@ -175,8 +185,40 @@ class PollDecisionRequest(BaseModel):
option_key: str | None = None
class PollTransitionRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
action: PollTransitionAction
option_id: str | None = None
option_key: str | None = None
class PollLifecycleTransitionResponse(BaseModel):
id: str
action: PollTransitionAction
from_status: PollStatus
to_status: PollStatus
decision_option_id: str | None = None
previous_decision_option_id: str | None = None
idempotency_key: str | None = None
actor_user_id: str | None = None
actor_api_key_id: str | None = None
created_at: datetime
metadata: dict[str, Any] = Field(default_factory=dict)
class PollStatusResponse(BaseModel):
poll: PollResponse
transition: PollLifecycleTransitionResponse | None = None
replayed: bool = False
class PollLifecycleResponse(BaseModel):
poll_id: str
status: PollStatus
archived_from_status: str | None = None
actions: list[PollTransitionAvailabilityResponse] = Field(default_factory=list)
history: list[PollLifecycleTransitionResponse] = Field(default_factory=list)
class PollResponseListResponse(BaseModel):