Sistema de gestión de cartera

SaaS multiempresa REST API IA predictiva v1.0

Plataforma web para administración de cuentas por cobrar de múltiples empresas. Gestiona clientes, facturas, pagos, cartera vencida y análisis de riesgo mediante inteligencia artificial.

Frontend
Vue.js 3
Backend
Flask · Python
Base de datos
PostgreSQL 15
IA / ML
Scikit-Learn

Arquitectura general

El frontend en Vue.js 3 consume una API REST desarrollada en Flask. El backend centraliza toda la lógica de negocio, autenticación, autorización y ejecución de modelos de IA. Todos los datos se almacenan en PostgreSQL. La infraestructura corre sobre Docker con Nginx como reverse proxy y Gunicorn como servidor WSGI.

El módulo de inteligencia artificial está disponible únicamente para empresas con plan Enterprise. El control se aplica en el backend con @requiere_plan y en el frontend con el composable usePlan().

Modelo multiempresa

Cada registro del sistema lleva un campo empresa_id que lo aísla completamente. Ninguna empresa puede ver ni modificar datos de otra. El empresa_id se extrae siempre del token JWT, nunca de parámetros del cliente.

Planes comerciales

Básico

Clientes · Facturas · Pagos · Reportes básicos

Profesional

Dashboard avanzado · Reportes financieros · Exportación · KPIs

Enterprise IA

Predicción mora · Segmentación · Reportes inteligentes · Recomendaciones

Frontend — stack y estructura

Vue.js 3 · Composition API

Frontend

Stack tecnológico

TecnologíaVersiónPropósitoDocs
Vue.js3.xFramework principal (Composition API)Documentación
Vue Router4.xNavegación y rutas protegidasDocumentación
Pinia2.xEstado global por dominioDocumentación
Axios1.xComunicación con la API RESTDocumentación
Vuetify3.xComponentes UI y estilosDocumentación
Chart.js4.xGráficas del dashboardDocumentación
Vite5.xBundler y entorno de desarrolloDocumentación
VeeValidate4.xValidación de formulariosDocumentación
YupEsquemas de validación para VeeValidateDocumentación

Estructura de carpetas

frontend/ ├── src/ │ ├── assets/ // Imágenes, íconos, estilos globales │ ├── components/ │ │ ├── common/ // Botones, modales, tablas, alertas │ │ ├── charts/ // Componentes Chart.js │ │ └── layout/ // Navbar, Sidebar, Footer │ ├── views/ │ │ ├── auth/ // Login, recuperar, reset │ │ ├── dashboard/ │ │ ├── clientes/ │ │ ├── facturas/ │ │ ├── pagos/ │ │ ├── cartera/ │ │ ├── reportes/ │ │ └── configuracion/ │ ├── router/ // index.js — rutas + guards │ ├── stores/ // Pinia — un store por dominio │ ├── services/ // Llamadas Axios por módulo │ ├── utils/ // Formatters, constantes, composables │ ├── middleware/ // Guards de navegación │ ├── App.vue │ └── main.js ├── .env ├── vite.config.js └── package.json
Todas las vistas deben cargarse con () => import(...) (lazy loading). Nunca importar vistas estáticamente en el router.

Variables de entorno

VITE_API_URL=https://api.tudominio.com/api/v1
VITE_APP_NAME=GesCartera

Convenciones

  • Composition API con <script setup> en todos los componentes
  • PascalCase para vistas y componentes · camelCase para stores y services
  • Todo error de API manejado con try/catch y toast de notificación
  • No lógica de negocio en el frontend — saldos, estados y clasificaciones los retorna la API

Frontend — módulos y vistas

Vistas, formularios y comportamiento de UI

Frontend

Autenticación

Vistas: LoginView · RecuperarPasswordView · ResetPasswordView

Al iniciar sesión, almacenar access_token y refresh_token en el store de Pinia. Implementar interceptor de Axios para adjuntar el token y refrescarlo automáticamente ante error 401.

Dashboard

Vista única que carga todos los KPIs con una sola llamada a GET /api/v1/dashboard/resumen. Usar skeleton loaders mientras cargan los datos.

