import uuid import logging from typing import Dict, Optional, Tuple from datetime import datetime from sqlalchemy.ext.asyncio import AsyncSession from database import ( UniversignTransaction, EmailLog, StatutEmail, ) from data.data import templates_signature_email from services.signed_documents import signed_documents logger = logging.getLogger(__name__) class UniversignSyncService: """Service de synchronisation avec logique métier complète""" def __init__(self, api_url: str, api_key: str): self.api_url = api_url.rstrip("/") self.api_key = api_key self.sage_client = None self.email_queue = None self.settings = None def configure(self, sage_client, email_queue, settings): """Configure les dépendances injectées""" self.sage_client = sage_client self.email_queue = email_queue self.settings = settings async def handle_signature_completed( self, session: AsyncSession, transaction: UniversignTransaction, universign_data: Dict, ) -> Tuple[bool, Optional[str]]: """ Gère la complétion d'une signature: 1. Télécharge et stocke le document signé 2. Met à jour le statut Sage à 2 (accepté) 3. Envoie la notification avec lien de téléchargement """ try: logger.info( f"🎯 Traitement signature complétée: {transaction.transaction_id}" ) # Étape 1: Télécharger le document signé document_url = self._extract_document_url(universign_data) if not document_url: error = "URL du document signé non trouvée dans la réponse Universign" logger.error(error) return False, error ( success, file_path, error, ) = await signed_documents.download_and_store( session=session, transaction=transaction, document_url=document_url, api_key=self.api_key, ) if not success: return False, f"Échec téléchargement document: {error}" logger.info(f"✅ Document signé stocké: {file_path}") # Étape 2: Mettre à jour le statut Sage UNIQUEMENT si ≠ 2 current_sage_status = await self._get_current_sage_status(transaction) if current_sage_status != 2: success_sage = await self._update_sage_to_accepted(transaction) if success_sage: logger.info(f"✅ Statut Sage mis à jour: {current_sage_status} → 2") else: logger.warning( f"⚠️ Échec mise à jour statut Sage pour {transaction.sage_document_id}" ) else: logger.info(f"ℹ️ Statut Sage déjà à 2, pas de mise à jour") # Étape 3: Envoyer notification avec lien de téléchargement notification_sent = await self._send_signature_confirmation( session=session, transaction=transaction, download_link=self._generate_download_link(transaction), ) if not notification_sent: logger.warning("⚠️ Notification non envoyée (mais document stocké)") return True, None except Exception as e: error = f"Erreur handle_signature_completed: {str(e)}" logger.error(error, exc_info=True) return False, error def _extract_document_url(self, universign_data: Dict) -> Optional[str]: """Extrait l'URL du document signé depuis la réponse Universign""" try: # Structure: data['documents'][0]['url'] documents = universign_data.get("documents", []) if documents and len(documents) > 0: return documents[0].get("url") # Fallback: vérifier dans les actions actions = universign_data.get("actions", []) for action in actions: if action.get("type") == "download" and action.get("url"): return action["url"] return None except Exception as e: logger.error(f"Erreur extraction URL document: {e}") return None async def _get_current_sage_status(self, transaction: UniversignTransaction) -> int: """Récupère le statut actuel du document dans Sage""" try: if not self.sage_client: logger.warning("sage_client non configuré") return 0 doc = self.sage_client.lire_document( transaction.sage_document_id, transaction.sage_document_type.value ) return doc.get("statut", 0) if doc else 0 except Exception as e: logger.error(f"Erreur lecture statut Sage: {e}") return 0 async def _update_sage_to_accepted( self, transaction: UniversignTransaction ) -> bool: """Met à jour le statut Sage à 2 (accepté)""" try: if not self.sage_client: logger.warning("sage_client non configuré") return False self.sage_client.changer_statut_document( document_type_code=transaction.sage_document_type.value, numero=transaction.sage_document_id, nouveau_statut=2, # Accepté ) return True except Exception as e: logger.error(f"Erreur mise à jour Sage: {e}") return False def _generate_download_link(self, transaction: UniversignTransaction) -> str: """Génère le lien de téléchargement sécurisé""" base_url = ( self.settings.api_base_url if self.settings else "http://localhost:8000" ) return f"{base_url}/universign/documents/{transaction.id}/download" async def _send_signature_confirmation( self, session: AsyncSession, transaction: UniversignTransaction, download_link: str, ) -> bool: """Envoie l'email de confirmation avec lien de téléchargement""" try: if not self.email_queue or not self.settings: logger.warning("email_queue ou settings non configuré") return False template = templates_signature_email["signature_confirmee"] type_labels = { 0: "Devis", 10: "Commande", 30: "Bon de Livraison", 60: "Facture", 50: "Avoir", } variables = { "NOM_SIGNATAIRE": transaction.requester_name or "Client", "TYPE_DOC": type_labels.get( transaction.sage_document_type.value, "Document" ), "NUMERO": transaction.sage_document_id, "DATE_SIGNATURE": transaction.signed_at.strftime("%d/%m/%Y à %H:%M") if transaction.signed_at else datetime.now().strftime("%d/%m/%Y à %H:%M"), "TRANSACTION_ID": transaction.transaction_id, "CONTACT_EMAIL": self.settings.smtp_from, "DOWNLOAD_LINK": download_link, # Nouvelle variable } sujet = template["sujet"] # Corps modifié pour inclure le lien de téléchargement corps = template["corps_html"].replace( "\n \n \n ", f"""
📄 Télécharger le document signé :
| ⬇️ Télécharger le PDF signé |
Ce lien est valable pendant 1 an