From fafd5222a6766fd3eb150d0563831aa8937219e0 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Mon, 8 Dec 2025 10:58:24 +0300 Subject: [PATCH] feat(api): add OpenAPI tags metadata and update endpoint tags --- api.py | 156 +++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 112 insertions(+), 44 deletions(-) diff --git a/api.py b/api.py index f574f08..bcc7bbb 100644 --- a/api.py +++ b/api.py @@ -42,6 +42,73 @@ from email_queue import email_queue 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 # ===================================================== @@ -674,6 +741,7 @@ app = FastAPI( version="2.0.0", description="API de gestion commerciale - VPS Linux", lifespan=lifespan, + openapi_tags=TAGS_METADATA ) app.add_middleware( @@ -691,7 +759,7 @@ app.include_router(auth_router) # ===================================================== # 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)): """🔍 Recherche clients via gateway Windows""" try: @@ -701,7 +769,7 @@ async def rechercher_clients(query: Optional[str] = Query(None)): logger.error(f"Erreur recherche clients: {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): """ 📄 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)) -@app.put("/clients/{code}", tags=["US-A1"]) +@app.put("/clients/{code}", tags=["Clients"]) async def modifier_client( code: str, client_update: ClientUpdateRequest, @@ -774,7 +842,7 @@ async def modifier_client( logger.error(f"Erreur technique modification client {code}: {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( client: ClientCreateAPIRequest, session: AsyncSession = Depends(get_session) @@ -799,7 +867,7 @@ async def ajouter_client( status = 400 if "existe déjà" in str(e) else 500 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)): """🔍 Recherche articles via gateway Windows""" try: @@ -810,7 +878,7 @@ async def rechercher_articles(query: Optional[str] = Query(None)): 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): """📝 Création de devis via gateway Windows""" try: @@ -848,7 +916,7 @@ async def creer_devis(devis: DevisRequest): raise HTTPException(500, str(e)) -@app.put("/devis/{id}", tags=["US-A1"]) +@app.put("/devis/{id}", tags=["Devis"]) async def modifier_devis( id: str, devis_update: DevisUpdateRequest, @@ -925,7 +993,7 @@ async def modifier_devis( 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( commande: CommandeCreateRequest, session: AsyncSession = Depends(get_session) @@ -1003,7 +1071,7 @@ async def creer_commande( raise HTTPException(500, str(e)) -@app.put("/commandes/{id}", tags=["US-A2"]) +@app.put("/commandes/{id}", tags=["Commandes"]) async def modifier_commande( id: str, commande_update: CommandeUpdateRequest, @@ -1098,7 +1166,7 @@ async def modifier_commande( raise HTTPException(500, str(e)) -@app.get("/devis", tags=["US-A1"]) +@app.get("/devis", tags=["Devis"]) async def lister_devis( limit: int = Query(100, le=1000), statut: Optional[int] = Query(None), @@ -1127,7 +1195,7 @@ async def lister_devis( raise HTTPException(500, str(e)) -@app.get("/devis/{id}", tags=["US-A1"]) +@app.get("/devis/{id}", tags=["Devis"]) async def lire_devis(id: str): """ 📄 Lecture d'un devis via gateway Windows @@ -1167,7 +1235,7 @@ async def lire_devis(id: str): 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): """📄 Téléchargement PDF (généré via email_queue)""" try: @@ -1185,7 +1253,7 @@ async def telecharger_devis_pdf(id: str): 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( id: str, request: EmailEnvoiRequest, session: AsyncSession = Depends(get_session) ): @@ -1239,7 +1307,7 @@ async def envoyer_devis_email( 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( id: str, 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): """📄 Lecture d'une commande avec ses lignes""" try: @@ -1323,7 +1391,7 @@ async def lire_commande(id: str): raise HTTPException(500, str(e)) -@app.get("/commandes", tags=["US-A2"]) +@app.get("/commandes", tags=["Commandes"]) async def lister_commandes( 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)) -@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)): """ 🔧 Transformation Commande → Facture @@ -1443,7 +1511,7 @@ async def commande_vers_facture(id: str, session: AsyncSession = Depends(get_ses # ===================================================== # ENDPOINTS - US-A3 (SIGNATURE ÉLECTRONIQUE) # ===================================================== -@app.post("/signature/universign/send", tags=["US-A3"]) +@app.post("/signature/universign/send", tags=["Universign"]) async def envoyer_signature( demande: SignatureRequest, session: AsyncSession = Depends(get_session) ): @@ -1496,7 +1564,7 @@ async def envoyer_signature( 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(...)): """🔍 Récupération du statut de signature en temps réel""" # Chercher dans la DB locale @@ -1524,7 +1592,7 @@ async def statut_signature(docId: str = Query(...)): raise HTTPException(500, str(e)) -@app.get("/signatures", tags=["US-A3"]) +@app.get("/signatures", tags=["Universign"]) async def lister_signatures( statut: Optional[StatutSignature] = Query(None), 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( 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)): """🔄 Rafraîchit TOUS les statuts des signatures en attente""" 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( 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( client_id: str = Query(..., min_length=1), remise_pourcentage: float = Query(0.0, ge=0, le=100), @@ -1826,7 +1894,7 @@ async def valider_remise( # ===================================================== # 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( id: str, relance: RelanceDevisRequest, session: AsyncSession = Depends(get_session) ): @@ -1897,7 +1965,7 @@ class ContactClientResponse(BaseModel): 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): """👤 US-A6: Récupération du contact client associé au devis""" 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( limit: int = Query(100, le=1000), statut: Optional[int] = Query(None) ): @@ -1947,7 +2015,7 @@ async def lister_factures( 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): """ 📄 Lecture détaillée d'une facture avec ses lignes @@ -1979,7 +2047,7 @@ class RelanceFactureRequest(BaseModel): doc_id: str 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( facture: FactureCreateRequest, session: AsyncSession = Depends(get_session) @@ -2062,7 +2130,7 @@ async def creer_facture( raise HTTPException(500, str(e)) -@app.put("/factures/{id}", tags=["US-A7"]) +@app.put("/factures/{id}", tags=["Factures"]) async def modifier_facture( id: str, 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( id: str, 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( statut: Optional[StatutEmail] = 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( statut: Optional[StatutEmail] = Query(None), 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 -@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(): - """📧 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()] @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): """📖 Lecture d'un template par ID""" @@ -2397,7 +2465,7 @@ async def lire_template(template_id: str): 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): """➕ Création d'un nouveau template""" template_id = str(uuid.uuid4()) @@ -2416,7 +2484,7 @@ async def creer_template(template: TemplateEmail): @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): """✏️ 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()) -@app.delete("/templates/emails/{template_id}", tags=["US-A10"]) +@app.delete("/templates/emails/{template_id}", tags=["Emails"]) async def supprimer_template(template_id: str): """🗑️ Suppression d'un template""" 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é"} -@app.post("/templates/emails/preview", tags=["US-A10"]) +@app.post("/templates/emails/preview", tags=["Emails"]) async def previsualiser_email(preview: TemplatePreviewRequest): """👁️ US-A10: Prévisualisation email avec fusion variables""" 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)) -@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)): """ 🔧 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)) -@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)): """ 🔧 Transformation Commande → Bon de livraison