GráficaTipoDatos
Evolución de carteraLine chartÚltimos 6–12 meses
Evolución de recaudoBar chartPor mes
Cartera por edadesDoughnut0–30, 31–60, 61–90, +90 días
Clientes por riesgoPie chartBajo, medio, alto
Comparativo mensualGrouped barMes actual vs anterior

Facturas — estados

Pendiente Pagada Vencida En cobro Castigada
Deshabilitar edición para facturas en estado Pagada y Castigada. Al registrar un pago, actualizar el saldo reactivamente sin recargar la página.

Pagos — lógica de UI

  • Mostrar saldo pendiente de la factura seleccionada en tiempo real
  • Validar que el valor pagado no exceda el saldo pendiente
  • Emitir alerta si el pago cubre la totalidad de la factura (cambio a Pagada)

Reportes — planes

ReportePlan mínimo
Cartera general, vencida, recaudo, morososBásico
Desempeño financiero · Exportación PDF/ExcelProfesional
Narrativas IA · Recomendaciones automáticasEnterprise

Frontend — estado y routing

Pinia · Vue Router · Axios interceptors

Frontend

Store Pinia — estructura estándar

export const useClientesStore = defineStore('clientes', {
  state: () => ({
    clientes: [],
    clienteActual: null,
    loading: false,
    error: null,
    pagination: { page: 1, per_page: 20, total: 0 },
  }),
  actions: {
    async fetchClientes(params = {}) {
      this.loading = true
      try {
        const res = await ClientesService.getAll(params)
        this.clientes = res.data.items
        this.pagination = res.data.pagination
      } catch (e) {
        this.error = e.message
      } finally {
        this.loading = false
      }
    }
  }
})

Guards de navegación

router.beforeEach((to, from, next) => {
  const auth = useAuthStore()
  if (!to.meta.public && !auth.isAuthenticated) return next('/login')
  if (to.meta.roles && !to.meta.roles.includes(auth.userRole))
    return next('/dashboard')
  next()
})

Interceptor Axios — refresh automático

api.interceptors.response.use(
  res => res,
  async err => {
    if (err.response?.status === 401) {
      await useAuthStore().refresh()  // POST /auth/refresh
    }
    return Promise.reject(err)
  }
)

Paginación

Todos los listados usan paginación del lado del servidor con parámetros page y per_page. La respuesta incluye el objeto pagination con total y pages.

Frontend — auth y control de planes

Frontend

Control de acceso por plan

// utils/usePlan.js
export function usePlan() {
  const auth = useAuthStore()
  const plan = auth.empresa?.plan  // 'basico' | 'profesional' | 'enterprise'
  return {
    tieneIA:                plan === 'enterprise',
    tieneReportesAvanzados: ['profesional','enterprise'].includes(plan),
    tieneExportacion:       ['profesional','enterprise'].includes(plan),
  }
}

// Uso en componente:
const { tieneIA } = usePlan()
// <SeccionIA v-if="tieneIA" />
// <BannerUpgrade v-else />
El control de planes en el frontend es solo presentación. La validación real ocurre en el backend con @requiere_plan. Nunca asumir que el frontend es suficiente para proteger endpoints.

Permisos por rol

RolCrearEditarExportarConfig empresa
admin
supervisor
analista
operadorSolo pagos

Backend — stack y estructura

Python · Flask · PostgreSQL

Backend

Stack tecnológico

TecnologíaVersiónPropósitoDocs
Python3.11+Lenguaje principalDocumentación
Flask3.xFramework webDocumentación
Flask-RESTX1.xAPI REST + Swagger automáticoDocumentación
SQLAlchemy2.xORMDocumentación
Alembic1.xMigraciones de base de datosDocumentación
Marshmallow3.xSerialización y validaciónDocumentación
Flask-JWT-Extended4.xAutenticación JWTDocumentación
Argon2-cffiHash de contraseñasDocumentación
PandasProcesamiento de datos para IADocumentación
Scikit-LearnModelos de Machine LearningDocumentación
XGBoostPredicción de mora (opcional)Documentación
GunicornServidor WSGI en producciónDocumentación
DockerContenedorizaciónDocumentación

Estructura de carpetas

