"""Mandates router: CRUD for mandates and their steps.""" from __future__ import annotations import uuid from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.database import get_db from app.models.mandate import Mandate, MandateStep from app.models.user import DuniterIdentity from app.schemas.mandate import ( MandateAdvanceOut, MandateAssignRequest, MandateCreate, MandateOut, MandateStepCreate, MandateStepOut, MandateUpdate, ) from app.schemas.vote import VoteSessionOut from app.services.auth_service import get_current_identity from app.services.mandate_service import ( advance_mandate, assign_mandatee, create_vote_session_for_step, revoke_mandate, ) router = APIRouter() # ── Helpers ───────────────────────────────────────────────────────────────── async def _get_mandate(db: AsyncSession, mandate_id: uuid.UUID) -> Mandate: """Fetch a mandate by ID with its steps eagerly loaded, or raise 404.""" 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 HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Mandat introuvable") return mandate # ── Mandate routes ────────────────────────────────────────────────────────── @router.get("/", response_model=list[MandateOut]) async def list_mandates( db: AsyncSession = Depends(get_db), mandate_type: str | None = Query(default=None, description="Filtrer par type de mandat"), status_filter: str | None = Query(default=None, alias="status", description="Filtrer par statut"), skip: int = Query(default=0, ge=0), limit: int = Query(default=50, ge=1, le=200), ) -> list[MandateOut]: """List all mandates with optional filters.""" stmt = select(Mandate).options(selectinload(Mandate.steps)) if mandate_type is not None: stmt = stmt.where(Mandate.mandate_type == mandate_type) if status_filter is not None: stmt = stmt.where(Mandate.status == status_filter) stmt = stmt.order_by(Mandate.created_at.desc()).offset(skip).limit(limit) result = await db.execute(stmt) mandates = result.scalars().unique().all() return [MandateOut.model_validate(m) for m in mandates] @router.post("/", response_model=MandateOut, status_code=status.HTTP_201_CREATED) async def create_mandate( payload: MandateCreate, db: AsyncSession = Depends(get_db), identity: DuniterIdentity = Depends(get_current_identity), ) -> MandateOut: """Create a new mandate.""" mandate = Mandate(**payload.model_dump()) db.add(mandate) await db.commit() await db.refresh(mandate) # Reload with steps (empty at creation) mandate = await _get_mandate(db, mandate.id) return MandateOut.model_validate(mandate) @router.get("/{id}", response_model=MandateOut) async def get_mandate( id: uuid.UUID, db: AsyncSession = Depends(get_db), ) -> MandateOut: """Get a single mandate with all its steps.""" mandate = await _get_mandate(db, id) return MandateOut.model_validate(mandate) @router.put("/{id}", response_model=MandateOut) async def update_mandate( id: uuid.UUID, payload: MandateUpdate, db: AsyncSession = Depends(get_db), identity: DuniterIdentity = Depends(get_current_identity), ) -> MandateOut: """Update a mandate's metadata.""" mandate = await _get_mandate(db, id) update_data = payload.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(mandate, field, value) await db.commit() await db.refresh(mandate) # Reload with steps mandate = await _get_mandate(db, mandate.id) return MandateOut.model_validate(mandate) @router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_mandate( id: uuid.UUID, db: AsyncSession = Depends(get_db), identity: DuniterIdentity = Depends(get_current_identity), ) -> None: """Delete a mandate (only if in draft status).""" mandate = await _get_mandate(db, id) if mandate.status != "draft": raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Seuls les mandats en brouillon peuvent etre supprimes", ) await db.delete(mandate) await db.commit() # ── Mandate Step routes ───────────────────────────────────────────────────── @router.post("/{id}/steps", response_model=MandateStepOut, status_code=status.HTTP_201_CREATED) async def add_step( id: uuid.UUID, payload: MandateStepCreate, db: AsyncSession = Depends(get_db), identity: DuniterIdentity = Depends(get_current_identity), ) -> MandateStepOut: """Add a step to a mandate process.""" mandate = await _get_mandate(db, id) step = MandateStep( mandate_id=mandate.id, **payload.model_dump(), ) db.add(step) await db.commit() await db.refresh(step) return MandateStepOut.model_validate(step) @router.get("/{id}/steps", response_model=list[MandateStepOut]) async def list_steps( id: uuid.UUID, db: AsyncSession = Depends(get_db), ) -> list[MandateStepOut]: """List all steps for a mandate, ordered by step_order.""" mandate = await _get_mandate(db, id) return [MandateStepOut.model_validate(s) for s in mandate.steps] # ── Workflow routes ──────────────────────────────────────────────────────── @router.post("/{id}/advance", response_model=MandateAdvanceOut) async def advance_mandate_endpoint( id: uuid.UUID, db: AsyncSession = Depends(get_db), identity: DuniterIdentity = Depends(get_current_identity), ) -> MandateAdvanceOut: """Advance a mandate to its next step or status.""" try: mandate = await advance_mandate(id, db) except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) # Reload with steps for complete output mandate = await _get_mandate(db, mandate.id) data = MandateOut.model_validate(mandate).model_dump() data["message"] = f"Mandat avance au statut : {mandate.status}" return MandateAdvanceOut(**data) @router.post("/{id}/assign", response_model=MandateOut) async def assign_mandatee_endpoint( id: uuid.UUID, payload: MandateAssignRequest, db: AsyncSession = Depends(get_db), identity: DuniterIdentity = Depends(get_current_identity), ) -> MandateOut: """Assign a mandatee to a mandate.""" try: mandate = await assign_mandatee(id, payload.mandatee_id, db) except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) # Reload with steps mandate = await _get_mandate(db, mandate.id) return MandateOut.model_validate(mandate) @router.post("/{id}/revoke", response_model=MandateOut) async def revoke_mandate_endpoint( id: uuid.UUID, db: AsyncSession = Depends(get_db), identity: DuniterIdentity = Depends(get_current_identity), ) -> MandateOut: """Revoke an active mandate.""" try: mandate = await revoke_mandate(id, db) except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) # Reload with steps mandate = await _get_mandate(db, mandate.id) return MandateOut.model_validate(mandate) @router.post( "/{id}/steps/{step_id}/create-vote-session", response_model=VoteSessionOut, status_code=status.HTTP_201_CREATED, ) async def create_vote_session_for_step_endpoint( id: uuid.UUID, step_id: uuid.UUID, db: AsyncSession = Depends(get_db), identity: DuniterIdentity = Depends(get_current_identity), ) -> VoteSessionOut: """Create a vote session linked to a mandate step.""" try: session = await create_vote_session_for_step(id, step_id, db) except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) return VoteSessionOut.model_validate(session)