feat(signature): add email templates and tracking for signature workflow
This commit is contained in:
parent
4c53477efe
commit
282ffe4898
2 changed files with 702 additions and 48 deletions
749
api.py
749
api.py
|
|
@ -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.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from pydantic import BaseModel, Field, EmailStr, validator, field_validator
|
from pydantic import BaseModel, Field, EmailStr, validator, field_validator
|
||||||
|
|
@ -1050,34 +1050,389 @@ class MouvementStockResponse(BaseModel):
|
||||||
nb_lignes: int = Field(..., description="Nombre de lignes")
|
nb_lignes: int = Field(..., description="Nombre de lignes")
|
||||||
|
|
||||||
|
|
||||||
async def universign_envoyer(
|
|
||||||
doc_id: str, pdf_bytes: bytes, email: str, nom: str
|
|
||||||
) -> Dict:
|
|
||||||
"""Envoi signature via API Universign"""
|
|
||||||
import requests
|
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
import requests
|
||||||
|
|
||||||
try:
|
try:
|
||||||
api_key = settings.universign_api_key
|
api_key = settings.universign_api_key
|
||||||
api_url = settings.universign_api_url
|
api_url = settings.universign_api_url
|
||||||
auth = (api_key, "")
|
auth = (api_key, "")
|
||||||
|
|
||||||
# Étape 1: Créer transaction
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{api_url}/transactions",
|
f"{api_url}/transactions",
|
||||||
auth=auth,
|
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,
|
timeout=30,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
transaction_id = response.json().get("id")
|
transaction_id = response.json().get("id")
|
||||||
|
|
||||||
|
logger.info(f"✅ Transaction Universign créée: {transaction_id}")
|
||||||
|
|
||||||
# Étape 2: Upload PDF
|
files = {"file": (f"{doc_data.get('type_label', 'Document')}_{doc_id}.pdf", pdf_bytes, "application/pdf")}
|
||||||
files = {"file": (f"Devis_{doc_id}.pdf", pdf_bytes, "application/pdf")}
|
|
||||||
response = requests.post(f"{api_url}/files", auth=auth, files=files, timeout=30)
|
response = requests.post(f"{api_url}/files", auth=auth, files=files, timeout=30)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
file_id = response.json().get("id")
|
file_id = response.json().get("id")
|
||||||
|
|
||||||
# Étape 3: Ajouter document
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{api_url}/transactions/{transaction_id}/documents",
|
f"{api_url}/transactions/{transaction_id}/documents",
|
||||||
auth=auth,
|
auth=auth,
|
||||||
|
|
@ -1087,7 +1442,6 @@ async def universign_envoyer(
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
document_id = response.json().get("id")
|
document_id = response.json().get("id")
|
||||||
|
|
||||||
# Étape 4: Créer champ signature
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{api_url}/transactions/{transaction_id}/documents/{document_id}/fields",
|
f"{api_url}/transactions/{transaction_id}/documents/{document_id}/fields",
|
||||||
auth=auth,
|
auth=auth,
|
||||||
|
|
@ -1096,8 +1450,7 @@ async def universign_envoyer(
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
field_id = response.json().get("id")
|
field_id = response.json().get("id")
|
||||||
|
|
||||||
# Étape 5: Assigner signataire
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{api_url}/transactions/{transaction_id}/signatures",
|
f"{api_url}/transactions/{transaction_id}/signatures",
|
||||||
auth=auth,
|
auth=auth,
|
||||||
|
|
@ -1106,31 +1459,90 @@ async def universign_envoyer(
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
# Étape 6: Démarrer transaction
|
|
||||||
response = requests.post(
|
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()
|
response.raise_for_status()
|
||||||
|
|
||||||
final_data = response.json()
|
final_data = response.json()
|
||||||
signer_url = (
|
signer_url = (
|
||||||
final_data.get("actions", [{}])[0].get("url", "")
|
final_data.get("actions", [{}])[0].get("url", "")
|
||||||
if final_data.get("actions")
|
if final_data.get("actions")
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not signer_url:
|
||||||
|
raise ValueError("URL de signature non retournée par Universign")
|
||||||
|
|
||||||
|
logger.info(f"✅ Signature Universign démarrée: {transaction_id}")
|
||||||
|
|
||||||
logger.info(f"✅ Signature Universign envoyé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 {
|
return {
|
||||||
"transaction_id": transaction_id,
|
"transaction_id": transaction_id,
|
||||||
"signer_url": signer_url,
|
"signer_url": signer_url,
|
||||||
"statut": "ENVOYE",
|
"statut": "ENVOYE",
|
||||||
|
"email_log_id": email_log.id,
|
||||||
|
"email_sent": True
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Erreur Universign: {e}")
|
logger.error(f"❌ Erreur Universign+Email: {e}")
|
||||||
return {"error": str(e), "statut": "ERREUR"}
|
return {
|
||||||
|
"error": str(e),
|
||||||
|
"statut": "ERREUR",
|
||||||
|
"email_sent": False
|
||||||
|
}
|
||||||
|
|
||||||
async def universign_statut(transaction_id: str) -> Dict:
|
async def universign_statut(transaction_id: str) -> Dict:
|
||||||
"""Récupération statut signature"""
|
"""Récupération statut signature"""
|
||||||
|
|
@ -1986,26 +2398,58 @@ async def commande_vers_facture(id: str, session: AsyncSession = Depends(get_ses
|
||||||
logger.error(f"Erreur transformation: {e}")
|
logger.error(f"Erreur transformation: {e}")
|
||||||
raise HTTPException(500, str(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:
|
||||||
# ENDPOINTS - US-A3 (SIGNATURE ÉLECTRONIQUE)
|
raise ValueError(
|
||||||
# =====================================================
|
f"type_doc invalide ({type_doc}). Valeurs autorisées : {sorted(TYPES_AUTORISES)}"
|
||||||
@app.post("/signature/universign/send", tags=["Signatures"])
|
|
||||||
async def envoyer_signature(
|
|
||||||
demande: SignatureRequest, session: AsyncSession = Depends(get_session)
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
# Générer PDF
|
|
||||||
pdf_bytes = email_queue._generate_pdf(demande.doc_id, demande.type_doc)
|
|
||||||
|
|
||||||
# Envoi Universign
|
|
||||||
resultat = await universign_envoyer(
|
|
||||||
demande.doc_id, pdf_bytes, demande.email_signataire, demande.nom_signataire
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return type_doc if type_doc == 0 else type_doc // 10
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/signature/universign/send", tags=["Signatures"])
|
||||||
|
async def envoyer_signature_optimise(
|
||||||
|
demande: SignatureRequest,
|
||||||
|
session: AsyncSession = Depends(get_session)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
# 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")
|
||||||
|
|
||||||
|
# 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:
|
if "error" in resultat:
|
||||||
raise HTTPException(500, resultat["error"])
|
raise HTTPException(500, resultat["error"])
|
||||||
|
|
||||||
# Logger en DB
|
# Logger en DB
|
||||||
signature_log = SignatureLog(
|
signature_log = SignatureLog(
|
||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
|
|
@ -2018,29 +2462,238 @@ async def envoyer_signature(
|
||||||
statut=StatutSignatureEnum.ENVOYE,
|
statut=StatutSignatureEnum.ENVOYE,
|
||||||
date_envoi=datetime.now(),
|
date_envoi=datetime.now(),
|
||||||
)
|
)
|
||||||
|
|
||||||
session.add(signature_log)
|
session.add(signature_log)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
# MAJ champ libre Sage via gateway Windows
|
# MAJ champ libre Sage
|
||||||
sage_client.mettre_a_jour_champ_libre(
|
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 {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"transaction_id": resultat["transaction_id"],
|
"transaction_id": resultat["transaction_id"],
|
||||||
"signer_url": resultat["signer_url"],
|
"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:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Erreur signature: {e}")
|
logger.error(f"❌ Erreur signature: {e}")
|
||||||
raise HTTPException(500, str(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))
|
||||||
|
|
||||||
|
|
||||||
@app.get("/signature/universign/status", tags=["Signatures"])
|
@app.get("/signature/universign/status", tags=["Signatures"])
|
||||||
async def statut_signature(docId: str = Query(...)):
|
async def statut_signature(docId: str = Query(...)):
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,7 @@ class SignatureLog(Base):
|
||||||
# Relances
|
# Relances
|
||||||
est_relance = Column(Boolean, default=False)
|
est_relance = Column(Boolean, default=False)
|
||||||
nb_relances = Column(Integer, default=0)
|
nb_relances = Column(Integer, default=0)
|
||||||
|
derniere_relance = Column(DateTime, nullable=True)
|
||||||
|
|
||||||
# Métadonnées
|
# Métadonnées
|
||||||
raison_refus = Column(Text, nullable=True)
|
raison_refus = Column(Text, nullable=True)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue