feat(poll): extend response editing contract

This commit is contained in:
2026-07-20 18:08:58 +02:00
parent 9b5418db78
commit 2f559e3f0b
2 changed files with 75 additions and 0 deletions

View File

@@ -58,6 +58,21 @@ class PollUpdateCommand:
closes_at: datetime | None closes_at: datetime | None
@dataclass(frozen=True, slots=True)
class PollOptionUpdateCommand:
"""Complete mutable option snapshot supplied by an owning module.
Providers must treat an exact repeat as a no-op. When the snapshot
changes, answers for this option are invalidated while answers for other
options are retained.
"""
label: str
description: str | None = None
value: Mapping[str, object] | None = None
metadata: Mapping[str, object] = field(default_factory=dict)
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class PollInvitationCommand: class PollInvitationCommand:
respondent_id: str | None = None respondent_id: str | None = None
@@ -102,6 +117,14 @@ class PollInvitationRef:
token: str token: str
@dataclass(frozen=True, slots=True)
class PollAnswerRef:
option_id: str | None = None
option_key: str | None = None
value: object = None
rank: int | None = None
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class PollResponseRef: class PollResponseRef:
invitation_id: str | None invitation_id: str | None
@@ -110,6 +133,9 @@ class PollResponseRef:
# consumer can use it to reconcile an authenticated response without # consumer can use it to reconcile an authenticated response without
# trusting client-supplied invitation metadata. # trusting client-supplied invitation metadata.
respondent_id: str | None = None respondent_id: str | None = None
# Optional for backwards compatibility. Providers that support response
# editing expose the authoritative, still-valid answers here.
answers: tuple[PollAnswerRef, ...] = ()
@runtime_checkable @runtime_checkable
@@ -144,6 +170,19 @@ class PollSchedulingProvider(Protocol):
) -> PollRef: ) -> PollRef:
... ...
def update_option(
self,
session: object,
*,
tenant_id: str,
poll_id: str,
option_id: str,
command: PollOptionUpdateCommand,
) -> PollOptionRef:
"""Update one option and invalidate only answers for that option."""
...
def open_poll(self, session: object, *, tenant_id: str, poll_id: str) -> PollRef: def open_poll(self, session: object, *, tenant_id: str, poll_id: str) -> PollRef:
... ...

View File

@@ -0,0 +1,36 @@
from __future__ import annotations
import unittest
from datetime import datetime, timezone
from govoplan_core.core.poll import PollAnswerRef, PollOptionUpdateCommand, PollResponseRef
class PollContractTests(unittest.TestCase):
def test_response_ref_remains_backwards_compatible_and_can_carry_answers(self) -> None:
submitted_at = datetime(2026, 7, 20, tzinfo=timezone.utc)
legacy = PollResponseRef(None, submitted_at)
detailed = PollResponseRef(
invitation_id="invitation-1",
submitted_at=submitted_at,
respondent_id="person-1",
answers=(PollAnswerRef(option_id="option-1", option_key="slot-1", value="available"),),
)
self.assertEqual(legacy.answers, ())
self.assertEqual(detailed.answers[0].value, "available")
def test_option_update_command_is_a_complete_immutable_snapshot(self) -> None:
command = PollOptionUpdateCommand(
label="Tuesday 10:00",
value={"slot_id": "slot-1"},
metadata={"source": "scheduling"},
)
self.assertEqual(command.label, "Tuesday 10:00")
self.assertEqual(command.value, {"slot_id": "slot-1"})
if __name__ == "__main__":
unittest.main()