322 lines
9.5 KiB
Python
322 lines
9.5 KiB
Python
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"<UniversignTransaction {self.transaction_id} "
|
|
f"sage={self.sage_document_id} "
|
|
f"status={self.universign_status.value}>"
|
|
)
|
|
|
|
|
|
# ========================================
|
|
# 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"<UniversignSigner {self.email} status={self.status.value}>"
|
|
|
|
|
|
# ========================================
|
|
# 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("UniversignTransaction", back_populates="sync_logs")
|
|
|
|
def __repr__(self):
|
|
return f"<SyncLog {self.sync_type} at {self.sync_timestamp}>"
|
|
|
|
|
|
# ========================================
|
|
# 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"<UniversignConfig {self.environment}>"
|