Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# CODEOWNERS para proyecto IACT
# Referencia: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners

# Por defecto, el equipo de backend revisa todos los cambios
* @2-Coatl/backend-team

# Configuracion y CI/CD
/.github/ @2-Coatl/devops-team
/scripts/ @2-Coatl/devops-team
/Vagrantfile @2-Coatl/devops-team
/.devcontainer/ @2-Coatl/devops-team

# Backend - Django
/api/ @2-Coatl/backend-team

# Modelos y Migraciones - Requiere revision especial
/api/callcentersite/callcentersite/apps/*/models.py @2-Coatl/backend-team @2-Coatl/dba-team
/api/callcentersite/callcentersite/apps/*/migrations/ @2-Coatl/backend-team @2-Coatl/dba-team

# Sistema de Permisos - Seguridad critica
/api/callcentersite/callcentersite/apps/permissions/ @2-Coatl/backend-team @2-Coatl/security-team
/docs/backend/permisos/ @2-Coatl/backend-team @2-Coatl/security-team
/docs/adr/ADR-012*.md @2-Coatl/backend-team @2-Coatl/security-team

# Modulos Operativos (Prioridad 3)
/api/callcentersite/callcentersite/apps/llamadas/ @2-Coatl/backend-team @2-Coatl/operations-team
/api/callcentersite/callcentersite/apps/tickets/ @2-Coatl/backend-team @2-Coatl/support-team
/api/callcentersite/callcentersite/apps/clientes/ @2-Coatl/backend-team @2-Coatl/operations-team
/api/callcentersite/callcentersite/apps/metricas/ @2-Coatl/backend-team @2-Coatl/analytics-team
/api/callcentersite/callcentersite/apps/reportes/ @2-Coatl/backend-team @2-Coatl/analytics-team
/api/callcentersite/callcentersite/apps/alertas/ @2-Coatl/backend-team @2-Coatl/operations-team

# Modulos de Gestion (Prioridad 4)
/api/callcentersite/callcentersite/apps/equipos/ @2-Coatl/backend-team @2-Coatl/management-team
/api/callcentersite/callcentersite/apps/horarios/ @2-Coatl/backend-team @2-Coatl/management-team
/api/callcentersite/callcentersite/apps/evaluaciones/ @2-Coatl/backend-team @2-Coatl/quality-team
/api/callcentersite/callcentersite/apps/audit/ @2-Coatl/backend-team @2-Coatl/security-team

# Modulos Financieros (Prioridad 5) - Revision critica
/api/callcentersite/callcentersite/apps/pagos/ @2-Coatl/backend-team @2-Coatl/finance-team @2-Coatl/security-team
/api/callcentersite/callcentersite/apps/facturas/ @2-Coatl/backend-team @2-Coatl/finance-team
/api/callcentersite/callcentersite/apps/cobranza/ @2-Coatl/backend-team @2-Coatl/finance-team

# Modulos Estrategicos (Prioridad 6) - Solo directores
/api/callcentersite/callcentersite/apps/presupuestos/ @2-Coatl/backend-team @2-Coatl/executive-team
/api/callcentersite/callcentersite/apps/politicas/ @2-Coatl/backend-team @2-Coatl/executive-team

# Frontend - React
/ui/ @2-Coatl/frontend-team

# Componentes compartidos
/ui/src/components/ @2-Coatl/frontend-team
/ui/src/state/ @2-Coatl/frontend-team

# Modulos frontend por funcionalidad
/ui/src/modules/permissions/ @2-Coatl/frontend-team @2-Coatl/security-team
/ui/src/modules/llamadas/ @2-Coatl/frontend-team @2-Coatl/operations-team
/ui/src/modules/tickets/ @2-Coatl/frontend-team @2-Coatl/support-team
/ui/src/modules/clientes/ @2-Coatl/frontend-team @2-Coatl/operations-team
/ui/src/modules/metricas/ @2-Coatl/frontend-team @2-Coatl/analytics-team
/ui/src/modules/reportes/ @2-Coatl/frontend-team @2-Coatl/analytics-team

# Mock data para desarrollo
/ui/src/mocks/ @2-Coatl/frontend-team @2-Coatl/qa-team

# Documentacion
/docs/ @2-Coatl/documentation-team

# ADRs - Decisiones arquitectonicas
/docs/adr/ @2-Coatl/backend-team @2-Coatl/frontend-team @2-Coatl/architecture-team

# Documentacion backend
/docs/backend/ @2-Coatl/backend-team @2-Coatl/documentation-team

# Documentacion frontend
/docs/frontend/ @2-Coatl/frontend-team @2-Coatl/documentation-team

# Guias operativas
/docs/guias/ @2-Coatl/devops-team @2-Coatl/documentation-team

# Metricas DORA
/dora_metrics/ @2-Coatl/devops-team
/docs/backend/devops/metricas-dora.md @2-Coatl/devops-team

