refactor: clean up code by removing unnecessary comments

This commit is contained in:
Fanilo-Nantenaina 2026-01-19 20:32:40 +03:00
parent 89510537b3
commit 09eae50952
15 changed files with 80 additions and 115 deletions

3
api.py
View file

@ -132,7 +132,6 @@ async def lifespan(app: FastAPI):
api_url=settings.universign_api_url, api_key=settings.universign_api_key api_url=settings.universign_api_url, api_key=settings.universign_api_key
) )
# Configuration du service avec les dépendances
sync_service.configure( sync_service.configure(
sage_client=sage_client, email_queue=email_queue, settings=settings sage_client=sage_client, email_queue=email_queue, settings=settings
) )
@ -180,7 +179,6 @@ app.include_router(entreprises_router)
@app.get("/clients", response_model=List[ClientDetails], tags=["Clients"]) @app.get("/clients", response_model=List[ClientDetails], tags=["Clients"])
async def obtenir_clients( async def obtenir_clients(
query: Optional[str] = Query(None), query: Optional[str] = Query(None),
# sage: SageGatewayClient = Depends(get_sage_client_for_user),
): ):
try: try:
clients = sage_client.lister_clients(filtre=query or "") clients = sage_client.lister_clients(filtre=query or "")
@ -2768,7 +2766,6 @@ async def get_current_sage_config(
} }
# Routes Collaborateurs
@app.get( @app.get(
"/collaborateurs", "/collaborateurs",
response_model=List[CollaborateurDetails], response_model=List[CollaborateurDetails],

View file

@ -7,7 +7,6 @@ class Settings(BaseSettings):
env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore" env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore"
) )
# === JWT & Auth ===
jwt_secret: str jwt_secret: str
jwt_algorithm: str jwt_algorithm: str
access_token_expire_minutes: int access_token_expire_minutes: int
@ -21,15 +20,12 @@ class Settings(BaseSettings):
SAGE_TYPE_BON_AVOIR: int = 50 SAGE_TYPE_BON_AVOIR: int = 50
SAGE_TYPE_FACTURE: int = 60 SAGE_TYPE_FACTURE: int = 60
# === Sage Gateway (Windows) ===
sage_gateway_url: str sage_gateway_url: str
sage_gateway_token: str sage_gateway_token: str
frontend_url: str frontend_url: str
# === Base de données ===
database_url: str = "sqlite+aiosqlite:///./data/sage_dataven.db" database_url: str = "sqlite+aiosqlite:///./data/sage_dataven.db"
# === SMTP ===
smtp_host: str smtp_host: str
smtp_port: int = 587 smtp_port: int = 587
smtp_user: str smtp_user: str
@ -37,21 +33,17 @@ class Settings(BaseSettings):
smtp_from: str smtp_from: str
smtp_use_tls: bool = True smtp_use_tls: bool = True
# === Universign ===
universign_api_key: str universign_api_key: str
universign_api_url: str universign_api_url: str
# === API ===
api_host: str api_host: str
api_port: int api_port: int
api_reload: bool = False api_reload: bool = False
# === Email Queue ===
max_email_workers: int = 3 max_email_workers: int = 3
max_retry_attempts: int = 3 max_retry_attempts: int = 3
retry_delay_seconds: int = 3 retry_delay_seconds: int = 3
# === CORS ===
cors_origins: List[str] = ["*"] cors_origins: List[str] = ["*"]

View file

