Sage100-vps/services/universign_sync.py

340 lines
13 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(
"</td>\n </tr>\n \n <!-- Footer -->",
f"""</td>
</tr>
<!-- Download Section -->
<tr>
<td style="padding: 20px 30px; background-color: #f0f9ff; border: 1px solid #90cdf4; border-radius: 4px; margin: 20px 0;">
<p style="color: #2c5282; font-size: 14px; line-height: 1.6; margin: 0 0 15px;">
📄 <strong>Télécharger le document signé :</strong>
</p>
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<a href="{download_link}" style="display: inline-block; background: #48bb78; color: #ffffff; text-decoration: none; padding: 12px 30px; border-radius: 6px; font-size: 14px; font-weight: 600;">
⬇️ Télécharger le PDF signé
</a>
</td>
</tr>
</table>
<p style="color: #718096; font-size: 12px; margin: 15px 0 0; text-align: center;">
Ce lien est valable pendant 1 an
</p>
</td>
</tr>
<!-- Footer -->""",
)
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=transaction.requester_email,
sujet=sujet,
corps_html=corps,
document_ids=transaction.sage_document_id,
type_document=transaction.sage_document_type.value,
statut=StatutEmail.EN_ATTENTE,
date_creation=datetime.now(),
nb_tentatives=0,
)
session.add(email_log)
await session.flush()
self.email_queue.enqueue(email_log.id)
logger.info(f"📧 Email confirmation envoyé à {transaction.requester_email}")
return True
except Exception as e:
logger.error(f"Erreur envoi notification: {e}", exc_info=True)
return False
async def handle_status_transition(
self,
session: AsyncSession,
transaction: UniversignTransaction,
previous_status: str,
new_status: str,
universign_data: Dict,
) -> Tuple[bool, Optional[str]]:
"""
Gère les transitions de statut avec logique métier
"""
logger.info(
f"🔄 Transition: {transaction.transaction_id} "
f"{previous_status}{new_status}"
)
# Si passage à SIGNE (completed)
if new_status == "SIGNE" and previous_status != "SIGNE":
return await self.handle_signature_completed(
session=session,
transaction=transaction,
universign_data=universign_data,
)
# Si passage à REFUSE
elif new_status == "REFUSE" and previous_status != "REFUSE":
await self._update_sage_to_refused(transaction)
# Si passage à EXPIRE
elif new_status == "EXPIRE" and previous_status != "EXPIRE":
await self._update_sage_to_expired(transaction)
return True, None
async def _update_sage_to_refused(self, transaction: UniversignTransaction):
"""Met à jour Sage quand signature refusée"""
try:
if not self.sage_client:
return
# Statut 3 = Perdu/Refusé (selon config Sage)
self.sage_client.changer_statut_document(
document_type_code=transaction.sage_document_type.value,
numero=transaction.sage_document_id,
nouveau_statut=3,
)
logger.info(
f"📛 Statut Sage → 3 (Refusé) pour {transaction.sage_document_id}"
)
except Exception as e:
logger.error(f"Erreur mise à jour Sage (refusé): {e}")
async def _update_sage_to_expired(self, transaction: UniversignTransaction):
"""Met à jour Sage quand signature expirée"""
try:
if not self.sage_client:
return
# Statut 4 = Expiré/Archivé (selon config Sage)
self.sage_client.changer_statut_document(
document_type_code=transaction.sage_document_type.value,
numero=transaction.sage_document_id,
nouveau_statut=4,
)
logger.info(
f"⏰ Statut Sage → 4 (Expiré) pour {transaction.sage_document_id}"
)
except Exception as e:
logger.error(f"Erreur mise à jour Sage (expiré): {e}")