backend/ ├── app/ │ ├── __init__.py // Factory de la app Flask │ ├── config.py │ ├── extensions.py // db, jwt, ma │ ├── models/ │ │ ├── empresa.py · usuario.py · cliente.py │ │ ├── factura.py · pago.py · acuerdo_pago.py │ │ └── prediccion_ia.py · audit_log.py │ ├── schemas/ // Marshmallow por entidad │ ├── api/ // Namespaces Flask-RESTX │ │ ├── auth/ · empresas/ · usuarios/ │ │ ├── clientes/ · facturas/ · pagos/ │ │ ├── cartera/ · reportes/ · dashboard/ · ia/ │ ├── services/ // Lógica de negocio desacoplada │ ├── ia/ │ │ ├── prediccion_mora.py │ │ ├── segmentacion_clientes.py │ │ ├── reporte_inteligente.py │ │ └── modelos/ // .pkl — modelos entrenados │ ├── middleware/ // tenant.py · plan.py · audit.py │ └── utils/ ├── migrations/ // Alembic ├── tests/ ├── .env · requirements.txt · Dockerfile · wsgi.py

Variables de entorno

FLASK_ENV=production
DATABASE_URL=postgresql://user:pass@localhost:5432/cartera_db
JWT_SECRET_KEY=<generado con secrets.token_hex(32)>
JWT_ACCESS_TOKEN_EXPIRES=900       # 15 minutos
JWT_REFRESH_TOKEN_EXPIRES=604800   # 7 días
CORS_ORIGINS=https://app.tudominio.com

Backend — API REST

Todos los endpoints requieren Authorization: Bearer <token>

Backend

Autenticación

POST
/api/v1/auth/login
Inicio de sesión. Retorna access_token, refresh_token, datos del usuario y empresa.
POST
/api/v1/auth/refresh
Refrescar access token usando el refresh token.
POST
/api/v1/auth/logout
Revocar token activo.
POST
/api/v1/auth/recuperar-password
Solicitar correo de recuperación de contraseña.
POST
/api/v1/auth/reset-password
Cambiar contraseña usando el token del correo.

Clientes

GET
/api/v1/clientes
Listar con filtros: estado, ciudad, nivel_riesgo, q (búsqueda texto).
Todos los roles
POST
/api/v1/clientes
Crear nuevo cliente.
adminsupervisoranalista
GET
/api/v1/clientes/{id}
Detalle del cliente.
Todos
PUT
/api/v1/clientes/{id}
Actualizar datos del cliente.
adminsupervisoranalista
GET
/api/v1/clientes/{id}/historial
Facturas y pagos del cliente.
Todos
GET
/api/v1/clientes/{id}/prediccion
Predicción de riesgo. Solo plan Enterprise.
Enterprise

Facturas

GET
/api/v1/facturas
Listar con filtros: estado, cliente_id, fecha_desde, fecha_hasta, vencida.
Todos
POST
/api/v1/facturas
Crear factura.
adminsupervisoranalista
GET
/api/v1/facturas/{id}
Detalle con historial de pagos y saldo.
Todos
PUT
/api/v1/facturas/{id}
Editar. Bloqueado si estado es Pagada o Castigada.
adminsupervisor
PATCH
/api/v1/facturas/{id}/estado
Cambiar estado manualmente.
adminsupervisor

Pagos

GET
/api/v1/pagos
Historial de pagos con paginación.
Todos
POST
/api/v1/pagos
Registrar pago o abono. Actualiza saldo de la factura automáticamente (trigger BD).
Todos los roles
GET
/api/v1/pagos/{id}
Detalle de un pago.
Todos

Cartera

GET
/api/v1/cartera/resumen
Totales por rango de antigüedad: 0–30, 31–60, 61–90, +90 días.
GET
/api/v1/cartera/vencida
Facturas vencidas paginadas con clasificación de antigüedad.
GET
/api/v1/cartera/morosos
Clientes con deuda vencida.
POST
/api/v1/cartera/acuerdos
Crear acuerdo de pago.
adminsupervisoranalista
GET
/api/v1/cartera/acuerdos
Listar acuerdos de pago.

Dashboard

