"""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 from app.core.database import get_db from app.core.decisions import prochain_numero_decision from app.core.roles import require_tresorier from app.core.security import is_sysadmin 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.services.finance import solde_banque, total_reserves, transaction 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 _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") date_decision = req.date_envoi if is_sysadmin(membre) else date.today() numero_resolution = await prochain_numero_decision(membre.groupe_id, "contribution", date_decision.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_decision, date_adoption=date_decision, groupe_a_decide_de=f"Envoyer {req.montant:.2f} $ à {DESTINATIONS_LABELS[req.destination]}.", ) 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") solde_banque_actuel = await solde_banque(membre.groupe_id, db) if float(envoi.montant) > solde_banque_actuel: raise HTTPException(status_code=400, detail="Le solde en banque est insuffisant pour confirmer ce débit") total_reserves_actuel = await total_reserves(membre.groupe_id, db) if solde_banque_actuel - float(envoi.montant) < total_reserves_actuel: 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 db.add(transaction(membre.groupe_id, membre.id, date.today(), "contribution", "banque", envoi.montant, "credit", "envois_contributions", envoi.id, DESTINATIONS_LABELS.get(envoi.destination, envoi.destination))) 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