Files
govoplan-voting/src/govoplan_voting/backend/service.py
T

1434 lines
50 KiB
Python

from __future__ import annotations
from collections.abc import Mapping, Sequence
from dataclasses import asdict
from datetime import UTC, datetime
import hashlib
import json
from typing import Any
import uuid
from sqlalchemy import func
from sqlalchemy.orm import Session
from govoplan_core.core.voting import (
ExternalVotingCastRequest,
ExternalVotingFinalizationRequest,
ExternalVotingPreparationRequest,
ExternalVotingProvider,
InteractiveExternalVotingProvider,
VOTING_ASSURANCE_RECORDED,
VotingBallotCreateCommand,
VotingBallotRef,
VotingCastCommand,
VotingCapabilityError,
VotingReceipt,
VotingResult,
require_voting_provider_assurance,
voting_provider_capability,
)
from govoplan_voting.backend.db.models import (
VotingBallotRevision,
VotingCastRecord,
VotingCommandReplay,
VotingLifecycleEvent,
)
ALLOWED_ASSURANCE_PROFILES = frozenset(
{"recorded", "confidential", "secret", "external_certified"}
)
ALLOWED_METHODS = frozenset({"single_choice", "approval", "yes_no_abstain"})
TERMINAL_STATES = frozenset({"certified", "annulled"})
class VotingStoreError(ValueError):
pass
class SqlVotingBallots:
def __init__(self, registry: object | None = None) -> None:
self._registry = registry
def create_ballot(
self,
session: object,
principal: object,
*,
command: VotingBallotCreateCommand,
idempotency_key: str,
) -> VotingBallotRef:
typed_session = _session(session)
tenant_id = _principal_tenant(principal)
payload = _payload_from_command(command)
_validate_draft(payload)
request = {"command": payload}
replay = _read_replay(
typed_session,
tenant_id=tenant_id,
operation="create",
idempotency_key=idempotency_key,
request=request,
)
if replay is not None:
return _ref_from_mapping(replay)
ballot_id = str(uuid.uuid4())
now = _now()
row = VotingBallotRevision(
tenant_id=tenant_id,
ballot_id=ballot_id,
revision=1,
state="draft",
assurance_profile=str(payload["assurance_profile"]),
method=str(payload["method"]),
definition_sha256=None,
electorate_sha256=None,
recorded_at=now,
payload=payload,
created_by=_principal_actor(principal),
)
typed_session.add(row)
typed_session.flush()
_record_event(
typed_session,
row=row,
event_type="ballot.created",
principal=principal,
payload={"revision": 1},
)
result = _ref(row)
_write_replay(
typed_session,
tenant_id=tenant_id,
operation="create",
idempotency_key=idempotency_key,
request=request,
response=_ref_mapping(result),
)
return result
def update_draft(
self,
session: object,
principal: object,
*,
ballot_id: str,
command: VotingBallotCreateCommand,
expected_revision: int,
idempotency_key: str,
) -> VotingBallotRef:
typed_session = _session(session)
current = _current(typed_session, principal, ballot_id=ballot_id, lock=True)
payload = _payload_from_command(command)
_validate_draft(payload)
request = {
"ballot_id": ballot_id,
"expected_revision": expected_revision,
"command": payload,
}
operation = f"update:{ballot_id}"
replay = _read_replay(
typed_session,
tenant_id=current.tenant_id,
operation=operation,
idempotency_key=idempotency_key,
request=request,
)
if replay is not None:
return _ref_from_mapping(replay)
_expect(current, revision=expected_revision, states={"draft"})
revised = _revise(
typed_session,
current=current,
principal=principal,
state="draft",
payload=payload,
event_type="ballot.updated",
event_payload={"previous_revision": current.revision},
)
result = _ref(revised)
_write_replay(
typed_session,
tenant_id=current.tenant_id,
operation=operation,
idempotency_key=idempotency_key,
request=request,
response=_ref_mapping(result),
)
return result
def get_ballot(
self,
session: object,
principal: object,
*,
ballot_id: str,
) -> Mapping[str, object] | None:
typed_session = _session(session)
tenant_id = _principal_tenant(principal)
row = (
typed_session.query(VotingBallotRevision)
.filter(
VotingBallotRevision.tenant_id == tenant_id,
VotingBallotRevision.ballot_id == ballot_id,
VotingBallotRevision.superseded_at.is_(None),
)
.one_or_none()
)
return _ballot_mapping(row) if row is not None else None
def list_ballots(
self,
session: object,
principal: object,
*,
state: str | None = None,
limit: int = 100,
) -> tuple[Mapping[str, object], ...]:
typed_session = _session(session)
tenant_id = _principal_tenant(principal)
if not 1 <= limit <= 200:
raise VotingStoreError("Voting list limit must be between 1 and 200.")
query = typed_session.query(VotingBallotRevision).filter(
VotingBallotRevision.tenant_id == tenant_id,
VotingBallotRevision.superseded_at.is_(None),
)
if state:
query = query.filter(VotingBallotRevision.state == state)
rows = (
query.order_by(VotingBallotRevision.recorded_at.desc()).limit(limit).all()
)
return tuple(_ballot_mapping(row) for row in rows)
def history(
self,
session: object,
principal: object,
*,
ballot_id: str,
) -> tuple[Mapping[str, object], ...]:
typed_session = _session(session)
tenant_id = _principal_tenant(principal)
rows = (
typed_session.query(VotingLifecycleEvent)
.filter(
VotingLifecycleEvent.tenant_id == tenant_id,
VotingLifecycleEvent.ballot_id == ballot_id,
)
.order_by(VotingLifecycleEvent.sequence.asc())
.all()
)
return tuple(
{
"sequence": row.sequence,
"event_type": row.event_type,
"recorded_at": _datetime_text(row.recorded_at),
"actor_id": row.actor_id,
"payload": dict(row.payload),
}
for row in rows
)
def open_ballot(
self,
session: object,
principal: object,
*,
ballot_id: str,
expected_revision: int,
idempotency_key: str,
) -> VotingBallotRef:
typed_session = _session(session)
current = _current(typed_session, principal, ballot_id=ballot_id, lock=True)
request = {"ballot_id": ballot_id, "expected_revision": expected_revision}
operation = f"open:{ballot_id}"
replay = _read_replay(
typed_session,
tenant_id=current.tenant_id,
operation=operation,
idempotency_key=idempotency_key,
request=request,
)
if replay is not None:
return _ref_from_mapping(replay)
_expect(current, revision=expected_revision, states={"draft"})
payload = dict(current.payload)
_validate_draft(payload)
profile = str(payload["assurance_profile"])
provider: object | None = None
if profile != VOTING_ASSURANCE_RECORDED:
provider_id = str(payload.get("provider_id") or "").strip()
provider = (
_capability(self._registry, voting_provider_capability(provider_id))
if provider_id
else None
)
if not isinstance(provider, ExternalVotingProvider):
raise VotingStoreError(
"The selected Voting assurance profile requires an available external provider."
)
try:
declaration = require_voting_provider_assurance(
provider,
provider_id=provider_id,
assurance_profile=profile,
at=_now(),
)
except VotingCapabilityError as exc:
raise VotingStoreError(str(exc)) from exc
payload["provider_assurance"] = declaration.to_dict()
definition_hash, electorate_hash = _frozen_hashes(payload)
payload["definition_sha256"] = definition_hash
payload["electorate_sha256"] = electorate_hash
provider_evidence: tuple[Mapping[str, object], ...] = ()
if isinstance(provider, InteractiveExternalVotingProvider):
provider_id = str(payload["provider_id"])
try:
prepared = provider.prepare_ballot(
typed_session,
principal,
request=ExternalVotingPreparationRequest(
tenant_id=current.tenant_id,
ballot_id=current.ballot_id,
requested_provider_ballot_ref=_optional_text(
payload.get("provider_ballot_ref")
),
definition_sha256=definition_hash,
electorate_sha256=electorate_hash,
assurance_profile=profile, # type: ignore[arg-type]
method=str(payload["method"]),
options=tuple(
_option_from_mapping(item) for item in payload["options"]
),
electorate=tuple(
_elector_from_mapping(item)
for item in payload["electorate"]
),
allow_replacement=bool(payload.get("allow_replacement")),
quorum_weight=int(payload.get("quorum_weight") or 0),
threshold_numerator=int(
payload.get("threshold_numerator") or 1
),
threshold_denominator=int(
payload.get("threshold_denominator") or 2
),
opens_at=_parse_datetime(payload.get("opens_at")),
closes_at=_parse_datetime(payload.get("closes_at")),
requested_at=_now(),
idempotency_key=f"open:{_idempotency(idempotency_key)}",
),
)
except ValueError as exc:
raise VotingStoreError(
"Voting provider preparation was rejected."
) from exc
if (
prepared.provider_id != provider_id
or prepared.definition_sha256 != definition_hash
or prepared.electorate_sha256 != electorate_hash
or prepared.state not in {"prepared", "open"}
):
raise VotingStoreError(
"Voting provider preparation did not match the frozen ballot."
)
payload["provider_ballot_ref"] = prepared.provider_ballot_ref
provider_evidence = tuple(prepared.evidence)
elif (
profile != VOTING_ASSURANCE_RECORDED
and not str(payload.get("provider_ballot_ref") or "").strip()
):
raise VotingStoreError(
"This external Voting provider requires a ballot reference before opening."
)
revised = _revise(
typed_session,
current=current,
principal=principal,
state="open",
payload=payload,
definition_sha256=definition_hash,
electorate_sha256=electorate_hash,
event_type="ballot.opened",
event_payload={
"revision": current.revision + 1,
"definition_sha256": definition_hash,
"electorate_sha256": electorate_hash,
"provider_id": payload.get("provider_id"),
"provider_ballot_ref": payload.get("provider_ballot_ref"),
"provider_assurance": payload.get("provider_assurance"),
"provider_evidence": [dict(item) for item in provider_evidence],
},
)
result = _ref(revised)
_write_replay(
typed_session,
tenant_id=current.tenant_id,
operation=operation,
idempotency_key=idempotency_key,
request=request,
response=_ref_mapping(result),
)
return result
def cast_ballot(
self,
session: object,
principal: object,
*,
ballot_id: str,
command: VotingCastCommand,
) -> VotingReceipt:
typed_session = _session(session)
current = _current(typed_session, principal, ballot_id=ballot_id, lock=True)
if current.state != "open":
raise VotingStoreError("Only an open Voting ballot accepts votes.")
_validate_window(current.payload)
actor_id = _principal_actor(principal)
elector_id = str(command.elector_id or actor_id or "").strip()
if not elector_id or not actor_id:
raise VotingStoreError("Voting requires an authenticated elector identity.")
if elector_id != actor_id:
raise VotingStoreError(
"A Voting principal cannot cast a ballot for another elector."
)
electorate = {
str(item["subject_id"]): item
for item in list(current.payload.get("electorate") or [])
}
elector = electorate.get(elector_id)
if elector is None:
raise VotingStoreError(
"The current principal is not in the frozen electorate."
)
selections = tuple(
str(item).strip() for item in command.selections if str(item).strip()
)
_validate_selections(current.payload, selections)
idempotency_key = _idempotency(command.idempotency_key)
if current.assurance_profile != VOTING_ASSURANCE_RECORDED:
provider_id = str(current.payload.get("provider_id") or "").strip()
provider_ref = str(current.payload.get("provider_ballot_ref") or "").strip()
provider = _capability(
self._registry,
voting_provider_capability(provider_id),
)
if not isinstance(provider, InteractiveExternalVotingProvider):
raise VotingStoreError(
"This Voting provider does not expose an interactive cast capability."
)
_require_pinned_provider_assurance(current.payload, provider)
try:
receipt = provider.cast_ballot(
typed_session,
principal,
request=ExternalVotingCastRequest(
tenant_id=current.tenant_id,
ballot_id=current.ballot_id,
provider_ballot_ref=provider_ref,
definition_sha256=str(current.definition_sha256),
electorate_sha256=str(current.electorate_sha256),
elector_id=elector_id,
selections=selections,
allow_replacement=bool(
current.payload.get("allow_replacement")
),
requested_at=_now(),
idempotency_key=idempotency_key,
),
)
except ValueError as exc:
raise VotingStoreError("Voting provider cast was rejected.") from exc
if receipt.ballot_id != current.ballot_id:
raise VotingStoreError(
"Voting provider returned a receipt for another ballot."
)
normalized = VotingReceipt(
ballot_id=current.ballot_id,
revision=current.revision,
receipt_sha256=receipt.receipt_sha256,
cast_at=receipt.cast_at,
replaced_previous=receipt.replaced_previous,
replayed=receipt.replayed,
)
if not normalized.replayed:
_record_event(
typed_session,
row=current,
event_type="ballot.vote_cast",
principal=principal,
payload={
"receipt_sha256": normalized.receipt_sha256,
"provider_id": provider_id,
"replaced_previous": normalized.replaced_previous,
},
)
return normalized
replay = (
typed_session.query(VotingCastRecord)
.filter(
VotingCastRecord.tenant_id == current.tenant_id,
VotingCastRecord.ballot_id == ballot_id,
VotingCastRecord.idempotency_key == idempotency_key,
)
.one_or_none()
)
if replay is not None:
if (
replay.elector_id != elector_id
or tuple(replay.selections) != selections
):
raise VotingStoreError(
"Voting idempotency key was reused for another vote."
)
return VotingReceipt(
ballot_id=ballot_id,
revision=current.revision,
receipt_sha256=replay.receipt_sha256,
cast_at=_aware(replay.cast_at),
replaced_previous=replay.generation > 1,
replayed=True,
)
previous = (
typed_session.query(VotingCastRecord)
.filter(
VotingCastRecord.tenant_id == current.tenant_id,
VotingCastRecord.ballot_id == ballot_id,
VotingCastRecord.elector_id == elector_id,
VotingCastRecord.superseded_at.is_(None),
)
.with_for_update()
.one_or_none()
)
if previous is not None and not bool(current.payload.get("allow_replacement")):
raise VotingStoreError(
"This Voting ballot does not allow replacing a cast vote."
)
cast_at = _now()
generation = (previous.generation + 1) if previous is not None else 1
if previous is not None:
previous.superseded_at = cast_at
receipt_hash = _sha256(
{
"tenant_id": current.tenant_id,
"ballot_id": ballot_id,
"definition_sha256": current.definition_sha256,
"elector_id": elector_id,
"generation": generation,
"selections": selections,
"cast_at": cast_at.isoformat(),
"idempotency_key": idempotency_key,
}
)
typed_session.add(
VotingCastRecord(
tenant_id=current.tenant_id,
ballot_id=ballot_id,
definition_sha256=str(current.definition_sha256),
elector_id=elector_id,
generation=generation,
selections=list(selections),
weight=int(elector.get("weight") or 1),
cast_at=cast_at,
idempotency_key=idempotency_key,
receipt_sha256=receipt_hash,
actor_id=actor_id,
)
)
typed_session.flush()
_record_event(
typed_session,
row=current,
event_type="ballot.vote_cast",
principal=principal,
payload={
"receipt_sha256": receipt_hash,
"elector_id": elector_id,
"generation": generation,
"replaced_previous": previous is not None,
},
)
return VotingReceipt(
ballot_id=ballot_id,
revision=current.revision,
receipt_sha256=receipt_hash,
cast_at=cast_at,
replaced_previous=previous is not None,
)
def close_ballot(
self,
session: object,
principal: object,
*,
ballot_id: str,
expected_revision: int,
idempotency_key: str,
) -> VotingResult:
typed_session = _session(session)
current = _current(typed_session, principal, ballot_id=ballot_id, lock=True)
request = {"ballot_id": ballot_id, "expected_revision": expected_revision}
operation = f"close:{ballot_id}"
replay = _read_replay(
typed_session,
tenant_id=current.tenant_id,
operation=operation,
idempotency_key=idempotency_key,
request=request,
)
if replay is not None:
return _result_from_mapping(replay)
_expect(current, revision=expected_revision, states={"open"})
if current.assurance_profile == VOTING_ASSURANCE_RECORDED:
result = self._tally_recorded(typed_session, current=current)
else:
result = self._finalize_external(
typed_session,
principal,
current=current,
idempotency_key=idempotency_key,
)
normalized = VotingResult(
ballot_id=ballot_id,
revision=current.revision + 1,
counts=result.counts,
weighted_counts=result.weighted_counts,
cast_count=result.cast_count,
cast_weight=result.cast_weight,
eligible_count=result.eligible_count,
eligible_weight=result.eligible_weight,
quorum_met=result.quorum_met,
threshold_met=result.threshold_met,
winning_options=result.winning_options,
result_sha256=result.result_sha256,
evidence=result.evidence,
)
payload = dict(current.payload)
payload["result"] = _result_mapping(normalized)
revised = _revise(
typed_session,
current=current,
principal=principal,
state="closed",
payload=payload,
event_type="ballot.closed",
event_payload={
"result_sha256": result.result_sha256,
"cast_count": result.cast_count,
"quorum_met": result.quorum_met,
"threshold_met": result.threshold_met,
},
)
if revised.revision != normalized.revision:
raise VotingStoreError(
"Voting result revision did not match the persisted ballot revision."
)
_write_replay(
typed_session,
tenant_id=current.tenant_id,
operation=operation,
idempotency_key=idempotency_key,
request=request,
response=_result_mapping(normalized),
)
return normalized
def certify_ballot(
self,
session: object,
principal: object,
*,
ballot_id: str,
expected_revision: int,
evidence: Sequence[Mapping[str, object]],
idempotency_key: str,
) -> VotingBallotRef:
typed_session = _session(session)
current = _current(typed_session, principal, ballot_id=ballot_id, lock=True)
evidence_items = [dict(item) for item in evidence]
request = {
"ballot_id": ballot_id,
"expected_revision": expected_revision,
"evidence": evidence_items,
}
operation = f"certify:{ballot_id}"
replay = _read_replay(
typed_session,
tenant_id=current.tenant_id,
operation=operation,
idempotency_key=idempotency_key,
request=request,
)
if replay is not None:
return _ref_from_mapping(replay)
_expect(current, revision=expected_revision, states={"closed"})
result = dict(current.payload.get("result") or {})
if not result.get("result_sha256"):
raise VotingStoreError(
"A Voting ballot cannot be certified without a result hash."
)
if (
current.assurance_profile != VOTING_ASSURANCE_RECORDED
and not evidence_items
):
raise VotingStoreError(
"External Voting certification requires provider evidence."
)
certification = {
"certified_at": _now().isoformat(),
"certified_by": _principal_actor(principal),
"result_sha256": result["result_sha256"],
"evidence": evidence_items,
}
payload = dict(current.payload)
payload["certification"] = certification
revised = _revise(
typed_session,
current=current,
principal=principal,
state="certified",
payload=payload,
event_type="ballot.certified",
event_payload=certification,
)
response = _ref_mapping(_ref(revised))
_write_replay(
typed_session,
tenant_id=current.tenant_id,
operation=operation,
idempotency_key=idempotency_key,
request=request,
response=response,
)
return _ref_from_mapping(response)
def challenge_ballot(
self,
session: object,
principal: object,
*,
ballot_id: str,
expected_revision: int,
reason: str,
idempotency_key: str,
) -> VotingBallotRef:
return self._reasoned_transition(
session,
principal,
ballot_id=ballot_id,
expected_revision=expected_revision,
reason=reason,
idempotency_key=idempotency_key,
target_state="challenged",
allowed_states={"closed", "certified"},
)
def annul_ballot(
self,
session: object,
principal: object,
*,
ballot_id: str,
expected_revision: int,
reason: str,
idempotency_key: str,
) -> VotingBallotRef:
return self._reasoned_transition(
session,
principal,
ballot_id=ballot_id,
expected_revision=expected_revision,
reason=reason,
idempotency_key=idempotency_key,
target_state="annulled",
allowed_states={"draft", "open", "closed", "certified", "challenged"},
)
def _reasoned_transition(
self,
session: object,
principal: object,
*,
ballot_id: str,
expected_revision: int,
reason: str,
idempotency_key: str,
target_state: str,
allowed_states: set[str],
) -> VotingBallotRef:
typed_session = _session(session)
current = _current(typed_session, principal, ballot_id=ballot_id, lock=True)
normalized_reason = str(reason or "").strip()
if not normalized_reason:
raise VotingStoreError(f"Voting {target_state} requires a reason.")
request = {
"ballot_id": ballot_id,
"expected_revision": expected_revision,
"reason": normalized_reason,
}
operation = f"{target_state}:{ballot_id}"
replay = _read_replay(
typed_session,
tenant_id=current.tenant_id,
operation=operation,
idempotency_key=idempotency_key,
request=request,
)
if replay is not None:
return _ref_from_mapping(replay)
_expect(current, revision=expected_revision, states=allowed_states)
payload = dict(current.payload)
payload[f"{target_state}_reason"] = normalized_reason
revised = _revise(
typed_session,
current=current,
principal=principal,
state=target_state,
payload=payload,
event_type=f"ballot.{target_state}",
event_payload={"reason": normalized_reason},
)
response = _ref_mapping(_ref(revised))
_write_replay(
typed_session,
tenant_id=current.tenant_id,
operation=operation,
idempotency_key=idempotency_key,
request=request,
response=response,
)
return _ref_from_mapping(response)
def _tally_recorded(
self,
session: Session,
*,
current: VotingBallotRevision,
) -> VotingResult:
rows = (
session.query(VotingCastRecord)
.filter(
VotingCastRecord.tenant_id == current.tenant_id,
VotingCastRecord.ballot_id == current.ballot_id,
VotingCastRecord.superseded_at.is_(None),
)
.all()
)
option_keys = [str(item["key"]) for item in current.payload["options"]]
counts = {key: 0 for key in option_keys}
weighted = {key: 0 for key in option_keys}
for row in rows:
for selection in row.selections:
counts[selection] += 1
weighted[selection] += row.weight
eligible = list(current.payload["electorate"])
eligible_weight = sum(int(item.get("weight") or 1) for item in eligible)
cast_weight = sum(row.weight for row in rows)
quorum_met = cast_weight >= int(current.payload.get("quorum_weight") or 0)
max_weight = max(weighted.values(), default=0)
winners = tuple(
key for key in option_keys if max_weight > 0 and weighted[key] == max_weight
)
numerator = int(current.payload.get("threshold_numerator") or 1)
denominator = int(current.payload.get("threshold_denominator") or 2)
threshold_met = bool(
quorum_met
and winners
and max_weight * denominator >= max(cast_weight, 1) * numerator
)
body = {
"ballot_id": current.ballot_id,
"counts": counts,
"weighted_counts": weighted,
"cast_count": len(rows),
"cast_weight": cast_weight,
"eligible_count": len(eligible),
"eligible_weight": eligible_weight,
"quorum_met": quorum_met,
"threshold_met": threshold_met,
"winning_options": winners,
"definition_sha256": current.definition_sha256,
"electorate_sha256": current.electorate_sha256,
}
return VotingResult(
ballot_id=current.ballot_id,
revision=current.revision + 1,
counts=counts,
weighted_counts=weighted,
cast_count=len(rows),
cast_weight=cast_weight,
eligible_count=len(eligible),
eligible_weight=eligible_weight,
quorum_met=quorum_met,
threshold_met=threshold_met,
winning_options=winners,
result_sha256=_sha256(body),
)
def _finalize_external(
self,
session: Session,
principal: object,
*,
current: VotingBallotRevision,
idempotency_key: str,
) -> VotingResult:
provider_id = str(current.payload.get("provider_id") or "").strip()
provider_ref = str(current.payload.get("provider_ballot_ref") or "").strip()
provider = _capability(self._registry, voting_provider_capability(provider_id))
if not isinstance(provider, ExternalVotingProvider):
raise VotingStoreError(f"Voting provider is unavailable: {provider_id}.")
_require_pinned_provider_assurance(current.payload, provider)
electorate = list(current.payload["electorate"])
try:
result = provider.finalize_ballot(
session,
principal,
request=ExternalVotingFinalizationRequest(
tenant_id=current.tenant_id,
ballot_id=current.ballot_id,
provider_ballot_ref=provider_ref,
definition_sha256=str(current.definition_sha256),
electorate_sha256=str(current.electorate_sha256),
options=tuple(
_option_from_mapping(item)
for item in current.payload["options"]
),
eligible_count=len(electorate),
eligible_weight=sum(
int(item.get("weight") or 1) for item in electorate
),
requested_at=_now(),
idempotency_key=_idempotency(idempotency_key),
),
)
except ValueError as exc:
raise VotingStoreError(
"Voting provider finalization was rejected."
) from exc
option_keys = {str(item["key"]) for item in current.payload["options"]}
if result.ballot_id != current.ballot_id:
raise VotingStoreError(
"Voting provider returned a result for another ballot."
)
if (
set(result.counts) != option_keys
or set(result.weighted_counts) != option_keys
):
raise VotingStoreError(
"Voting provider result must cover exactly the frozen options."
)
if sum(result.counts.values()) < result.cast_count:
raise VotingStoreError(
"Voting provider counts are inconsistent with cast_count."
)
if result.cast_count > len(electorate):
raise VotingStoreError(
"Voting provider cast_count exceeds the frozen electorate."
)
if not result.evidence:
raise VotingStoreError("Voting provider finalization requires evidence.")
return result
def _payload_from_command(command: VotingBallotCreateCommand) -> dict[str, Any]:
return {
"title": str(command.title or "").strip(),
"description": _optional_text(command.description),
"method": str(command.method or "").strip(),
"assurance_profile": str(command.assurance_profile or "").strip(),
"options": [asdict(item) for item in command.options],
"electorate": [
{
"subject_id": item.subject_id,
"label": item.label,
"weight": item.weight,
"provenance": dict(item.provenance),
}
for item in command.electorate
],
"context": {
"module": _optional_text(command.context_module),
"resource_type": _optional_text(command.context_resource_type),
"resource_id": _optional_text(command.context_resource_id),
},
"quorum_weight": command.quorum_weight,
"threshold_numerator": command.threshold_numerator,
"threshold_denominator": command.threshold_denominator,
"allow_replacement": command.allow_replacement,
"opens_at": _datetime_text(command.opens_at),
"closes_at": _datetime_text(command.closes_at),
"provider_id": _optional_text(command.provider_id),
"provider_ballot_ref": _optional_text(command.provider_ballot_ref),
"provider_assurance": None,
"metadata": dict(command.metadata),
"definition_sha256": None,
"electorate_sha256": None,
"result": None,
"certification": None,
}
def _validate_draft(payload: Mapping[str, Any]) -> None:
if not str(payload.get("title") or "").strip():
raise VotingStoreError("Voting ballot title is required.")
method = str(payload.get("method") or "")
if method not in ALLOWED_METHODS:
raise VotingStoreError("Unsupported Voting method.")
assurance = str(payload.get("assurance_profile") or "")
if assurance not in ALLOWED_ASSURANCE_PROFILES:
raise VotingStoreError("Unsupported Voting assurance profile.")
options = list(payload.get("options") or [])
option_keys = [str(item.get("key") or "").strip() for item in options]
if (
len(options) < 2
or any(not key for key in option_keys)
or len(set(option_keys)) != len(option_keys)
):
raise VotingStoreError(
"Voting ballots require at least two uniquely keyed options."
)
electorate = list(payload.get("electorate") or [])
elector_ids = [str(item.get("subject_id") or "").strip() for item in electorate]
if (
not electorate
or any(not item for item in elector_ids)
or len(set(elector_ids)) != len(elector_ids)
):
raise VotingStoreError(
"Voting electorate entries must have unique subject ids."
)
if any(int(item.get("weight") or 0) < 1 for item in electorate):
raise VotingStoreError("Voting electorate weights must be positive integers.")
eligible_weight = sum(int(item.get("weight") or 1) for item in electorate)
quorum = int(payload.get("quorum_weight") or 0)
if quorum < 0 or quorum > eligible_weight:
raise VotingStoreError("Voting quorum cannot exceed the electorate weight.")
numerator = int(payload.get("threshold_numerator") or 0)
denominator = int(payload.get("threshold_denominator") or 0)
if numerator < 1 or denominator < 1 or numerator > denominator:
raise VotingStoreError(
"Voting threshold must be a fraction between zero and one."
)
opens_at = _parse_datetime(payload.get("opens_at"))
closes_at = _parse_datetime(payload.get("closes_at"))
if opens_at and closes_at and closes_at <= opens_at:
raise VotingStoreError("Voting close time must be after its open time.")
if assurance != VOTING_ASSURANCE_RECORDED:
if not str(payload.get("provider_id") or "").strip():
raise VotingStoreError("External Voting assurance requires a provider id.")
def _validate_selections(
payload: Mapping[str, Any], selections: tuple[str, ...]
) -> None:
option_keys = {str(item["key"]) for item in payload["options"]}
if (
not selections
or len(selections) != len(set(selections))
or not set(selections) <= option_keys
):
raise VotingStoreError(
"Voting selections must be unique configured option keys."
)
method = str(payload["method"])
if method in {"single_choice", "yes_no_abstain"} and len(selections) != 1:
raise VotingStoreError("This Voting method requires exactly one selection.")
def _validate_window(payload: Mapping[str, Any]) -> None:
now = _now()
opens_at = _parse_datetime(payload.get("opens_at"))
closes_at = _parse_datetime(payload.get("closes_at"))
if opens_at and now < opens_at:
raise VotingStoreError("Voting ballot has not opened yet.")
if closes_at and now >= closes_at:
raise VotingStoreError("Voting ballot has already reached its close time.")
def _require_pinned_provider_assurance(
payload: Mapping[str, Any],
provider: object,
) -> None:
provider_id = str(payload.get("provider_id") or "").strip()
assurance_profile = str(payload.get("assurance_profile") or "").strip()
try:
current = require_voting_provider_assurance(
provider,
provider_id=provider_id,
assurance_profile=assurance_profile,
at=_now(),
)
except VotingCapabilityError as exc:
raise VotingStoreError(str(exc)) from exc
pinned = payload.get("provider_assurance")
if not isinstance(pinned, Mapping) or dict(pinned) != current.to_dict():
raise VotingStoreError(
"Voting provider assurance changed after the ballot was frozen."
)
def _frozen_hashes(payload: Mapping[str, Any]) -> tuple[str, str]:
electorate = list(payload.get("electorate") or [])
definition = {
key: value
for key, value in payload.items()
if key
not in {
"electorate",
"electorate_sha256",
"definition_sha256",
"result",
"certification",
"provider_ballot_ref",
}
}
return _sha256(definition), _sha256(electorate)
def _current(
session: Session,
principal: object,
*,
ballot_id: str,
lock: bool,
) -> VotingBallotRevision:
tenant_id = _principal_tenant(principal)
query = session.query(VotingBallotRevision).filter(
VotingBallotRevision.tenant_id == tenant_id,
VotingBallotRevision.ballot_id == ballot_id,
VotingBallotRevision.superseded_at.is_(None),
)
if lock:
query = query.with_for_update()
row = query.one_or_none()
if row is None:
raise LookupError("Voting ballot not found.")
return row
def _expect(row: VotingBallotRevision, *, revision: int, states: set[str]) -> None:
if row.revision != revision:
raise VotingStoreError(
"Voting revision conflict: the expected revision is stale."
)
if row.state not in states:
raise VotingStoreError(
f"Voting transition is not allowed from state {row.state}."
)
def _revise(
session: Session,
*,
current: VotingBallotRevision,
principal: object,
state: str,
payload: Mapping[str, Any],
event_type: str,
event_payload: Mapping[str, Any],
definition_sha256: str | None = None,
electorate_sha256: str | None = None,
) -> VotingBallotRevision:
now = _now()
current.superseded_at = now
row = VotingBallotRevision(
tenant_id=current.tenant_id,
ballot_id=current.ballot_id,
revision=current.revision + 1,
previous_revision_id=current.id,
state=state,
assurance_profile=str(payload["assurance_profile"]),
method=str(payload["method"]),
definition_sha256=definition_sha256 or current.definition_sha256,
electorate_sha256=electorate_sha256 or current.electorate_sha256,
recorded_at=now,
payload=dict(payload),
created_by=_principal_actor(principal),
)
session.add(row)
session.flush()
_record_event(
session,
row=row,
event_type=event_type,
principal=principal,
payload=dict(event_payload),
)
return row
def _record_event(
session: Session,
*,
row: VotingBallotRevision,
event_type: str,
principal: object,
payload: Mapping[str, Any],
) -> None:
sequence = (
session.query(func.max(VotingLifecycleEvent.sequence))
.filter(
VotingLifecycleEvent.tenant_id == row.tenant_id,
VotingLifecycleEvent.ballot_id == row.ballot_id,
)
.scalar()
or 0
) + 1
session.add(
VotingLifecycleEvent(
tenant_id=row.tenant_id,
ballot_id=row.ballot_id,
sequence=sequence,
event_type=event_type,
recorded_at=_now(),
actor_id=_principal_actor(principal),
payload=dict(payload),
)
)
session.flush()
def _read_replay(
session: Session,
*,
tenant_id: str,
operation: str,
idempotency_key: str,
request: Mapping[str, Any],
) -> Mapping[str, Any] | None:
key = _idempotency(idempotency_key)
row = (
session.query(VotingCommandReplay)
.filter(
VotingCommandReplay.tenant_id == tenant_id,
VotingCommandReplay.operation == operation,
VotingCommandReplay.idempotency_key == key,
)
.one_or_none()
)
if row is None:
return None
if row.request_sha256 != _sha256(request):
raise VotingStoreError("Voting idempotency key was reused for another command.")
return dict(row.response)
def _write_replay(
session: Session,
*,
tenant_id: str,
operation: str,
idempotency_key: str,
request: Mapping[str, Any],
response: Mapping[str, Any],
) -> None:
session.add(
VotingCommandReplay(
tenant_id=tenant_id,
operation=operation,
idempotency_key=_idempotency(idempotency_key),
request_sha256=_sha256(request),
response=dict(response),
)
)
session.flush()
def _ballot_mapping(row: VotingBallotRevision) -> dict[str, object]:
return {
"id": row.ballot_id,
"revision": row.revision,
"state": row.state,
"assurance_profile": row.assurance_profile,
"method": row.method,
"definition_sha256": row.definition_sha256,
"electorate_sha256": row.electorate_sha256,
"recorded_at": _datetime_text(row.recorded_at),
**dict(row.payload),
}
def _ref(row: VotingBallotRevision) -> VotingBallotRef:
return VotingBallotRef(
id=row.ballot_id,
revision=row.revision,
state=row.state,
assurance_profile=row.assurance_profile,
definition_sha256=row.definition_sha256,
electorate_sha256=row.electorate_sha256,
)
def _ref_mapping(value: VotingBallotRef) -> dict[str, object]:
return {
"id": value.id,
"revision": value.revision,
"state": value.state,
"assurance_profile": value.assurance_profile,
"definition_sha256": value.definition_sha256,
"electorate_sha256": value.electorate_sha256,
}
def _ref_from_mapping(value: Mapping[str, Any]) -> VotingBallotRef:
return VotingBallotRef(
id=str(value["id"]),
revision=int(value["revision"]),
state=str(value["state"]),
assurance_profile=str(value["assurance_profile"]),
definition_sha256=_optional_text(value.get("definition_sha256")),
electorate_sha256=_optional_text(value.get("electorate_sha256")),
)
def _result_mapping(value: VotingResult) -> dict[str, object]:
return {
"ballot_id": value.ballot_id,
"revision": value.revision,
"counts": dict(value.counts),
"weighted_counts": dict(value.weighted_counts),
"cast_count": value.cast_count,
"cast_weight": value.cast_weight,
"eligible_count": value.eligible_count,
"eligible_weight": value.eligible_weight,
"quorum_met": value.quorum_met,
"threshold_met": value.threshold_met,
"winning_options": list(value.winning_options),
"result_sha256": value.result_sha256,
"evidence": [dict(item) for item in value.evidence],
}
def _result_from_mapping(value: Mapping[str, Any]) -> VotingResult:
return VotingResult(
ballot_id=str(value["ballot_id"]),
revision=int(value["revision"]),
counts={str(key): int(count) for key, count in dict(value["counts"]).items()},
weighted_counts={
str(key): int(count)
for key, count in dict(value["weighted_counts"]).items()
},
cast_count=int(value["cast_count"]),
cast_weight=int(value["cast_weight"]),
eligible_count=int(value["eligible_count"]),
eligible_weight=int(value["eligible_weight"]),
quorum_met=bool(value["quorum_met"]),
threshold_met=bool(value["threshold_met"]),
winning_options=tuple(str(item) for item in value.get("winning_options") or ()),
result_sha256=str(value["result_sha256"]),
evidence=tuple(dict(item) for item in value.get("evidence") or ()),
)
def _option_from_mapping(value: Mapping[str, Any]):
from govoplan_core.core.voting import VotingOption
return VotingOption(
key=str(value["key"]),
label=str(value["label"]),
description=_optional_text(value.get("description")),
)
def _elector_from_mapping(value: Mapping[str, Any]):
from govoplan_core.core.voting import VotingElector
return VotingElector(
subject_id=str(value["subject_id"]),
label=_optional_text(value.get("label")),
weight=int(value.get("weight") or 1),
provenance=dict(value.get("provenance") or {}),
)
def _sha256(value: object) -> str:
encoded = json.dumps(
value,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=True,
default=_json_default,
).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
def _json_default(value: object) -> object:
if isinstance(value, datetime):
return _datetime_text(value)
if isinstance(value, tuple):
return list(value)
raise TypeError(f"Unsupported canonical Voting value: {type(value).__name__}")
def _idempotency(value: str) -> str:
normalized = str(value or "").strip()
if not normalized or len(normalized) > 160:
raise VotingStoreError(
"Voting idempotency key is required and limited to 160 characters."
)
return normalized
def _principal_tenant(principal: object) -> str:
value = str(getattr(principal, "tenant_id", "") or "").strip()
if not value:
raise VotingStoreError("Voting operations require a tenant-bound principal.")
return value
def _principal_actor(principal: object) -> str | None:
for name in ("account_id", "user_id", "identity_id", "membership_id", "subject"):
value = str(getattr(principal, name, "") or "").strip()
if value:
return value
return None
def _session(value: object) -> Session:
if not hasattr(value, "query"):
raise VotingStoreError("Voting requires a database session.")
return value # type: ignore[return-value]
def _capability(registry: object | None, name: str) -> object | None:
if (
registry is None
or not hasattr(registry, "has_capability")
or not registry.has_capability(name)
):
return None
return registry.capability(name)
def _optional_text(value: object) -> str | None:
normalized = str(value or "").strip()
return normalized or None
def _parse_datetime(value: object) -> datetime | None:
if value in (None, ""):
return None
parsed = (
value if isinstance(value, datetime) else datetime.fromisoformat(str(value))
)
if parsed.tzinfo is None:
raise VotingStoreError("Voting date-times must include a timezone.")
return parsed
def _aware(value: datetime) -> datetime:
return value if value.tzinfo is not None else value.replace(tzinfo=UTC)
def _datetime_text(value: datetime | None) -> str | None:
if value is None:
return None
return _aware(value).isoformat()
def _now() -> datetime:
return datetime.now(UTC)
__all__ = ["SqlVotingBallots", "VotingStoreError"]