"""Routes M4 — Envoi de contributions aux organismes AA.""" from datetime import date from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, desc, func from app.core.database import get_db from app.core.security import get_current_membre from app.models.envoi_contribution import EnvoiContribution from app.models.config_repartition import ConfigRepartition from app.models.membre import Membre from app.models.affectation import Affectation from app.models.poste import Poste from app.schemas.phase7 import ( EnvoiContributionCreate, EnvoiContributionOut, CalculRepartitionIn, CalculRepartitionOut, DESTINATIONS, DESTINATIONS_LABELS, METHODES_CONTRIBUTION, ) router = APIRouter(prefix="/contributions", tags=["M4 — Envoi contributions"]) # Répartition par défaut selon le Guide du groupe DEFAUT_REPARTITION = { "district": 30.0, "region": 30.0, "bsg": 30.0, "intergroupe": 10.0, } async def _prochain_numero_resolution(groupe_id, annee: int, db: AsyncSession) -> str: from app.models.proposition import Proposition result = await db.execute( select(func.count(Proposition.id)).where( Proposition.groupe_id == groupe_id, Proposition.numero_resolution.isnot(None), Proposition.numero_resolution.like(f"RES-{annee}-%"), ) ) count = result.scalar() or 0 return f"RES-{annee}-{count + 1:03d}" async def require_tresorier(membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db)): result = await db.execute( select(Affectation).join(Poste).where( Affectation.membre_id == membre.id, Affectation.statut == "actif", Poste.nom.ilike("%trésorier%"), ) ) if not result.scalars().first(): raise HTTPException(status_code=403, detail="Réservé au trésorier") return membre async def _get_repartition(groupe_id, db: AsyncSession) -> dict[str, float]: """Récupère la répartition active ou utilise les défauts.""" result = await db.execute( select(ConfigRepartition) .where(ConfigRepartition.groupe_id == groupe_id) .order_by(desc(ConfigRepartition.actif_depuis)) ) configs = result.scalars().all() # Garder la plus récente par destination repartition = {} for c in configs: if c.destination not in repartition: repartition[c.destination] = float(c.pourcentage) # Compléter avec les défauts for dest in DESTINATIONS: if dest not in repartition: repartition[dest] = DEFAUT_REPARTITION[dest] return repartition @router.post("/calculer", response_model=list[CalculRepartitionOut]) async def calculer_repartition( req: CalculRepartitionIn, membre=Depends(require_tresorier), db: AsyncSession = Depends(get_db), ): """Calculer la répartition d'un montant selon les % configurés.""" if req.montant_total <= 0: raise HTTPException(status_code=400, detail="Le montant doit être positif") repartition = await _get_repartition(membre.groupe_id, db) return [ CalculRepartitionOut( destination=dest, destination_label=DESTINATIONS_LABELS[dest], pourcentage=pct, montant=round(req.montant_total * pct / 100, 2), ) for dest, pct in repartition.items() ] @router.post("/", response_model=EnvoiContributionOut) async def enregistrer_envoi( req: EnvoiContributionCreate, membre=Depends(require_tresorier), db: AsyncSession = Depends(get_db), ): """M4 — Enregistrer un envoi de contribution et créer sa résolution.""" from app.models.proposition import Proposition if req.destination not in DESTINATIONS: raise HTTPException(status_code=400, detail=f"Destination invalide. Choix: {DESTINATIONS}") if req.methode.upper() not in METHODES_CONTRIBUTION: raise HTTPException(status_code=400, detail=f"Méthode invalide. Choix: {METHODES_CONTRIBUTION}") if req.proposeur_id == req.secondeur_id: raise HTTPException(status_code=400, detail="Le proposeur et le secondeur doivent être différents") proposeur = await db.get(Membre, req.proposeur_id) secondeur = await db.get(Membre, req.secondeur_id) if not proposeur or proposeur.groupe_id != membre.groupe_id or not proposeur.actif: raise HTTPException(status_code=404, detail="Proposeur introuvable ou inactif") if not secondeur or secondeur.groupe_id != membre.groupe_id or not secondeur.actif: raise HTTPException(status_code=404, detail="Secondeur introuvable ou inactif") numero_resolution = await _prochain_numero_resolution(membre.groupe_id, date.today().year, db) resolution = Proposition( groupe_id=membre.groupe_id, type_proposition="contribution", sujet=f"Envoi de {req.montant:.2f} $ à {DESTINATIONS_LABELS[req.destination]}", description=( f"Contribution enregistrée le {req.date_envoi} par le module Envoi contributions. " f"Méthode prévue : {req.methode.upper()}." ), proposeur_id=req.proposeur_id, secondeur_id=req.secondeur_id, statut="adoptee", numero_resolution=numero_resolution, date_vote=date.today(), ) db.add(resolution) await db.flush() envoi = EnvoiContribution( groupe_id=membre.groupe_id, date_envoi=req.date_envoi, destination=req.destination, montant=req.montant, methode=req.methode.upper(), resolution_id=resolution.id, engagee=True, debitee=False, saisi_par=membre.id, ) db.add(envoi) await db.flush() return EnvoiContributionOut( id=envoi.id, date_envoi=envoi.date_envoi, destination=envoi.destination, destination_label=DESTINATIONS_LABELS[envoi.destination], montant=float(envoi.montant), methode=envoi.methode, resolution_id=envoi.resolution_id, engagee=envoi.engagee, debitee=envoi.debitee, saisi_par_prenom=membre.prenom, cree_le=envoi.cree_le, ) @router.post("/{envoi_id}/confirmer-debit", response_model=EnvoiContributionOut) async def confirmer_debit_contribution( envoi_id: str, membre=Depends(require_tresorier), db: AsyncSession = Depends(get_db), ): """Le trésorier confirme que la contribution a été débitée du compte.""" result = await db.execute( select(EnvoiContribution).where( EnvoiContribution.id == envoi_id, EnvoiContribution.groupe_id == membre.groupe_id, ) ) envoi = result.scalar_one_or_none() if not envoi: raise HTTPException(status_code=404, detail="Envoi introuvable") if envoi.debitee: raise HTTPException(status_code=400, detail="Contribution déjà débitée") from app.routers.operations_bancaires import _solde_banque_brut, _total_reserves solde_banque = await _solde_banque_brut(membre.groupe_id, db) if float(envoi.montant) > solde_banque: raise HTTPException(status_code=400, detail="Le solde en banque est insuffisant pour confirmer ce débit") total_reserves = await _total_reserves(membre.groupe_id, db) if solde_banque - float(envoi.montant) < total_reserves: raise HTTPException(status_code=400, detail="Ce débit ferait dépasser les réserves par rapport au solde en banque") envoi.debitee = True envoi.engagee = True await db.flush() saisi = await db.get(Membre, envoi.saisi_par) if envoi.saisi_par else None return EnvoiContributionOut( id=envoi.id, date_envoi=envoi.date_envoi, destination=envoi.destination, destination_label=DESTINATIONS_LABELS.get(envoi.destination, envoi.destination), montant=float(envoi.montant), methode=envoi.methode, resolution_id=envoi.resolution_id, engagee=envoi.engagee, debitee=envoi.debitee, saisi_par_prenom=saisi.prenom if saisi else None, cree_le=envoi.cree_le, ) @router.get("/", response_model=list[EnvoiContributionOut]) async def liste_envois( limite: int = 30, membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db), ): """Lister les envois de contributions récents.""" result = await db.execute( select(EnvoiContribution) .where(EnvoiContribution.groupe_id == membre.groupe_id) .order_by(desc(EnvoiContribution.date_envoi)) .limit(limite) ) envois = result.scalars().all() out = [] for e in envois: saisi = await db.get(Membre, e.saisi_par) if e.saisi_par else None out.append(EnvoiContributionOut( id=e.id, date_envoi=e.date_envoi, destination=e.destination, destination_label=DESTINATIONS_LABELS.get(e.destination, e.destination), montant=float(e.montant), methode=e.methode, resolution_id=e.resolution_id, engagee=e.engagee, debitee=e.debitee, saisi_par_prenom=saisi.prenom if saisi else None, cree_le=e.cree_le, )) return out