groupe-meditation/backend/app/routers/jetons.py

233 lines
8.4 KiB
Python

"""Routes S2 — Remise de jeton."""
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.ownership import ensure_owned_or_404
from app.core.security import get_current_membre, is_sysadmin
from app.core.roles import membre_a_module
from app.models.jeton import Jeton
from app.models.vente_jeton import VenteJeton
from app.models.membre import Membre
from app.models.affectation import Affectation
from app.models.poste import Poste
from app.models.transaction_comptable import TransactionComptable
from app.models.reunion import Reunion
from app.schemas.phase5 import JetonCreate, JetonOut, TYPES_JETONS, VenteJetonCreate
router = APIRouter(prefix="/jetons", tags=["S2 — Remise de jeton"])
async def require_resp_jetons(membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db)):
"""Vérifie que le membre est responsable des jetons."""
if is_sysadmin(membre) or await membre_a_module(membre.id, "S2", db):
return membre
result = await db.execute(
select(Affectation).join(Poste).where(
Affectation.membre_id == membre.id,
Affectation.groupe_id == membre.groupe_id,
Affectation.statut == "actif",
Poste.groupe_id == membre.groupe_id,
(Poste.nom.ilike("%jeton%") | (Poste.categorie == "executif")),
)
)
if not result.scalars().first():
raise HTTPException(status_code=403, detail="Réservé au responsable des jetons")
return membre
@router.get("/types")
async def types_jetons():
"""Liste des types de jetons disponibles."""
return TYPES_JETONS
@router.post("/", response_model=JetonOut)
async def remettre_jeton(
req: JetonCreate,
membre=Depends(require_resp_jetons),
db: AsyncSession = Depends(get_db),
):
"""S2 — Enregistrer la remise d'un jeton."""
# Validation
types_valides = [t["value"] for t in TYPES_JETONS]
if req.type_jeton not in types_valides:
raise HTTPException(status_code=400, detail=f"Type de jeton invalide. Valeurs: {types_valides}")
if req.type_jeton == "multiple" and (not req.annees or req.annees < 2):
raise HTTPException(status_code=400, detail="Le nombre d'années est requis (≥ 2) pour un jeton multiple")
# Vérifier que le membre existe si spécifié
membre_prenom = None
if req.membre_id:
cible = await db.get(Membre, req.membre_id)
if not cible or cible.groupe_id != membre.groupe_id or not cible.actif:
raise HTTPException(status_code=404, detail="Membre introuvable")
membre_prenom = cible.prenom
if req.reunion_id:
await ensure_owned_or_404(db, Reunion, req.reunion_id, membre.groupe_id, "Réunion introuvable")
jeton = Jeton(
groupe_id=membre.groupe_id,
reunion_id=req.reunion_id,
membre_id=req.membre_id,
type_jeton=req.type_jeton,
date_remise=req.date_remise,
annees=req.annees if req.type_jeton == "multiple" else None,
remis_par=membre.id,
)
db.add(jeton)
await db.flush()
return JetonOut(
id=jeton.id,
membre_prenom=membre_prenom,
type_jeton=jeton.type_jeton,
date_remise=jeton.date_remise,
annees=jeton.annees,
remis_par_prenom=membre.prenom,
cree_le=jeton.cree_le,
)
@router.post("/ventes")
async def enregistrer_vente_jeton(
req: VenteJetonCreate,
membre=Depends(require_resp_jetons),
db: AsyncSession = Depends(get_db),
):
"""Enregistrer une vente de jetons. Les remises ne touchent pas l'inventaire."""
types_valides = [t["value"] for t in TYPES_JETONS]
if req.type_jeton not in types_valides:
raise HTTPException(status_code=400, detail=f"Type de jeton invalide. Valeurs: {types_valides}")
if req.quantite <= 0:
raise HTTPException(status_code=400, detail="La quantité doit être positive")
if req.montant < 0:
raise HTTPException(status_code=400, detail="Le montant ne peut pas être négatif")
vendeur_id = req.vendu_par_id or membre.id
vendeur = await db.get(Membre, vendeur_id)
if not vendeur or vendeur.groupe_id != membre.groupe_id or not vendeur.actif:
raise HTTPException(status_code=404, detail="Vendeur introuvable")
if req.reunion_id:
await ensure_owned_or_404(db, Reunion, req.reunion_id, membre.groupe_id, "Réunion introuvable")
vente = VenteJeton(
groupe_id=membre.groupe_id,
reunion_id=req.reunion_id,
type_jeton=req.type_jeton,
quantite=req.quantite,
montant=req.montant,
date_vente=req.date_vente,
vendu_par=vendeur_id,
)
db.add(vente)
await db.flush()
return {
"id": str(vente.id),
"type_jeton": vente.type_jeton,
"quantite": vente.quantite,
"montant": float(vente.montant),
"date_vente": str(vente.date_vente),
"vendu_par_prenom": vendeur.prenom,
"remis_tresorier": vente.remis_tresorier,
}
@router.get("/ventes/a-recevoir")
async def ventes_jetons_a_recevoir(
membre=Depends(get_current_membre),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(VenteJeton)
.where(VenteJeton.groupe_id == membre.groupe_id, VenteJeton.remis_tresorier == False)
.order_by(desc(VenteJeton.date_vente))
)
out = []
for vente in result.scalars().all():
vendeur = await db.get(Membre, vente.vendu_par) if vente.vendu_par else None
out.append({
"id": str(vente.id),
"type_jeton": vente.type_jeton,
"quantite": vente.quantite,
"montant": float(vente.montant),
"date_vente": str(vente.date_vente),
"vendu_par_prenom": vendeur.prenom if vendeur else None,
})
return out
@router.post("/ventes/{vente_id}/recevoir")
async def confirmer_reception_vente_jeton(
vente_id: str,
membre=Depends(get_current_membre),
db: AsyncSession = Depends(get_db),
):
if not is_sysadmin(membre) and not await membre_a_module(membre.id, "S1T", db):
result = await db.execute(
select(Affectation).join(Poste).where(
Affectation.membre_id == membre.id,
Affectation.groupe_id == membre.groupe_id,
Affectation.statut == "actif",
Poste.groupe_id == membre.groupe_id,
(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")
vente = await db.get(VenteJeton, vente_id)
if not vente or vente.groupe_id != membre.groupe_id:
raise HTTPException(status_code=404, detail="Vente introuvable")
if vente.remis_tresorier:
raise HTTPException(status_code=400, detail="Déjà reçue")
from datetime import date as date_type
vente.remis_tresorier = True
vente.recu_par = membre.id
vente.date_remise = date_type.today()
db.add(TransactionComptable(
groupe_id=vente.groupe_id,
date_transaction=vente.date_vente,
type="vente_jeton",
compte="encaisse",
montant=vente.montant,
sens="debit",
source_table="ventes_jetons",
source_id=vente.id,
note="Vente de jetons reçue",
cree_par=membre.id,
))
await db.flush()
return {"message": "Réception confirmée"}
@router.get("/", response_model=list[JetonOut])
async def liste_jetons(
limite: int = 30,
membre=Depends(get_current_membre),
db: AsyncSession = Depends(get_db),
):
"""Lister les jetons remis récemment (tout le groupe)."""
result = await db.execute(
select(Jeton)
.where(Jeton.groupe_id == membre.groupe_id)
.order_by(desc(Jeton.date_remise))
.limit(limite)
)
jetons = result.scalars().all()
out = []
for j in jetons:
m = await db.get(Membre, j.membre_id) if j.membre_id else None
r = await db.get(Membre, j.remis_par) if j.remis_par else None
out.append(JetonOut(
id=j.id,
membre_prenom=m.prenom if m else None,
type_jeton=j.type_jeton,
date_remise=j.date_remise,
annees=j.annees,
remis_par_prenom=r.prenom if r else None,
cree_le=j.cree_le,
))
return out