# Tests - QA debe revisar
/api/**/tests/ @2-Coatl/backend-team @2-Coatl/qa-team
/ui/**/*.test.* @2-Coatl/frontend-team @2-Coatl/qa-team
/ui/**/*.spec.* @2-Coatl/frontend-team @2-Coatl/qa-team

# Scripts de validacion - DevOps + Security
/scripts/check_no_emojis.py @2-Coatl/devops-team
/scripts/validate_critical_restrictions.sh @2-Coatl/devops-team @2-Coatl/security-team

# Configuracion sensible - Multiples equipos
/api/callcentersite/callcentersite/settings/ @2-Coatl/backend-team @2-Coatl/devops-team @2-Coatl/security-team

# Base de datos - DBA debe aprobar
/api/callcentersite/callcentersite/database_router.py @2-Coatl/backend-team @2-Coatl/dba-team

# IVR Legacy - Protegido, solo lectura
/api/callcentersite/callcentersite/apps/ivr_legacy/ @2-Coatl/backend-team @2-Coatl/dba-team @2-Coatl/legacy-team

# README y archivos raiz
/README.md @2-Coatl/documentation-team
/CONTRIBUTING.md @2-Coatl/documentation-team
/LICENSE @2-Coatl/legal-team

# Este archivo
/CODEOWNERS @2-Coatl/architecture-team @2-Coatl/devops-team
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
11 changes: 11 additions & 0 deletions api/callcentersite/callcentersite/apps/llamadas/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Django app configuration para Llamadas."""

from django.apps import AppConfig


class LlamadasConfig(AppConfig):
"""Configuracion de la app Llamadas."""

default_auto_field = 'django.db.models.BigAutoField'
name = 'callcentersite.apps.llamadas'
verbose_name = 'Llamadas'
Empty file.
147 changes: 147 additions & 0 deletions api/callcentersite/callcentersite/apps/llamadas/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"""
Modelos para gestión de Llamadas.

Sistema de Permisos Granular - Prioridad 3: Módulo Operativo Llamadas
REF: ADR-012-sistema-permisos-sin-roles-jerarquicos.md
"""

from django.db import models
from django.contrib.auth import get_user_model
from django.utils import timezone
import uuid


User = get_user_model()


class EstadoLlamada(models.Model):
"""Estados posibles de una llamada."""

codigo = models.CharField(max_length=50, unique=True, help_text='Codigo unico del estado')
nombre = models.CharField(max_length=100, help_text='Nombre del estado')
descripcion = models.TextField(blank=True, help_text='Descripcion del estado')
es_final = models.BooleanField(default=False, help_text='Si es un estado final')
activo = models.BooleanField(default=True, help_text='Si esta activo')
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
db_table = 'llamadas_estados'
verbose_name = 'Estado de Llamada'
verbose_name_plural = 'Estados de Llamadas'
ordering = ['nombre']

def __str__(self):
return self.nombre


class TipoLlamada(models.Model):
"""Tipos de llamadas."""

codigo = models.CharField(max_length=50, unique=True, help_text='Codigo unico del tipo')
nombre = models.CharField(max_length=100, help_text='Nombre del tipo')
descripcion = models.TextField(blank=True, help_text='Descripcion del tipo')
activo = models.BooleanField(default=True, help_text='Si esta activo')
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
db_table = 'llamadas_tipos'
verbose_name = 'Tipo de Llamada'
verbose_name_plural = 'Tipos de Llamadas'
ordering = ['nombre']

def __str__(self):
return self.nombre


class Llamada(models.Model):
"""Registro de llamadas telefónicas."""

codigo = models.CharField(max_length=50, unique=True, editable=False, help_text='Codigo unico generado')
numero_telefono = models.CharField(max_length=20, help_text='Numero telefonico')
tipo = models.ForeignKey(TipoLlamada, on_delete=models.PROTECT, related_name='llamadas')
estado = models.ForeignKey(EstadoLlamada, on_delete=models.PROTECT, related_name='llamadas')
agente = models.ForeignKey(User, on_delete=models.PROTECT, related_name='llamadas_atendidas')

# Informacion del cliente
cliente_nombre = models.CharField(max_length=200, blank=True, null=True)
cliente_email = models.EmailField(blank=True, null=True)
cliente_id = models.IntegerField(blank=True, null=True, help_text='ID del cliente si existe')

# Fechas y tiempos
fecha_inicio = models.DateTimeField(default=timezone.now)
fecha_fin = models.DateTimeField(blank=True, null=True)

# Metadata
metadata = models.JSONField(default=dict, blank=True, help_text='Datos adicionales JSON')
notas = models.TextField(blank=True, help_text='Notas de la llamada')

created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
db_table = 'llamadas'
verbose_name = 'Llamada'
verbose_name_plural = 'Llamadas'
ordering = ['-fecha_inicio']
indexes = [
models.Index(fields=['numero_telefono']),
models.Index(fields=['agente', 'fecha_inicio']),
models.Index(fields=['estado']),
models.Index(fields=['fecha_inicio']),
]

def save(self, *args, **kwargs):
"""Generar codigo unico al crear."""
if not self.codigo:
self.codigo = f"CALL-{uuid.uuid4().hex[:12].upper()}"
super().save(*args, **kwargs)

def calcular_duracion(self):
"""Calcular duracion de la llamada en segundos."""
if self.fecha_fin:
delta = self.fecha_fin - self.fecha_inicio
return int(delta.total_seconds())
return None

def __str__(self):
return f"{self.codigo} - {self.numero_telefono}"


class LlamadaTranscripcion(models.Model):
"""Transcripción de llamadas."""

llamada = models.ForeignKey(Llamada, on_delete=models.CASCADE, related_name='transcripciones')
texto = models.TextField(help_text='Texto transcrito')
timestamp_inicio = models.IntegerField(help_text='Segundo de inicio en la grabacion')
timestamp_fin = models.IntegerField(help_text='Segundo de fin en la grabacion')
hablante = models.CharField(max_length=50, help_text='Identificador del hablante (agente/cliente)')
confianza = models.FloatField(blank=True, null=True, help_text='Nivel de confianza de la transcripcion')
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
db_table = 'llamadas_transcripciones'
verbose_name = 'Transcripcion de Llamada'
verbose_name_plural = 'Transcripciones de Llamadas'
ordering = ['llamada', 'timestamp_inicio']

def __str__(self):
return f"Transcripcion {self.llamada.codigo} - {self.hablante}"


class LlamadaGrabacion(models.Model):
"""Grabaciones de llamadas."""

llamada = models.OneToOneField(Llamada, on_delete=models.CASCADE, related_name='grabacion')
archivo_url = models.URLField(max_length=500, help_text='URL del archivo de grabacion')
formato = models.CharField(max_length=10, help_text='Formato del audio (mp3, wav, etc)')
duracion_segundos = models.IntegerField(help_text='Duracion en segundos')
tamano_bytes = models.BigIntegerField(blank=True, null=True, help_text='Tamano del archivo en bytes')
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
db_table = 'llamadas_grabaciones'
verbose_name = 'Grabacion de Llamada'
verbose_name_plural = 'Grabaciones de Llamadas'

def __str__(self):
return f"Grabacion {self.llamada.codigo}"
80 changes: 80 additions & 0 deletions api/callcentersite/callcentersite/apps/llamadas/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""
Serializers para Llamadas.

Sistema de Permisos Granular - Prioridad 3: Módulo Operativo Llamadas
"""

