feat(signature): add email templates and tracking for signature workflow

This commit is contained in:
Fanilo-Nantenaina 2025-12-18 10:09:48 +03:00
parent 4c53477efe
commit 282ffe4898
2 changed files with 702 additions and 48 deletions

715
api.py
View file

@ -1,4 +1,4 @@
from fastapi import FastAPI, HTTPException, Path, Query, Depends, status, Body
from fastapi import FastAPI, HTTPException, Path, Query, Depends, status, Body, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field, EmailStr, validator, field_validator
@ -1050,10 +1050,363 @@ class MouvementStockResponse(BaseModel):
nb_lignes: int = Field(..., description="Nombre de lignes")
async def universign_envoyer(
doc_id: str, pdf_bytes: bytes, email: str, nom: str
templates_signature_email = {
"demande_signature": {
"id": "demande_signature",
"nom": "Demande de Signature Électronique",
"sujet": "📝 Signature requise - {{TYPE_DOC}} {{NUMERO}}",
"corps_html": """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 0; padding: 0; font-family: 'Segoe UI', Arial, sans-serif; background-color: #f4f7fa;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f7fa; padding: 40px 0;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<!-- Header -->
<tr>
<td style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="color: #ffffff; margin: 0; font-size: 24px; font-weight: 600;">
📝 Signature Électronique Requise
</h1>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 40px 30px;">
<p style="color: #2d3748; font-size: 16px; line-height: 1.6; margin: 0 0 20px;">
Bonjour <strong>{{NOM_SIGNATAIRE}}</strong>,
</p>
<p style="color: #4a5568; font-size: 15px; line-height: 1.6; margin: 0 0 25px;">
Nous vous invitons à signer électroniquement le document suivant :
</p>
<!-- Document Info Box -->
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f7fafc; border-left: 4px solid #667eea; border-radius: 4px; margin-bottom: 30px;">
<tr>
<td style="padding: 20px;">
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td style="color: #718096; font-size: 13px; padding: 5px 0;">Type de document</td>
<td style="color: #2d3748; font-size: 15px; font-weight: 600; text-align: right; padding: 5px 0;">{{TYPE_DOC}}</td>
</tr>
<tr>
<td style="color: #718096; font-size: 13px; padding: 5px 0;">Numéro</td>
<td style="color: #2d3748; font-size: 15px; font-weight: 600; text-align: right; padding: 5px 0;">{{NUMERO}}</td>
</tr>
<tr>
<td style="color: #718096; font-size: 13px; padding: 5px 0;">Date</td>
<td style="color: #2d3748; font-size: 15px; font-weight: 600; text-align: right; padding: 5px 0;">{{DATE}}</td>
</tr>
<tr>
<td style="color: #718096; font-size: 13px; padding: 5px 0;">Montant TTC</td>
<td style="color: #2d3748; font-size: 15px; font-weight: 600; text-align: right; padding: 5px 0;">{{MONTANT_TTC}} </td>
</tr>
</table>
</td>
</tr>
</table>
<p style="color: #4a5568; font-size: 15px; line-height: 1.6; margin: 0 0 30px;">
Cliquez sur le bouton ci-dessous pour accéder au document et apposer votre signature électronique sécurisée :
</p>
<!-- CTA Button -->
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center" style="padding: 10px 0 30px;">
<a href="{{SIGNER_URL}}" style="display: inline-block; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff; text-decoration: none; padding: 16px 40px; border-radius: 6px; font-size: 16px; font-weight: 600; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);">
Signer le document
</a>
</td>
</tr>
</table>
<!-- Info Box -->
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #fffaf0; border: 1px solid #fbd38d; border-radius: 4px; margin-bottom: 20px;">
<tr>
<td style="padding: 15px;">
<p style="color: #744210; font-size: 13px; line-height: 1.5; margin: 0;">
<strong>Important :</strong> Ce lien de signature est valable pendant <strong>30 jours</strong>.
Nous vous recommandons de signer ce document dès que possible.
</p>
</td>
</tr>
</table>
<p style="color: #718096; font-size: 13px; line-height: 1.5; margin: 0;">
<strong>🔒 Signature électronique sécurisée</strong><br>
Votre signature est protégée par notre partenaire de confiance <strong>Universign</strong>,
certifié eIDAS et conforme au RGPD. Votre identité sera vérifiée et le document sera
horodaté de manière infalsifiable.
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #f7fafc; padding: 25px 30px; border-radius: 0 0 8px 8px; border-top: 1px solid #e2e8f0;">
<p style="color: #718096; font-size: 12px; line-height: 1.5; margin: 0 0 10px;">
Vous avez des questions ? Contactez-nous à <a href="mailto:{{CONTACT_EMAIL}}" style="color: #667eea; text-decoration: none;">{{CONTACT_EMAIL}}</a>
</p>
<p style="color: #a0aec0; font-size: 11px; line-height: 1.4; margin: 0;">
Cet email a été envoyé automatiquement par le système Sage 100c Dataven.<br>
Si vous avez reçu cet email par erreur, veuillez nous en informer.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
""",
"variables_disponibles": [
"NOM_SIGNATAIRE",
"TYPE_DOC",
"NUMERO",
"DATE",
"MONTANT_TTC",
"SIGNER_URL",
"CONTACT_EMAIL"
]
},
"signature_confirmee": {
"id": "signature_confirmee",
"nom": "Confirmation de Signature",
"sujet": "✅ Document signé - {{TYPE_DOC}} {{NUMERO}}",
"corps_html": """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 0; padding: 0; font-family: 'Segoe UI', Arial, sans-serif; background-color: #f4f7fa;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f7fa; padding: 40px 0;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<!-- Header -->
<tr>
<td style="background: linear-gradient(135deg, #48bb78 0%, #38a169 100%); padding: 30px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="color: #ffffff; margin: 0; font-size: 24px; font-weight: 600;">
Document Signé avec Succès
</h1>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 40px 30px;">
<p style="color: #2d3748; font-size: 16px; line-height: 1.6; margin: 0 0 20px;">
Bonjour <strong>{{NOM_SIGNATAIRE}}</strong>,
</p>
<p style="color: #4a5568; font-size: 15px; line-height: 1.6; margin: 0 0 25px;">
Nous confirmons la signature électronique du document suivant :
</p>
<!-- Success Box -->
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f0fff4; border-left: 4px solid #48bb78; border-radius: 4px; margin-bottom: 30px;">
<tr>
<td style="padding: 20px;">
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td style="color: #2f855a; font-size: 13px; padding: 5px 0;">Document</td>
<td style="color: #22543d; font-size: 15px; font-weight: 600; text-align: right; padding: 5px 0;">{{TYPE_DOC}} {{NUMERO}}</td>
</tr>
<tr>
<td style="color: #2f855a; font-size: 13px; padding: 5px 0;">Signé le</td>
<td style="color: #22543d; font-size: 15px; font-weight: 600; text-align: right; padding: 5px 0;">{{DATE_SIGNATURE}}</td>
</tr>
<tr>
<td style="color: #2f855a; font-size: 13px; padding: 5px 0;">ID Transaction</td>
<td style="color: #22543d; font-size: 13px; font-family: monospace; text-align: right; padding: 5px 0;">{{TRANSACTION_ID}}</td>
</tr>
</table>
</td>
</tr>
</table>
<p style="color: #4a5568; font-size: 15px; line-height: 1.6; margin: 0 0 25px;">
Le document signé a été automatiquement archivé et est disponible dans votre espace client.
Un certificat de signature électronique conforme eIDAS a été généré.
</p>
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #ebf8ff; border: 1px solid #90cdf4; border-radius: 4px; margin-bottom: 20px;">
<tr>
<td style="padding: 15px;">
<p style="color: #2c5282; font-size: 13px; line-height: 1.5; margin: 0;">
🔐 <strong>Signature certifiée :</strong> Ce document a été signé avec une signature
électronique qualifiée, ayant la même valeur juridique qu'une signature manuscrite
conformément au règlement eIDAS.
</p>
</td>
</tr>
</table>
<p style="color: #718096; font-size: 14px; line-height: 1.6; margin: 0;">
Merci pour votre confiance. Notre équipe reste à votre disposition pour toute question.
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #f7fafc; padding: 25px 30px; border-radius: 0 0 8px 8px; border-top: 1px solid #e2e8f0;">
<p style="color: #718096; font-size: 12px; line-height: 1.5; margin: 0 0 10px;">
Contact : <a href="mailto:{{CONTACT_EMAIL}}" style="color: #48bb78; text-decoration: none;">{{CONTACT_EMAIL}}</a>
</p>
<p style="color: #a0aec0; font-size: 11px; line-height: 1.4; margin: 0;">
Sage 100c Dataven - Système de signature électronique sécurisée
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
""",
"variables_disponibles": [
"NOM_SIGNATAIRE",
"TYPE_DOC",
"NUMERO",
"DATE_SIGNATURE",
"TRANSACTION_ID",
"CONTACT_EMAIL"
]
},
"relance_signature": {
"id": "relance_signature",
"nom": "Relance Signature en Attente",
"sujet": "⏰ Rappel - Signature en attente {{TYPE_DOC}} {{NUMERO}}",
"corps_html": """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 0; padding: 0; font-family: 'Segoe UI', Arial, sans-serif; background-color: #f4f7fa;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f7fa; padding: 40px 0;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<!-- Header -->
<tr>
<td style="background: linear-gradient(135deg, #ed8936 0%, #dd6b20 100%); padding: 30px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="color: #ffffff; margin: 0; font-size: 24px; font-weight: 600;">
Signature en Attente
</h1>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 40px 30px;">
<p style="color: #2d3748; font-size: 16px; line-height: 1.6; margin: 0 0 20px;">
Bonjour <strong>{{NOM_SIGNATAIRE}}</strong>,
</p>
<p style="color: #4a5568; font-size: 15px; line-height: 1.6; margin: 0 0 25px;">
Nous vous avons envoyé il y a <strong>{{NB_JOURS}}</strong> jours un document à signer électroniquement.
Nous constatons que celui-ci n'a pas encore été signé.
</p>
<!-- Warning Box -->
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #fffaf0; border-left: 4px solid #ed8936; border-radius: 4px; margin-bottom: 30px;">
<tr>
<td style="padding: 20px;">
<p style="color: #744210; font-size: 14px; line-height: 1.5; margin: 0 0 10px;">
<strong>Document en attente :</strong> {{TYPE_DOC}} {{NUMERO}}
</p>
<p style="color: #744210; font-size: 13px; line-height: 1.5; margin: 0;">
Le lien de signature expirera dans <strong>{{JOURS_RESTANTS}}</strong> jours
</p>
</td>
</tr>
</table>
<p style="color: #4a5568; font-size: 15px; line-height: 1.6; margin: 0 0 30px;">
Pour éviter tout retard dans le traitement de votre dossier, nous vous invitons à signer ce document dès maintenant :
</p>
<!-- CTA Button -->
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center" style="padding: 10px 0 30px;">
<a href="{{SIGNER_URL}}" style="display: inline-block; background: linear-gradient(135deg, #ed8936 0%, #dd6b20 100%); color: #ffffff; text-decoration: none; padding: 16px 40px; border-radius: 6px; font-size: 16px; font-weight: 600; box-shadow: 0 4px 12px rgba(237, 137, 54, 0.4);">
Signer maintenant
</a>
</td>
</tr>
</table>
<p style="color: #718096; font-size: 13px; line-height: 1.5; margin: 0;">
Si vous rencontrez des difficultés ou avez des questions, n'hésitez pas à nous contacter.
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #f7fafc; padding: 25px 30px; border-radius: 0 0 8px 8px; border-top: 1px solid #e2e8f0;">
<p style="color: #718096; font-size: 12px; line-height: 1.5; margin: 0 0 10px;">
Contact : <a href="mailto:{{CONTACT_EMAIL}}" style="color: #ed8936; text-decoration: none;">{{CONTACT_EMAIL}}</a>
</p>
<p style="color: #a0aec0; font-size: 11px; line-height: 1.4; margin: 0;">
Sage 100c Dataven - Relance automatique
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
""",
"variables_disponibles": [
"NOM_SIGNATAIRE",
"TYPE_DOC",
"NUMERO",
"NB_JOURS",
"JOURS_RESTANTS",
"SIGNER_URL",
"CONTACT_EMAIL"
]
}
}
async def universign_envoyer_avec_email(
doc_id: str,
pdf_bytes: bytes,
email: str,
nom: str,
doc_data: Dict, # Données du document (type, montant, date, etc.)
session: AsyncSession
) -> Dict:
"""Envoi signature via API Universign"""
import requests
try:
@ -1061,23 +1414,25 @@ async def universign_envoyer(
api_url = settings.universign_api_url
auth = (api_key, "")
# Étape 1: Créer transaction
response = requests.post(
f"{api_url}/transactions",
auth=auth,
json={"name": f"Devis {doc_id}", "language": "fr"},
json={
"name": f"{doc_data.get('type_label', 'Document')} {doc_id}",
"language": "fr"
},
timeout=30,
)
response.raise_for_status()
transaction_id = response.json().get("id")
# Étape 2: Upload PDF
files = {"file": (f"Devis_{doc_id}.pdf", pdf_bytes, "application/pdf")}
logger.info(f"✅ Transaction Universign créée: {transaction_id}")
files = {"file": (f"{doc_data.get('type_label', 'Document')}_{doc_id}.pdf", pdf_bytes, "application/pdf")}
response = requests.post(f"{api_url}/files", auth=auth, files=files, timeout=30)
response.raise_for_status()
file_id = response.json().get("id")
# Étape 3: Ajouter document
response = requests.post(
f"{api_url}/transactions/{transaction_id}/documents",
auth=auth,
@ -1087,7 +1442,6 @@ async def universign_envoyer(
response.raise_for_status()
document_id = response.json().get("id")
# Étape 4: Créer champ signature
response = requests.post(
f"{api_url}/transactions/{transaction_id}/documents/{document_id}/fields",
auth=auth,
@ -1097,7 +1451,6 @@ async def universign_envoyer(
response.raise_for_status()
field_id = response.json().get("id")
# Étape 5: Assigner signataire
response = requests.post(
f"{api_url}/transactions/{transaction_id}/signatures",
auth=auth,
@ -1106,9 +1459,10 @@ async def universign_envoyer(
)
response.raise_for_status()
# Étape 6: Démarrer transaction
response = requests.post(
f"{api_url}/transactions/{transaction_id}/start", auth=auth, timeout=30
f"{api_url}/transactions/{transaction_id}/start",
auth=auth,
timeout=30
)
response.raise_for_status()
@ -1119,18 +1473,76 @@ async def universign_envoyer(
else ""
)
logger.info(f"✅ Signature Universign envoyée: {transaction_id}")
if not signer_url:
raise ValueError("URL de signature non retournée par Universign")
logger.info(f"✅ Signature Universign démarrée: {transaction_id}")
template = templates_signature_email["demande_signature"]
# Préparer les variables
type_labels = {
0: "Devis",
10: "Commande",
30: "Bon de Livraison",
60: "Facture",
50: "Avoir"
}
variables = {
"NOM_SIGNATAIRE": nom,
"TYPE_DOC": type_labels.get(doc_data.get("type_doc", 0), "Document"),
"NUMERO": doc_id,
"DATE": doc_data.get("date", datetime.now().strftime("%d/%m/%Y")),
"MONTANT_TTC": f"{doc_data.get('montant_ttc', 0):.2f}",
"SIGNER_URL": signer_url,
"CONTACT_EMAIL": settings.smtp_from
}
# Remplacer les variables dans le template
sujet = template["sujet"]
corps = template["corps_html"]
for var, valeur in variables.items():
sujet = sujet.replace(f"{{{{{var}}}}}", str(valeur))
corps = corps.replace(f"{{{{{var}}}}}", str(valeur))
# Créer log email
email_log = EmailLog(
id=str(uuid.uuid4()),
destinataire=email,
sujet=sujet,
corps_html=corps,
document_ids=doc_id,
type_document=doc_data.get("type_doc"),
statut=StatutEmailEnum.EN_ATTENTE,
date_creation=datetime.now(),
nb_tentatives=0,
)
session.add(email_log)
await session.flush()
# Enqueue l'email
email_queue.enqueue(email_log.id)
logger.info(f"📧 Email de signature envoyé en file: {email}")
return {
"transaction_id": transaction_id,
"signer_url": signer_url,
"statut": "ENVOYE",
"email_log_id": email_log.id,
"email_sent": True
}
except Exception as e:
logger.error(f"❌ Erreur Universign: {e}")
return {"error": str(e), "statut": "ERREUR"}
logger.error(f"❌ Erreur Universign+Email: {e}")
return {
"error": str(e),
"statut": "ERREUR",
"email_sent": False
}
async def universign_statut(transaction_id: str) -> Dict:
"""Récupération statut signature"""
@ -1986,21 +2398,53 @@ async def commande_vers_facture(id: str, session: AsyncSession = Depends(get_ses
logger.error(f"Erreur transformation: {e}")
raise HTTPException(500, str(e))
def normaliser_type_doc(type_doc: int) -> int:
TYPES_AUTORISES = {0, 10, 30, 50, 60}
if type_doc not in TYPES_AUTORISES:
raise ValueError(
f"type_doc invalide ({type_doc}). Valeurs autorisées : {sorted(TYPES_AUTORISES)}"
)
return type_doc if type_doc == 0 else type_doc // 10
# =====================================================
# ENDPOINTS - US-A3 (SIGNATURE ÉLECTRONIQUE)
# =====================================================
@app.post("/signature/universign/send", tags=["Signatures"])
async def envoyer_signature(
demande: SignatureRequest, session: AsyncSession = Depends(get_session)
async def envoyer_signature_optimise(
demande: SignatureRequest,
session: AsyncSession = Depends(get_session)
):
try:
# Générer PDF
pdf_bytes = email_queue._generate_pdf(demande.doc_id, demande.type_doc)
# Récupérer le document depuis Sage
doc = sage_client.lire_document(demande.doc_id, normaliser_type_doc(demande.type_doc))
if not doc:
raise HTTPException(404, f"Document {demande.doc_id} introuvable")
# Envoi Universign
resultat = await universign_envoyer(
demande.doc_id, pdf_bytes, demande.email_signataire, demande.nom_signataire
# Générer PDF
pdf_bytes = email_queue._generate_pdf(demande.doc_id, normaliser_type_doc(demande.type_doc))
# Préparer les données du document pour l'email
doc_data = {
"type_doc": demande.type_doc,
"type_label": {
0: "Devis",
10: "Commande",
30: "Bon de Livraison",
60: "Facture",
50: "Avoir"
}.get(demande.type_doc, "Document"),
"montant_ttc": doc.get("total_ttc", 0),
"date": doc.get("date", datetime.now().strftime("%d/%m/%Y"))
}
# Envoi Universign + Email automatique
resultat = await universign_envoyer_avec_email(
doc_id=demande.doc_id,
pdf_bytes=pdf_bytes,
email=demande.email_signataire,
nom=demande.nom_signataire,
doc_data=doc_data,
session=session
)
if "error" in resultat:
@ -2022,23 +2466,232 @@ async def envoyer_signature(
session.add(signature_log)
await session.commit()
# MAJ champ libre Sage via gateway Windows
# MAJ champ libre Sage
sage_client.mettre_a_jour_champ_libre(
demande.doc_id, demande.type_doc, "UniversignID", resultat["transaction_id"]
demande.doc_id,
demande.type_doc,
"UniversignID",
resultat["transaction_id"]
)
logger.info(f"✅ Signature envoyée: {demande.doc_id}")
logger.info(f"✅ Signature envoyée: {demande.doc_id} (Email: {resultat['email_sent']})")
return {
"success": True,
"transaction_id": resultat["transaction_id"],
"signer_url": resultat["signer_url"],
"email_sent": resultat["email_sent"],
"email_log_id": resultat.get("email_log_id"),
"message": f"Demande de signature envoyée à {demande.email_signataire}"
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Erreur signature: {e}")
logger.error(f"❌ Erreur signature: {e}")
raise HTTPException(500, str(e))
@app.post("/webhooks/universign", tags=["Signatures"])
async def webhook_universign(
request: Request,
session: AsyncSession = Depends(get_session)
):
try:
payload = await request.json()
event_type = payload.get("event")
transaction_id = payload.get("transaction_id")
if not transaction_id:
logger.warning("⚠️ Webhook sans transaction_id")
return {"status": "ignored"}
# Chercher la signature dans la DB
query = select(SignatureLog).where(
SignatureLog.transaction_id == transaction_id
)
result = await session.execute(query)
signature_log = result.scalar_one_or_none()
if not signature_log:
logger.warning(f"⚠️ Transaction {transaction_id} introuvable en DB")
return {"status": "not_found"}
# =============================================
# TRAITER L'EVENT SELON LE TYPE
# =============================================
if event_type == "transaction.completed":
# ✅ SIGNATURE RÉUSSIE
signature_log.statut = StatutSignatureEnum.SIGNE
signature_log.date_signature = datetime.now()
logger.info(f"✅ Signature confirmée: {signature_log.document_id}")
# ENVOYER EMAIL DE CONFIRMATION
template = templates_signature_email["signature_confirmee"]
type_labels = {
0: "Devis", 10: "Commande", 30: "Bon de Livraison",
60: "Facture", 50: "Avoir"
}
variables = {
"NOM_SIGNATAIRE": signature_log.nom_signataire,
"TYPE_DOC": type_labels.get(signature_log.type_document, "Document"),
"NUMERO": signature_log.document_id,
"DATE_SIGNATURE": datetime.now().strftime("%d/%m/%Y à %H:%M"),
"TRANSACTION_ID": transaction_id,
"CONTACT_EMAIL": settings.smtp_from
}
sujet = template["sujet"]
corps = template["corps_html"]
for var, valeur in variables.items():
sujet = sujet.replace(f"{{{{{var}}}}}", str(valeur))
corps = corps.replace(f"{{{{{var}}}}}", str(valeur))
# Créer email de confirmation
email_log = EmailLog(
id=str(uuid.uuid4()),
destinataire=signature_log.email_signataire,
sujet=sujet,
corps_html=corps,
document_ids=signature_log.document_id,
type_document=signature_log.type_document,
statut=StatutEmailEnum.EN_ATTENTE,
date_creation=datetime.now(),
nb_tentatives=0,
)
session.add(email_log)
email_queue.enqueue(email_log.id)
logger.info(f"📧 Email de confirmation envoyé: {signature_log.email_signataire}")
elif event_type == "transaction.refused":
# ❌ SIGNATURE REFUSÉE
signature_log.statut = StatutSignatureEnum.REFUSE
logger.warning(f"❌ Signature refusée: {signature_log.document_id}")
elif event_type == "transaction.expired":
# ⏰ TRANSACTION EXPIRÉE
signature_log.statut = StatutSignatureEnum.EXPIRE
logger.warning(f"⏰ Transaction expirée: {signature_log.document_id}")
await session.commit()
return {
"status": "processed",
"event": event_type,
"transaction_id": transaction_id
}
except Exception as e:
logger.error(f"❌ Erreur webhook Universign: {e}")
return {"status": "error", "message": str(e)}
@app.get("/admin/signatures/relances-auto", tags=["Admin"])
async def relancer_signatures_automatique(
session: AsyncSession = Depends(get_session)
):
try:
from datetime import timedelta
# Chercher signatures en attente depuis > 7 jours
date_limite = datetime.now() - timedelta(days=7)
query = select(SignatureLog).where(
SignatureLog.statut.in_([
StatutSignatureEnum.EN_ATTENTE,
StatutSignatureEnum.ENVOYE
]),
SignatureLog.date_envoi < date_limite,
SignatureLog.nb_relances < 3 # Max 3 relances
)
result = await session.execute(query)
signatures_a_relancer = result.scalars().all()
nb_relances = 0
for signature in signatures_a_relancer:
try:
# Calculer jours écoulés
nb_jours = (datetime.now() - signature.date_envoi).days
jours_restants = 30 - nb_jours # Lien expire après 30 jours
if jours_restants <= 0:
# Transaction expirée
signature.statut = StatutSignatureEnum.EXPIRE
continue
# Préparer email de relance
template = templates_signature_email["relance_signature"]
type_labels = {
0: "Devis", 10: "Commande", 30: "Bon de Livraison",
60: "Facture", 50: "Avoir"
}
variables = {
"NOM_SIGNATAIRE": signature.nom_signataire,
"TYPE_DOC": type_labels.get(signature.type_document, "Document"),
"NUMERO": signature.document_id,
"NB_JOURS": str(nb_jours),
"JOURS_RESTANTS": str(jours_restants),
"SIGNER_URL": signature.signer_url,
"CONTACT_EMAIL": settings.smtp_from
}
sujet = template["sujet"]
corps = template["corps_html"]
for var, valeur in variables.items():
sujet = sujet.replace(f"{{{{{var}}}}}", str(valeur))
corps = corps.replace(f"{{{{{var}}}}}", str(valeur))
# Créer email de relance
email_log = EmailLog(
id=str(uuid.uuid4()),
destinataire=signature.email_signataire,
sujet=sujet,
corps_html=corps,
document_ids=signature.document_id,
type_document=signature.type_document,
statut=StatutEmailEnum.EN_ATTENTE,
date_creation=datetime.now(),
nb_tentatives=0,
)
session.add(email_log)
email_queue.enqueue(email_log.id)
# Incrémenter compteur de relances
signature.est_relance = True
signature.nb_relances = (signature.nb_relances or 0) + 1
nb_relances += 1
logger.info(f"📧 Relance envoyée: {signature.document_id} ({signature.nb_relances}/3)")
except Exception as e:
logger.error(f"❌ Erreur relance signature {signature.id}: {e}")
continue
await session.commit()
return {
"success": True,
"signatures_verifiees": len(signatures_a_relancer),
"relances_envoyees": nb_relances,
"message": f"{nb_relances} email(s) de relance envoyé(s)"
}
except Exception as e:
logger.error(f"❌ Erreur relances automatiques: {e}")
raise HTTPException(500, str(e))

View file

@ -124,6 +124,7 @@ class SignatureLog(Base):
# Relances
est_relance = Column(Boolean, default=False)
nb_relances = Column(Integer, default=0)
derniere_relance = Column(DateTime, nullable=True)
# Métadonnées
raison_refus = Column(Text, nullable=True)