feat: Add API endpoints and SageClient methods for managing prospects, suppliers, credit notes, and delivery notes.

This commit is contained in:
Fanilo-Nantenaina 2025-12-04 13:47:28 +03:00
parent a73bdc4d9e
commit b4a76579b8
2 changed files with 288 additions and 127 deletions

361
api.py
View file

@ -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
# =====================================================

View file

@ -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)
# =====================================================