@ -19,7 +19,6 @@ async def create_admin():
print(" Création d'un compte administrateur") print(" Création d'un compte administrateur")
print("=" * 60 + "\n") print("=" * 60 + "\n")
# Saisie des informations
email = input("Email de l'admin: ").strip().lower() email = input("Email de l'admin: ").strip().lower()
if not email or "@" not in email: if not email or "@" not in email:
print(" Email invalide") print(" Email invalide")
@ -32,7 +31,6 @@ async def create_admin():
print(" Prénom et nom requis") print(" Prénom et nom requis")
return False return False
# Mot de passe avec validation
while True: while True:
password = input( password = input(
"Mot de passe (min 8 car., 1 maj, 1 min, 1 chiffre, 1 spécial): " "Mot de passe (min 8 car., 1 maj, 1 min, 1 chiffre, 1 spécial): "
@ -58,7 +56,6 @@ async def create_admin():
print(f"\n Un utilisateur avec l'email {email} existe déjà") print(f"\n Un utilisateur avec l'email {email} existe déjà")
return False return False
# Créer l'admin
admin = User( admin = User(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
email=email, email=email,

View file

@ -0,0 +1,64 @@
from sqlalchemy import Column, String, Boolean, DateTime, Integer, Text
from datetime import datetime
import uuid
from database.models.generic_model import Base
class ApiKey(Base):
"""Modèle pour les clés API publiques"""
__tablename__ = "api_keys"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
key_hash = Column(String(64), unique=True, nullable=False, index=True)
key_prefix = Column(
String(10), nullable=False
) # Premiers caractères pour identification
name = Column(String(255), nullable=False)
description = Column(Text, nullable=True)
# Métadonnées
user_id = Column(String(36), nullable=True) # Optionnel si associé à un utilisateur
created_by = Column(String(255), nullable=False)
# Contrôle d'accès
is_active = Column(Boolean, default=True, nullable=False)
rate_limit_per_minute = Column(Integer, default=60, nullable=False)
allowed_endpoints = Column(
Text, nullable=True
) # JSON array des endpoints autorisés
# Statistiques
total_requests = Column(Integer, default=0, nullable=False)
last_used_at = Column(DateTime, nullable=True)
# Dates
created_at = Column(DateTime, default=datetime.now, nullable=False)
expires_at = Column(DateTime, nullable=True)
revoked_at = Column(DateTime, nullable=True)
def __repr__(self):
return f"<ApiKey(name='{self.name}', prefix='{self.key_prefix}', active={self.is_active})>"
class SwaggerUser(Base):
"""Modèle pour les utilisateurs autorisés à accéder au Swagger"""
__tablename__ = "swagger_users"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
username = Column(String(100), unique=True, nullable=False, index=True)
hashed_password = Column(String(255), nullable=False)
full_name = Column(String(255), nullable=True)
email = Column(String(255), nullable=True)
is_active = Column(Boolean, default=True, nullable=False)
created_at = Column(DateTime, default=datetime.now, nullable=False)
last_login = Column(DateTime, nullable=True)
def __repr__(self):
return f"<SwaggerUser(username='{self.username}', active={self.is_active})>"

View file

@ -22,7 +22,6 @@ async def rechercher_entreprise(
try: try:
logger.info(f" Recherche entreprise: '{q}'") logger.info(f" Recherche entreprise: '{q}'")
# Appel API
api_response = await rechercher_entreprise_api(q, per_page) api_response = await rechercher_entreprise_api(q, per_page)
resultats_api = api_response.get("results", []) resultats_api = api_response.get("results", [])

View file

@ -511,7 +511,6 @@ async def webhook_universign(
transaction_id = None transaction_id = None
if payload.get("type", "").startswith("transaction.") and "payload" in payload: if payload.get("type", "").startswith("transaction.") and "payload" in payload:
# Le transaction_id est dans payload.object.id
nested_object = payload.get("payload", {}).get("object", {}) nested_object = payload.get("payload", {}).get("object", {})
if nested_object.get("object") == "transaction": if nested_object.get("object") == "transaction":
transaction_id = nested_object.get("id") transaction_id = nested_object.get("id")

View file

@ -1,4 +1,3 @@
# sage_client.py
import requests import requests
from typing import Dict, List, Optional from typing import Dict, List, Optional
from config.config import settings from config.config import settings
@ -468,7 +467,6 @@ class SageGatewayClient:
"tva_encaissement": tva_encaissement, "tva_encaissement": tva_encaissement,
} }
# Champs optionnels
if date_reglement: if date_reglement:
payload["date_reglement"] = date_reglement payload["date_reglement"] = date_reglement
if code_journal: if code_journal:

View file

@ -76,7 +76,6 @@ class Article(BaseModel):
) )
nb_emplacements: int = Field(0, description="Nombre d'emplacements") nb_emplacements: int = Field(0, description="Nombre d'emplacements")
# Champs énumérés normalisés
suivi_stock: Optional[int] = Field( suivi_stock: Optional[int] = Field(
None, None,
description="Type de suivi de stock (AR_SuiviStock): 0=Aucun, 1=CMUP, 2=FIFO/LIFO, 3=Sérialisé", description="Type de suivi de stock (AR_SuiviStock): 0=Aucun, 1=CMUP, 2=FIFO/LIFO, 3=Sérialisé",

View file

@ -10,12 +10,10 @@ logger = logging.getLogger(__name__)
class ReglementFactureCreate(BaseModel): class ReglementFactureCreate(BaseModel):
"""Requête de règlement d'une facture côté VPS""" """Requête de règlement d'une facture côté VPS"""
# Montant et devise
montant: Decimal = Field(..., gt=0, description="Montant à régler") montant: Decimal = Field(..., gt=0, description="Montant à régler")
devise_code: Optional[int] = Field(0, description="Code devise (0=EUR par défaut)") devise_code: Optional[int] = Field(0, description="Code devise (0=EUR par défaut)")
cours_devise: Optional[Decimal] = Field(1.0, description="Cours de la devise") cours_devise: Optional[Decimal] = Field(1.0, description="Cours de la devise")
# Mode et journal
mode_reglement: int = Field( mode_reglement: int = Field(
..., ge=0, description="Code mode règlement depuis /reglements/modes" ..., ge=0, description="Code mode règlement depuis /reglements/modes"
) )
@ -23,13 +21,11 @@ class ReglementFactureCreate(BaseModel):
..., min_length=1, description="Code journal depuis /journaux/tresorerie" ..., min_length=1, description="Code journal depuis /journaux/tresorerie"
) )
# Dates
date_reglement: Optional[date] = Field( date_reglement: Optional[date] = Field(
None, description="Date du règlement (défaut: aujourd'hui)" None, description="Date du règlement (défaut: aujourd'hui)"
) )
date_echeance: Optional[date] = Field(None, description="Date d'échéance") date_echeance: Optional[date] = Field(None, description="Date d'échéance")
# Références
reference: Optional[str] = Field( reference: Optional[str] = Field(
"", max_length=17, description="Référence pièce règlement" "", max_length=17, description="Référence pièce règlement"
) )
@ -37,7 +33,6 @@ class ReglementFactureCreate(BaseModel):
"", max_length=35, description="Libellé du règlement" "", max_length=35, description="Libellé du règlement"
) )
# TVA sur encaissement
tva_encaissement: Optional[bool] = Field( tva_encaissement: Optional[bool] = Field(
False, description="Appliquer TVA sur encaissement" False, description="Appliquer TVA sur encaissement"
) )
@ -81,7 +76,6 @@ class ReglementMultipleCreate(BaseModel):
libelle: Optional[str] = Field("") libelle: Optional[str] = Field("")
tva_encaissement: Optional[bool] = Field(False) tva_encaissement: Optional[bool] = Field(False)
# Factures spécifiques (optionnel)
numeros_factures: Optional[List[str]] = Field( numeros_factures: Optional[List[str]] = Field(
None, description="Si vide, règle les plus anciennes en premier" None, description="Si vide, règle les plus anciennes en premier"
) )

