refactor(api): improve universign transaction flow with better error handling

This commit is contained in:
Fanilo-Nantenaina 2025-12-22 10:26:08 +03:00
parent 0a6dfcdf64
commit f357e9614b

287
api.py
View file

@ -1430,182 +1430,262 @@ async def universign_envoyer_avec_email(
api_url = settings.universign_api_url api_url = settings.universign_api_url
auth = (api_key, "") auth = (api_key, "")
logger.info(f"🔐 Démarrage processus Universign pour {email}")
logger.info(f"📌 API URL: {api_url}")
logger.info(f"📌 Document: {doc_id} - Type: {doc_data.get('type_label')}")
# ========================================
# VÉRIFICATION PRÉLIMINAIRE : PDF valide ?
# ========================================
if not pdf_bytes or len(pdf_bytes) == 0:
logger.error(f"❌ PDF vide ou None")
raise Exception("Le PDF généré est vide. Vérifiez la génération du PDF.")
logger.info(f"✅ PDF reçu : {len(pdf_bytes)} octets")
# ======================================== # ========================================
# ÉTAPE 1 : Créer la transaction # ÉTAPE 1 : Créer la transaction
# ======================================== # ========================================
logger.info(f"🔐 Création transaction Universign pour {email}") logger.info(f"📝 ÉTAPE 1/7 : Création transaction")
transaction_payload = {
"name": f"{doc_data.get('type_label', 'Document')} {doc_id}",
"language": "fr",
}
response = requests.post( response = requests.post(
f"{api_url}/transactions", f"{api_url}/transactions",
auth=auth, auth=auth,
json={ json=transaction_payload,
"name": f"{doc_data.get('type_label', 'Document')} {doc_id}",
"language": "fr",
"profile": "default", # ✅ Ajout du profil
},
timeout=30, timeout=30,
) )
logger.info(f"Status création transaction: {response.status_code}")
if response.status_code != 200: if response.status_code != 200:
logger.error(f"❌ Erreur création transaction: {response.status_code} - {response.text}") logger.error(f"Réponse: {response.text}")
raise Exception(f"Erreur création transaction: {response.status_code}") raise Exception(f"Erreur création transaction: {response.status_code} - {response.text}")
response.raise_for_status() transaction_data = response.json()
transaction_id = response.json().get("id") transaction_id = transaction_data.get("id")
if not transaction_id:
logger.error(f"❌ Pas de transaction_id dans la réponse: {transaction_data}")
raise Exception("Transaction créée mais ID manquant")
logger.info(f"✅ Transaction créée: {transaction_id}") logger.info(f"✅ Transaction créée: {transaction_id}")
# ======================================== # ========================================
# ÉTAPE 2 : Upload du fichier PDF # ÉTAPE 2 : Upload du fichier PDF
# ======================================== # ========================================
logger.info(f"📄 Upload PDF ({len(pdf_bytes)} octets)") logger.info(f"📄 ÉTAPE 2/7 : Upload PDF")
# Préparer le fichier multipart
filename = f"{doc_data.get('type_label', 'Document')}_{doc_id}.pdf"
files = { files = {
"file": ( "file": (filename, pdf_bytes, "application/pdf")
f"{doc_data.get('type_label', 'Document')}_{doc_id}.pdf",
pdf_bytes,
"application/pdf",
)
} }
logger.debug(f"Upload fichier: {filename}, taille: {len(pdf_bytes)} octets")
response = requests.post( response = requests.post(
f"{api_url}/files", f"{api_url}/files",
auth=auth, auth=auth,
files=files, files=files,
timeout=30 timeout=60, # Plus de timeout pour gros fichiers
) )
if response.status_code != 200: logger.info(f"Status upload fichier: {response.status_code}")
logger.error(f"❌ Erreur upload fichier: {response.status_code} - {response.text}")
raise Exception(f"Erreur upload fichier: {response.status_code}") if response.status_code not in [200, 201]:
logger.error(f"❌ Erreur upload: {response.text}")
response.raise_for_status() raise Exception(f"Erreur upload fichier: {response.status_code} - {response.text}")
file_id = response.json().get("id")
# Parser la réponse
try:
file_data = response.json()
logger.debug(f"Réponse upload: {file_data}")
except Exception as e:
logger.error(f"❌ Erreur parsing réponse upload: {e}")
logger.error(f"Réponse brute: {response.text[:500]}")
raise Exception(f"Réponse upload invalide: {e}")
file_id = file_data.get("id")
if not file_id:
logger.error(f"❌ Pas de file_id dans la réponse")
logger.error(f"Réponse complète: {file_data}")
raise Exception("Upload réussi mais file_id manquant")
logger.info(f"✅ Fichier uploadé: {file_id}") logger.info(f"✅ Fichier uploadé: {file_id}")
# ======================================== # ========================================
# ÉTAPE 3 : Créer le document dans la transaction # ÉTAPE 3 : Ajouter le document à la transaction
# ======================================== # ========================================
logger.info(f"📋 Ajout document à la transaction") logger.info(f"📋 ÉTAPE 3/7 : Ajout document à transaction {transaction_id}")
document_payload = {
"document": file_id # ✅ Utiliser "document" pas "file"
}
logger.debug(f"Payload document: {document_payload}")
logger.debug(f"URL: {api_url}/transactions/{transaction_id}/documents")
response = requests.post( response = requests.post(
f"{api_url}/transactions/{transaction_id}/documents", f"{api_url}/transactions/{transaction_id}/documents",
auth=auth, auth=auth,
json={"document": file_id}, # ✅ Utiliser 'file' au lieu de 'document' json=document_payload,
timeout=30, timeout=30,
) )
if response.status_code != 200: logger.info(f"Status ajout document: {response.status_code}")
logger.error(f"❌ Erreur ajout document: {response.status_code} - {response.text}")
raise Exception(f"Erreur ajout document: {response.status_code}") if response.status_code not in [200, 201]:
logger.error(f"❌ Erreur ajout document: {response.text}")
response.raise_for_status() logger.error(f"Payload envoyé: {document_payload}")
document_id = response.json().get("id") logger.error(f"file_id utilisé: '{file_id}' (type: {type(file_id)})")
raise Exception(f"Erreur ajout document: {response.status_code} - {response.text}")
try:
document_data = response.json()
logger.debug(f"Réponse ajout document: {document_data}")
except Exception as e:
logger.error(f"❌ Erreur parsing réponse document: {e}")
raise
document_id = document_data.get("id")
if not document_id:
logger.error(f"❌ Pas de document_id dans la réponse")
logger.error(f"Réponse complète: {document_data}")
raise Exception("Document ajouté mais ID manquant")
logger.info(f"✅ Document ajouté: {document_id}") logger.info(f"✅ Document ajouté: {document_id}")
# ======================================== # ========================================
# ÉTAPE 4 : Ajouter un champ de signature # ÉTAPE 4 : Créer le signataire
# ======================================== # ========================================
logger.info(f"✍️ Ajout champ signature") logger.info(f"👤 ÉTAPE 4/7 : Création signataire")
response = requests.post( nom_parts = nom.split()
f"{api_url}/transactions/{transaction_id}/documents/{document_id}/fields", first_name = nom_parts[0] if len(nom_parts) > 0 else nom
auth=auth, last_name = " ".join(nom_parts[1:]) if len(nom_parts) > 1 else ""
json={
"type": "signature",
"page": 1, # ✅ Préciser la page
# Position optionnelle - Universign peut la placer automatiquement
# "x": 100,
# "y": 600,
},
timeout=30,
)
if response.status_code != 200: signer_payload = {
logger.error(f"❌ Erreur ajout champ: {response.status_code} - {response.text}") "email": email,
raise Exception(f"Erreur ajout champ signature: {response.status_code}") "firstName": first_name,
"lastName": last_name,
response.raise_for_status() }
field_id = response.json().get("id")
logger.debug(f"Payload signataire: {signer_payload}")
logger.info(f"✅ Champ signature créé: {field_id}")
# ========================================
# ÉTAPE 5 : Ajouter le signataire
# ========================================
logger.info(f"👤 Ajout signataire: {nom} ({email})")
response = requests.post( response = requests.post(
f"{api_url}/transactions/{transaction_id}/signers", f"{api_url}/transactions/{transaction_id}/signers",
auth=auth, auth=auth,
json={ json=signer_payload,
"email": email,
"firstName": nom.split()[0] if ' ' in nom else nom, # ✅ Prénom
"lastName": nom.split()[-1] if ' ' in nom else "", # ✅ Nom
# ✅ Lier le signataire au champ de signature
"fields": [field_id],
},
timeout=30, timeout=30,
) )
if response.status_code != 200: logger.info(f"Status création signataire: {response.status_code}")
logger.error(f"❌ Erreur ajout signataire: {response.status_code} - {response.text}")
raise Exception(f"Erreur ajout signataire: {response.status_code}") if response.status_code not in [200, 201]:
logger.error(f"❌ Erreur création signataire: {response.text}")
response.raise_for_status() raise Exception(f"Erreur création signataire: {response.status_code} - {response.text}")
signer_id = response.json().get("id")
signer_data = response.json()
signer_id = signer_data.get("id")
if not signer_id:
logger.error(f"❌ Pas de signer_id dans la réponse")
raise Exception("Signataire créé mais ID manquant")
logger.info(f"✅ Signataire créé: {signer_id}")
logger.info(f"✅ Signataire ajouté: {signer_id}") # ========================================
# ÉTAPE 5 : Créer le champ de signature
# ========================================
logger.info(f"✍️ ÉTAPE 5/7 : Création champ signature")
field_payload = {
"type": "signature",
"page": 1,
"signer": signer_id,
}
logger.debug(f"Payload champ: {field_payload}")
response = requests.post(
f"{api_url}/transactions/{transaction_id}/documents/{document_id}/fields",
auth=auth,
json=field_payload,
timeout=30,
)
logger.info(f"Status création champ: {response.status_code}")
if response.status_code not in [200, 201]:
logger.error(f"❌ Erreur création champ: {response.text}")
raise Exception(f"Erreur création champ: {response.status_code} - {response.text}")
field_data = response.json()
field_id = field_data.get("id")
logger.info(f"✅ Champ créé: {field_id}")
# ======================================== # ========================================
# ÉTAPE 6 : Démarrer la transaction # ÉTAPE 6 : Démarrer la transaction
# ======================================== # ========================================
logger.info(f"🚀 Démarrage de la transaction") logger.info(f"🚀 ÉTAPE 6/7 : Démarrage transaction")
response = requests.post( response = requests.post(
f"{api_url}/transactions/{transaction_id}/start", f"{api_url}/transactions/{transaction_id}/start",
auth=auth, auth=auth,
json={}, # ✅ Body vide mais présent json={},
timeout=30 timeout=30
) )
if response.status_code != 200: logger.info(f"Status démarrage: {response.status_code}")
logger.error(f"❌ Erreur démarrage transaction: {response.status_code}")
logger.error(f"Réponse complète: {response.text}") if response.status_code not in [200, 201]:
raise Exception(f"Erreur démarrage transaction: {response.status_code} - {response.text}") logger.error(f"❌ Erreur démarrage: {response.text}")
raise Exception(f"Erreur démarrage: {response.status_code} - {response.text}")
response.raise_for_status()
final_data = response.json() final_data = response.json()
# ======================================== # ========================================
# ÉTAPE 7 : Récupérer l'URL de signature # ÉTAPE 7 : Récupérer l'URL de signature
# ======================================== # ========================================
logger.info(f"🔗 ÉTAPE 7/7 : Récupération URL")
signer_url = "" signer_url = ""
if final_data.get("signers"): if final_data.get("signers"):
for signer in final_data["signers"]: for signer in final_data["signers"]:
if signer.get("email") == email: if signer.get("email") == email:
signer_url = signer.get("url", "") signer_url = signer.get("url", "")
break break
if not signer_url:
response = requests.get(
f"{api_url}/transactions/{transaction_id}/signers/{signer_id}",
auth=auth,
timeout=10,
)
if response.status_code == 200:
signer_url = response.json().get("url", "")
if not signer_url: if not signer_url:
logger.warning("⚠️ URL de signature non trouvée dans la réponse") logger.error(f"❌ URL de signature introuvable")
raise ValueError("URL de signature non retournée par Universign") raise ValueError("URL de signature non retournée")
logger.info(f"✅ Signature Universign prête: {transaction_id}") logger.info(f"URL récupérée")
# ======================================== # ========================================
# ÉTAPE 8 : Créer l'email de notification # Créer l'email de notification
# ======================================== # ========================================
template = templates_signature_email["demande_signature"] template = templates_signature_email["demande_signature"]
type_labels = { type_labels = {0: "Devis", 10: "Commande", 30: "Bon de Livraison", 60: "Facture", 50: "Avoir"}
0: "Devis",
10: "Commande",
30: "Bon de Livraison",
60: "Facture",
50: "Avoir",
}
variables = { variables = {
"NOM_SIGNATAIRE": nom, "NOM_SIGNATAIRE": nom,
@ -1624,7 +1704,6 @@ async def universign_envoyer_avec_email(
sujet = sujet.replace(f"{{{{{var}}}}}", str(valeur)) sujet = sujet.replace(f"{{{{{var}}}}}", str(valeur))
corps = corps.replace(f"{{{{{var}}}}}", str(valeur)) corps = corps.replace(f"{{{{{var}}}}}", str(valeur))
# Créer log email
email_log = EmailLog( email_log = EmailLog(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
destinataire=email, destinataire=email,
@ -1639,11 +1718,9 @@ async def universign_envoyer_avec_email(
session.add(email_log) session.add(email_log)
await session.flush() await session.flush()
# Enqueue l'email
email_queue.enqueue(email_log.id) email_queue.enqueue(email_log.id)
logger.info(f"📧 Email de signature envoyé en file: {email}") logger.info(f"✅ Processus terminé avec succès")
return { return {
"transaction_id": transaction_id, "transaction_id": transaction_id,
@ -1653,18 +1730,10 @@ async def universign_envoyer_avec_email(
"email_sent": True, "email_sent": True,
} }
except requests.exceptions.HTTPError as e:
logger.error(f"❌ Erreur HTTP Universign: {e}")
logger.error(f"Réponse: {e.response.text if e.response else 'N/A'}")
return {
"error": f"Erreur Universign: {e.response.status_code} - {e.response.text if e.response else str(e)}",
"statut": "ERREUR",
"email_sent": False,
}
except Exception as e: except Exception as e:
logger.error(f"❌ Erreur Universign+Email: {e}", exc_info=True) logger.error(f"❌ Erreur Universign: {e}", exc_info=True)
return {"error": str(e), "statut": "ERREUR", "email_sent": False} 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"""
import requests import requests