"""Routes — Opérations bancaires (dépôts, retraits).""" 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.operation_bancaire import OperationBancaire from app.models.collecte import Collecte from app.models.depense import Depense from app.models.reserve import Reserve from app.models.vente_litterature import VenteLitterature from app.models.membre import Membre from app.models.affectation import Affectation from app.models.poste import Poste from app.schemas.phase1 import OperationBancaireCreate, OperationBancaireOut, ReserveCreate, ReserveOut, ReserveUpdate router = APIRouter(prefix="/banque", tags=["Opérations bancaires"]) async def _require_tresorier(membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db)): """Vérifie que le membre est trésorier ou exécutif.""" result = await db.execute( select(Affectation).join(Poste).where( Affectation.membre_id == membre.id, Affectation.statut == "actif", (Poste.nom.ilike("%trésorier%") | (Poste.categorie == "executif")), ) ) if not result.scalars().first(): raise HTTPException(status_code=403, detail="Réservé au trésorier ou à l'exécutif") return membre async def _encaisse(groupe_id, db: AsyncSession) -> float: total_collectes = float((await db.execute( select(func.coalesce(func.sum(Collecte.montant), 0)).where( Collecte.groupe_id == groupe_id, Collecte.statut == "recu") )).scalar()) ventes_recues = float((await db.execute( select(func.coalesce(func.sum(VenteLitterature.montant), 0)).where( VenteLitterature.groupe_id == groupe_id, VenteLitterature.remis_tresorier == True) )).scalar()) retraits = float((await db.execute( select(func.coalesce(func.sum(OperationBancaire.montant), 0)).where( OperationBancaire.groupe_id == groupe_id, OperationBancaire.type == "retrait") )).scalar()) depots = float((await db.execute( select(func.coalesce(func.sum(OperationBancaire.montant), 0)).where( OperationBancaire.groupe_id == groupe_id, OperationBancaire.type == "depot") )).scalar()) depenses_cash = float((await db.execute( select(func.coalesce(func.sum(Depense.montant), 0)).where( Depense.groupe_id == groupe_id, Depense.paye == True, Depense.methode == "COLLECTE") )).scalar()) return total_collectes + ventes_recues + retraits - depots - depenses_cash async def _solde_banque_brut(groupe_id, db: AsyncSession) -> float: from app.models.envoi_contribution import EnvoiContribution result = await db.execute( select(OperationBancaire).where( OperationBancaire.groupe_id == groupe_id, OperationBancaire.type == "releve", ).order_by(desc(OperationBancaire.date_operation)).limit(1) ) dernier_releve = result.scalar_one_or_none() solde_releve = float(dernier_releve.montant) if dernier_releve else 0.0 filtre_depuis = dernier_releve.date_operation if dernier_releve else None q_depots = select(func.coalesce(func.sum(OperationBancaire.montant), 0)).where( OperationBancaire.groupe_id == groupe_id, OperationBancaire.type == "depot") q_retraits = select(func.coalesce(func.sum(OperationBancaire.montant), 0)).where( OperationBancaire.groupe_id == groupe_id, OperationBancaire.type == "retrait") q_contribs = select(func.coalesce(func.sum(EnvoiContribution.montant), 0)).where( EnvoiContribution.groupe_id == groupe_id, EnvoiContribution.debitee == True) q_depenses = select(func.coalesce(func.sum(Depense.montant), 0)).where( Depense.groupe_id == groupe_id, Depense.paye == True, Depense.methode.in_(["VIREMENT", "CHEQUE"]), ) if filtre_depuis: q_depots = q_depots.where(OperationBancaire.date_operation > filtre_depuis) q_retraits = q_retraits.where(OperationBancaire.date_operation > filtre_depuis) q_contribs = q_contribs.where(EnvoiContribution.date_envoi > filtre_depuis) q_depenses = q_depenses.where(Depense.date_confirmation > filtre_depuis) depots = float((await db.execute(q_depots)).scalar()) retraits = float((await db.execute(q_retraits)).scalar()) contribs = float((await db.execute(q_contribs)).scalar()) depenses = float((await db.execute(q_depenses)).scalar()) return solde_releve + depots - retraits - contribs - depenses async def _total_reserves(groupe_id, db: AsyncSession, exclure_id: str | None = None) -> float: query = select(func.coalesce(func.sum(Reserve.montant), 0)).where( Reserve.groupe_id == groupe_id, Reserve.active == True, ) if exclure_id: query = query.where(Reserve.id != exclure_id) return float((await db.execute(query)).scalar()) @router.post("/", response_model=OperationBancaireOut) async def enregistrer_operation( req: OperationBancaireCreate, membre=Depends(_require_tresorier), db: AsyncSession = Depends(get_db), ): """Enregistrer un dépôt ou retrait bancaire (trésorier).""" if req.type not in ("depot", "retrait", "releve"): raise HTTPException(status_code=400, detail="Type doit être 'depot', 'retrait' ou 'releve'") if req.montant <= 0: raise HTTPException(status_code=400, detail="Le montant doit être positif") solde_banque = await _solde_banque_brut(membre.groupe_id, db) total_reserves = await _total_reserves(membre.groupe_id, db) if req.type == "depot" and req.montant > await _encaisse(membre.groupe_id, db): raise HTTPException(status_code=400, detail="Le dépôt ne peut pas dépasser l'encaisse disponible") if req.type == "retrait" and req.montant > solde_banque: raise HTTPException(status_code=400, detail="Le retrait ne peut pas dépasser le solde en banque") if req.type == "retrait" and solde_banque - req.montant < total_reserves: raise HTTPException(status_code=400, detail="Le retrait ferait dépasser les réserves par rapport au solde en banque") if req.type == "releve" and req.montant < total_reserves: raise HTTPException(status_code=400, detail="Le relevé ne peut pas être inférieur au total des réserves") op = OperationBancaire( groupe_id=membre.groupe_id, type=req.type, montant=req.montant, date_operation=req.date_operation, effectue_par=membre.id, note=req.note, ) db.add(op) await db.flush() return OperationBancaireOut( id=op.id, type=op.type, montant=float(op.montant), date_operation=op.date_operation, effectue_par_prenom=membre.prenom, note=op.note, cree_le=op.cree_le, ) @router.get("/reserves", response_model=list[ReserveOut]) async def liste_reserves(membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db)): result = await db.execute( select(Reserve).where(Reserve.groupe_id == membre.groupe_id, Reserve.active == True) .order_by(Reserve.nom) ) return [ ReserveOut(id=r.id, nom=r.nom, montant=float(r.montant), note=r.note, active=r.active, cree_le=r.cree_le) for r in result.scalars().all() ] @router.post("/reserves", response_model=ReserveOut) async def creer_reserve(req: ReserveCreate, membre=Depends(_require_tresorier), db: AsyncSession = Depends(get_db)): if not req.nom.strip(): raise HTTPException(status_code=400, detail="Nom requis") if req.montant < 0: raise HTTPException(status_code=400, detail="Le montant doit être positif ou nul") total = await _total_reserves(membre.groupe_id, db) + req.montant solde_banque = await _solde_banque_brut(membre.groupe_id, db) if total > solde_banque: raise HTTPException(status_code=400, detail="Le total des réserves ne peut pas dépasser le solde en banque") reserve = Reserve(groupe_id=membre.groupe_id, nom=req.nom.strip(), montant=req.montant, note=req.note) db.add(reserve) await db.flush() return ReserveOut(id=reserve.id, nom=reserve.nom, montant=float(reserve.montant), note=reserve.note, active=reserve.active, cree_le=reserve.cree_le) @router.patch("/reserves/{reserve_id}", response_model=ReserveOut) async def modifier_reserve(reserve_id: str, req: ReserveUpdate, membre=Depends(_require_tresorier), db: AsyncSession = Depends(get_db)): reserve = await db.get(Reserve, reserve_id) if not reserve or reserve.groupe_id != membre.groupe_id or not reserve.active: raise HTTPException(status_code=404, detail="Réserve introuvable") montant = float(reserve.montant) if req.montant is None else req.montant if montant < 0: raise HTTPException(status_code=400, detail="Le montant doit être positif ou nul") total = await _total_reserves(membre.groupe_id, db, reserve_id) + montant solde_banque = await _solde_banque_brut(membre.groupe_id, db) if total > solde_banque: raise HTTPException(status_code=400, detail="Le total des réserves ne peut pas dépasser le solde en banque") if req.nom is not None: if not req.nom.strip(): raise HTTPException(status_code=400, detail="Nom requis") reserve.nom = req.nom.strip() reserve.montant = montant if req.note is not None: reserve.note = req.note.strip() or None await db.flush() return ReserveOut(id=reserve.id, nom=reserve.nom, montant=float(reserve.montant), note=reserve.note, active=reserve.active, cree_le=reserve.cree_le) @router.delete("/reserves/{reserve_id}") async def supprimer_reserve(reserve_id: str, membre=Depends(_require_tresorier), db: AsyncSession = Depends(get_db)): reserve = await db.get(Reserve, reserve_id) if not reserve or reserve.groupe_id != membre.groupe_id or not reserve.active: raise HTTPException(status_code=404, detail="Réserve introuvable") reserve.active = False await db.flush() return {"message": "Réserve supprimée"} @router.get("/", response_model=list[OperationBancaireOut]) async def liste_operations( limite: int = 20, membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db), ): """Lister les opérations bancaires du groupe.""" result = await db.execute( select(OperationBancaire) .where(OperationBancaire.groupe_id == membre.groupe_id) .order_by(desc(OperationBancaire.date_operation)) .limit(limite) ) ops = result.scalars().all() out = [] for op in ops: m = await db.get(Membre, op.effectue_par) out.append(OperationBancaireOut( id=op.id, type=op.type, montant=float(op.montant), date_operation=op.date_operation, effectue_par_prenom=m.prenom if m else "?", note=op.note, cree_le=op.cree_le, )) return out