From 410d4553d544a12b0834020b74dd052e2476e65f Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Tue, 6 Jan 2026 11:29:36 +0300 Subject: [PATCH] refactor(universign): clean up code and update status constants --- api.py | 3 +- routes/universign.py | 8 +-- services/universign_sync.py | 99 +--------------------------- utils/generic_functions.py | 128 +++++------------------------------- 4 files changed, 24 insertions(+), 214 deletions(-) diff --git a/api.py b/api.py index b4448e2..0ebea1f 100644 --- a/api.py +++ b/api.py @@ -1,4 +1,4 @@ -from fastapi import FastAPI, HTTPException, Path, Query, Depends, status, Body, Request +from fastapi import FastAPI, HTTPException, Path, Query, Depends, status, Body from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse from fastapi.encoders import jsonable_encoder @@ -90,7 +90,6 @@ from core.sage_context import ( ) from utils.generic_functions import ( _preparer_lignes_document, - normaliser_type_doc, universign_envoyer, universign_statut, ) diff --git a/routes/universign.py b/routes/universign.py index ff90d85..d423c7e 100644 --- a/routes/universign.py +++ b/routes/universign.py @@ -481,22 +481,22 @@ async def get_sync_stats(session: AsyncSession = Depends(get_session)): # Par statut signed_query = select(func.count(UniversignTransaction.id)).where( - UniversignTransaction.local_status == LocalDocumentStatus.SIGNE + UniversignTransaction.local_status == LocalDocumentStatus.SIGNED ) signed = (await session.execute(signed_query)).scalar() in_progress_query = select(func.count(UniversignTransaction.id)).where( - UniversignTransaction.local_status == LocalDocumentStatus.EN_COURS + UniversignTransaction.local_status == LocalDocumentStatus.IN_PROGRESS ) in_progress = (await session.execute(in_progress_query)).scalar() refused_query = select(func.count(UniversignTransaction.id)).where( - UniversignTransaction.local_status == LocalDocumentStatus.REFUSE + UniversignTransaction.local_status == LocalDocumentStatus.REJECTED ) refused = (await session.execute(refused_query)).scalar() expired_query = select(func.count(UniversignTransaction.id)).where( - UniversignTransaction.local_status == LocalDocumentStatus.EXPIRE + UniversignTransaction.local_status == LocalDocumentStatus.EXPIRED ) expired = (await session.execute(expired_query)).scalar() diff --git a/services/universign_sync.py b/services/universign_sync.py index 1abd24b..da89390 100644 --- a/services/universign_sync.py +++ b/services/universign_sync.py @@ -1,16 +1,10 @@ -""" -Service de synchronisation Universign -Architecture : polling + webhooks avec retry et gestion d'erreurs -""" - import requests import json import logging -from typing import Dict, List, Optional, Tuple +from typing import Dict, Optional, Tuple from datetime import datetime, timedelta from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, and_, or_ -from sqlalchemy.orm import selectinload from database import ( UniversignTransaction, @@ -22,7 +16,6 @@ from database import ( ) from utils.universign_status_mapping import ( map_universign_to_local, - get_sage_status_code, is_transition_allowed, get_status_actions, is_final_status, @@ -33,30 +26,13 @@ logger = logging.getLogger(__name__) class UniversignSyncService: - """ - Service centralisé de synchronisation Universign - """ - def __init__(self, api_url: str, api_key: str, timeout: int = 30): self.api_url = api_url.rstrip("/") self.api_key = api_key self.timeout = timeout self.auth = (api_key, "") - # ======================================== - # 1. RÉCUPÉRATION DEPUIS UNIVERSIGN - # ======================================== - def fetch_transaction_status(self, transaction_id: str) -> Optional[Dict]: - """ - Récupère le statut actuel d'une transaction depuis Universign - - Args: - transaction_id: ID Universign (ex: tr_abc123) - - Returns: - Dictionnaire avec les données Universign ou None - """ start_time = datetime.now() try: @@ -104,30 +80,12 @@ class UniversignSyncService: logger.error(f"Erreur fetch {transaction_id}: {e}", exc_info=True) return None - # ======================================== - # 2. SYNCHRONISATION UNITAIRE - # ======================================== - async def sync_transaction( self, session: AsyncSession, transaction: UniversignTransaction, force: bool = False, ) -> Tuple[bool, Optional[str]]: - """ - Synchronise UNE transaction avec Universign - - Args: - session: Session BDD - transaction: Transaction à synchroniser - force: Forcer même si statut final - - Returns: - (success: bool, error_message: Optional[str]) - """ - # === VÉRIFICATIONS PRÉALABLES === - - # Si statut final et pas de force, skip if is_final_status(transaction.local_status.value) and not force: logger.debug( f"Skip {transaction.transaction_id}: " @@ -263,18 +221,6 @@ class UniversignSyncService: async def sync_all_pending( self, session: AsyncSession, max_transactions: int = 50 ) -> Dict[str, int]: - """ - Synchronise toutes les transactions en attente - - Args: - session: Session BDD - max_transactions: Nombre max de transactions à traiter - - Returns: - Statistiques de synchronisation - """ - # === SÉLECTION TRANSACTIONS À SYNCHRONISER === - query = ( select(UniversignTransaction) .where( @@ -304,8 +250,6 @@ class UniversignSyncService: result = await session.execute(query) transactions = result.scalars().all() - # === STATISTIQUES === - stats = { "total_found": len(transactions), "success": 0, @@ -314,8 +258,6 @@ class UniversignSyncService: "status_changes": 0, } - # === TRAITEMENT === - for transaction in transactions: try: previous_status = transaction.local_status.value @@ -345,34 +287,16 @@ class UniversignSyncService: return stats - # ======================================== - # 4. WEBHOOK HANDLER - # ======================================== - async def process_webhook( self, session: AsyncSession, payload: Dict ) -> Tuple[bool, Optional[str]]: - """ - Traite un webhook Universign - - Args: - session: Session BDD - payload: Corps du webhook - - Returns: - (success: bool, error_message: Optional[str]) - """ try: - # === EXTRACTION DONNÉES === - event_type = payload.get("event") transaction_id = payload.get("transaction_id") or payload.get("id") if not transaction_id: return False, "Pas de transaction_id dans le webhook" - # === RECHERCHE TRANSACTION === - query = select(UniversignTransaction).where( UniversignTransaction.transaction_id == transaction_id ) @@ -385,18 +309,12 @@ class UniversignSyncService: ) return False, "Transaction inconnue" - # === MARQUAGE WEBHOOK === - transaction.webhook_received = True - # === SYNCHRONISATION === - success, error = await self.sync_transaction( session, transaction, force=True ) - # === LOG WEBHOOK === - await self._log_sync_attempt( session=session, transaction=transaction, @@ -419,10 +337,6 @@ class UniversignSyncService: logger.error(f"Erreur traitement webhook: {e}", exc_info=True) return False, str(e) - # ======================================== - # 5. HELPERS PRIVÉS - # ======================================== - async def _sync_signers( self, session: AsyncSession, @@ -524,20 +438,11 @@ class UniversignSyncService: return None try: return datetime.fromisoformat(date_str.replace("Z", "+00:00")) - except: + except Exception: return None -# ======================================== -# 6. TÂCHE PLANIFIÉE (BACKGROUND) -# ======================================== - - class UniversignSyncScheduler: - """ - Planificateur de synchronisation automatique - """ - def __init__(self, sync_service: UniversignSyncService, interval_minutes: int = 5): self.sync_service = sync_service self.interval_minutes = interval_minutes diff --git a/utils/generic_functions.py b/utils/generic_functions.py index a628612..753e7d2 100644 --- a/utils/generic_functions.py +++ b/utils/generic_functions.py @@ -1,7 +1,6 @@ -from typing import Dict, List, Optional +from typing import Dict, List from config.config import settings import logging -from enum import Enum from datetime import datetime import uuid @@ -290,47 +289,31 @@ def _preparer_lignes_document(lignes: List) -> List[Dict]: ] -# ======================================== -# MAPPING UNIVERSIGN → LOCAL -# ======================================== - UNIVERSIGN_TO_LOCAL: Dict[str, str] = { # États initiaux - "draft": "EN_ATTENTE", # Transaction créée - "ready": "EN_ATTENTE", # Prête mais pas envoyée + "draft": "EN_ATTENTE", + "ready": "EN_ATTENTE", # En cours - "started": "EN_COURS", # Envoyée, en attente de signature + "started": "EN_COURS", # États finaux (succès) - "completed": "SIGNE", # ✓ Signé avec succès + "completed": "SIGNE", # États finaux (échec) - "refused": "REFUSE", # ✗ Refusé par un signataire - "expired": "EXPIRE", # ⏰ Délai expiré - "canceled": "REFUSE", # ✗ Annulé manuellement - "failed": "ERREUR", # ⚠️ Erreur technique + "refused": "REFUSE", + "expired": "EXPIRE", + "canceled": "REFUSE", + "failed": "ERREUR", } -# ======================================== -# MAPPING LOCAL → SAGE (CHAMP LIBRE) -# ======================================== - LOCAL_TO_SAGE_STATUS: Dict[str, int] = { - """ - À stocker dans un champ libre Sage (ex: CB_STATUT_SIGNATURE) - """ - "EN_ATTENTE": 0, # Non envoyé - "EN_COURS": 1, # Envoyé, en attente - "SIGNE": 2, # ✓ Signé (peut déclencher workflow) - "REFUSE": 3, # ✗ Refusé - "EXPIRE": 4, # ⏰ Expiré - "ERREUR": 5, # ⚠️ Erreur + "EN_ATTENTE": 0, + "EN_COURS": 1, + "SIGNE": 2, + "REFUSE": 3, + "EXPIRE": 4, + "ERREUR": 5, } - -# ======================================== -# ACTIONS MÉTIER PAR STATUT -# ======================================== - STATUS_ACTIONS: Dict[str, Dict[str, any]] = { """ Actions automatiques à déclencher selon le statut @@ -366,10 +349,6 @@ STATUS_ACTIONS: Dict[str, Dict[str, any]] = { } -# ======================================== -# RÈGLES DE TRANSITION -# ======================================== - ALLOWED_TRANSITIONS: Dict[str, list] = { """ Transitions de statuts autorisées (validation) @@ -383,21 +362,7 @@ ALLOWED_TRANSITIONS: Dict[str, list] = { } -# ======================================== -# FONCTION DE MAPPING -# ======================================== - - def map_universign_to_local(universign_status: str) -> str: - """ - Convertit un statut Universign en statut local - - Args: - universign_status: Statut brut Universign - - Returns: - Statut local normalisé - """ return UNIVERSIGN_TO_LOCAL.get( universign_status.lower(), "ERREUR", # Fallback si statut inconnu @@ -405,29 +370,10 @@ def map_universign_to_local(universign_status: str) -> str: def get_sage_status_code(local_status: str) -> int: - """ - Obtient le code numérique pour Sage - - Args: - local_status: Statut local (EN_ATTENTE, SIGNE, etc.) - - Returns: - Code numérique pour champ libre Sage - """ return LOCAL_TO_SAGE_STATUS.get(local_status, 5) def is_transition_allowed(from_status: str, to_status: str) -> bool: - """ - Vérifie si une transition de statut est valide - - Args: - from_status: Statut actuel - to_status: Nouveau statut - - Returns: - True si la transition est autorisée - """ if from_status == to_status: return True # Même statut = OK (idempotence) @@ -436,70 +382,30 @@ def is_transition_allowed(from_status: str, to_status: str) -> bool: def get_status_actions(local_status: str) -> Dict[str, any]: - """ - Obtient les actions à exécuter pour un statut - - Args: - local_status: Statut local - - Returns: - Dictionnaire des actions à exécuter - """ return STATUS_ACTIONS.get(local_status, {}) def is_final_status(local_status: str) -> bool: - """ - Détermine si le statut est final (pas de synchronisation nécessaire) - - Args: - local_status: Statut local - - Returns: - True si statut final - """ return local_status in ["SIGNE", "REFUSE", "EXPIRE"] -# ======================================== -# PRIORITÉS DE STATUTS (POUR CONFLITS) -# ======================================== - STATUS_PRIORITY: Dict[str, int] = { - """ - En cas de conflit de synchronisation, prendre le statut - avec la priorité la plus élevée - """ - "ERREUR": 0, # Priorité la plus basse + "ERREUR": 0, "EN_ATTENTE": 1, "EN_COURS": 2, "EXPIRE": 3, "REFUSE": 4, - "SIGNE": 5, # Priorité la plus élevée (état final souhaité) + "SIGNE": 5, } def resolve_status_conflict(status_a: str, status_b: str) -> str: - """ - Résout un conflit entre deux statuts (prend le plus prioritaire) - - Args: - status_a: Premier statut - status_b: Second statut - - Returns: - Statut prioritaire - """ priority_a = STATUS_PRIORITY.get(status_a, 0) priority_b = STATUS_PRIORITY.get(status_b, 0) return status_a if priority_a >= priority_b else status_b -# ======================================== -# MESSAGES UTILISATEUR -# ======================================== - STATUS_MESSAGES: Dict[str, Dict[str, str]] = { "EN_ATTENTE": { "fr": "Document en attente d'envoi",