View file

@ -10,7 +10,6 @@ class GatewayHealthStatus(str, Enum):
UNKNOWN = "unknown" UNKNOWN = "unknown"
# === CREATE ===
class SageGatewayCreate(BaseModel): class SageGatewayCreate(BaseModel):
name: str = Field( name: str = Field(
@ -71,7 +70,6 @@ class SageGatewayUpdate(BaseModel):
return v.rstrip("/") if v else v return v.rstrip("/") if v else v
# === RESPONSE ===
class SageGatewayResponse(BaseModel): class SageGatewayResponse(BaseModel):
id: str id: str

View file

@ -9,7 +9,6 @@ class CollaborateurBase(BaseModel):
prenom: Optional[str] = Field(None, max_length=50) prenom: Optional[str] = Field(None, max_length=50)
fonction: Optional[str] = Field(None, max_length=50) fonction: Optional[str] = Field(None, max_length=50)
# Adresse
adresse: Optional[str] = Field(None, max_length=100) adresse: Optional[str] = Field(None, max_length=100)
complement: Optional[str] = Field(None, max_length=100) complement: Optional[str] = Field(None, max_length=100)
code_postal: Optional[str] = Field(None, max_length=10) code_postal: Optional[str] = Field(None, max_length=10)
@ -17,7 +16,6 @@ class CollaborateurBase(BaseModel):
code_region: Optional[str] = Field(None, max_length=50) code_region: Optional[str] = Field(None, max_length=50)
pays: Optional[str] = Field(None, max_length=50) pays: Optional[str] = Field(None, max_length=50)
# Services
service: Optional[str] = Field(None, max_length=50) service: Optional[str] = Field(None, max_length=50)
vendeur: bool = Field(default=False) vendeur: bool = Field(default=False)
caissier: bool = Field(default=False) caissier: bool = Field(default=False)
@ -25,18 +23,15 @@ class CollaborateurBase(BaseModel):
chef_ventes: bool = Field(default=False) chef_ventes: bool = Field(default=False)
numero_chef_ventes: Optional[int] = None numero_chef_ventes: Optional[int] = None
# Contact
telephone: Optional[str] = Field(None, max_length=20) telephone: Optional[str] = Field(None, max_length=20)
telecopie: Optional[str] = Field(None, max_length=20) telecopie: Optional[str] = Field(None, max_length=20)
email: Optional[EmailStr] = None email: Optional[EmailStr] = None
tel_portable: Optional[str] = Field(None, max_length=20) tel_portable: Optional[str] = Field(None, max_length=20)
# Réseaux sociaux
facebook: Optional[str] = Field(None, max_length=100) facebook: Optional[str] = Field(None, max_length=100)
linkedin: Optional[str] = Field(None, max_length=100) linkedin: Optional[str] = Field(None, max_length=100)
skype: Optional[str] = Field(None, max_length=100) skype: Optional[str] = Field(None, max_length=100)
# Autres
matricule: Optional[str] = Field(None, max_length=20) matricule: Optional[str] = Field(None, max_length=20)
sommeil: bool = Field(default=False) sommeil: bool = Field(default=False)

View file

@ -14,7 +14,6 @@ class TypeTiersInt(IntEnum):
class TiersDetails(BaseModel): class TiersDetails(BaseModel):
# IDENTIFICATION
numero: Optional[str] = Field(None, description="Code tiers (CT_Num)") numero: Optional[str] = Field(None, description="Code tiers (CT_Num)")
intitule: Optional[str] = Field( intitule: Optional[str] = Field(
None, description="Raison sociale ou Nom complet (CT_Intitule)" None, description="Raison sociale ou Nom complet (CT_Intitule)"
@ -37,7 +36,6 @@ class TiersDetails(BaseModel):
) )
code_naf: Optional[str] = Field(None, description="Code NAF/APE (CT_Ape)") code_naf: Optional[str] = Field(None, description="Code NAF/APE (CT_Ape)")
# ADRESSE
contact: Optional[str] = Field( contact: Optional[str] = Field(
None, description="Nom du contact principal (CT_Contact)" None, description="Nom du contact principal (CT_Contact)"
) )
@ -50,7 +48,6 @@ class TiersDetails(BaseModel):
region: Optional[str] = Field(None, description="Région/État (CT_CodeRegion)") region: Optional[str] = Field(None, description="Région/État (CT_CodeRegion)")
pays: Optional[str] = Field(None, description="Pays (CT_Pays)") pays: Optional[str] = Field(None, description="Pays (CT_Pays)")
# TELECOM
telephone: Optional[str] = Field(None, description="Téléphone fixe (CT_Telephone)") telephone: Optional[str] = Field(None, description="Téléphone fixe (CT_Telephone)")
telecopie: Optional[str] = Field(None, description="Fax (CT_Telecopie)") telecopie: Optional[str] = Field(None, description="Fax (CT_Telecopie)")
email: Optional[str] = Field(None, description="Email principal (CT_EMail)") email: Optional[str] = Field(None, description="Email principal (CT_EMail)")
@ -58,13 +55,11 @@ class TiersDetails(BaseModel):
facebook: Optional[str] = Field(None, description="Profil Facebook (CT_Facebook)") facebook: Optional[str] = Field(None, description="Profil Facebook (CT_Facebook)")
linkedin: Optional[str] = Field(None, description="Profil LinkedIn (CT_LinkedIn)") linkedin: Optional[str] = Field(None, description="Profil LinkedIn (CT_LinkedIn)")
# TAUX
taux01: Optional[float] = Field(None, description="Taux personnalisé 1 (CT_Taux01)") taux01: Optional[float] = Field(None, description="Taux personnalisé 1 (CT_Taux01)")
taux02: Optional[float] = Field(None, description="Taux personnalisé 2 (CT_Taux02)") taux02: Optional[float] = Field(None, description="Taux personnalisé 2 (CT_Taux02)")
taux03: Optional[float] = Field(None, description="Taux personnalisé 3 (CT_Taux03)") taux03: Optional[float] = Field(None, description="Taux personnalisé 3 (CT_Taux03)")
taux04: Optional[float] = Field(None, description="Taux personnalisé 4 (CT_Taux04)") taux04: Optional[float] = Field(None, description="Taux personnalisé 4 (CT_Taux04)")
# STATISTIQUES
statistique01: Optional[str] = Field( statistique01: Optional[str] = Field(
None, description="Statistique 1 (CT_Statistique01)" None, description="Statistique 1 (CT_Statistique01)"
) )
@ -96,7 +91,6 @@ class TiersDetails(BaseModel):
None, description="Statistique 10 (CT_Statistique10)" None, description="Statistique 10 (CT_Statistique10)"
) )
# COMMERCIAL
encours_autorise: Optional[float] = Field( encours_autorise: Optional[float] = Field(
None, description="Encours maximum autorisé (CT_Encours)" None, description="Encours maximum autorisé (CT_Encours)"
) )
@ -113,7 +107,6 @@ class TiersDetails(BaseModel):
None, description="Détails du commercial/collaborateur" None, description="Détails du commercial/collaborateur"
) )
# FACTURATION
lettrage_auto: Optional[bool] = Field( lettrage_auto: Optional[bool] = Field(
None, description="Lettrage automatique (CT_Lettrage)" None, description="Lettrage automatique (CT_Lettrage)"
) )
@ -146,7 +139,6 @@ class TiersDetails(BaseModel):
None, description="Bon à payer obligatoire (CT_BonAPayer)" None, description="Bon à payer obligatoire (CT_BonAPayer)"
) )
# LOGISTIQUE
priorite_livraison: Optional[int] = Field( priorite_livraison: Optional[int] = Field(
None, description="Priorité livraison (CT_PrioriteLivr)" None, description="Priorité livraison (CT_PrioriteLivr)"
) )
@ -160,17 +152,14 @@ class TiersDetails(BaseModel):
None, description="Délai appro jours (CT_DelaiAppro)" None, description="Délai appro jours (CT_DelaiAppro)"
) )
# COMMENTAIRE
commentaire: Optional[str] = Field( commentaire: Optional[str] = Field(
None, description="Commentaire libre (CT_Commentaire)" None, description="Commentaire libre (CT_Commentaire)"
) )
# ANALYTIQUE
section_analytique: Optional[str] = Field( section_analytique: Optional[str] = Field(
None, description="Section analytique (CA_Num)" None, description="Section analytique (CA_Num)"
) )
# ORGANISATION / SURVEILLANCE
mode_reglement_code: Optional[int] = Field( mode_reglement_code: Optional[int] = Field(
None, description="Code mode règlement (MR_No)" None, description="Code mode règlement (MR_No)"
) )
@ -200,7 +189,6 @@ class TiersDetails(BaseModel):
None, description="Résultat financier (CT_SvResultat)" None, description="Résultat financier (CT_SvResultat)"
) )
# COMPTE GENERAL ET CATEGORIES
compte_general: Optional[str] = Field( compte_general: Optional[str] = Field(
None, description="Compte général principal (CG_NumPrinc)" None, description="Compte général principal (CG_NumPrinc)"
) )
@ -211,7 +199,6 @@ class TiersDetails(BaseModel):
None, description="Catégorie comptable (N_CatCompta)" None, description="Catégorie comptable (N_CatCompta)"
) )
# CONTACTS
contacts: Optional[List[Contact]] = Field( contacts: Optional[List[Contact]] = Field(
default_factory=list, description="Liste des contacts du tiers" default_factory=list, description="Liste des contacts du tiers"
) )

