feat(phase5): integracion auth hibrida, email dinamico y moodle 4.1 backend con tolerancia a fallos

This commit is contained in:
2026-09-23 13:34:59 -03:00
parent 7ad4e3f099
commit 1aae432bba
22 changed files with 2993 additions and 49 deletions
+69
View File
@@ -0,0 +1,69 @@
from app import db
from datetime import datetime
import json
class MoodleSyncTask(db.Model):
"""
Cola persistente de eventos y tareas de sincronización asíncrona hacia Moodle 4.1.
Garantiza tolerancia a fallos ante caídas o saturación del servidor Moodle,
incorporando reintentos exponenciales y Dead Letter Queue (DLQ).
"""
__tablename__ = 'moodle_sync_tasks'
id = db.Column(db.Integer, primary_key=True)
action = db.Column(db.String(50), nullable=False, index=True) # CREATE_USER, UPDATE_USER, ENROL_USER, UNENROL_USER, ASSIGN_ROLE
entity_type = db.Column(db.String(50), nullable=False) # user, commission, enrollment
entity_id = db.Column(db.String(100), nullable=True)
_payload = db.Column('payload', db.Text, nullable=False) # JSON serializado en Text
status = db.Column(db.String(20), default='PENDING', index=True) # PENDING, PROCESSING, RETRYING, COMPLETED, FAILED
attempts = db.Column(db.Integer, default=0)
max_attempts = db.Column(db.Integer, default=5)
error_message = db.Column(db.Text, nullable=True)
next_retry_at = db.Column(db.DateTime, default=datetime.utcnow, index=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def __init__(self, **kwargs):
if 'payload' in kwargs:
raw_payload = kwargs.pop('payload')
if isinstance(raw_payload, (dict, list)):
kwargs['_payload'] = json.dumps(raw_payload)
else:
kwargs['_payload'] = str(raw_payload or '{}')
super().__init__(**kwargs)
@property
def payload(self):
try:
return json.loads(self._payload) if self._payload else {}
except Exception:
return {}
@payload.setter
def payload(self, val):
if isinstance(val, (dict, list)):
self._payload = json.dumps(val)
else:
self._payload = str(val or '{}')
def get_payload_dict(self):
return self.payload
def to_dict(self):
return {
'id': self.id,
'action': self.action,
'entity_type': self.entity_type,
'entity_id': self.entity_id,
'payload': self.payload,
'status': self.status,
'attempts': self.attempts,
'max_attempts': self.max_attempts,
'error_message': self.error_message,
'next_retry_at': self.next_retry_at.isoformat() if self.next_retry_at else None,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
}
def __repr__(self):
return f'<MoodleSyncTask #{self.id} {self.action} [{self.status}]>'