feat(poll): extend response editing contract
This commit is contained in:
@@ -58,6 +58,21 @@ class PollUpdateCommand:
|
||||
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)
|
||||
class PollInvitationCommand:
|
||||
respondent_id: str | None = None
|
||||
@@ -102,6 +117,14 @@ class PollInvitationRef:
|
||||
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)
|
||||
class PollResponseRef:
|
||||
invitation_id: str | None
|
||||
@@ -110,6 +133,9 @@ class PollResponseRef:
|
||||
# consumer can use it to reconcile an authenticated response without
|
||||
# trusting client-supplied invitation metadata.
|
||||
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
|
||||
@@ -144,6 +170,19 @@ class PollSchedulingProvider(Protocol):
|
||||
) -> 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:
|
||||
...
|
||||
|
||||
|
||||
36
tests/test_poll_contract.py
Normal file
36
tests/test_poll_contract.py
Normal 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()
|
||||
Reference in New Issue
Block a user