feat(api): add OpenAPI tags metadata and update endpoint tags

This commit is contained in:
Fanilo-Nantenaina 2025-12-08 10:58:24 +03:00
parent 5a6a721f16
commit fafd5222a6

156
api.py
View file

@ -42,6 +42,73 @@ from email_queue import email_queue
from sage_client import sage_client from sage_client import sage_client
TAGS_METADATA = [
{
"name": "Clients",
"description": "Gestion des clients (recherche, création, modification)"
},
{
"name": "Articles",
"description": "Gestion des articles et produits"
},
{
"name": "Devis",
"description": "Création, consultation et gestion des devis"
},
{
"name": "Commandes",
"description": "Création, consultation et gestion des commandes"
},
{
"name": "Livraisons",
"description": "Création, consultation et gestion des bons de livraison"
},
{
"name": "Factures",
"description": "Création, consultation et gestion des factures"
},
{
"name": "Avoirs",
"description": "Création, consultation et gestion des avoirs"
},
{
"name": "Fournisseurs",
"description": "Gestion des fournisseurs"
},
{
"name": "Prospects",
"description": "Gestion des prospects"
},
{
"name": "Workflows",
"description": "Transformations de documents (devis→commande, commande→facture, etc.)"
},
{
"name": "Signatures",
"description": "Signature électronique via Universign"
},
{
"name": "Emails",
"description": "Envoi d'emails, templates et logs d'envoi"
},
{
"name": "Validation",
"description": "Validation de données (remises, etc.)"
},
{
"name": "Admin",
"description": "🔧 Administration système (cache, queue)"
},
{
"name": "System",
"description": "🏥 Health checks et informations système"
},
{
"name": "Debug",
"description": "🐛 Routes de debug et diagnostics"
}
]
# ===================================================== # =====================================================
# ENUMS # ENUMS
# ===================================================== # =====================================================
@ -674,6 +741,7 @@ app = FastAPI(
version="2.0.0", version="2.0.0",
description="API de gestion commerciale - VPS Linux", description="API de gestion commerciale - VPS Linux",
lifespan=lifespan, lifespan=lifespan,
openapi_tags=TAGS_METADATA
) )
app.add_middleware( app.add_middleware(
@ -691,7 +759,7 @@ app.include_router(auth_router)
# ===================================================== # =====================================================
# ENDPOINTS - US-A1 (CRÉATION RAPIDE DEVIS) # ENDPOINTS - US-A1 (CRÉATION RAPIDE DEVIS)
# ===================================================== # =====================================================
@app.get("/clients", response_model=List[ClientResponse], tags=["US-A1"]) @app.get("/clients", response_model=List[ClientResponse], tags=["Clients"])
async def rechercher_clients(query: Optional[str] = Query(None)): async def rechercher_clients(query: Optional[str] = Query(None)):
"""🔍 Recherche clients via gateway Windows""" """🔍 Recherche clients via gateway Windows"""
try: try:
@ -701,7 +769,7 @@ async def rechercher_clients(query: Optional[str] = Query(None)):
logger.error(f"Erreur recherche clients: {e}") logger.error(f"Erreur recherche clients: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.get("/clients/{code}", tags=["US-A1"]) @app.get("/clients/{code}", tags=["Clients"])
async def lire_client_detail(code: str): async def lire_client_detail(code: str):
""" """
📄 Lecture détaillée d'un client par son code 📄 Lecture détaillée d'un client par son code
@ -730,7 +798,7 @@ async def lire_client_detail(code: str):
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.put("/clients/{code}", tags=["US-A1"]) @app.put("/clients/{code}", tags=["Clients"])
async def modifier_client( async def modifier_client(
code: str, code: str,
client_update: ClientUpdateRequest, client_update: ClientUpdateRequest,
@ -774,7 +842,7 @@ async def modifier_client(
logger.error(f"Erreur technique modification client {code}: {e}") logger.error(f"Erreur technique modification client {code}: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.post("/clients", status_code=201, tags=["US-A8"]) @app.post("/clients", status_code=201, tags=["Clients"])
async def ajouter_client( async def ajouter_client(
client: ClientCreateAPIRequest, client: ClientCreateAPIRequest,
session: AsyncSession = Depends(get_session) session: AsyncSession = Depends(get_session)
@ -799,7 +867,7 @@ async def ajouter_client(
status = 400 if "existe déjà" in str(e) else 500 status = 400 if "existe déjà" in str(e) else 500
raise HTTPException(status, str(e)) raise HTTPException(status, str(e))
@app.get("/articles", response_model=List[ArticleResponse], tags=["US-A1"]) @app.get("/articles", response_model=List[ArticleResponse], tags=["Articles"])
async def rechercher_articles(query: Optional[str] = Query(None)): async def rechercher_articles(query: Optional[str] = Query(None)):
"""🔍 Recherche articles via gateway Windows""" """🔍 Recherche articles via gateway Windows"""
try: try:
@ -810,7 +878,7 @@ async def rechercher_articles(query: Optional[str] = Query(None)):
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.post("/devis", response_model=DevisResponse, status_code=201, tags=["US-A1"]) @app.post("/devis", response_model=DevisResponse, status_code=201, tags=["Devis"])
async def creer_devis(devis: DevisRequest): async def creer_devis(devis: DevisRequest):
"""📝 Création de devis via gateway Windows""" """📝 Création de devis via gateway Windows"""
try: try:
@ -848,7 +916,7 @@ async def creer_devis(devis: DevisRequest):
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.put("/devis/{id}", tags=["US-A1"]) @app.put("/devis/{id}", tags=["Devis"])
async def modifier_devis( async def modifier_devis(
id: str, id: str,
devis_update: DevisUpdateRequest, devis_update: DevisUpdateRequest,
@ -925,7 +993,7 @@ async def modifier_devis(
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.post("/commandes", status_code=201, tags=["US-A2"]) @app.post("/commandes", status_code=201, tags=["Commandes"])
async def creer_commande( async def creer_commande(
commande: CommandeCreateRequest, commande: CommandeCreateRequest,
session: AsyncSession = Depends(get_session) session: AsyncSession = Depends(get_session)
@ -1003,7 +1071,7 @@ async def creer_commande(
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.put("/commandes/{id}", tags=["US-A2"]) @app.put("/commandes/{id}", tags=["Commandes"])
async def modifier_commande( async def modifier_commande(
id: str, id: str,
commande_update: CommandeUpdateRequest, commande_update: CommandeUpdateRequest,
@ -1098,7 +1166,7 @@ async def modifier_commande(
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.get("/devis", tags=["US-A1"]) @app.get("/devis", tags=["Devis"])
async def lister_devis( async def lister_devis(
limit: int = Query(100, le=1000), limit: int = Query(100, le=1000),
statut: Optional[int] = Query(None), statut: Optional[int] = Query(None),
@ -1127,7 +1195,7 @@ async def lister_devis(
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.get("/devis/{id}", tags=["US-A1"]) @app.get("/devis/{id}", tags=["Devis"])
async def lire_devis(id: str): async def lire_devis(id: str):
""" """
📄 Lecture d'un devis via gateway Windows 📄 Lecture d'un devis via gateway Windows
@ -1167,7 +1235,7 @@ async def lire_devis(id: str):
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.get("/devis/{id}/pdf", tags=["US-A1"]) @app.get("/devis/{id}/pdf", tags=["Devis"])
async def telecharger_devis_pdf(id: str): async def telecharger_devis_pdf(id: str):
"""📄 Téléchargement PDF (généré via email_queue)""" """📄 Téléchargement PDF (généré via email_queue)"""
try: try:
@ -1185,7 +1253,7 @@ async def telecharger_devis_pdf(id: str):
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.post("/devis/{id}/envoyer", tags=["US-A1"]) @app.post("/devis/{id}/envoyer", tags=["Devis"])
async def envoyer_devis_email( async def envoyer_devis_email(
id: str, request: EmailEnvoiRequest, session: AsyncSession = Depends(get_session) id: str, request: EmailEnvoiRequest, session: AsyncSession = Depends(get_session)
): ):
@ -1239,7 +1307,7 @@ async def envoyer_devis_email(
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.put("/devis/{id}/statut", tags=["US-A1"]) @app.put("/devis/{id}/statut", tags=["Devis"])
async def changer_statut_devis( async def changer_statut_devis(
id: str, id: str,
nouveau_statut: int = Query(..., ge=0, le=6, description="0=Brouillon, 2=Accepté, 5=Transformé, 6=Annulé") nouveau_statut: int = Query(..., ge=0, le=6, description="0=Brouillon, 2=Accepté, 5=Transformé, 6=Annulé")
@ -1305,10 +1373,10 @@ async def changer_statut_devis(
# ===================================================== # =====================================================
# ENDPOINTS - US-A2 (WORKFLOW SANS RESSAISIE) # ENDPOINTS - Commandes (WORKFLOW SANS RESSAISIE)
# ===================================================== # =====================================================
@app.get("/commandes/{id}", tags=["US-A2"]) @app.get("/commandes/{id}", tags=["Commandes"])
async def lire_commande(id: str): async def lire_commande(id: str):
"""📄 Lecture d'une commande avec ses lignes""" """📄 Lecture d'une commande avec ses lignes"""
try: try:
@ -1323,7 +1391,7 @@ async def lire_commande(id: str):
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.get("/commandes", tags=["US-A2"]) @app.get("/commandes", tags=["Commandes"])
async def lister_commandes( async def lister_commandes(
limit: int = Query(100, le=1000), statut: Optional[int] = Query(None) limit: int = Query(100, le=1000), statut: Optional[int] = Query(None)
): ):
@ -1397,7 +1465,7 @@ async def devis_vers_commande(id: str, session: AsyncSession = Depends(get_sessi
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.post("/workflow/commande/{id}/to-facture", tags=["US-A2"]) @app.post("/workflow/commande/{id}/to-facture", tags=["Commandes"])
async def commande_vers_facture(id: str, session: AsyncSession = Depends(get_session)): async def commande_vers_facture(id: str, session: AsyncSession = Depends(get_session)):
""" """
🔧 Transformation Commande Facture 🔧 Transformation Commande Facture
@ -1443,7 +1511,7 @@ async def commande_vers_facture(id: str, session: AsyncSession = Depends(get_ses
# ===================================================== # =====================================================
# ENDPOINTS - US-A3 (SIGNATURE ÉLECTRONIQUE) # ENDPOINTS - US-A3 (SIGNATURE ÉLECTRONIQUE)
# ===================================================== # =====================================================
@app.post("/signature/universign/send", tags=["US-A3"]) @app.post("/signature/universign/send", tags=["Universign"])
async def envoyer_signature( async def envoyer_signature(
demande: SignatureRequest, session: AsyncSession = Depends(get_session) demande: SignatureRequest, session: AsyncSession = Depends(get_session)
): ):
@ -1496,7 +1564,7 @@ async def envoyer_signature(
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.get("/signature/universign/status", tags=["US-A3"]) @app.get("/signature/universign/status", tags=["Universign"])
async def statut_signature(docId: str = Query(...)): async def statut_signature(docId: str = Query(...)):
"""🔍 Récupération du statut de signature en temps réel""" """🔍 Récupération du statut de signature en temps réel"""
# Chercher dans la DB locale # Chercher dans la DB locale
@ -1524,7 +1592,7 @@ async def statut_signature(docId: str = Query(...)):
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.get("/signatures", tags=["US-A3"]) @app.get("/signatures", tags=["Universign"])
async def lister_signatures( async def lister_signatures(
statut: Optional[StatutSignature] = Query(None), statut: Optional[StatutSignature] = Query(None),
limit: int = Query(100, le=1000), limit: int = Query(100, le=1000),
@ -1562,7 +1630,7 @@ async def lister_signatures(
] ]
@app.get("/signatures/{transaction_id}/status", tags=["US-A3"]) @app.get("/signatures/{transaction_id}/status", tags=["Universign"])
async def statut_signature_detail( async def statut_signature_detail(
transaction_id: str, session: AsyncSession = Depends(get_session) transaction_id: str, session: AsyncSession = Depends(get_session)
): ):
@ -1616,7 +1684,7 @@ async def statut_signature_detail(
} }
@app.post("/signatures/refresh-all", tags=["US-A3"]) @app.post("/signatures/refresh-all", tags=["Universign"])
async def rafraichir_statuts_signatures(session: AsyncSession = Depends(get_session)): async def rafraichir_statuts_signatures(session: AsyncSession = Depends(get_session)):
"""🔄 Rafraîchit TOUS les statuts des signatures en attente""" """🔄 Rafraîchit TOUS les statuts des signatures en attente"""
query = select(SignatureLog).where( query = select(SignatureLog).where(
@ -1665,7 +1733,7 @@ async def rafraichir_statuts_signatures(session: AsyncSession = Depends(get_sess
} }
@app.post("/devis/{id}/signer", tags=["US-A3"]) @app.post("/devis/{id}/signer", tags=["Devis"])
async def envoyer_devis_signature( async def envoyer_devis_signature(
id: str, request: SignatureRequest, session: AsyncSession = Depends(get_session) id: str, request: SignatureRequest, session: AsyncSession = Depends(get_session)
): ):
@ -1788,7 +1856,7 @@ async def envoyer_emails_lot(
# ===================================================== # =====================================================
@app.post("/devis/valider-remise", response_model=BaremeRemiseResponse, tags=["US-A5"]) @app.post("/devis/valider-remise", response_model=BaremeRemiseResponse, tags=["Devis"])
async def valider_remise( async def valider_remise(
client_id: str = Query(..., min_length=1), client_id: str = Query(..., min_length=1),
remise_pourcentage: float = Query(0.0, ge=0, le=100), remise_pourcentage: float = Query(0.0, ge=0, le=100),
@ -1826,7 +1894,7 @@ async def valider_remise(
# ===================================================== # =====================================================
# ENDPOINTS - US-A6 (RELANCE DEVIS) # ENDPOINTS - US-A6 (RELANCE DEVIS)
# ===================================================== # =====================================================
@app.post("/devis/{id}/relancer-signature", tags=["US-A6"]) @app.post("/devis/{id}/relancer-signature", tags=["Devis"])
async def relancer_devis_signature( async def relancer_devis_signature(
id: str, relance: RelanceDevisRequest, session: AsyncSession = Depends(get_session) id: str, relance: RelanceDevisRequest, session: AsyncSession = Depends(get_session)
): ):
@ -1897,7 +1965,7 @@ class ContactClientResponse(BaseModel):
peut_etre_relance: bool peut_etre_relance: bool
@app.get("/devis/{id}/contact", response_model=ContactClientResponse, tags=["US-A6"]) @app.get("/devis/{id}/contact", response_model=ContactClientResponse, tags=["Devis"])
async def recuperer_contact_devis(id: str): async def recuperer_contact_devis(id: str):
"""👤 US-A6: Récupération du contact client associé au devis""" """👤 US-A6: Récupération du contact client associé au devis"""
try: try:
@ -1928,7 +1996,7 @@ async def recuperer_contact_devis(id: str):
# ===================================================== # =====================================================
@app.get("/factures", tags=["US-A7"]) @app.get("/factures", tags=["Factures"])
async def lister_factures( async def lister_factures(
limit: int = Query(100, le=1000), statut: Optional[int] = Query(None) limit: int = Query(100, le=1000), statut: Optional[int] = Query(None)
): ):
@ -1947,7 +2015,7 @@ async def lister_factures(
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.get("/factures/{numero}", tags=["US-A7"]) @app.get("/factures/{numero}", tags=["Factures"])
async def lire_facture_detail(numero: str): async def lire_facture_detail(numero: str):
""" """
📄 Lecture détaillée d'une facture avec ses lignes 📄 Lecture détaillée d'une facture avec ses lignes
@ -1979,7 +2047,7 @@ class RelanceFactureRequest(BaseModel):
doc_id: str doc_id: str
message_personnalise: Optional[str] = None message_personnalise: Optional[str] = None
@app.post("/factures", status_code=201, tags=["US-A7"]) @app.post("/factures", status_code=201, tags=["Factures"])
async def creer_facture( async def creer_facture(
facture: FactureCreateRequest, facture: FactureCreateRequest,
session: AsyncSession = Depends(get_session) session: AsyncSession = Depends(get_session)
@ -2062,7 +2130,7 @@ async def creer_facture(
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.put("/factures/{id}", tags=["US-A7"]) @app.put("/factures/{id}", tags=["Factures"])
async def modifier_facture( async def modifier_facture(
id: str, id: str,
facture_update: FactureUpdateRequest, facture_update: FactureUpdateRequest,
@ -2183,7 +2251,7 @@ templates_email_db = {
} }
@app.post("/factures/{id}/relancer", tags=["US-A7"]) @app.post("/factures/{id}/relancer", tags=["Factures"])
async def relancer_facture( async def relancer_facture(
id: str, id: str,
relance: RelanceFactureRequest, relance: RelanceFactureRequest,
@ -2263,7 +2331,7 @@ async def relancer_facture(
# ============================================ # ============================================
@app.get("/emails/logs", tags=["US-A9"]) @app.get("/emails/logs", tags=["Emails"])
async def journal_emails( async def journal_emails(
statut: Optional[StatutEmail] = Query(None), statut: Optional[StatutEmail] = Query(None),
destinataire: Optional[str] = Query(None), destinataire: Optional[str] = Query(None),
@ -2300,7 +2368,7 @@ async def journal_emails(
] ]
@app.get("/emails/logs/export", tags=["US-A9"]) @app.get("/emails/logs/export", tags=["Emails"])
async def exporter_logs_csv( async def exporter_logs_csv(
statut: Optional[StatutEmail] = Query(None), statut: Optional[StatutEmail] = Query(None),
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
@ -2362,7 +2430,7 @@ async def exporter_logs_csv(
# ============================================ # ============================================
# US-A10 - MODÈLES D'E-MAILS # Devis0 - MODÈLES D'E-MAILS
# ============================================ # ============================================
@ -2380,14 +2448,14 @@ class TemplatePreviewRequest(BaseModel):
type_document: TypeDocument type_document: TypeDocument
@app.get("/templates/emails", response_model=List[TemplateEmail], tags=["US-A10"]) @app.get("/templates/emails", response_model=List[TemplateEmail], tags=["Emails"])
async def lister_templates(): async def lister_templates():
"""📧 US-A10: Liste tous les templates d'emails""" """📧 Emails: Liste tous les templates d'emails"""
return [TemplateEmail(**template) for template in templates_email_db.values()] return [TemplateEmail(**template) for template in templates_email_db.values()]
@app.get( @app.get(
"/templates/emails/{template_id}", response_model=TemplateEmail, tags=["US-A10"] "/templates/emails/{template_id}", response_model=TemplateEmail, tags=["Emails"]
) )
async def lire_template(template_id: str): async def lire_template(template_id: str):
"""📖 Lecture d'un template par ID""" """📖 Lecture d'un template par ID"""
@ -2397,7 +2465,7 @@ async def lire_template(template_id: str):
return TemplateEmail(**templates_email_db[template_id]) return TemplateEmail(**templates_email_db[template_id])
@app.post("/templates/emails", response_model=TemplateEmail, tags=["US-A10"]) @app.post("/templates/emails", response_model=TemplateEmail, tags=["Emails"])
async def creer_template(template: TemplateEmail): async def creer_template(template: TemplateEmail):
""" Création d'un nouveau template""" """ Création d'un nouveau template"""
template_id = str(uuid.uuid4()) template_id = str(uuid.uuid4())
@ -2416,7 +2484,7 @@ async def creer_template(template: TemplateEmail):
@app.put( @app.put(
"/templates/emails/{template_id}", response_model=TemplateEmail, tags=["US-A10"] "/templates/emails/{template_id}", response_model=TemplateEmail, tags=["Emails"]
) )
async def modifier_template(template_id: str, template: TemplateEmail): async def modifier_template(template_id: str, template: TemplateEmail):
"""✏️ Modification d'un template existant""" """✏️ Modification d'un template existant"""
@ -2440,7 +2508,7 @@ async def modifier_template(template_id: str, template: TemplateEmail):
return TemplateEmail(id=template_id, **template.dict()) return TemplateEmail(id=template_id, **template.dict())
@app.delete("/templates/emails/{template_id}", tags=["US-A10"]) @app.delete("/templates/emails/{template_id}", tags=["Emails"])
async def supprimer_template(template_id: str): async def supprimer_template(template_id: str):
"""🗑️ Suppression d'un template""" """🗑️ Suppression d'un template"""
if template_id not in templates_email_db: if template_id not in templates_email_db:
@ -2456,7 +2524,7 @@ async def supprimer_template(template_id: str):
return {"success": True, "message": f"Template {template_id} supprimé"} return {"success": True, "message": f"Template {template_id} supprimé"}
@app.post("/templates/emails/preview", tags=["US-A10"]) @app.post("/templates/emails/preview", tags=["Emails"])
async def previsualiser_email(preview: TemplatePreviewRequest): async def previsualiser_email(preview: TemplatePreviewRequest):
"""👁️ US-A10: Prévisualisation email avec fusion variables""" """👁️ US-A10: Prévisualisation email avec fusion variables"""
if preview.template_id not in templates_email_db: if preview.template_id not in templates_email_db:
@ -3170,7 +3238,7 @@ async def livraison_vers_facture(id: str, session: AsyncSession = Depends(get_se
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.post("/workflow/devis/{id}/to-facture", tags=["US-A2"]) @app.post("/workflow/devis/{id}/to-facture", tags=["Devis"])
async def devis_vers_facture_direct(id: str, session: AsyncSession = Depends(get_session)): async def devis_vers_facture_direct(id: str, session: AsyncSession = Depends(get_session)):
""" """
🔧 Transformation Devis Facture (DIRECT, sans commande) 🔧 Transformation Devis Facture (DIRECT, sans commande)
@ -3257,7 +3325,7 @@ async def devis_vers_facture_direct(id: str, session: AsyncSession = Depends(get
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.post("/workflow/commande/{id}/to-livraison", tags=["US-A2"]) @app.post("/workflow/commande/{id}/to-livraison", tags=["Commandes"])
async def commande_vers_livraison(id: str, session: AsyncSession = Depends(get_session)): async def commande_vers_livraison(id: str, session: AsyncSession = Depends(get_session)):
""" """
🔧 Transformation Commande Bon de livraison 🔧 Transformation Commande Bon de livraison