"""Mandates router: CRUD for mandates and their steps.""" from __future__ import annotations import uuid from fastapi import APIRouter, Depends, HTTPException, Query, Response, 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.dependencies.org import get_active_org_id 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, org_id: uuid.UUID | None = None ) -> Mandate: stmt = ( select(Mandate) .options( selectinload(Mandate.steps), selectinload(Mandate.origin_identity), selectinload(Mandate.mandatee), ) .where(Mandate.id == mandate_id) ) if org_id is not None: stmt = stmt.where(Mandate.organization_id == org_id) else: stmt = stmt.where(Mandate.organization_id.is_(None)) result = await db.execute(stmt) mandate = result.scalar_one_or_none() if mandate is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Mandat introuvable") return mandate def _mandate_out(mandate: Mandate) -> MandateOut: out = MandateOut.model_validate(mandate) out.origin_display_name = mandate.origin_display_name out.mandatee_display_name = mandate.mandatee_display_name return out # ── Mandate routes ────────────────────────────────────────────────────────── @router.get("/", response_model=list[MandateOut]) async def list_mandates( db: AsyncSession = Depends(get_db), org_id: uuid.UUID | None = Depends(get_active_org_id), mandate_type: str | None = Query(default=None), status_filter: str | None = Query(default=None, alias="status"), skip: int = Query(default=0, ge=0), limit: int = Query(default=50, ge=1, le=200), ) -> list[MandateOut]: stmt = select(Mandate).options( selectinload(Mandate.steps), selectinload(Mandate.origin_identity), selectinload(Mandate.mandatee), ) if org_id is not None: stmt = stmt.where(Mandate.organization_id == org_id) else: stmt = stmt.where(Mandate.organization_id.is_(None)) 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 [_mandate_out(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), org_id: uuid.UUID | None = Depends(get_active_org_id), ) -> MandateOut: data = payload.model_dump() nomination_mode = data.pop("nomination_mode", "postpone") mandate = Mandate(**data, organization_id=org_id) if nomination_mode == "auto": mandate.mandatee_id = identity.id db.add(mandate) await db.commit() await db.refresh(mandate) mandate = await _get_mandate(db, mandate.id, org_id) return _mandate_out(mandate) @router.get("/{id}", response_model=MandateOut) async def get_mandate( id: uuid.UUID, db: AsyncSession = Depends(get_db), org_id: uuid.UUID | None = Depends(get_active_org_id), ) -> MandateOut: mandate = await _get_mandate(db, id, org_id) return _mandate_out(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), org_id: uuid.UUID | None = Depends(get_active_org_id), ) -> MandateOut: mandate = await _get_mandate(db, id, org_id) for field, value in payload.model_dump(exclude_unset=True).items(): setattr(mandate, field, value) await db.commit() mandate = await _get_mandate(db, mandate.id, org_id) return _mandate_out(mandate) @router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, response_model=None) async def delete_mandate( id: uuid.UUID, db: AsyncSession = Depends(get_db), identity: DuniterIdentity = Depends(get_current_identity), org_id: uuid.UUID | None = Depends(get_active_org_id), ) -> None: mandate = await _get_mandate(db, id, org_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), org_id: uuid.UUID | None = Depends(get_active_org_id), ) -> MandateStepOut: mandate = await _get_mandate(db, id, org_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), org_id: uuid.UUID | None = Depends(get_active_org_id), ) -> list[MandateStepOut]: mandate = await _get_mandate(db, id, org_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), org_id: uuid.UUID | None = Depends(get_active_org_id), ) -> MandateAdvanceOut: try: mandate = await advance_mandate(id, db) except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) mandate = await _get_mandate(db, mandate.id, org_id) out = _mandate_out(mandate) return MandateAdvanceOut( **out.model_dump(), message=f"Mandat avance au statut : {mandate.status}", ) @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), org_id: uuid.UUID | None = Depends(get_active_org_id), ) -> MandateOut: 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)) mandate = await _get_mandate(db, mandate.id, org_id) return _mandate_out(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), org_id: uuid.UUID | None = Depends(get_active_org_id), ) -> MandateOut: try: mandate = await revoke_mandate(id, db) except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) mandate = await _get_mandate(db, mandate.id, org_id) return _mandate_out(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), org_id: uuid.UUID | None = Depends(get_active_org_id), ) -> VoteSessionOut: 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)