Files
decision/backend/app/services/mandate_service.py
Yvv 25437f24e3 Sprint 1 : scaffolding complet de Glibredecision
Plateforme de decisions collectives pour Duniter/G1.
Backend FastAPI async + PostgreSQL (14 tables, 8 routers, 6 services,
moteur de vote avec formule d'inertie WoT/Smith/TechComm).
Frontend Nuxt 4 + Nuxt UI v3 + Pinia (9 pages, 5 stores).
Infrastructure Docker + Woodpecker CI + Traefik.
Documentation technique et utilisateur (15 fichiers).
Seed : Licence G1, Engagement Forgeron v2.0.0, 4 protocoles de vote.
30 tests unitaires (formules, mode params, vote nuance) -- tous verts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 12:46:11 +01:00

119 lines
3.2 KiB
Python

"""Mandate service: step advancement logic."""
from __future__ import annotations
import uuid
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.mandate import Mandate, MandateStep
# Valid status transitions for mandates
_MANDATE_STATUS_ORDER = [
"draft",
"candidacy",
"voting",
"active",
"reporting",
"completed",
]
async def advance_mandate(mandate_id: uuid.UUID, db: AsyncSession) -> Mandate:
"""Move a mandate to its next step.
Completes the current active step and activates the next pending step.
If no more steps remain, the mandate status advances to the next phase.
Parameters
----------
mandate_id:
UUID of the Mandate to advance.
db:
Async database session.
Returns
-------
Mandate
The updated mandate.
Raises
------
ValueError
If the mandate is not found, already completed/revoked, or
no further advancement is possible.
"""
result = await db.execute(
select(Mandate)
.options(selectinload(Mandate.steps))
.where(Mandate.id == mandate_id)
)
mandate = result.scalar_one_or_none()
if mandate is None:
raise ValueError(f"Mandat introuvable : {mandate_id}")
if mandate.status in ("completed", "revoked"):
raise ValueError(f"Le mandat est deja en statut terminal : {mandate.status}")
steps: list[MandateStep] = sorted(mandate.steps, key=lambda s: s.step_order)
# Find the current active step
active_step: MandateStep | None = None
for step in steps:
if step.status == "active":
active_step = step
break
if active_step is not None:
# Complete the active step
active_step.status = "completed"
# Activate the next pending step
next_step: MandateStep | None = None
for step in steps:
if step.step_order > active_step.step_order and step.status == "pending":
next_step = step
break
if next_step is not None:
next_step.status = "active"
else:
# No more steps: advance mandate status
_advance_mandate_status(mandate)
else:
# No active step: activate the first pending one
first_pending: MandateStep | None = None
for step in steps:
if step.status == "pending":
first_pending = step
break
if first_pending is not None:
first_pending.status = "active"
# Move out of draft
if mandate.status == "draft":
mandate.status = "candidacy"
else:
# All steps completed: advance status
_advance_mandate_status(mandate)
await db.commit()
await db.refresh(mandate)
return mandate
def _advance_mandate_status(mandate: Mandate) -> None:
"""Move a mandate to its next status in the lifecycle."""
try:
current_index = _MANDATE_STATUS_ORDER.index(mandate.status)
except ValueError:
return
next_index = current_index + 1
if next_index < len(_MANDATE_STATUS_ORDER):
mandate.status = _MANDATE_STATUS_ORDER[next_index]