"""Decisions router: CRUD for decision processes 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.decision import Decision, DecisionStep from app.models.user import DuniterIdentity from app.schemas.decision import ( DecisionAdvanceOut, DecisionCreate, DecisionOut, DecisionStepCreate, DecisionStepOut, DecisionUpdate, ) from app.schemas.vote import VoteSessionOut from app.services.auth_service import get_current_identity from app.services.decision_service import advance_decision, create_vote_session_for_step router = APIRouter() # ── Helpers ───────────────────────────────────────────────────────────────── async def _get_decision(db: AsyncSession, decision_id: uuid.UUID) -> Decision: """Fetch a decision by ID with its steps eagerly loaded, or raise 404.""" 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 HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Decision introuvable") return decision # ── Decision routes ───────────────────────────────────────────────────────── @router.get("/", response_model=list[DecisionOut]) async def list_decisions( db: AsyncSession = Depends(get_db), decision_type: str | None = Query(default=None, description="Filtrer par type de decision"), 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[DecisionOut]: """List all decisions with optional filters.""" stmt = select(Decision).options(selectinload(Decision.steps)) if decision_type is not None: stmt = stmt.where(Decision.decision_type == decision_type) if status_filter is not None: stmt = stmt.where(Decision.status == status_filter) stmt = stmt.order_by(Decision.created_at.desc()).offset(skip).limit(limit) result = await db.execute(stmt) decisions = result.scalars().unique().all() return [DecisionOut.model_validate(d) for d in decisions] @router.post("/", response_model=DecisionOut, status_code=status.HTTP_201_CREATED) async def create_decision( payload: DecisionCreate, db: AsyncSession = Depends(get_db), identity: DuniterIdentity = Depends(get_current_identity), ) -> DecisionOut: """Create a new decision process.""" decision = Decision( **payload.model_dump(), created_by_id=identity.id, ) db.add(decision) await db.commit() await db.refresh(decision) # Reload with steps (empty at creation) decision = await _get_decision(db, decision.id) return DecisionOut.model_validate(decision) @router.get("/{id}", response_model=DecisionOut) async def get_decision( id: uuid.UUID, db: AsyncSession = Depends(get_db), ) -> DecisionOut: """Get a single decision with all its steps.""" decision = await _get_decision(db, id) return DecisionOut.model_validate(decision) @router.put("/{id}", response_model=DecisionOut) async def update_decision( id: uuid.UUID, payload: DecisionUpdate, db: AsyncSession = Depends(get_db), identity: DuniterIdentity = Depends(get_current_identity), ) -> DecisionOut: """Update a decision's metadata (title, description, status, protocol).""" decision = await _get_decision(db, id) update_data = payload.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(decision, field, value) await db.commit() await db.refresh(decision) # Reload with steps decision = await _get_decision(db, decision.id) return DecisionOut.model_validate(decision) # ── Decision Step routes ──────────────────────────────────────────────────── @router.post("/{id}/steps", response_model=DecisionStepOut, status_code=status.HTTP_201_CREATED) async def add_step( id: uuid.UUID, payload: DecisionStepCreate, db: AsyncSession = Depends(get_db), identity: DuniterIdentity = Depends(get_current_identity), ) -> DecisionStepOut: """Add a step to a decision process.""" # Verify decision exists decision = await _get_decision(db, id) step = DecisionStep( decision_id=decision.id, **payload.model_dump(), ) db.add(step) await db.commit() await db.refresh(step) return DecisionStepOut.model_validate(step) # ── Workflow routes ──────────────────────────────────────────────────────── @router.post("/{id}/advance", response_model=DecisionAdvanceOut) async def advance_decision_endpoint( id: uuid.UUID, db: AsyncSession = Depends(get_db), identity: DuniterIdentity = Depends(get_current_identity), ) -> DecisionAdvanceOut: """Advance a decision to its next step or status.""" try: decision = await advance_decision(id, db) except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) # Reload with steps for complete output decision = await _get_decision(db, decision.id) data = DecisionOut.model_validate(decision).model_dump() data["message"] = f"Decision avancee au statut : {decision.status}" return DecisionAdvanceOut(**data) @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 decision 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) @router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, response_model=None) async def delete_decision( id: uuid.UUID, db: AsyncSession = Depends(get_db), identity: DuniterIdentity = Depends(get_current_identity), ) -> None: """Delete a decision (only if in draft status).""" decision = await _get_decision(db, id) if decision.status != "draft": raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Seules les decisions en brouillon peuvent etre supprimees", ) await db.delete(decision) await db.commit()