from rest_framework import serializers
from callcentersite.apps.llamadas.models import (
EstadoLlamada,
TipoLlamada,
Llamada,
LlamadaTranscripcion,
LlamadaGrabacion,
)


class EstadoLlamadaSerializer(serializers.ModelSerializer):
"""Serializer para EstadoLlamada."""

class Meta:
model = EstadoLlamada
fields = ['id', 'codigo', 'nombre', 'descripcion', 'es_final', 'activo', 'created_at']
read_only_fields = ['id', 'created_at']


class TipoLlamadaSerializer(serializers.ModelSerializer):
"""Serializer para TipoLlamada."""

class Meta:
model = TipoLlamada
fields = ['id', 'codigo', 'nombre', 'descripcion', 'activo', 'created_at']
read_only_fields = ['id', 'created_at']


class LlamadaSerializer(serializers.ModelSerializer):
"""Serializer para Llamada."""

duracion = serializers.SerializerMethodField()
agente_username = serializers.CharField(source='agente.username', read_only=True)
estado_nombre = serializers.CharField(source='estado.nombre', read_only=True)
tipo_nombre = serializers.CharField(source='tipo.nombre', read_only=True)

class Meta:
model = Llamada
fields = [
'id', 'codigo', 'numero_telefono', 'tipo', 'estado', 'agente',
'cliente_nombre', 'cliente_email', 'cliente_id',
'fecha_inicio', 'fecha_fin', 'metadata', 'notas',
'created_at', 'updated_at',
'duracion', 'agente_username', 'estado_nombre', 'tipo_nombre'
]
read_only_fields = ['id', 'codigo', 'created_at', 'updated_at']

def get_duracion(self, obj):
"""Obtener duracion calculada."""
return obj.calcular_duracion()


class LlamadaTranscripcionSerializer(serializers.ModelSerializer):
"""Serializer para LlamadaTranscripcion."""

class Meta:
model = LlamadaTranscripcion
fields = [
'id', 'llamada', 'texto', 'timestamp_inicio', 'timestamp_fin',
'hablante', 'confianza', 'created_at'
]
read_only_fields = ['id', 'created_at']


class LlamadaGrabacionSerializer(serializers.ModelSerializer):
"""Serializer para LlamadaGrabacion."""

class Meta:
model = LlamadaGrabacion
fields = [
'id', 'llamada', 'archivo_url', 'formato', 'duracion_segundos',
'tamano_bytes', 'created_at'
]
read_only_fields = ['id', 'created_at']
Empty file.
Loading
Loading