From b4a76579b81936dc262a8cd6a1d5dd6fc4705ff1 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Thu, 4 Dec 2025 13:47:28 +0300 Subject: [PATCH] feat: Add API endpoints and SageClient methods for managing prospects, suppliers, credit notes, and delivery notes. --- api.py | 361 ++++++++++++++++++++++++++++++++----------------- sage_client.py | 54 ++++++++ 2 files changed, 288 insertions(+), 127 deletions(-) diff --git a/api.py b/api.py index f01767f..daec94d 100644 --- a/api.py +++ b/api.py @@ -154,8 +154,10 @@ from datetime import datetime # MODÈLES PYDANTIC POUR USERS # ===================================================== + class UserResponse(BaseModel): """Modèle de réponse pour un utilisateur""" + id: str email: str nom: str @@ -166,10 +168,11 @@ class UserResponse(BaseModel): created_at: str last_login: Optional[str] = None failed_login_attempts: int = 0 - + class Config: from_attributes = True + # ===================================================== # SERVICES EXTERNES (Universign) # ===================================================== @@ -300,7 +303,7 @@ async def lifespan(app: FastAPI): # ✅ CORRECTION: Injecter session_factory ET sage_client dans email_queue email_queue.session_factory = async_session_factory email_queue.sage_client = sage_client - + logger.info("✅ sage_client injecté dans email_queue") # Démarrer queue @@ -1583,116 +1586,237 @@ async def statut_queue(): } +# ===================================================== +# ENDPOINTS - PROSPECTS +# ===================================================== +@app.get("/prospects", tags=["Prospects"]) +async def rechercher_prospects(query: Optional[str] = Query(None)): + """🔍 Recherche prospects via gateway Windows""" + try: + prospects = sage_client.lister_prospects(filtre=query or "") + return prospects + except Exception as e: + logger.error(f"Erreur recherche prospects: {e}") + raise HTTPException(500, str(e)) + + +@app.get("/prospects/{code}", tags=["Prospects"]) +async def lire_prospect(code: str): + """📄 Lecture d'un prospect par code""" + try: + prospect = sage_client.lire_prospect(code) + if not prospect: + raise HTTPException(404, f"Prospect {code} introuvable") + return prospect + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur lecture prospect: {e}") + raise HTTPException(500, str(e)) + + +# ===================================================== +# ENDPOINTS - FOURNISSEURS +# ===================================================== +@app.get("/fournisseurs", tags=["Fournisseurs"]) +async def rechercher_fournisseurs(query: Optional[str] = Query(None)): + """🔍 Recherche fournisseurs via gateway Windows""" + try: + fournisseurs = sage_client.lister_fournisseurs(filtre=query or "") + return fournisseurs + except Exception as e: + logger.error(f"Erreur recherche fournisseurs: {e}") + raise HTTPException(500, str(e)) + + +@app.get("/fournisseurs/{code}", tags=["Fournisseurs"]) +async def lire_fournisseur(code: str): + """📄 Lecture d'un fournisseur par code""" + try: + fournisseur = sage_client.lire_fournisseur(code) + if not fournisseur: + raise HTTPException(404, f"Fournisseur {code} introuvable") + return fournisseur + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur lecture fournisseur: {e}") + raise HTTPException(500, str(e)) + + +# ===================================================== +# ENDPOINTS - AVOIRS +# ===================================================== +@app.get("/avoirs", tags=["Avoirs"]) +async def lister_avoirs( + limit: int = Query(100, le=1000), statut: Optional[int] = Query(None) +): + """📋 Liste tous les avoirs via gateway Windows""" + try: + avoirs = sage_client.lister_avoirs(limit=limit, statut=statut) + return avoirs + except Exception as e: + logger.error(f"Erreur liste avoirs: {e}") + raise HTTPException(500, str(e)) + + +@app.get("/avoirs/{numero}", tags=["Avoirs"]) +async def lire_avoir(numero: str): + """📄 Lecture d'un avoir avec ses lignes""" + try: + avoir = sage_client.lire_avoir(numero) + if not avoir: + raise HTTPException(404, f"Avoir {numero} introuvable") + return avoir + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur lecture avoir: {e}") + raise HTTPException(500, str(e)) + + +# ===================================================== +# ENDPOINTS - LIVRAISONS +# ===================================================== +@app.get("/livraisons", tags=["Livraisons"]) +async def lister_livraisons( + limit: int = Query(100, le=1000), statut: Optional[int] = Query(None) +): + """📋 Liste tous les bons de livraison via gateway Windows""" + try: + livraisons = sage_client.lister_livraisons(limit=limit, statut=statut) + return livraisons + except Exception as e: + logger.error(f"Erreur liste livraisons: {e}") + raise HTTPException(500, str(e)) + + +@app.get("/livraisons/{numero}", tags=["Livraisons"]) +async def lire_livraison(numero: str): + """📄 Lecture d'une livraison avec ses lignes""" + try: + livraison = sage_client.lire_livraison(numero) + if not livraison: + raise HTTPException(404, f"Livraison {numero} introuvable") + return livraison + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur lecture livraison: {e}") + raise HTTPException(500, str(e)) + @app.get("/debug/users", response_model=List[UserResponse], tags=["Debug"]) async def lister_utilisateurs_debug( session: AsyncSession = Depends(get_session), limit: int = Query(100, le=1000), role: Optional[str] = Query(None), - verified_only: bool = Query(False) + verified_only: bool = Query(False), ): """ 🔓 **ROUTE DEBUG** - Liste tous les utilisateurs inscrits - + ⚠️ **ATTENTION**: Cette route n'est PAS protégée par authentification. À utiliser uniquement en développement ou à sécuriser en production. - + Args: limit: Nombre maximum d'utilisateurs à retourner role: Filtrer par rôle (user, admin, commercial) verified_only: Afficher uniquement les utilisateurs vérifiés - + Returns: Liste des utilisateurs avec leurs informations (mot de passe masqué) """ from database import User from sqlalchemy import select - + try: # Construction de la requête query = select(User) - + # Filtres optionnels if role: query = query.where(User.role == role) - + if verified_only: query = query.where(User.is_verified == True) - + # Tri par date de création (plus récents en premier) query = query.order_by(User.created_at.desc()).limit(limit) - + # Exécution result = await session.execute(query) users = result.scalars().all() - + # Conversion en réponse users_response = [] for user in users: - users_response.append(UserResponse( - id=user.id, - email=user.email, - nom=user.nom, - prenom=user.prenom, - role=user.role, - is_verified=user.is_verified, - is_active=user.is_active, - created_at=user.created_at.isoformat() if user.created_at else "", - last_login=user.last_login.isoformat() if user.last_login else None, - failed_login_attempts=user.failed_login_attempts or 0 - )) - - logger.info(f"📋 Liste utilisateurs retournée: {len(users_response)} résultat(s)") - + users_response.append( + UserResponse( + id=user.id, + email=user.email, + nom=user.nom, + prenom=user.prenom, + role=user.role, + is_verified=user.is_verified, + is_active=user.is_active, + created_at=user.created_at.isoformat() if user.created_at else "", + last_login=user.last_login.isoformat() if user.last_login else None, + failed_login_attempts=user.failed_login_attempts or 0, + ) + ) + + logger.info( + f"📋 Liste utilisateurs retournée: {len(users_response)} résultat(s)" + ) + return users_response - + except Exception as e: logger.error(f"❌ Erreur liste utilisateurs: {e}") raise HTTPException(500, str(e)) @app.get("/debug/users/stats", tags=["Debug"]) -async def statistiques_utilisateurs( - session: AsyncSession = Depends(get_session) -): +async def statistiques_utilisateurs(session: AsyncSession = Depends(get_session)): """ 📊 **ROUTE DEBUG** - Statistiques sur les utilisateurs - + ⚠️ Non protégée - à sécuriser en production """ from database import User from sqlalchemy import select, func - + try: # Total utilisateurs total_query = select(func.count(User.id)) total_result = await session.execute(total_query) total = total_result.scalar() - + # Utilisateurs vérifiés verified_query = select(func.count(User.id)).where(User.is_verified == True) verified_result = await session.execute(verified_query) verified = verified_result.scalar() - + # Utilisateurs actifs active_query = select(func.count(User.id)).where(User.is_active == True) active_result = await session.execute(active_query) active = active_result.scalar() - + # Par rôle roles_query = select(User.role, func.count(User.id)).group_by(User.role) roles_result = await session.execute(roles_query) roles_stats = {role: count for role, count in roles_result.all()} - + return { "total_utilisateurs": total, "utilisateurs_verifies": verified, "utilisateurs_actifs": active, "utilisateurs_non_verifies": total - verified, "repartition_roles": roles_stats, - "taux_verification": f"{(verified/total*100):.1f}%" if total > 0 else "0%" + "taux_verification": f"{(verified/total*100):.1f}%" if total > 0 else "0%", } - + except Exception as e: logger.error(f"❌ Erreur stats utilisateurs: {e}") raise HTTPException(500, str(e)) @@ -1700,25 +1824,24 @@ async def statistiques_utilisateurs( @app.get("/debug/users/{user_id}", response_model=UserResponse, tags=["Debug"]) async def lire_utilisateur_debug( - user_id: str, - session: AsyncSession = Depends(get_session) + user_id: str, session: AsyncSession = Depends(get_session) ): """ 👤 **ROUTE DEBUG** - Détails d'un utilisateur par ID - + ⚠️ Non protégée - à sécuriser en production """ from database import User from sqlalchemy import select - + try: query = select(User).where(User.id == user_id) result = await session.execute(query) user = result.scalar_one_or_none() - + if not user: raise HTTPException(404, f"Utilisateur {user_id} introuvable") - + return UserResponse( id=user.id, email=user.email, @@ -1729,9 +1852,9 @@ async def lire_utilisateur_debug( is_active=user.is_active, created_at=user.created_at.isoformat() if user.created_at else "", last_login=user.last_login.isoformat() if user.last_login else None, - failed_login_attempts=user.failed_login_attempts or 0 + failed_login_attempts=user.failed_login_attempts or 0, ) - + except HTTPException: raise except Exception as e: @@ -1739,98 +1862,85 @@ async def lire_utilisateur_debug( raise HTTPException(500, str(e)) -# À ajouter dans api.py dans la section Debug - @app.get("/debug/database/check", tags=["Debug"]) async def verifier_integrite_database(session: AsyncSession = Depends(get_session)): """ 🔍 Vérification de l'intégrité de la base de données - + Retourne des statistiques détaillées sur toutes les tables """ from database import User, RefreshToken, LoginAttempt, EmailLog, SignatureLog from sqlalchemy import func, text - + try: diagnostics = {} - + # === TABLE USERS === # Compter tous les users total_users = await session.execute(select(func.count(User.id))) - diagnostics["users"] = { - "total": total_users.scalar(), - "details": [] - } - + diagnostics["users"] = {"total": total_users.scalar(), "details": []} + # Lister tous les users avec détails all_users = await session.execute(select(User)) users_list = all_users.scalars().all() - + for u in users_list: - diagnostics["users"]["details"].append({ - "id": u.id, - "email": u.email, - "nom": f"{u.prenom} {u.nom}", - "role": u.role, - "is_active": u.is_active, - "is_verified": u.is_verified, - "created_at": u.created_at.isoformat() if u.created_at else None, - "has_reset_token": u.reset_token is not None, - "has_verification_token": u.verification_token is not None, - }) - + diagnostics["users"]["details"].append( + { + "id": u.id, + "email": u.email, + "nom": f"{u.prenom} {u.nom}", + "role": u.role, + "is_active": u.is_active, + "is_verified": u.is_verified, + "created_at": u.created_at.isoformat() if u.created_at else None, + "has_reset_token": u.reset_token is not None, + "has_verification_token": u.verification_token is not None, + } + ) + # === TABLE REFRESH_TOKENS === total_tokens = await session.execute(select(func.count(RefreshToken.id))) - diagnostics["refresh_tokens"] = { - "total": total_tokens.scalar() - } - + diagnostics["refresh_tokens"] = {"total": total_tokens.scalar()} + # === TABLE LOGIN_ATTEMPTS === total_attempts = await session.execute(select(func.count(LoginAttempt.id))) - diagnostics["login_attempts"] = { - "total": total_attempts.scalar() - } - + diagnostics["login_attempts"] = {"total": total_attempts.scalar()} + # === TABLE EMAIL_LOGS === total_emails = await session.execute(select(func.count(EmailLog.id))) - diagnostics["email_logs"] = { - "total": total_emails.scalar() - } - + diagnostics["email_logs"] = {"total": total_emails.scalar()} + # === TABLE SIGNATURE_LOGS === total_signatures = await session.execute(select(func.count(SignatureLog.id))) - diagnostics["signature_logs"] = { - "total": total_signatures.scalar() - } - + diagnostics["signature_logs"] = {"total": total_signatures.scalar()} + # === VÉRIFIER LES FICHIERS SQLITE === import os + db_file = "sage_dataven.db" diagnostics["database_file"] = { "exists": os.path.exists(db_file), "size_bytes": os.path.getsize(db_file) if os.path.exists(db_file) else 0, - "path": os.path.abspath(db_file) + "path": os.path.abspath(db_file), } - + # === TESTER UNE REQUÊTE RAW SQL === try: raw_count = await session.execute(text("SELECT COUNT(*) FROM users")) diagnostics["raw_sql_check"] = { "users_count": raw_count.scalar(), - "status": "✅ Connexion DB OK" + "status": "✅ Connexion DB OK", } except Exception as e: - diagnostics["raw_sql_check"] = { - "status": "❌ Erreur", - "error": str(e) - } - + diagnostics["raw_sql_check"] = {"status": "❌ Erreur", "error": str(e)} + return { "success": True, "timestamp": datetime.now().isoformat(), - "diagnostics": diagnostics + "diagnostics": diagnostics, } - + except Exception as e: logger.error(f"❌ Erreur diagnostic DB: {e}", exc_info=True) raise HTTPException(500, f"Erreur diagnostic: {str(e)}") @@ -1840,7 +1950,7 @@ async def verifier_integrite_database(session: AsyncSession = Depends(get_sessio async def tester_persistance_utilisateur(session: AsyncSession = Depends(get_session)): """ 🧪 Test de création/lecture/modification d'un utilisateur de test - + Crée un utilisateur de test, le modifie, et vérifie la persistance """ import uuid @@ -1849,7 +1959,7 @@ async def tester_persistance_utilisateur(session: AsyncSession = Depends(get_ses try: test_email = f"test_{uuid.uuid4().hex[:8]}@example.com" - + # === ÉTAPE 1: CRÉATION === test_user = User( id=str(uuid.uuid4()), @@ -1860,65 +1970,61 @@ async def tester_persistance_utilisateur(session: AsyncSession = Depends(get_ses role="user", is_verified=True, is_active=True, - created_at=datetime.now() + created_at=datetime.now(), ) - + session.add(test_user) await session.flush() user_id = test_user.id await session.commit() - + logger.info(f"✅ ÉTAPE 1: User créé - {user_id}") - + # === ÉTAPE 2: LECTURE === - result = await session.execute( - select(User).where(User.id == user_id) - ) + result = await session.execute(select(User).where(User.id == user_id)) loaded_user = result.scalar_one_or_none() - + if not loaded_user: return { "success": False, "error": "❌ User introuvable après création !", - "step": "LECTURE" + "step": "LECTURE", } - + logger.info(f"✅ ÉTAPE 2: User chargé - {loaded_user.email}") - + # === ÉTAPE 3: MODIFICATION (simulate reset password) === loaded_user.hashed_password = hash_password("NewPassword456!") loaded_user.reset_token = None loaded_user.reset_token_expires = None - + session.add(loaded_user) await session.flush() await session.commit() await session.refresh(loaded_user) - + logger.info(f"✅ ÉTAPE 3: User modifié") - + # === ÉTAPE 4: RE-LECTURE === - result2 = await session.execute( - select(User).where(User.id == user_id) - ) + result2 = await session.execute(select(User).where(User.id == user_id)) reloaded_user = result2.scalar_one_or_none() - + if not reloaded_user: return { "success": False, "error": "❌ User DISPARU après modification !", "step": "RE-LECTURE", - "user_id": user_id + "user_id": user_id, } - + logger.info(f"✅ ÉTAPE 4: User re-chargé - {reloaded_user.email}") - + # === ÉTAPE 5: SUPPRESSION DU TEST === await session.delete(reloaded_user) await session.commit() - + logger.info(f"✅ ÉTAPE 5: User test supprimé") - + return { "success": True, "message": "✅ Tous les tests de persistance sont OK", @@ -1929,22 +2035,23 @@ async def tester_persistance_utilisateur(session: AsyncSession = Depends(get_ses "2. Lecture", "3. Modification (reset password simulé)", "4. Re-lecture (vérification persistance)", - "5. Suppression (cleanup)" - ] + "5. Suppression (cleanup)", + ], } - + except Exception as e: logger.error(f"❌ Erreur test persistance: {e}", exc_info=True) - + # Rollback en cas d'erreur await session.rollback() - + return { "success": False, "error": str(e), - "traceback": str(e.__class__.__name__) + "traceback": str(e.__class__.__name__), } + # ===================================================== # LANCEMENT # ===================================================== diff --git a/sage_client.py b/sage_client.py index 9ea5265..bc5568f 100644 --- a/sage_client.py +++ b/sage_client.py @@ -256,6 +256,60 @@ class SageGatewayClient: logger.error(f"Erreur génération PDF: {e}") raise + # ===================================================== + # PROSPECTS + # ===================================================== + def lister_prospects(self, filtre: str = "") -> List[Dict]: + """Liste tous les prospects avec filtre optionnel""" + return self._post("/sage/prospects/list", {"filtre": filtre}).get("data", []) + + def lire_prospect(self, code: str) -> Optional[Dict]: + """Lecture d'un prospect par code""" + return self._post("/sage/prospects/get", {"code": code}).get("data") + + # ===================================================== + # FOURNISSEURS + # ===================================================== + def lister_fournisseurs(self, filtre: str = "") -> List[Dict]: + """Liste tous les fournisseurs avec filtre optionnel""" + return self._post("/sage/fournisseurs/list", {"filtre": filtre}).get("data", []) + + def lire_fournisseur(self, code: str) -> Optional[Dict]: + """Lecture d'un fournisseur par code""" + return self._post("/sage/fournisseurs/get", {"code": code}).get("data") + + # ===================================================== + # AVOIRS + # ===================================================== + def lister_avoirs( + self, limit: int = 100, statut: Optional[int] = None + ) -> List[Dict]: + """Liste tous les avoirs""" + payload = {"limit": limit} + if statut is not None: + payload["statut"] = statut + return self._post("/sage/avoirs/list", payload).get("data", []) + + def lire_avoir(self, numero: str) -> Optional[Dict]: + """Lecture d'un avoir avec ses lignes""" + return self._post("/sage/avoirs/get", {"code": numero}).get("data") + + # ===================================================== + # LIVRAISONS + # ===================================================== + def lister_livraisons( + self, limit: int = 100, statut: Optional[int] = None + ) -> List[Dict]: + """Liste tous les bons de livraison""" + payload = {"limit": limit} + if statut is not None: + payload["statut"] = statut + return self._post("/sage/livraisons/list", payload).get("data", []) + + def lire_livraison(self, numero: str) -> Optional[Dict]: + """Lecture d'une livraison avec ses lignes""" + return self._post("/sage/livraisons/get", {"code": numero}).get("data") + # ===================================================== # CACHE (ADMIN) # =====================================================