Sprint 4 : decisions et mandats -- workflow complet + vote integration

Backend: 7 nouveaux endpoints (advance, assign, revoke, create-vote-session),
services enrichis avec creation de sessions de vote, assignation de mandataire
et revocation. 35 nouveaux tests (104 total). Frontend: store mandates, page
cadrage decisions, detail mandats, composants DecisionWorkflow, DecisionCadrage,
DecisionCard, MandateTimeline, MandateCard. Documentation mise a jour.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-02-28 14:28:34 +01:00
parent cede2a585f
commit 3cb1754592
24 changed files with 3988 additions and 354 deletions

View File

@@ -1,14 +1,17 @@
"""Decision service: step advancement logic."""
"""Decision service: step advancement logic and vote session integration."""
from __future__ import annotations
import uuid
from datetime import datetime, timedelta, timezone
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.decision import Decision, DecisionStep
from app.models.protocol import VotingProtocol, FormulaConfig
from app.models.vote import VoteSession
# Valid status transitions for decisions
@@ -115,3 +118,90 @@ def _advance_decision_status(decision: Decision) -> None:
next_index = current_index + 1
if next_index < len(_DECISION_STATUS_ORDER):
decision.status = _DECISION_STATUS_ORDER[next_index]
async def create_vote_session_for_step(
decision_id: uuid.UUID,
step_id: uuid.UUID,
db: AsyncSession,
) -> VoteSession:
"""Create a VoteSession linked to a decision step.
Uses the decision's voting_protocol to configure the session.
Raises ValueError if the decision or step is not found, the step
already has a vote session, or no voting protocol is configured.
Parameters
----------
decision_id:
UUID of the Decision.
step_id:
UUID of the DecisionStep to attach the vote session to.
db:
Async database session.
Returns
-------
VoteSession
The newly created vote session.
"""
# Fetch decision with steps
result = await db.execute(
select(Decision)
.options(selectinload(Decision.steps))
.where(Decision.id == decision_id)
)
decision = result.scalar_one_or_none()
if decision is None:
raise ValueError(f"Decision introuvable : {decision_id}")
# Find the step
step: DecisionStep | None = None
for s in decision.steps:
if s.id == step_id:
step = s
break
if step is None:
raise ValueError(f"Etape introuvable : {step_id}")
# Check step doesn't already have a vote session
if step.vote_session_id is not None:
raise ValueError("Cette etape possede deja une session de vote")
# Check voting protocol is configured
if decision.voting_protocol_id is None:
raise ValueError("Aucun protocole de vote configure pour cette decision")
# Fetch the voting protocol and its formula config for duration
proto_result = await db.execute(
select(VotingProtocol)
.options(selectinload(VotingProtocol.formula_config))
.where(VotingProtocol.id == decision.voting_protocol_id)
)
protocol = proto_result.scalar_one_or_none()
if protocol is None:
raise ValueError("Protocole de vote introuvable")
duration_days = protocol.formula_config.duration_days if protocol.formula_config else 30
now = datetime.now(timezone.utc)
# Create the vote session with explicit UUID (needed before flush)
session_id = uuid.uuid4()
session = VoteSession(
id=session_id,
decision_id=decision.id,
voting_protocol_id=protocol.id,
starts_at=now,
ends_at=now + timedelta(days=duration_days),
status="open",
)
db.add(session)
# Link session to step
step.vote_session_id = session_id
await db.commit()
await db.refresh(session)
return session