GET
/api/v1/dashboard/resumen
KPIs: cartera total, vencida, recaudo, facturas pendientes, clientes morosos, % recuperación.
GET
/api/v1/dashboard/graficas
Series históricas para gráficas. Param: ?meses=6 o ?meses=12.

Reportes

GET
/api/v1/reportes/cartera
Reporte de cartera general.
GET
/api/v1/reportes/cartera-vencida
Reporte de cartera vencida.
GET
/api/v1/reportes/recaudo
Reporte de recaudo por período.
GET
/api/v1/reportes/morosos
Reporte de clientes morosos.
GET
/api/v1/reportes/{tipo}/exportar
Exportar a PDF o Excel. Param: ?formato=pdf · ?formato=excel.
ProfesionalEnterprise

Inteligencia artificial Solo Enterprise

POST
/api/v1/ia/predecir-mora
Ejecutar predicción de mora para un cliente.
POST
/api/v1/ia/predecir-mora/lote
Predicción masiva para todos los clientes de la empresa.
GET
/api/v1/ia/segmentacion
Segmentación actual: excelente · bueno · ocasional · riesgo · crítico.
GET
/api/v1/ia/reporte-inteligente
Narrativa generada automáticamente con análisis de cartera.
GET
/api/v1/ia/recomendaciones
Acciones sugeridas de cobranza para la empresa.

Backend — seguridad y JWT

Backend

Tokens JWT

TokenVigenciaPayload
access_token15 minutosuser_id, empresa_id, rol, plan
refresh_token7 díasuser_id
El empresa_id se extrae siempre del JWT — nunca de la request. Todo query lleva filter_by(empresa_id=get_empresa_id()). Un query sin este filtro es un bug de seguridad crítico.

Decoradores de control de acceso

@jwt_required()
@requiere_roles('admin', 'supervisor', 'analista')
@requiere_plan('enterprise')
def post(self):
    ...

Regla de aislamiento multiempresa

# CORRECTO
Cliente.query.filter_by(id=cliente_id, empresa_id=get_empresa_id()).first()

# INCORRECTO — nunca sin empresa_id
Cliente.query.filter_by(id=cliente_id).first()

Hash de contraseñas (Argon2id)

from argon2 import PasswordHasher
ph = PasswordHasher()
hashear   = lambda pwd: ph.hash(pwd)
verificar = lambda h, pwd: ph.verify(h, pwd)

Acciones auditadas obligatoriamente

LOGINCREATE_FACTURAREGISTER_PAGOUPDATE_FACTURADELETE_CLIENTECAMBIO_ESTADOEJECUTAR_IAEXPORTAR_REPORTE

Protecciones activas

ProtecciónImplementación
SQL InjectionSQLAlchemy ORM — nunca queries en string raw
XSSMarshmallow sanitiza inputs · Respuestas JSON
CSRFAPI stateless con JWT · CORS restringido por origen
Fuerza brutaRate limiting en endpoints de auth
ContraseñasArgon2id — estándar de seguridad actual
TransporteHTTPS obligatorio en producción

Backend — módulo de IA

Solo plan Enterprise · Scikit-Learn · XGBoost

Backend
Los modelos requieren datos históricos reales. En una instalación nueva la IA no tiene valor predictivo hasta acumular 3–6 meses de historial de pagos.

Variables del modelo de predicción

VariableTipoDescripción
facturas_vencidas_totalintFacturas que alguna vez estuvieron vencidas
valor_deuda_acumuladadecimalSuma de saldos pendientes actuales
frecuencia_morafloat 0–1Proporción de facturas que entraron en mora
dias_promedio_retrasointPromedio de días de retraso en pagos
antiguedad_diasintDías desde la creación del cliente
facturas_pagadas_totalintFacturas saldadas correctamente

Niveles de riesgo

Bajoscore < 0.30
Medioscore 0.30–0.65
Altoscore > 0.65

Segmentación de clientes

SegmentoCriterio
Excelente pagadorSin mora histórica · score < 0.15
Buen pagadorMora < 10% · score < 0.30
Pagador ocasionalMora 10–30% · score 0.30–0.50
Cliente de riesgoScore 0.50–0.65
Cliente críticoScore > 0.65

