from sqlalchemy import ( Column, String, DateTime, Boolean, Integer, Text, Enum as SQLEnum, ForeignKey, Index, ) from sqlalchemy.orm import relationship from datetime import datetime from enum import Enum from database.models.generic_model import Base # ======================================== # ENUMS DE STATUTS # ======================================== class UniversignTransactionStatus(str, Enum): """Statuts de transaction Universign (exhaustifs)""" DRAFT = "draft" # Transaction créée, non démarrée READY = "ready" # Prête à démarrer STARTED = "started" # Démarrée, en cours COMPLETED = "completed" # Tous signés ✓ REFUSED = "refused" # Refusé par un signataire EXPIRED = "expired" # Délai expiré CANCELED = "canceled" # Annulée manuellement FAILED = "failed" # Erreur technique class UniversignSignerStatus(str, Enum): """Statuts d'un signataire individuel""" WAITING = "waiting" # En attente VIEWED = "viewed" # Document ouvert SIGNED = "signed" # Signé ✓ REFUSED = "refused" # Refusé EXPIRED = "expired" # Expiré class LocalDocumentStatus(str, Enum): """Statuts métier locaux (simplifié pour l'UI)""" PENDING = "EN_ATTENTE" # Transaction non démarrée ou en attente IN_PROGRESS = "EN_COURS" # Envoyé, en cours de signature SIGNED = "SIGNE" # Signé avec succès REJECTED = "REFUSE" # Refusé ou annulé EXPIRED = "EXPIRE" # Expiré ERROR = "ERREUR" # Erreur technique class SageDocumentType(int, Enum): """Types de documents Sage (synchronisé avec Sage)""" DEVIS = 0 BON_COMMANDE = 10 PREPARATION = 20 BON_LIVRAISON = 30 BON_RETOUR = 40 BON_AVOIR = 50 FACTURE = 60 # ======================================== # TABLE PRINCIPALE : TRANSACTIONS UNIVERSIGN # ======================================== class UniversignTransaction(Base): """ Table centrale : synchronisation bidirectionnelle Universign ↔ Local """ __tablename__ = "universign_transactions" # === IDENTIFIANTS === id = Column(String(36), primary_key=True) # UUID local transaction_id = Column( String(255), unique=True, nullable=False, index=True, comment="ID Universign (ex: tr_abc123)", ) # === LIEN AVEC LE DOCUMENT SAGE === sage_document_id = Column( String(50), nullable=False, index=True, comment="Numéro du document Sage (ex: DE00123)", ) sage_document_type = Column( SQLEnum(SageDocumentType), nullable=False, comment="Type de document Sage" ) # === STATUTS UNIVERSIGN (SOURCE DE VÉRITÉ) === universign_status = Column( SQLEnum(UniversignTransactionStatus), nullable=False, default=UniversignTransactionStatus.DRAFT, index=True, comment="Statut brut Universign", ) universign_status_updated_at = Column( DateTime, nullable=True, comment="Dernière MAJ du statut Universign" ) # === STATUT LOCAL (DÉDUIT) === local_status = Column( SQLEnum(LocalDocumentStatus), nullable=False, default=LocalDocumentStatus.PENDING, index=True, comment="Statut métier simplifié pour l'UI", ) # === URLS ET MÉTADONNÉES UNIVERSIGN === signer_url = Column(Text, nullable=True, comment="URL de signature") document_url = Column(Text, nullable=True, comment="URL du document signé") certificate_url = Column(Text, nullable=True, comment="URL du certificat") # === SIGNATAIRES === signers_data = Column( Text, nullable=True, comment="JSON des signataires (snapshot)" ) # === INFORMATIONS MÉTIER === requester_email = Column(String(255), nullable=True) requester_name = Column(String(255), nullable=True) document_name = Column(String(500), nullable=True) # === DATES CLÉS === created_at = Column( DateTime, default=datetime.now, nullable=False, comment="Date de création locale", ) sent_at = Column( DateTime, nullable=True, comment="Date d'envoi Universign (started)" ) signed_at = Column(DateTime, nullable=True, comment="Date de signature complète") refused_at = Column(DateTime, nullable=True) expired_at = Column(DateTime, nullable=True) canceled_at = Column(DateTime, nullable=True) # === SYNCHRONISATION === last_synced_at = Column( DateTime, nullable=True, comment="Dernière sync réussie avec Universign" ) sync_attempts = Column(Integer, default=0, comment="Nombre de tentatives de sync") sync_error = Column(Text, nullable=True) # === FLAGS === is_test = Column( Boolean, default=False, comment="Transaction en environnement .alpha" ) needs_sync = Column( Boolean, default=True, index=True, comment="À synchroniser avec Universign" ) webhook_received = Column(Boolean, default=False, comment="Webhook Universign reçu") # === RELATION === signers = relationship( "UniversignSigner", back_populates="transaction", cascade="all, delete-orphan" ) sync_logs = relationship( "UniversignSyncLog", back_populates="transaction", cascade="all, delete-orphan" ) # === INDEXES COMPOSITES === __table_args__ = ( Index("idx_sage_doc", "sage_document_id", "sage_document_type"), Index("idx_sync_status", "needs_sync", "universign_status"), Index("idx_dates", "created_at", "signed_at"), ) def __repr__(self): return ( f"" ) # ======================================== # TABLE SECONDAIRE : SIGNATAIRES # ======================================== class UniversignSigner(Base): """ Détail de chaque signataire d'une transaction """ __tablename__ = "universign_signers" id = Column(String(36), primary_key=True) transaction_id = Column( String(36), ForeignKey("universign_transactions.id", ondelete="CASCADE"), nullable=False, index=True, ) # === DONNÉES SIGNATAIRE === email = Column(String(255), nullable=False, index=True) name = Column(String(255), nullable=True) phone = Column(String(50), nullable=True) # === STATUT === status = Column( SQLEnum(UniversignSignerStatus), default=UniversignSignerStatus.WAITING, nullable=False, ) # === ACTIONS === viewed_at = Column(DateTime, nullable=True) signed_at = Column(DateTime, nullable=True) refused_at = Column(DateTime, nullable=True) refusal_reason = Column(Text, nullable=True) # === MÉTADONNÉES === ip_address = Column(String(45), nullable=True) user_agent = Column(Text, nullable=True) signature_method = Column(String(50), nullable=True) # === ORDRE === order_index = Column(Integer, default=0) # === RELATION === transaction = relationship("UniversignTransaction", back_populates="signers") def __repr__(self): return f"" # ======================================== # TABLE DE LOGS : SYNCHRONISATION # ======================================== class UniversignSyncLog(Base): """ Journal de toutes les synchronisations (audit trail) """ __tablename__ = "universign_sync_logs" id = Column(Integer, primary_key=True, autoincrement=True) transaction_id = Column( String(36), ForeignKey("universign_transactions.id", ondelete="CASCADE"), nullable=False, index=True, ) # === SYNC INFO === sync_type = Column(String(50), nullable=False, comment="webhook, polling, manual") sync_timestamp = Column(DateTime, default=datetime.now, nullable=False, index=True) # === CHANGEMENTS DÉTECTÉS === previous_status = Column(String(50), nullable=True) new_status = Column(String(50), nullable=True) changes_detected = Column(Text, nullable=True, comment="JSON des changements") # === RÉSULTAT === success = Column(Boolean, default=True) error_message = Column(Text, nullable=True) http_status_code = Column(Integer, nullable=True) response_time_ms = Column(Integer, nullable=True) # === RELATION === transaction = relationship("UniversignSyncLog", back_populates="sync_logs") def __repr__(self): return f"" # ======================================== # TABLE DE CONFIGURATION # ======================================== class UniversignConfig(Base): """ Configuration Universign par environnement/utilisateur """ __tablename__ = "universign_configs" id = Column(String(36), primary_key=True) user_id = Column(String(36), nullable=True, index=True) environment = Column( String(50), nullable=False, default="alpha", comment="alpha, prod" ) api_url = Column(String(500), nullable=False) api_key = Column(String(500), nullable=False, comment="⚠️ À chiffrer") # === OPTIONS === webhook_url = Column(String(500), nullable=True) webhook_secret = Column(String(255), nullable=True) auto_sync_enabled = Column(Boolean, default=True) sync_interval_minutes = Column(Integer, default=5) signature_expiry_days = Column(Integer, default=30) is_active = Column(Boolean, default=True) created_at = Column(DateTime, default=datetime.now) def __repr__(self): return f""