diff --git a/api.py b/api.py index 6ee5d3f..bbac204 100644 --- a/api.py +++ b/api.py @@ -16,9 +16,10 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select import os from pathlib import Path as FilePath -from data.data import TAGS_METADATA, templates_signature_email +from data.data import TAGS_METADATA from config.config import settings from database import ( + User, init_db, async_session_factory, get_session, @@ -58,7 +59,6 @@ from schemas import ( FactureUpdate, LivraisonCreate, LivraisonUpdate, - StatutSignature, ArticleCreate, Article, ArticleUpdate, @@ -93,9 +93,10 @@ from core.sage_context import ( from utils.generic_functions import ( _preparer_lignes_document, universign_envoyer, - universign_statut, ) +from core.dependencies import get_current_user + if os.path.exists("/app"): LOGS_DIR = FilePath("/app/logs") else: @@ -976,266 +977,6 @@ async def commande_vers_facture( raise HTTPException(500, str(e)) -@app.get("/admin/signatures/relances-auto", tags=["Admin"]) -async def relancer_signatures_automatique(session: AsyncSession = Depends(get_session)): - try: - from datetime import timedelta - - date_limite = datetime.now() - timedelta(days=7) - - query = select(SignatureLog).where( - SignatureLog.statut.in_( - [StatutSignatureDB.EN_ATTENTE, StatutSignatureDB.ENVOYE] - ), - SignatureLog.date_envoi < date_limite, - SignatureLog.nb_relances < 3, # Max 3 relances - ) - - result = await session.execute(query) - signatures_a_relancer = result.scalars().all() - - nb_relances = 0 - - for signature in signatures_a_relancer: - try: - nb_jours = (datetime.now() - signature.date_envoi).days - jours_restants = 30 - nb_jours # Lien expire après 30 jours - - if jours_restants <= 0: - signature.statut = StatutSignatureDB.EXPIRE - continue - - template = templates_signature_email["relance_signature"] - - type_labels = { - 0: "Devis", - 10: "Commande", - 30: "Bon de Livraison", - 60: "Facture", - 50: "Avoir", - } - - variables = { - "NOM_SIGNATAIRE": signature.nom_signataire, - "TYPE_DOC": type_labels.get(signature.type_document, "Document"), - "NUMERO": signature.document_id, - "NB_JOURS": str(nb_jours), - "JOURS_RESTANTS": str(jours_restants), - "SIGNER_URL": signature.signer_url, - "CONTACT_EMAIL": settings.smtp_from, - } - - sujet = template["sujet"] - corps = template["corps_html"] - - for var, valeur in variables.items(): - sujet = sujet.replace(f"{{{{{var}}}}}", str(valeur)) - corps = corps.replace(f"{{{{{var}}}}}", str(valeur)) - - email_log = EmailLog( - id=str(uuid.uuid4()), - destinataire=signature.email_signataire, - sujet=sujet, - corps_html=corps, - document_ids=signature.document_id, - type_document=signature.type_document, - statut=StatutEmailDB.EN_ATTENTE, - date_creation=datetime.now(), - nb_tentatives=0, - ) - - session.add(email_log) - email_queue.enqueue(email_log.id) - - signature.est_relance = True - signature.nb_relances = (signature.nb_relances or 0) + 1 - - nb_relances += 1 - - logger.info( - f" Relance envoyée: {signature.document_id} ({signature.nb_relances}/3)" - ) - - except Exception as e: - logger.error(f"Erreur relance signature {signature.id}: {e}") - continue - - await session.commit() - - return { - "success": True, - "signatures_verifiees": len(signatures_a_relancer), - "relances_envoyees": nb_relances, - "message": f"{nb_relances} email(s) de relance envoyé(s)", - } - - except Exception as e: - logger.error(f"Erreur relances automatiques: {e}") - raise HTTPException(500, str(e)) - - -@app.get("/signature/universign/status", tags=["Signatures"]) -async def statut_signature(docId: str = Query(...)): - try: - async with async_session_factory() as session: - query = select(SignatureLog).where(SignatureLog.document_id == docId) - result = await session.execute(query) - signature_log = result.scalar_one_or_none() - - if not signature_log: - raise HTTPException(404, "Signature introuvable") - - statut = await universign_statut(signature_log.transaction_id) - - return { - "doc_id": docId, - "statut": statut["statut"], - "date_signature": statut.get("date_signature"), - } - except HTTPException: - raise - except Exception as e: - logger.error(f"Erreur statut signature: {e}") - raise HTTPException(500, str(e)) - - -@app.get("/signatures", tags=["Signatures"]) -async def lister_signatures( - statut: Optional[StatutSignature] = Query(None), - limit: int = Query(100, le=1000), - session: AsyncSession = Depends(get_session), -): - query = select(SignatureLog).order_by(SignatureLog.date_envoi.desc()) - - if statut: - statut_db = StatutSignatureDB[statut.value] - query = query.where(SignatureLog.statut == statut_db) - - query = query.limit(limit) - result = await session.execute(query) - signatures = result.scalars().all() - - return [ - { - "id": sig.id, - "document_id": sig.document_id, - "type_document": sig.type_document.value, - "transaction_id": sig.transaction_id, - "signer_url": sig.signer_url, - "email_signataire": sig.email_signataire, - "nom_signataire": sig.nom_signataire, - "statut": sig.statut.value, - "date_envoi": sig.date_envoi.isoformat() if sig.date_envoi else None, - "date_signature": ( - sig.date_signature.isoformat() if sig.date_signature else None - ), - "est_relance": sig.est_relance, - "nb_relances": sig.nb_relances or 0, - } - for sig in signatures - ] - - -@app.get("/signatures/{transaction_id}/status", tags=["Signatures"]) -async def statut_signature_detail( - transaction_id: str, session: AsyncSession = Depends(get_session) -): - query = select(SignatureLog).where(SignatureLog.transaction_id == transaction_id) - result = await session.execute(query) - signature_log = result.scalar_one_or_none() - - if not signature_log: - raise HTTPException(404, f"Transaction {transaction_id} introuvable") - - statut_universign = await universign_statut(transaction_id) - - if statut_universign.get("statut") != "ERREUR": - statut_map = { - "EN_ATTENTE": StatutSignatureDB.EN_ATTENTE, - "ENVOYE": StatutSignatureDB.ENVOYE, - "SIGNE": StatutSignatureDB.SIGNE, - "REFUSE": StatutSignatureDB.REFUSE, - "EXPIRE": StatutSignatureDB.EXPIRE, - } - - nouveau_statut = statut_map.get( - statut_universign["statut"], StatutSignatureDB.EN_ATTENTE - ) - - signature_log.statut = nouveau_statut - - if statut_universign.get("date_signature"): - signature_log.date_signature = datetime.fromisoformat( - statut_universign["date_signature"].replace("Z", "+00:00") - ) - - await session.commit() - - return { - "transaction_id": transaction_id, - "document_id": signature_log.document_id, - "statut": signature_log.statut.value, - "email_signataire": signature_log.email_signataire, - "date_envoi": ( - signature_log.date_envoi.isoformat() if signature_log.date_envoi else None - ), - "date_signature": ( - signature_log.date_signature.isoformat() - if signature_log.date_signature - else None - ), - "signer_url": signature_log.signer_url, - } - - -@app.post("/signatures/refresh-all", tags=["Signatures"]) -async def rafraichir_statuts_signatures(session: AsyncSession = Depends(get_session)): - query = select(SignatureLog).where( - SignatureLog.statut.in_( - [StatutSignatureDB.EN_ATTENTE, StatutSignatureDB.ENVOYE] - ) - ) - - result = await session.execute(query) - signatures = result.scalars().all() - nb_mises_a_jour = 0 - - for sig in signatures: - try: - statut_universign = await universign_statut(sig.transaction_id) - - if statut_universign.get("statut") != "ERREUR": - statut_map = { - "SIGNE": StatutSignatureDB.SIGNE, - "REFUSE": StatutSignatureDB.REFUSE, - "EXPIRE": StatutSignatureDB.EXPIRE, - } - - nouveau = statut_map.get(statut_universign["statut"]) - - if nouveau and nouveau != sig.statut: - sig.statut = nouveau - - if statut_universign.get("date_signature"): - sig.date_signature = datetime.fromisoformat( - statut_universign["date_signature"].replace("Z", "+00:00") - ) - - nb_mises_a_jour += 1 - - except Exception as e: - logger.error(f"Erreur refresh signature {sig.transaction_id}: {e}") - continue - - await session.commit() - - return { - "success": True, - "nb_signatures_verifiees": len(signatures), - "nb_mises_a_jour": nb_mises_a_jour, - } - - class EmailBatch(BaseModel): destinataires: List[EmailStr] = Field(..., min_length=1, max_length=100) sujet: str = Field(..., min_length=1, max_length=500) @@ -1756,14 +1497,19 @@ class TemplatePreview(BaseModel): @app.get("/templates/emails", response_model=List[TemplateEmail], tags=["Emails"]) -async def lister_templates(): +async def lister_templates( + user: User = Depends(get_current_user), +): return [TemplateEmail(**template) for template in templates_email_db.values()] @app.get( "/templates/emails/{template_id}", response_model=TemplateEmail, tags=["Emails"] ) -async def lire_template(template_id: str): +async def lire_template( + template_id: str, + user: User = Depends(get_current_user), +): if template_id not in templates_email_db: raise HTTPException(404, f"Template {template_id} introuvable") @@ -1771,7 +1517,10 @@ async def lire_template(template_id: str): @app.post("/templates/emails", response_model=TemplateEmail, tags=["Emails"]) -async def creer_template(template: TemplateEmail): +async def creer_template( + template: TemplateEmail, + user: User = Depends(get_current_user), +): template_id = str(uuid.uuid4()) templates_email_db[template_id] = { @@ -1790,7 +1539,11 @@ async def creer_template(template: TemplateEmail): @app.put( "/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, + user: User = Depends(get_current_user), +): if template_id not in templates_email_db: raise HTTPException(404, f"Template {template_id} introuvable") @@ -1811,7 +1564,10 @@ async def modifier_template(template_id: str, template: TemplateEmail): @app.delete("/templates/emails/{template_id}", tags=["Emails"]) -async def supprimer_template(template_id: str): +async def supprimer_template( + template_id: str, + user: User = Depends(get_current_user), +): if template_id not in templates_email_db: raise HTTPException(404, f"Template {template_id} introuvable") @@ -2641,6 +2397,7 @@ async def lister_utilisateurs_debug( limit: int = Query(100, le=1000), role: Optional[str] = Query(None), verified_only: bool = Query(False), + user: User = Depends(get_current_user), ): from database import User from sqlalchemy import select diff --git a/routes/auth.py b/routes/auth.py index 5fc2554..d6e6761 100644 --- a/routes/auth.py +++ b/routes/auth.py @@ -7,6 +7,7 @@ from typing import Optional import uuid from database import get_session, User, RefreshToken, LoginAttempt +from core.dependencies import get_current_user from security.auth import ( hash_password, verify_password, @@ -19,7 +20,6 @@ from security.auth import ( hash_token, ) from services.email_service import AuthEmailService -from core.dependencies import get_current_user from config.config import settings import logging diff --git a/routes/universign.py b/routes/universign.py index 20d7960..e8dfada 100644 --- a/routes/universign.py +++ b/routes/universign.py @@ -1,11 +1,11 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Request -from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select, func, and_ +from sqlalchemy import select, func from sqlalchemy.orm import selectinload from typing import List, Optional -from datetime import datetime, timedelta +from datetime import datetime import logging +from core.dependencies import get_current_user from data.data import templates_signature_email from email_queue import email_queue from database import UniversignSignerStatus, UniversignTransactionStatus, get_session @@ -32,7 +32,9 @@ from schemas import ( logger = logging.getLogger(__name__) -router = APIRouter(prefix="/universign", tags=["Universign"]) +router = APIRouter( + prefix="/universign", tags=["Universign"], dependencies=[Depends(get_current_user)] +) sync_service = UniversignSyncService( api_url=settings.universign_api_url, api_key=settings.universign_api_key @@ -494,14 +496,11 @@ async def sync_all_transactions( return {"success": True, "stats": stats, "timestamp": datetime.now().isoformat()} -@router.post("/webhook") -@router.post("/webhook/") +@router.post("/webhook", dependencies=[]) +@router.post("/webhook/", dependencies=[]) async def webhook_universign( request: Request, session: AsyncSession = Depends(get_session) ): - """ - CORRECTION : Extraction correcte du transaction_id selon la structure réelle d'Universign - """ try: payload = await request.json() @@ -1082,159 +1081,6 @@ async def trouver_transactions_inconsistantes( raise HTTPException(500, str(e)) -@router.post("/admin/nettoyer-transactions-erreur", tags=["Admin"]) -async def nettoyer_transactions_erreur( - age_jours: int = Query( - 7, description="Supprimer les transactions en erreur de plus de X jours" - ), - session: AsyncSession = Depends(get_session), -): - try: - date_limite = datetime.now() - timedelta(days=age_jours) - - query = select(UniversignTransaction).where( - and_( - UniversignTransaction.local_status == LocalDocumentStatus.ERROR, - UniversignTransaction.created_at < date_limite, - ) - ) - - result = await session.execute(query) - transactions = result.scalars().all() - - supprimees = [] - for tx in transactions: - supprimees.append( - { - "transaction_id": tx.transaction_id, - "document_id": tx.sage_document_id, - "date_creation": tx.created_at.isoformat(), - "erreur": tx.sync_error, - } - ) - await session.delete(tx) - - await session.commit() - - return { - "success": True, - "transactions_supprimees": len(supprimees), - "age_limite_jours": age_jours, - "details": supprimees, - } - - except Exception as e: - logger.error(f"Erreur nettoyage: {e}") - raise HTTPException(500, str(e)) - - -@router.get("/debug/webhook-payload/{transaction_id}", tags=["Debug"]) -async def voir_dernier_webhook( - transaction_id: str, session: AsyncSession = Depends(get_session) -): - try: - query = select(UniversignTransaction).where( - UniversignTransaction.transaction_id == transaction_id - ) - result = await session.execute(query) - tx = result.scalar_one_or_none() - - if not tx: - raise HTTPException(404, "Transaction introuvable") - - logs_query = ( - select(UniversignSyncLog) - .where( - and_( - UniversignSyncLog.transaction_id == tx.id, - UniversignSyncLog.sync_type.like("webhook:%"), - ) - ) - .order_by(UniversignSyncLog.sync_timestamp.desc()) - .limit(1) - ) - - logs_result = await session.execute(logs_query) - last_webhook_log = logs_result.scalar_one_or_none() - - if not last_webhook_log: - return { - "transaction_id": transaction_id, - "webhook_recu": tx.webhook_received, - "dernier_payload": None, - "message": "Aucun webhook reçu pour cette transaction", - } - - return { - "transaction_id": transaction_id, - "webhook_recu": tx.webhook_received, - "dernier_webhook": { - "timestamp": last_webhook_log.sync_timestamp.isoformat(), - "type": last_webhook_log.sync_type, - "success": last_webhook_log.success, - "payload": json.loads(last_webhook_log.changes_detected) - if last_webhook_log.changes_detected - else None, - }, - } - - except HTTPException: - raise - except Exception as e: - logger.error(f"Erreur debug webhook: {e}") - raise HTTPException(500, str(e)) - - -@router.get( - "/transactions/{transaction_id}/document/download", tags=["Documents Signés"] -) -async def telecharger_document_signe( - transaction_id: str, session: AsyncSession = Depends(get_session) -): - try: - query = select(UniversignTransaction).where( - UniversignTransaction.transaction_id == transaction_id - ) - result = await session.execute(query) - transaction = result.scalar_one_or_none() - - if not transaction: - raise HTTPException(404, f"Transaction {transaction_id} introuvable") - - if not transaction.signed_document_path: - raise HTTPException( - 404, - "Document signé non disponible localement. " - "Utilisez POST /admin/download-missing-documents pour le récupérer.", - ) - - file_path = Path(transaction.signed_document_path) - - if not file_path.exists(): - logger.warning(f"Fichier perdu : {file_path}") - raise HTTPException( - 404, - "Fichier introuvable sur le serveur. " - "Utilisez POST /admin/download-missing-documents pour le récupérer.", - ) - - download_name = ( - f"{transaction.sage_document_id}_" - f"{transaction.sage_document_type.name}_" - f"signe.pdf" - ) - - return FileResponse( - path=str(file_path), media_type="application/pdf", filename=download_name - ) - - except HTTPException: - raise - except Exception as e: - logger.error(f"Erreur téléchargement document : {e}", exc_info=True) - raise HTTPException(500, str(e)) - - @router.get("/transactions/{transaction_id}/document/info", tags=["Documents Signés"]) async def info_document_signe( transaction_id: str, session: AsyncSession = Depends(get_session) diff --git a/services/sage_gateway.py b/services/sage_gateway.py index feccaaf..29abf1e 100644 --- a/services/sage_gateway.py +++ b/services/sage_gateway.py @@ -6,7 +6,7 @@ import httpx from datetime import datetime from typing import Optional, Tuple, List from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import false, select, true, update, and_ +from sqlalchemy import false, select, update, and_ import logging from config.config import settings