feat: completar Fase 2 del Roadmap MVP (Co-docencia, Matriz de Conflictos y Regla Diaria) y auditoria de seguridad

This commit is contained in:
2026-09-19 14:05:16 -03:00
parent 05e197e49f
commit ada39248d7
36 changed files with 3037 additions and 443 deletions
+1
View File
@@ -0,0 +1 @@
from .document_types import DOCUMENT_TYPES, DOCUMENT_TYPE_MAP, get_document_type, validate_document, format_document
+160
View File
@@ -0,0 +1,160 @@
import re
DOCUMENT_TYPES = [
{
'code': 'DNI',
'name': 'Documento Nacional de Identidad',
'short_name': 'DNI',
'category': 'Nacional',
'description': 'Documento oficial para ciudadanos argentinos nativos, naturalizados y residentes permanentes.',
'regex': r'^\d{7,8}$',
'placeholder': 'Ej: 38123456 (7 u 8 dígitos sin puntos)',
'mask_example': '38.123.456'
},
{
'code': 'CUIL',
'name': 'Código Único de Identificación Laboral (CUIL/CUIT)',
'short_name': 'CUIL',
'category': 'Nacional / Laboral',
'description': 'Identificador fiscal y laboral en Argentina (11 dígitos).',
'regex': r'^\d{11}$|^\d{2}-\d{8}-\d{1}$',
'placeholder': 'Ej: 20381234567 o 20-38123456-7',
'mask_example': '20-38123456-7'
},
{
'code': 'PAS',
'name': 'Pasaporte Argentino',
'short_name': 'Pasaporte ARG',
'category': 'Internacional',
'description': 'Pasaporte emitido por la República Argentina para viajes y acreditación internacional.',
'regex': r'^[a-zA-Z0-9]{6,12}$',
'placeholder': 'Ej: AAB123456',
'mask_example': 'PAS AAB123456'
},
{
'code': 'PASEXT',
'name': 'Pasaporte Extranjero',
'short_name': 'Pasaporte Extranjero',
'category': 'Internacional / Extranjero',
'description': 'Pasaporte oficial emitido por país de origen (docentes invitados, alumnos de intercambio).',
'regex': r'^[a-zA-Z0-9\-\.]{4,20}$',
'placeholder': 'Ej: US987654321 / B1234567',
'mask_example': 'PASEXT US987654321'
},
{
'code': 'DNIEXT',
'name': 'DNI para Extranjeros (Residente)',
'short_name': 'DNI Extranjero',
'category': 'Extranjero / Residente',
'description': 'DNI argentino otorgado a ciudadanos extranjeros con radicación o residencia temporaria.',
'regex': r'^[a-zA-Z0-9]{7,10}$',
'placeholder': 'Ej: 94123456 o E94123456',
'mask_example': 'DNI-EXT 94.123.456'
},
{
'code': 'CI',
'name': 'Cédula de Identidad (PFA / Policía Provincial)',
'short_name': 'Cédula Identidad',
'category': 'Nacional (Histórico)',
'description': 'Cédula de Identidad emitida por Policía Federal Argentina o Policías Provinciales.',
'regex': r'^\d{6,9}$',
'placeholder': 'Ej: 6123456',
'mask_example': 'CI 6.123.456'
},
{
'code': 'CIEXT',
'name': 'Cédula de Identidad Extranjera (Mercosur)',
'short_name': 'Cédula Mercosur',
'category': 'Regional / Mercosur',
'description': 'Cédula de Identidad emitida por países miembros o asociados al Mercosur (Brasil, Uruguay, etc.).',
'regex': r'^[a-zA-Z0-9\-\.]{5,18}$',
'placeholder': 'Ej: RG12345678 (Brasil) o 4.123.456-7 (Uruguay)',
'mask_example': 'CI-EXT 12345678'
},
{
'code': 'LC',
'name': 'Libreta Cívica',
'short_name': 'Libreta Cívica',
'category': 'Histórico Nacional',
'description': 'Documento histórico nacional para ciudadanas mujeres argentinas.',
'regex': r'^\d{6,8}$',
'placeholder': 'Ej: 5123456',
'mask_example': 'LC 5.123.456'
},
{
'code': 'LE',
'name': 'Libreta de Enrolamiento',
'short_name': 'Libreta Enrolamiento',
'category': 'Histórico Nacional',
'description': 'Documento histórico nacional para ciudadanos varones argentinos.',
'regex': r'^\d{6,8}$',
'placeholder': 'Ej: 4123456',
'mask_example': 'LE 4.123.456'
}
]
DOCUMENT_TYPE_MAP = {doc['code']: doc for doc in DOCUMENT_TYPES}
def get_document_type(code):
"""Devuelve la especificación de un tipo de documento o None."""
if not code:
return None
return DOCUMENT_TYPE_MAP.get(str(code).strip().upper())
def validate_document(doc_type, doc_number):
"""
Valida un número de documento según las reglas de negocio de su tipificación.
Retorna (is_valid: bool, cleaned_number: str, error_message: str|None).
"""
if not doc_type:
return False, '', 'Debe especificar el tipo de documento.'
spec = get_document_type(doc_type)
if not spec:
return False, '', f"El tipo de documento '{doc_type}' no está registrado en el catálogo oficial."
if not doc_number:
return True, '', None # Document number may be optional if pending registration
cleaned = str(doc_number).strip().upper()
# Si es DNI, LC, LE, CI podemos remover puntos y guiones intermedios para validar y almacenar de forma uniforme
if spec['code'] in ['DNI', 'LC', 'LE', 'CI', 'DNIEXT']:
numeric_only = re.sub(r'[\.\-\s]', '', cleaned)
if re.match(spec['regex'], numeric_only):
return True, numeric_only, None
return False, cleaned, f"Formato inválido para {spec['name']}. Debe contener {spec['placeholder']}."
elif spec['code'] == 'CUIL':
numeric_only = re.sub(r'[\.\-\s]', '', cleaned)
if re.match(r'^\d{11}$', numeric_only):
return True, numeric_only, None
return False, cleaned, "El CUIL debe contener 11 dígitos numéricos (ej. 20-38123456-7)."
else:
# Pasaportes o Cédulas Extranjeras
if re.match(spec['regex'], cleaned):
return True, cleaned, None
return False, cleaned, f"Formato inválido para {spec['name']} ({spec['placeholder']})."
def format_document(doc_type, doc_number):
"""
Formatea visualmente un número de documento con puntos/guiones estándar.
"""
if not doc_number:
return ''
raw = str(doc_number).strip().upper()
dtype = (doc_type or 'DNI').upper()
if dtype in ['DNI', 'LC', 'LE', 'CI']:
clean = re.sub(r'\D', '', raw)
if len(clean) == 8:
return f"{clean[:2]}.{clean[2:5]}.{clean[5:]}"
elif len(clean) == 7:
return f"{clean[:1]}.{clean[1:4]}.{clean[4:]}"
return clean
elif dtype == 'CUIL':
clean = re.sub(r'\D', '', raw)
if len(clean) == 11:
return f"{clean[:2]}-{clean[2:10]}-{clean[10:]}"
return clean
return raw