Integrate committee ballots with Voting
This commit is contained in:
@@ -12,6 +12,10 @@ from govoplan_core.core.institutional import (
|
||||
EvidenceReference,
|
||||
InstitutionalReference,
|
||||
)
|
||||
from govoplan_core.core.voting import (
|
||||
CAPABILITY_VOTING_BALLOTS,
|
||||
VotingBallotProvider,
|
||||
)
|
||||
from govoplan_committee.backend.workspace import (
|
||||
CommitteeWorkspaceError,
|
||||
CommitteeWorkspaceRecord,
|
||||
@@ -54,9 +58,7 @@ class BallotFinalizationRequest:
|
||||
"Committee provider ballot reference is required."
|
||||
)
|
||||
if len(self.choices) < 2 or len(self.choices) != len(set(self.choices)):
|
||||
raise CommitteeWorkspaceError(
|
||||
"Committee ballot choices must be unique."
|
||||
)
|
||||
raise CommitteeWorkspaceError("Committee ballot choices must be unique.")
|
||||
if self.eligible_count < 0:
|
||||
raise CommitteeWorkspaceError(
|
||||
"Committee ballot eligible count cannot be negative."
|
||||
@@ -95,9 +97,7 @@ class BallotFinalizationResult:
|
||||
if self.cast_count < 0 or any(
|
||||
not isinstance(value, int) or value < 0 for value in self.counts.values()
|
||||
):
|
||||
raise CommitteeWorkspaceError(
|
||||
"Committee ballot counts cannot be negative."
|
||||
)
|
||||
raise CommitteeWorkspaceError("Committee ballot counts cannot be negative.")
|
||||
if not self.evidence:
|
||||
raise CommitteeWorkspaceError(
|
||||
"Committee ballot finalization requires provider evidence."
|
||||
@@ -209,6 +209,120 @@ class CommitteeBallotFinalizer:
|
||||
_provider_finalization=True,
|
||||
)
|
||||
|
||||
def finalize_voting_ballot(
|
||||
self,
|
||||
session: Session,
|
||||
principal: object,
|
||||
*,
|
||||
vote_id: str,
|
||||
voting_ballot_id: str,
|
||||
voting_expected_revision: int,
|
||||
approval_ref: InstitutionalReference,
|
||||
expected_revision: int,
|
||||
recorded_at: datetime,
|
||||
change_reason: str,
|
||||
idempotency_key: str,
|
||||
) -> CommitteeWorkspaceRecord:
|
||||
"""Close a Voting-owned ballot and project its aggregate into Committee."""
|
||||
|
||||
current = get_workspace_object(
|
||||
session,
|
||||
principal,
|
||||
object_kind="vote",
|
||||
object_id=vote_id,
|
||||
)
|
||||
if current is None:
|
||||
raise LookupError("Committee vote not found.")
|
||||
if current.state != "open":
|
||||
raise CommitteeWorkspaceError(
|
||||
"Only an open Committee vote can be finalized."
|
||||
)
|
||||
if current.revision != expected_revision:
|
||||
raise CommitteeWorkspaceError(
|
||||
"Committee revision conflict: the expected revision is stale."
|
||||
)
|
||||
configured_id = str(current.attributes.get("voting_ballot_id") or "").strip()
|
||||
if not configured_id or configured_id != voting_ballot_id:
|
||||
raise CommitteeWorkspaceError(
|
||||
"Committee vote is not bound to the requested Voting ballot."
|
||||
)
|
||||
provider = _capability(self._registry, CAPABILITY_VOTING_BALLOTS)
|
||||
if not isinstance(provider, VotingBallotProvider):
|
||||
raise CommitteeWorkspaceError("Voting ballot capability is unavailable.")
|
||||
ballot = provider.get_ballot(
|
||||
session,
|
||||
principal,
|
||||
ballot_id=voting_ballot_id,
|
||||
)
|
||||
if ballot is None:
|
||||
raise CommitteeWorkspaceError("Referenced Voting ballot was not found.")
|
||||
context = ballot.get("context")
|
||||
if isinstance(context, Mapping):
|
||||
context_module = str(context.get("module") or "").strip()
|
||||
context_id = str(context.get("resource_id") or "").strip()
|
||||
if context_module and context_module != "committee":
|
||||
raise CommitteeWorkspaceError(
|
||||
"Referenced Voting ballot belongs to another module context."
|
||||
)
|
||||
if context_id and context_id != vote_id:
|
||||
raise CommitteeWorkspaceError(
|
||||
"Referenced Voting ballot belongs to another Committee vote."
|
||||
)
|
||||
result = provider.close_ballot(
|
||||
session,
|
||||
principal,
|
||||
ballot_id=voting_ballot_id,
|
||||
expected_revision=voting_expected_revision,
|
||||
idempotency_key=f"committee:{idempotency_key}",
|
||||
)
|
||||
choices = tuple(str(item) for item in current.attributes.get("choices", ()))
|
||||
if set(result.counts) != set(choices):
|
||||
raise CommitteeWorkspaceError(
|
||||
"Voting result options do not match the Committee vote choices."
|
||||
)
|
||||
evidence = EvidenceReference(
|
||||
kind="snapshot",
|
||||
owner_module="voting",
|
||||
evidence_id=f"result-{result.result_sha256}",
|
||||
tenant_id=current.tenant_id,
|
||||
version=str(result.revision),
|
||||
checksum=result.result_sha256,
|
||||
source_ref=f"voting:{voting_ballot_id}",
|
||||
captured_at=recorded_at,
|
||||
)
|
||||
next_record = replace(
|
||||
current,
|
||||
revision=current.revision + 1,
|
||||
state="closed",
|
||||
recorded_at=recorded_at,
|
||||
change_reason=change_reason,
|
||||
attributes={
|
||||
**dict(current.attributes),
|
||||
"voting_ballot_id": voting_ballot_id,
|
||||
"voting_ballot_revision": result.revision,
|
||||
"voting_result_sha256": result.result_sha256,
|
||||
"counts": {key: int(value) for key, value in result.counts.items()},
|
||||
"weighted_counts": {
|
||||
key: int(value) for key, value in result.weighted_counts.items()
|
||||
},
|
||||
"cast_count": result.cast_count,
|
||||
"cast_weight": result.cast_weight,
|
||||
"quorum_met": result.quorum_met,
|
||||
"threshold_met": result.threshold_met,
|
||||
"winning_options": list(result.winning_options),
|
||||
"approval_ref": approval_ref.to_dict(),
|
||||
},
|
||||
evidence=_merge_evidence(current.evidence, (evidence,)),
|
||||
)
|
||||
return record_workspace_object(
|
||||
session,
|
||||
principal,
|
||||
record=next_record,
|
||||
expected_revision=expected_revision,
|
||||
idempotency_key=idempotency_key,
|
||||
_provider_finalization=True,
|
||||
)
|
||||
|
||||
|
||||
def _validate_result(
|
||||
result: BallotFinalizationResult,
|
||||
|
||||
Reference in New Issue
Block a user