View file

@ -38,7 +38,6 @@ class UniversignDocumentService:
logger.info(f"{len(documents)} document(s) trouvé(s)") logger.info(f"{len(documents)} document(s) trouvé(s)")
# Log détaillé de chaque document
for idx, doc in enumerate(documents): for idx, doc in enumerate(documents):
logger.debug( logger.debug(
f" Document {idx}: id={doc.get('id')}, " f" Document {idx}: id={doc.get('id')}, "
@ -64,7 +63,7 @@ class UniversignDocumentService:
logger.error(f"⏱️ Timeout récupération transaction {transaction_id}") logger.error(f"⏱️ Timeout récupération transaction {transaction_id}")
return None return None
except Exception as e: except Exception as e:
logger.error(f" Erreur fetch documents: {e}", exc_info=True) logger.error(f" Erreur fetch documents: {e}", exc_info=True)
return None return None
def download_signed_document( def download_signed_document(
@ -94,7 +93,6 @@ class UniversignDocumentService:
f"Content-Type={content_type}, Size={content_length}" f"Content-Type={content_type}, Size={content_length}"
) )
# Vérification du type de contenu
if ( if (
"pdf" not in content_type.lower() "pdf" not in content_type.lower()
and "octet-stream" not in content_type.lower() and "octet-stream" not in content_type.lower()
@ -104,31 +102,30 @@ class UniversignDocumentService:
f"Tentative de lecture quand même..." f"Tentative de lecture quand même..."
) )
# Lecture du contenu
content = response.content content = response.content
if len(content) < 1024: if len(content) < 1024:
logger.error(f" Document trop petit: {len(content)} octets") logger.error(f" Document trop petit: {len(content)} octets")
return None return None
return content return content
elif response.status_code == 404: elif response.status_code == 404:
logger.error( logger.error(
f" Document {document_id} introuvable pour transaction {transaction_id}" f" Document {document_id} introuvable pour transaction {transaction_id}"
) )
return None return None
elif response.status_code == 403: elif response.status_code == 403:
logger.error( logger.error(
f" Accès refusé au document {document_id}. " f" Accès refusé au document {document_id}. "
f"Vérifiez que la transaction est bien signée." f"Vérifiez que la transaction est bien signée."
) )
return None return None
else: else:
logger.error( logger.error(
f" Erreur HTTP {response.status_code}: {response.text[:500]}" f" Erreur HTTP {response.status_code}: {response.text[:500]}"
) )
return None return None
@ -136,13 +133,12 @@ class UniversignDocumentService:
logger.error(f"⏱️ Timeout téléchargement document {document_id}") logger.error(f"⏱️ Timeout téléchargement document {document_id}")
return None return None
except Exception as e: except Exception as e:
logger.error(f" Erreur téléchargement: {e}", exc_info=True) logger.error(f" Erreur téléchargement: {e}", exc_info=True)
return None return None
async def download_and_store_signed_document( async def download_and_store_signed_document(
self, session: AsyncSession, transaction, force: bool = False self, session: AsyncSession, transaction, force: bool = False
) -> Tuple[bool, Optional[str]]: ) -> Tuple[bool, Optional[str]]:
# Vérification si déjà téléchargé
if not force and transaction.signed_document_path: if not force and transaction.signed_document_path:
if os.path.exists(transaction.signed_document_path): if os.path.exists(transaction.signed_document_path):
logger.debug( logger.debug(
@ -153,7 +149,6 @@ class UniversignDocumentService:
transaction.download_attempts += 1 transaction.download_attempts += 1
try: try:
# ÉTAPE 1: Récupérer les documents de la transaction
logger.info( logger.info(
f"Récupération document signé pour: {transaction.transaction_id}" f"Récupération document signé pour: {transaction.transaction_id}"
) )
@ -167,13 +162,11 @@ class UniversignDocumentService:
await session.commit() await session.commit()
return False, error return False, error
# ÉTAPE 2: Récupérer le premier document (ou chercher celui qui est signé)
document_id = None document_id = None
for doc in documents: for doc in documents:
doc_id = doc.get("id") doc_id = doc.get("id")
doc_status = doc.get("status", "").lower() doc_status = doc.get("status", "").lower()
# Priorité aux documents marqués comme signés/complétés
if doc_status in ["signed", "completed", "closed"]: if doc_status in ["signed", "completed", "closed"]:
document_id = doc_id document_id = doc_id
logger.info( logger.info(
@ -181,34 +174,30 @@ class UniversignDocumentService:
) )
break break
# Fallback sur le premier document si aucun n'est explicitement signé
if document_id is None: if document_id is None:
document_id = doc_id document_id = doc_id
if not document_id: if not document_id:
error = "Impossible de déterminer l'ID du document à télécharger" error = "Impossible de déterminer l'ID du document à télécharger"
logger.error(f" {error}") logger.error(f" {error}")
transaction.download_error = error transaction.download_error = error
await session.commit() await session.commit()
return False, error return False, error
# Stocker le document_id pour référence future
if hasattr(transaction, "universign_document_id"): if hasattr(transaction, "universign_document_id"):
transaction.universign_document_id = document_id transaction.universign_document_id = document_id
# ÉTAPE 3: Télécharger le document signé
pdf_content = self.download_signed_document( pdf_content = self.download_signed_document(
transaction_id=transaction.transaction_id, document_id=document_id transaction_id=transaction.transaction_id, document_id=document_id
) )
if not pdf_content: if not pdf_content:
error = f"Échec téléchargement document {document_id}" error = f"Échec téléchargement document {document_id}"
logger.error(f" {error}") logger.error(f" {error}")
transaction.download_error = error transaction.download_error = error
await session.commit() await session.commit()
return False, error return False, error
# ÉTAPE 4: Stocker le fichier localement
filename = self._generate_filename(transaction) filename = self._generate_filename(transaction)
file_path = SIGNED_DOCS_DIR / filename file_path = SIGNED_DOCS_DIR / filename
@ -217,13 +206,11 @@ class UniversignDocumentService:
file_size = os.path.getsize(file_path) file_size = os.path.getsize(file_path)
# Mise à jour de la transaction
transaction.signed_document_path = str(file_path) transaction.signed_document_path = str(file_path)
transaction.signed_document_downloaded_at = datetime.now() transaction.signed_document_downloaded_at = datetime.now()
transaction.signed_document_size_bytes = file_size transaction.signed_document_size_bytes = file_size
transaction.download_error = None transaction.download_error = None
# Stocker aussi l'URL de téléchargement pour référence
transaction.document_url = ( transaction.document_url = (
f"{self.api_url}/transactions/{transaction.transaction_id}" f"{self.api_url}/transactions/{transaction.transaction_id}"
f"/documents/{document_id}/download" f"/documents/{document_id}/download"
@ -239,14 +226,14 @@ class UniversignDocumentService:
except OSError as e: except OSError as e:
error = f"Erreur filesystem: {str(e)}" error = f"Erreur filesystem: {str(e)}"
logger.error(f" {error}") logger.error(f" {error}")
transaction.download_error = error transaction.download_error = error
await session.commit() await session.commit()
return False, error return False, error
except Exception as e: except Exception as e:
error = f"Erreur inattendue: {str(e)}" error = f"Erreur inattendue: {str(e)}"
logger.error(f" {error}", exc_info=True) logger.error(f" {error}", exc_info=True)
transaction.download_error = error transaction.download_error = error
await session.commit() await session.commit()
return False, error return False, error
@ -294,7 +281,6 @@ class UniversignDocumentService:
return deleted, int(size_freed_mb) return deleted, int(size_freed_mb)
# === MÉTHODES DE DIAGNOSTIC ===
def diagnose_transaction(self, transaction_id: str) -> Dict: def diagnose_transaction(self, transaction_id: str) -> Dict:
""" """
@ -308,7 +294,6 @@ class UniversignDocumentService:
} }
try: try:
# Test 1: Récupération de la transaction
logger.info(f"Diagnostic transaction: {transaction_id}") logger.info(f"Diagnostic transaction: {transaction_id}")
response = requests.get( response = requests.get(
@ -334,7 +319,6 @@ class UniversignDocumentService:
"participants_count": len(data.get("participants", [])), "participants_count": len(data.get("participants", [])),
} }
# Test 2: Documents disponibles
documents = data.get("documents", []) documents = data.get("documents", [])
result["checks"]["documents"] = [] result["checks"]["documents"] = []
@ -345,7 +329,6 @@ class UniversignDocumentService:
"status": doc.get("status"), "status": doc.get("status"),
} }
# Test téléchargement
if doc.get("id"): if doc.get("id"):
download_url = ( download_url = (
f"{self.api_url}/transactions/{transaction_id}" f"{self.api_url}/transactions/{transaction_id}"

View file

@ -159,7 +159,6 @@ class UniversignSyncService:
return stats return stats
# CORRECTION 1 : process_webhook dans universign_sync.py
async def process_webhook( async def process_webhook(
self, session: AsyncSession, payload: Dict, transaction_id: str = None self, session: AsyncSession, payload: Dict, transaction_id: str = None
) -> Tuple[bool, Optional[str]]: ) -> Tuple[bool, Optional[str]]:
@ -167,9 +166,7 @@ class UniversignSyncService:
Traite un webhook Universign - CORRECTION : meilleure gestion des payloads Traite un webhook Universign - CORRECTION : meilleure gestion des payloads
""" """
try: try:
# Si transaction_id n'est pas fourni, essayer de l'extraire
if not transaction_id: if not transaction_id:
# Même logique que dans universign.py
if ( if (
payload.get("type", "").startswith("transaction.") payload.get("type", "").startswith("transaction.")
and "payload" in payload and "payload" in payload
@ -195,7 +192,6 @@ class UniversignSyncService:
f"📨 Traitement webhook: transaction={transaction_id}, event={event_type}" f"📨 Traitement webhook: transaction={transaction_id}, event={event_type}"
) )
# Récupérer la transaction locale
query = ( query = (
select(UniversignTransaction) select(UniversignTransaction)
.options(selectinload(UniversignTransaction.signers)) .options(selectinload(UniversignTransaction.signers))
@ -208,25 +204,20 @@ class UniversignSyncService:
logger.warning(f"Transaction {transaction_id} inconnue localement") logger.warning(f"Transaction {transaction_id} inconnue localement")
return False, "Transaction inconnue" return False, "Transaction inconnue"
# Marquer comme webhook reçu
transaction.webhook_received = True transaction.webhook_received = True
# Stocker l'ancien statut pour comparaison
old_status = transaction.local_status.value old_status = transaction.local_status.value
# Force la synchronisation complète
success, error = await self.sync_transaction( success, error = await self.sync_transaction(
session, transaction, force=True session, transaction, force=True
) )
# Log du changement de statut
if success and transaction.local_status.value != old_status: if success and transaction.local_status.value != old_status:
logger.info( logger.info(
f"Webhook traité: {transaction_id} | " f"Webhook traité: {transaction_id} | "
f"{old_status}{transaction.local_status.value}" f"{old_status}{transaction.local_status.value}"
) )
# Enregistrer le log du webhook
await self._log_sync_attempt( await self._log_sync_attempt(
session=session, session=session,
transaction=transaction, transaction=transaction,
@ -248,7 +239,6 @@ class UniversignSyncService:
logger.error(f"💥 Erreur traitement webhook: {e}", exc_info=True) logger.error(f"💥 Erreur traitement webhook: {e}", exc_info=True)
return False, str(e) return False, str(e)
# CORRECTION 2 : _sync_signers - Ne pas écraser les signers existants
async def _sync_signers( async def _sync_signers(
self, self,
session: AsyncSession, session: AsyncSession,
@ -271,7 +261,6 @@ class UniversignSyncService:
logger.warning(f"Signataire sans email à l'index {idx}, ignoré") logger.warning(f"Signataire sans email à l'index {idx}, ignoré")
continue continue
# PROTECTION : gérer les statuts inconnus
raw_status = signer_data.get("status") or signer_data.get( raw_status = signer_data.get("status") or signer_data.get(
"state", "waiting" "state", "waiting"
) )
@ -302,7 +291,6 @@ class UniversignSyncService:
if signer_data.get("name") and not signer.name: if signer_data.get("name") and not signer.name:
signer.name = signer_data.get("name") signer.name = signer_data.get("name")
else: else:
# Nouveau signer avec gestion d'erreur intégrée
try: try:
signer = UniversignSigner( signer = UniversignSigner(
id=f"{transaction.id}_signer_{idx}_{int(datetime.now().timestamp())}", id=f"{transaction.id}_signer_{idx}_{int(datetime.now().timestamp())}",
@ -330,7 +318,6 @@ class UniversignSyncService:
): ):
import json import json
# Si statut final et pas de force, skip
if is_final_status(transaction.local_status.value) and not force: if is_final_status(transaction.local_status.value) and not force:
logger.debug( logger.debug(
f"⏭️ Skip {transaction.transaction_id}: statut final " f"⏭️ Skip {transaction.transaction_id}: statut final "
@ -340,14 +327,13 @@ class UniversignSyncService:
await session.commit() await session.commit()
return True, None return True, None
# Récupération du statut distant
logger.info(f"Synchronisation: {transaction.transaction_id}") logger.info(f"Synchronisation: {transaction.transaction_id}")
result = self.fetch_transaction_status(transaction.transaction_id) result = self.fetch_transaction_status(transaction.transaction_id)
if not result: if not result:
error = "Échec récupération données Universign" error = "Échec récupération données Universign"
logger.error(f" {error}: {transaction.transaction_id}") logger.error(f" {error}: {transaction.transaction_id}")
transaction.sync_attempts += 1 transaction.sync_attempts += 1
transaction.sync_error = error transaction.sync_error = error
await self._log_sync_attempt(session, transaction, "polling", False, error) await self._log_sync_attempt(session, transaction, "polling", False, error)
@ -360,7 +346,6 @@ class UniversignSyncService:
logger.info(f"📊 Statut Universign brut: {universign_status_raw}") logger.info(f"📊 Statut Universign brut: {universign_status_raw}")
# Convertir le statut
new_local_status = map_universign_to_local(universign_status_raw) new_local_status = map_universign_to_local(universign_status_raw)
previous_local_status = transaction.local_status.value previous_local_status = transaction.local_status.value
@ -369,7 +354,6 @@ class UniversignSyncService:
f"{new_local_status} (Local) | Actuel: {previous_local_status}" f"{new_local_status} (Local) | Actuel: {previous_local_status}"
) )
# Vérifier la transition
if not is_transition_allowed(previous_local_status, new_local_status): if not is_transition_allowed(previous_local_status, new_local_status):
logger.warning( logger.warning(
f"Transition refusée: {previous_local_status}{new_local_status}" f"Transition refusée: {previous_local_status}{new_local_status}"
@ -386,7 +370,6 @@ class UniversignSyncService:
f"🔔 CHANGEMENT DÉTECTÉ: {previous_local_status}{new_local_status}" f"🔔 CHANGEMENT DÉTECTÉ: {previous_local_status}{new_local_status}"
) )
# Mise à jour du statut Universign brut
try: try:
transaction.universign_status = UniversignTransactionStatus( transaction.universign_status = UniversignTransactionStatus(
universign_status_raw universign_status_raw
@ -404,11 +387,9 @@ class UniversignSyncService:
else: else:
transaction.universign_status = UniversignTransactionStatus.STARTED transaction.universign_status = UniversignTransactionStatus.STARTED
# Mise à jour du statut local
transaction.local_status = LocalDocumentStatus(new_local_status) transaction.local_status = LocalDocumentStatus(new_local_status)
transaction.universign_status_updated_at = datetime.now() transaction.universign_status_updated_at = datetime.now()
# Mise à jour des dates
if new_local_status == "EN_COURS" and not transaction.sent_at: if new_local_status == "EN_COURS" and not transaction.sent_at:
transaction.sent_at = datetime.now() transaction.sent_at = datetime.now()
logger.info("📅 Date d'envoi mise à jour") logger.info("📅 Date d'envoi mise à jour")
@ -419,15 +400,12 @@ class UniversignSyncService:
if new_local_status == "REFUSE" and not transaction.refused_at: if new_local_status == "REFUSE" and not transaction.refused_at:
transaction.refused_at = datetime.now() transaction.refused_at = datetime.now()
logger.info(" Date de refus mise à jour") logger.info(" Date de refus mise à jour")
if new_local_status == "EXPIRE" and not transaction.expired_at: if new_local_status == "EXPIRE" and not transaction.expired_at:
transaction.expired_at = datetime.now() transaction.expired_at = datetime.now()
logger.info("⏰ Date d'expiration mise à jour") logger.info("⏰ Date d'expiration mise à jour")
# === SECTION CORRIGÉE: Gestion des documents ===
# Ne plus chercher document_url dans la réponse (elle n'existe pas!)
# Le téléchargement se fait via le service document qui utilise le bon endpoint
documents = universign_data.get("documents", []) documents = universign_data.get("documents", [])
if documents: if documents:
@ -437,7 +415,6 @@ class UniversignSyncService:
f"status={first_doc.get('status')}" f"status={first_doc.get('status')}"
) )
# Téléchargement automatique du document signé
if new_local_status == "SIGNE" and not transaction.signed_document_path: if new_local_status == "SIGNE" and not transaction.signed_document_path:
logger.info("Déclenchement téléchargement document signé...") logger.info("Déclenchement téléchargement document signé...")
@ -456,20 +433,16 @@ class UniversignSyncService:
except Exception as e: except Exception as e:
logger.error( logger.error(
f" Erreur téléchargement document: {e}", exc_info=True f" Erreur téléchargement document: {e}", exc_info=True
) )
# === FIN SECTION CORRIGÉE ===
# Synchroniser les signataires
await self._sync_signers(session, transaction, universign_data) await self._sync_signers(session, transaction, universign_data)
# Mise à jour des métadonnées de sync
transaction.last_synced_at = datetime.now() transaction.last_synced_at = datetime.now()
transaction.sync_attempts += 1 transaction.sync_attempts += 1
transaction.needs_sync = not is_final_status(new_local_status) transaction.needs_sync = not is_final_status(new_local_status)
transaction.sync_error = None transaction.sync_error = None
# Log de la tentative
await self._log_sync_attempt( await self._log_sync_attempt(
session=session, session=session,
transaction=transaction, transaction=transaction,
@ -491,7 +464,6 @@ class UniversignSyncService:
await session.commit() await session.commit()
# Exécuter les actions post-changement
if status_changed: if status_changed:
logger.info(f"🎬 Exécution actions pour statut: {new_local_status}") logger.info(f"🎬 Exécution actions pour statut: {new_local_status}")
await self._execute_status_actions( await self._execute_status_actions(
@ -507,7 +479,7 @@ class UniversignSyncService:
except Exception as e: except Exception as e:
error_msg = f"Erreur lors de la synchronisation: {str(e)}" error_msg = f"Erreur lors de la synchronisation: {str(e)}"
logger.error(f" {error_msg}", exc_info=True) logger.error(f" {error_msg}", exc_info=True)
transaction.sync_error = error_msg[:1000] transaction.sync_error = error_msg[:1000]
transaction.sync_attempts += 1 transaction.sync_attempts += 1
@ -519,20 +491,16 @@ class UniversignSyncService:
return False, error_msg return False, error_msg
# CORRECTION 3 : Amélioration du logging dans sync_transaction
async def _sync_transaction_documents_corrected( async def _sync_transaction_documents_corrected(
self, session, transaction, universign_data: dict, new_local_status: str self, session, transaction, universign_data: dict, new_local_status: str
): ):
# Récupérer et stocker les infos documents
documents = universign_data.get("documents", []) documents = universign_data.get("documents", [])
if documents: if documents:
# Stocker le premier document_id pour référence
first_doc = documents[0] first_doc = documents[0]
first_doc_id = first_doc.get("id") first_doc_id = first_doc.get("id")
if first_doc_id: if first_doc_id:
# Stocker l'ID du document (si le champ existe dans le modèle)
if hasattr(transaction, "universign_document_id"): if hasattr(transaction, "universign_document_id"):
transaction.universign_document_id = first_doc_id transaction.universign_document_id = first_doc_id
@ -543,7 +511,6 @@ class UniversignSyncService:
else: else:
logger.debug("Aucun document dans la réponse Universign") logger.debug("Aucun document dans la réponse Universign")
# Téléchargement automatique si signé
if new_local_status == "SIGNE": if new_local_status == "SIGNE":
if not transaction.signed_document_path: if not transaction.signed_document_path:
logger.info("Déclenchement téléchargement document signé...") logger.info("Déclenchement téléchargement document signé...")
@ -563,7 +530,7 @@ class UniversignSyncService:
except Exception as e: except Exception as e:
logger.error( logger.error(
f" Erreur téléchargement document: {e}", exc_info=True f" Erreur téléchargement document: {e}", exc_info=True
) )
else: else:
logger.debug( logger.debug(

View file

@ -290,15 +290,11 @@ def _preparer_lignes_document(lignes: List) -> List[Dict]:
UNIVERSIGN_TO_LOCAL: Dict[str, str] = { UNIVERSIGN_TO_LOCAL: Dict[str, str] = {
# États initiaux
"draft": "EN_ATTENTE", "draft": "EN_ATTENTE",
"ready": "EN_ATTENTE", "ready": "EN_ATTENTE",
# En cours
"started": "EN_COURS", "started": "EN_COURS",
# États finaux (succès)
"completed": "SIGNE", "completed": "SIGNE",
"closed": "SIGNE", "closed": "SIGNE",
# États finaux (échec)
"refused": "REFUSE", "refused": "REFUSE",
"expired": "EXPIRE", "expired": "EXPIRE",
"canceled": "REFUSE", "canceled": "REFUSE",
@ -441,7 +437,7 @@ STATUS_MESSAGES: Dict[str, Dict[str, str]] = {
"ERREUR": { "ERREUR": {
"fr": "Erreur technique", "fr": "Erreur technique",
"en": "Technical error", "en": "Technical error",
"icon": "", "icon": "⚠️",
"color": "red", "color": "red",
}, },
} }