""" Mapping exhaustif : Universign ↔ Local ↔ Sage """ from typing import Dict, Optional from enum import Enum # ======================================== # 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 # En cours "started": "EN_COURS", # Envoyée, en attente de signature # États finaux (succès) "completed": "SIGNE", # ✓ Signé avec succès # États finaux (échec) "refused": "REFUSE", # ✗ Refusé par un signataire "expired": "EXPIRE", # ⏰ Délai expiré "canceled": "REFUSE", # ✗ Annulé manuellement "failed": "ERREUR", # ⚠️ Erreur technique } # ======================================== # 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 } # ======================================== # ACTIONS MÉTIER PAR STATUT # ======================================== STATUS_ACTIONS: Dict[str, Dict[str, any]] = { """ Actions automatiques à déclencher selon le statut """ "SIGNE": { "update_sage_status": True, # Mettre à jour Sage "trigger_workflow": True, # Déclencher transformation (devis→commande) "send_notification": True, # Email de confirmation "archive_document": True, # Archiver le PDF signé "update_sage_field": "CB_DateSignature", # Champ libre Sage }, "REFUSE": { "update_sage_status": True, "trigger_workflow": False, "send_notification": True, "archive_document": False, "alert_sales": True, # Alerter commercial }, "EXPIRE": { "update_sage_status": True, "trigger_workflow": False, "send_notification": True, "archive_document": False, "schedule_reminder": True, # Programmer relance }, "ERREUR": { "update_sage_status": False, "trigger_workflow": False, "send_notification": False, "log_error": True, "retry_sync": True, }, } # ======================================== # RÈGLES DE TRANSITION # ======================================== ALLOWED_TRANSITIONS: Dict[str, list] = { """ Transitions de statuts autorisées (validation) """ "EN_ATTENTE": ["EN_COURS", "ERREUR"], "EN_COURS": ["SIGNE", "REFUSE", "EXPIRE", "ERREUR"], "SIGNE": [], # État final, pas de retour "REFUSE": [], # État final "EXPIRE": [], # État final "ERREUR": ["EN_ATTENTE", "EN_COURS"], # Retry possible } # ======================================== # 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 ) 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) allowed = ALLOWED_TRANSITIONS.get(from_status, []) return to_status in allowed 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 "EN_ATTENTE": 1, "EN_COURS": 2, "EXPIRE": 3, "REFUSE": 4, "SIGNE": 5, # Priorité la plus élevée (état final souhaité) } 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", "en": "Document pending", "icon": "⏳", "color": "gray", }, "EN_COURS": { "fr": "En attente de signature", "en": "Awaiting signature", "icon": "✍️", "color": "blue", }, "SIGNE": { "fr": "Signé avec succès", "en": "Successfully signed", "icon": "✅", "color": "green", }, "REFUSE": { "fr": "Signature refusée", "en": "Signature refused", "icon": "❌", "color": "red", }, "EXPIRE": { "fr": "Délai de signature expiré", "en": "Signature expired", "icon": "⏰", "color": "orange", }, "ERREUR": { "fr": "Erreur technique", "en": "Technical error", "icon": "⚠️", "color": "red", }, } def get_status_message(local_status: str, lang: str = "fr") -> str: """ Obtient le message utilisateur pour un statut Args: local_status: Statut local lang: Langue (fr, en) Returns: Message formaté """ status_info = STATUS_MESSAGES.get(local_status, {}) icon = status_info.get("icon", "") message = status_info.get(lang, local_status) return f"{icon} {message}"