Ejemplo de narrativa generada

"El 72% de la cartera vencida corresponde a clientes con más de 90 días de mora."

"Se observa un incremento del 15% en la cartera vencida durante el último trimestre."

"Se recomienda priorizar acciones de cobro sobre los 20 clientes con mayor nivel de riesgo."

Base de datos — modelo de datos

PostgreSQL 15+ · 10 tablas · ENUMs · Constraints

Base de datos

Tipos enumerados (ENUMs)

plan_empresarol_usuarioestado_clienteestado_facturametodo_pagoestado_acuerdonivel_riesgosegmento_clienteaccion_audit

Tablas

empresasTenants del sistema
ColumnaTipoRestricciones / Notas
idSERIALPrimary key
nombreVARCHAR(200)NOT NULL
nitVARCHAR(30)NOT NULL UNIQUE
planplan_empresabasico | profesional | enterprise · Default: basico
dias_alerta_vencimientoINTDefault 5 · Configurable por empresa
tasa_mora_mensualNUMERIC(5,2)Default 0.00
activaBOOLEANDefault TRUE
creada_en / actualizada_enTIMESTAMPTZAuto-updated por trigger
usuariosUsuarios internos por empresa
ColumnaTipoRestricciones / Notas
idSERIALPrimary key
empresa_idINTFK → empresas ON DELETE CASCADE
correoVARCHAR(150)UNIQUE (empresa_id, correo)
password_hashVARCHAR(256)NOT NULL · Hash Argon2id
rolrol_usuarioadmin | supervisor | analista | operador
ultimo_loginTIMESTAMPTZNullable · Se actualiza en cada login
clientesClientes de cada empresa
ColumnaTipoRestricciones / Notas
idSERIALPrimary key
empresa_idINTFK → empresas ON DELETE CASCADE
documentoVARCHAR(30)UNIQUE (empresa_id, documento)
tipo_documentoVARCHAR(20)NIT | CC | CE | Pasaporte
estadoestado_clienteactivo | inactivo · Default: activo
nombre, correo, telefono, ciudadVARCHARDatos de contacto
facturasFacturas emitidas
ColumnaTipoRestricciones / Notas
idSERIALPrimary key
empresa_idINTFK → empresas
cliente_idINTFK → clientes ON DELETE RESTRICT
numeroVARCHAR(50)UNIQUE (empresa_id, numero)
valor_totalNUMERIC(18,2)CHECK > 0
saldo_pendienteNUMERIC(18,2)CHECK ≤ valor_total · Actualizado por trigger al insertar pago
estadoestado_facturapendiente | pagada | vencida | en_cobro | castigada
fecha_vencimientoDATECHECK ≥ fecha_emision
pagosPagos y abonos parciales
ColumnaTipoRestricciones / Notas
idSERIALPrimary key
empresa_idINTFK → empresas
factura_idINTFK → facturas ON DELETE RESTRICT
valor_pagadoNUMERIC(18,2)CHECK > 0 · El trigger valida que no exceda el saldo
metodo_pagometodo_pagoefectivo | transferencia | cheque | tarjeta | otro
referenciaVARCHAR(100)Número de transacción / cheque · Nullable
registrado_porINTFK → usuarios ON DELETE SET NULL
acuerdos_pagoAcuerdos de refinanciación
ColumnaTipoRestricciones / Notas
idSERIALPrimary key
cliente_idINTFK → clientes
monto_totalNUMERIC(18,2)CHECK > 0
cuotasINTCHECK ≥ 1
valor_cuotaNUMERIC(18,2)CHECK > 0
estadoestado_acuerdovigente | cumplido | incumplido
acuerdo_facturasFacturas incluidas en un acuerdo (N:M)
ColumnaTipoNotas
acuerdo_idINTFK → acuerdos_pago · PK compuesta
factura_idINTFK → facturas · PK compuesta
predicciones_iaHistorial de predicciones — solo Enterprise
ColumnaTipoRestricciones / Notas
cliente_idINTFK → clientes
nivel_riesgonivel_riesgobajo | medio | alto
segmentosegmento_clienteexcelente | bueno | ocasional | riesgo | critico
scoreNUMERIC(6,4)CHECK BETWEEN 0 AND 1
version_modeloVARCHAR(20)Trazabilidad al reentrenar el modelo
variables (6 columnas)INT / NUMERICSnapshot de variables usadas en la predicción
reportesMetadata de reportes generados
ColumnaTipoNotas
tipoVARCHAR(50)cartera | cartera_vencida | recaudo | morosos | desempeno
filtros_jsonJSONBParámetros usados: fechas, filtros, etc.
generado_porINTFK → usuarios ON DELETE SET NULL
audit_logRegistro inmutable de acciones críticas
ColumnaTipoNotas
idBIGSERIALBIGINT — alto volumen de registros
accionaccion_auditLOGIN | CREATE | UPDATE | DELETE | REGISTER_PAGO | etc.
entidad / entidad_idVARCHAR / INT'factura', 'pago', 'cliente'…
detalle_jsonJSONBSnapshot antes/después del cambio
ip_origenINETIP del cliente que realizó la acción
timestampTIMESTAMPTZInmutable — sin trigger de update

