340 lines
13 KiB
Python
340 lines
13 KiB
Python
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}")
|