Base de datos — vistas y triggers

Base de datos

Vistas disponibles

VistaDescripciónUsado por
v_cartera_vencidaFacturas vencidas con días de mora y rango de antigüedad calculadosMódulo cartera, reportes
v_resumen_carteraKPIs agregados por empresa: total, vencida, vigente, morososDashboard
v_recaudo_mensualRecaudo mensual por empresaDashboard, reportes
v_prediccion_vigenteÚltima predicción de riesgo por cliente (DISTINCT ON)IA, listado clientes

Trigger — trg_aplicar_pago

Al insertar en pagos, descuenta el saldo_pendiente de la factura. Si el saldo llega a cero, cambia el estado a pagada. También valida que el pago no exceda el saldo.

Este trigger garantiza consistencia a nivel de base de datos, independientemente de si el pago se inserta desde la API o directamente en la BD.

Trigger — timestamps automáticos

Las tablas empresas, usuarios, clientes, facturas y acuerdos_pago tienen triggers BEFORE UPDATE que actualizan automáticamente actualizada_en.

Función — marcar_facturas_vencidas()

Marca como vencida toda factura pendiente cuya fecha_vencimiento ya pasó. Debe ejecutarse con un cron job diario.

-- Con pg_cron (instalar extensión)
SELECT cron.schedule('0 1 * * *', 'SELECT marcar_facturas_vencidas()');
-- Se ejecuta todos los días a la 1:00 AM

Base de datos — índices y rendimiento

Base de datos
La arquitectura soporta más de 1.000 empresas, 100.000 clientes y millones de facturas. Los índices son críticos para tiempos de respuesta bajos a ese volumen.

Índices por tabla

TablaColumnas indexadasJustificación
empresasplan, activaFiltrar empresas por plan o estado
usuariosempresa_id, correoLogin y listado por empresa
clientesempresa_id, estado, ciudadFiltros más usados en el listado
clientesGIN sobre nombre (tsvector)Búsqueda de texto por nombre
facturasempresa_id, cliente_id, estadoFiltros principales
facturasfecha_vencimientoCálculo de cartera vencida y alertas
facturas(empresa_id, estado)Índice compuesto para dashboard
pagosempresa_id, factura_id, fecha_pagoHistorial y recaudo mensual
predicciones_iaempresa_id, cliente_id, nivel_riesgo, generada_en DESCSegmentación y última predicción
audit_logempresa_id, usuario_id, timestamp DESC, (entidad, entidad_id)Consultas de auditoría

Capacidad esperada

Empresas
1.000+
Clientes
100.000+
Facturas
Millones
Modelo
Multi-tenant

Buenas prácticas

  • Usar NUMERIC(18,2) para valores monetarios — nunca FLOAT (imprecisión flotante)
  • Usar TIMESTAMPTZ para fechas con hora — almacena en UTC
  • audit_log usa BIGSERIAL (BIGINT) por el alto volumen esperado
  • Evaluar particionamiento de audit_log por empresa_id al superar 10M filas
  • Configurar backups automáticos diarios con retención mínima de 30 días