System Design & Contrato API
«Creciendo Juntos Financiera»
Esta página documenta de forma visual la frontera HTTP entre la SPA Angular y el backend Spring Boot, y el diseño de sistema que la sostiene: arquitectura, reparto de responsabilidades, máquinas de estado, modelo de datos, catálogo de errores y reglas pendientes. Sustituye a un sistema Access en operación: un negocio, 6 usuarios, un solo consumidor del API.
01Las 17 pantallas en 7 módulos
Alcance cerrado en el Anexo A. El Anexo C incorporó las pantallas 5 y 7 (expediente digital y avales), desplazando la numeración del Anexo A §A.1. Esta numeración es la vigente en todo el contrato: cada endpoint indica a qué pantalla sostiene.
| # | Pantalla | Módulo | Origen |
|---|---|---|---|
| 1 | Inicio de sesión | Seguridad | Anexo A §A.1 |
| 2 | Gestión de usuarios | Seguridad | Anexo A §A.1 |
| 3 | Listado de clientes | Clientes | Anexo A §A.1 |
| 4 | Ficha de cliente | Clientes | Anexo A §A.1 |
| 5 | Expediente digital del cliente | Clientes | Nueva · Anexo C §C.1.1 |
| 6 | Alta de crédito | Créditos | Anexo A §A.1 |
| 7 | Avales del crédito | Créditos | Nueva · Anexo C §C.1.2 |
| 8 | Tabla de amortización | Créditos | Anexo A §A.1 |
| 9 | Historial del cliente | Cobranza | Anexo A §A.1 |
| 10 | Registro de pago de cuota | Cobranza | Anexo A §A.1 |
| 11 | Abono parcial | Cobranza | Anexo A §A.1 |
| 12 | Abono a capital | Cobranza | Anexo A §A.1 |
| 13 | Cuotas vencidas | Cobranza | Anexo A §A.1 |
| 14 | Próximos a vencer | Cobranza | Anexo A §A.1 |
| 15 | Ingresos y egresos (caja) | Caja | Anexo A §A.1 |
| 16 | Consultas por rango de fechas | Consultas | Anexo A §A.1 |
| 17 | Impresión de listados y recibos | Impresión | Anexo A §A.1 · Anexo B §B.2 |
02Arquitectura de alto nivel
Sin caché distribuida, sin colas de mensajes, sin servicios adicionales. Con 6 usuarios y ~18 mil cuotas, cualquier pieza extra cuesta más de lo que resuelve.
NAVEGADOR — SPA Angular
Presentación (17 pantallas) · Dominio de UI: formateo, validación de captura, motor de amortización espejo (solo vista previa) · Capa de acceso: interfaz FinancieraApi — MockApiService (semanas 1–2 y tests) / HttpApiService (desde la semana 3)
Spring Boot
API REST /api · Seguridad: autenticación + autorización por permiso · Reglas financieras: amortización, distribución de pagos, mora, saldos, caja · Expediente digital: recepción, validación y entrega autenticada de archivos · Reportes y renderizado de PDF / Excel · Migración Access → PostgreSQL (proceso aparte, una vez)
PostgreSQL — modelo v1.1
Metadatos del expediente · logotipo del negocio
Almacenamiento de archivos
Documentos del cliente · ubicación a criterio del backend, siempre con entrega autenticada
Decisiones transversales y sus contrapartidas
| Decisión | Se gana | Se paga | Cuándo revisarla |
|---|---|---|---|
API sin versionado — /api/…, nunca /api/v1/… | URLs limpias, cero ceremonia | Frontend y backend se despliegan juntos siempre | Si aparece una app móvil de cobradores — no antes |
| Estado de cuota derivado, no almacenado | Nunca se desincroniza; sin proceso nocturno | Se calcula en cada consulta | Si la cartera supera ~100 mil cuotas. Hoy son 18 mil |
| PDF en servidor (Anexo B §B.4) | Papel y archivo idénticos | El frontend no puede generar PDF sin backend | No se revisa: requisito contractual |
| Archivos fuera de la base, metadatos dentro | La base no crece con binarios; respaldo simple | Dos cosas que respaldar y desincronizar | Si aparece un archivo huérfano en producción |
| Descarga autenticada por endpoint | El expediente no se filtra por URL | Previsualización vía blob: | Si los PDFs grandes se sienten lentos → URL firmada |
| Tablero en una sola llamada | Una petición, no seis | Un endpoint que agrega seis consultas | Si algún indicador se vuelve costoso, se separa |
| Sin caché ni cola | Menos piezas que operar | Los reportes pesados se recalculan cada vez | Si un reporte anual supera 3 s |
| Un solo consumidor del API | Contratos ajustados a las pantallas | Endpoints específicos, poco reutilizables | Solo si aparece un segundo consumidor |
Estrategia de integración — módulo por módulo
El frontend arranca contra una capa mock que implementa esta misma frontera y se conecta al backend real conforme los grupos de endpoints quedan estables. No hay una integración única al final.
| Semana 1–2 | Todos los módulos contra mock |
| Semana 3 | Seguridad + Clientes → backend real · resto → mock |
| Semana 4 | + Expediente digital y préstamos |
| Semana 5 | + Avales y amortización |
| Semana 6–7 | + Cobranza y pagos |
| Semana 8 | + Caja |
| Semana 9 | + Reportes, impresión y tablero |
03Reparto de responsabilidades
La línea que importa acordar: todo cálculo financiero con consecuencia contable vive en el backend. El frontend calcula únicamente para previsualizar mientras el usuario teclea, y nunca envía un resultado calculado por él como dato a persistir. En producción manda el backend siempre.
| Responsabilidad | Backend | Frontend |
|---|---|---|
| Generación del plan de cuotas | Autoridad | Espejo, solo para vista previa |
| Distribución de un pago sobre cuotas | Autoridad | Muestra lo que devuelve la previsualización |
| Cálculo de mora | Autoridad | Solo muestra el importe y la explicación |
| Estado de cada cuota | Autoridad (derivado) | Solo pinta el badge |
| Saldos del préstamo | Autoridad | Solo muestra |
| Movimiento de caja de un pago | Autoridad (automático) | No lo envía ni lo compone |
| Permisos efectivos del usuario | Autoridad | Oculta la UI según los recibe |
| Totales de reportes y del tablero | Autoridad | No suma columnas en el cliente |
| Validación de tipo y tamaño de archivo | Autoridad (revalida siempre) | Primera línea, para no subir 40 MB en vano |
| Almacenamiento y entrega de archivos | Autoridad | Solo sube y descarga |
| Estado del expediente (completo / incompleto) | Autoridad, si la regla abierta #13 lo exige | Solo muestra el distintivo |
| Formato de moneda, fecha y número | — | Autoridad |
| Validación de captura (obligatorios, rangos, formato) | Revalida siempre | Primera línea, para respuesta inmediata |
| Paginación, orden y filtros | Autoridad | Envía los parámetros |
| Estilos de impresión en pantalla | — | Autoridad |
| Generación del archivo PDF | Autoridad (Anexo B §B.4) | Solo descarga |
amortizacion-casos.json con pares entrada → salida, versionado y consumido por los tests de ambos lados.
Si divergen más de una vez, se elimina el motor local y la vista previa llama a
POST /api/prestamos/simulacion con debounce.
04Máquinas de estado
Cinco máquinas relevantes para la interfaz. El frontend las pinta sin ambigüedad: los estados llegan ya resueltos desde el backend y nunca se recalculan en el cliente.
Cuota — derivado, nunca almacenado
Se resuelve al consultar; no requiere proceso nocturno. VENCIDA tiene precedencia sobre PARCIAL: una cuota vencida con abono parcial se pinta roja — el cobrador debe verla como pendiente de cobro.
Por cuota el frontend recibe, además: saldoPendiente, diasAtraso y moraAlDia — no puede derivarlos sin reimplementar la fórmula de mora.
Préstamo — la interfaz opera 4 de 8 estados
VENCIDO: lectura acordada (b) — condición derivada («tiene al menos una cuota vencida»)
sobre un préstamo ACTIVO, no un estado asignado. EN_REVISION, AUTORIZADO,
RECHAZADO y REFINANCIADO existen en el modelo sin flujo (Anexo C §C.2).
Pago
La cancelación revierte aplicaciones, recalcula cuotas, ajusta saldos y anula o compensa el movimiento de caja — en una sola transacción. El pago cancelado se conserva visible en el historial, marcado, con fecha y usuario de cancelación: requisito de auditoría.
Usuario
activo es estado, no permiso. Un usuario inactivo recibe 403 USUARIO_INACTIVO en el login, con mensaje distinto al de credenciales incorrectas.
Documento del expediente
Baja lógica (activo = false), nunca borrado físico. El reemplazo conserva el documento
anterior (acuerdo #24): reemplazaAId permite ofrecer «Ver versiones anteriores» en la pantalla 5.
Expediente completo — sujeto a la regla abierta #13
Solo existe si el cliente decide que el expediente incompleto bloquea el alta de crédito. Mientras no se resuelva, el frontend maqueta la advertencia, que es lo reversible.
05Modelo de datos v1.1
El modelo es del backend y tiene prioridad sobre los anexos cuando algo no coincida. El detalle completo entidad por entidad está en el documento Modelo de Datos Financiero Reconciliado.
Entidades de primer nivel
Relaciones principales
Empresa ── Configuracion
Usuario ── UsuarioRole ── Role ── RolePermiso ── Permiso
└────── UsuarioPermiso (excepciones opcionales)
Ruta ── Cliente ── ClienteDocumento
└── Prestamo ── Aval
├── Cuota ── PagoAplicacion ── Pago
├── Movimiento
└── MovimientoCaja (cuando el movimiento de caja está relacionado)
Cinco decisiones del modelo que la interfaz hace visibles
Pago separado de Cuota
Un pago puede cubrir varias cuotas y dejar saldo a favor. El panel de cobro captura un monto recibido y muestra cómo se reparte — un cambio de mentalidad respecto del Access.
PagoAplicacion como detalle
Es lo que permite que el recibo impreso diga exactamente cuánto fue a capital, interés y mora. Sin eso, el recibo sería un total opaco.
Movimiento ≠ MovimientoCaja
El estado de cuenta del cliente y la caja del negocio son dos contabilidades: dos pantallas (9 y 15) que no se suman entre sí.
ClienteDocumento como tabla
Un cliente puede tener tres comprobantes de ingresos sin que nadie toque el esquema — lista con carga múltiple, no casillas fijas.
Aval colgado de Prestamo
Si el mismo avalista respalda dos créditos del mismo cliente, son dos registros. La pantalla 7 vive dentro del crédito.
Snapshot de condiciones
Tasa, periodicidad, esquema y número de cuotas quedan congelados en el crédito: cambiar tasas del negocio no altera créditos históricos.
Campos derivados que el frontend espera recibir calculados
Ninguno se deriva en el cliente. Llegan ya resueltos en cada respuesta.
| Recurso | Campo | Definición |
|---|---|---|
| Cuota | estado | PENDIENTE · PARCIAL · VENCIDA · PAGADA — derivado (§3.1) |
| saldoPendiente | totalCuota − (capitalPagado + interesPagado + moratorioPagado) | |
| diasAtraso | Fórmula del Anexo A §A.4; 0 si no aplica | |
| moraAlDia | Mora acumulada a la fecha de consulta, sin persistir | |
| totalACobrarHoy | saldoPendiente + moraAlDia — el total del panel de cobro | |
| Prestamo | cuotasPagadas / numeroCuotas | El «5 de 12» que aparece en toda la interfaz |
| cuotasVencidas | Conteo, para el distintivo de riesgo | |
| proximoVencimiento | Fecha de la siguiente cuota no pagada | |
| condicionVencido | Booleano derivado, lectura (b) de VENCIDO | |
| totalAvales | Conteo para el distintivo, sin traer la lista | |
| tienePlanGenerado | false en los 24 créditos migrados sin movimientos | |
| Cliente | totalPrestamos | La columna que hace visible la separación cliente/crédito |
| prestamosActivos | Conteo | |
| saldoTotal | Suma de saldos de sus créditos activos | |
| rutaNombre | Junto a rutaId, para no resolver el catálogo en cada fila | |
| totalDocumentos | Documentos vigentes del expediente | |
| estadoExpediente | Solo si la regla abierta #13 lo hace necesario | |
| ClienteDocumento | tamanoBytes | Para mostrar «2.4 MB» sin descargar el archivo |
| urlContenido | Ruta relativa del endpoint de descarga, armada por el backend. rutaArchivo no debe salir jamás | |
| usuarioCarga | Quién lo subió — expediente que revisan varias personas | |
| MovimientoCaja | naturaleza | INGRESO · EGRESO · AJUSTE, derivada del prefijo del tipo |
| clienteNombre / prestamoId | Trazabilidad cuando el movimiento nació de un pago | |
| generadoPorSistema | Determina si la fila es editable en la pantalla 15 |
Presente en el modelo, sin superficie en esta entrega
Anexo C §C.2: la presencia de una estructura en la base de datos no implica funcionalidad contratada.
| Elemento del modelo | Estado en la entrega |
|---|---|
| Prestamo.prestamoOrigenId | Campo creado y nullable. Sin flujo de refinanciamiento |
| PrestamoStatus: EN_REVISION, AUTORIZADO, RECHAZADO, REFINANCIADO | Definidos. La interfaz opera los cuatro de §3.2 |
| Periodicidad: DIARIO, TRIMESTRAL, ANUAL | Para migrar créditos históricos. No se ofrecen en el alta |
| UsuarioPermiso | Tabla creada. Se expone solo si el cliente lo confirma |
| TipoMovimiento.REFINANCIAMIENTO | Valor definido. Sin operación que lo genere |
06Convenciones de la frontera
Reglas vinculantes de representación. El versionado no existe por decisión de diseño: un solo consumidor desplegado junto al backend; cualquier cambio incompatible obliga a desplegar ambos juntos.
| Tema | Regla |
|---|---|
| Prefijo y recursos | /api · plural, español, minúsculas: /api/clientes, /api/prestamos (no /creditos) |
| Campos JSON | camelCase, igual en Java y TypeScript |
| Enums | Texto, MAYÚSCULAS, español — exactamente los valores del modelo v1.1 |
| Importes | Número JSON con 2 decimales (1058.33), nunca cadena; el backend calcula con BigDecimal |
| Tasas | Fracción por periodo: 0.0225 = 2.25 % |
| Fechas de negocio | aaaa-mm-dd, sin hora ni zona — evita el desplazamiento de un día |
| Marcas de tiempo | ISO-8601 con offset: 2026-09-02T14:30:00-06:00 |
| Nulos | Campo presente con null, nunca campo ausente |
| Paginación | ?page=0&size=50 → { contenido, pagina, tamano, totalElementos, totalPaginas } — siempre de servidor |
| Orden | ?orden=campo,asc |
| Errores | Sobre { codigo, mensaje, campos? } con el HTTP que corresponda — ver catálogo |
| Archivos | multipart/form-data, un archivo por petición; tamaños en bytes |
07Seguridad
Autenticación
JWT en Authorization: Bearer <token>. Sin refresh token: con 6 usuarios y jornada de oficina, un token de vigencia larga y relogin manual basta. Al recargar la página el frontend llama GET /api/auth/sesion en vez de confiar en lo que guardó.
Permisos efectivos
Los 5 permisos del Anexo A §A.2 agrupados en roles. Llegan ya resueltos en el login — rol más excepciones de UsuarioPermiso, aplanados en códigos. El frontend no reconstruye la precedencia.
Ocultar no es autorizar
Cada endpoint valida el permiso por su cuenta. Si el frontend falla y muestra una acción indebida, la respuesta correcta es 403 SIN_PERMISO — no que la operación pase.
Contraseñas
Hash irreversible (BCrypt), nunca de vuelta al cliente. Las 6 migradas estaban en texto plano: requiereCambioPassword llega en el login y el frontend bloquea la navegación hasta el cambio.
Documentos: descarga autenticada
Nunca una URL pública adivinable. Como <img src> y <iframe src> no envían el encabezado Authorization, el frontend descarga los bytes y los convierte en blob:. Content-Disposition: attachment con ?descarga=true, inline para previsualizar.
Validación en dos líneas
Tipo y tamaño de archivo se validan también en el navegador para no subir 40 MB en vano, pero el servidor revalida siempre: la validación del cliente es cortesía, no control.
08Idempotencia en las operaciones de dinero
Un doble clic, una reconexión o un reintento del navegador no deben producir dos pagos: es el riesgo más caro del sistema, porque un pago duplicado ensucia cuotas, saldos y caja a la vez.
POST /api/prestamos/{id}/pagos
Clave-Idempotencia: 7f9c24e5-1b3d-4a2f-9e8c-05d1a6b7c3f9 ← UUID del frontend
- Si llega una clave ya procesada, se devuelve el resultado original en lugar de crear un registro nuevo.
- Una clave por operación, no por pantalla.
IdempotenciaOperacion.hashSolicituddetecta la misma clave con cuerpo distinto: si el operador corrige el monto tras un fallo, es una operación nueva y genera clave nueva. expiraEn: ventana de retención propuesta de 24 horas — debe superar con holgura el tiempo de un reintento tras perder la red.- Ante clave repetida con cuerpo distinto se propone
409 CONFLICTO_CONCURRENCIAcon mensaje explícito, no un 400 genérico. - Fuera del mecanismo: la subida de documentos — un documento duplicado es molesto pero inofensivo, y el operador lo borra.
09Flujos clave
Los recorridos que concentran el riesgo. Cada uno indica dónde vive cada responsabilidad.
Alta de crédito con vista previa — pantalla 6
1. Motor espejo TS pinta el plan al teclear (respuesta instantánea, sin red) ·
2. POST /api/prestamos/simulacion con debounce de 400 ms — si difieren, manda el backend y la discrepancia se registra en consola, nunca se muestra al usuario ·
3. POST /api/prestamos + Clave-Idempotencia: una transacción que crea el Prestamo con snapshot, genera todas las Cuota, registra el Movimiento de otorgamiento y el MovimientoCaja EGRESO_PRESTAMO_OTORGADO ·
4. Los avales se capturan después (pantalla 7): Aval.prestamoId exige que el préstamo exista.
Registro de un pago — pantallas 10 y 11
1. POST …/pagos/previsualizacion muestra qué cuotas se liquidan y cuánto va a mora, interés y capital — sin persistir ·
2. POST …/pagos + Clave-Idempotencia: una sola transacción — crea Pago, distribuye, crea PagoAplicacion, acumula en Cuota, actualiza saldos, registra Movimiento, registra un único MovimientoCaja con utilidad = interés + mora cobrados, e incrementa el saldo a favor ·
3. La respuesta trae la distribución final: con ella se pintan historial y recibo sin segunda consulta. Si difiere de la previsualización, manda la respuesta y se avisa del cambio. El abono parcial no es otro endpoint: es el mismo pago con monto menor.
Cancelación de un pago
Confirmación explícita (acción destructiva) → POST /api/pagos/{id}/cancelacion + idempotencia: revierte PagoAplicacion, recalcula cuotas, ajusta saldos, anula o compensa el movimiento de caja y marca CANCELADO. Si la ventana de tiempo pasó: 409 CANCELACION_NO_PERMITIDA con el motivo — nunca un 403 genérico.
Cobranza en ruta e impresión — pantallas 13, 14 y 17
GET /api/cobranza/vencidas es la lista de trabajo del cobrador ·
GET /api/impresion/{formato} trae datos + identidad del negocio para la vista previa con @media print ·
el PDF lo genera el backend con la misma plantilla que se imprime (Anexo B §B.4): archivo y papel idénticos.
10Referencia de la API
68 operaciones en 54 rutas, agrupadas en 11 etiquetas. Desplegable estilo Scalar/Postman: cada operación muestra descripción, parámetros, cuerpo y respuestas esperadas — incluidos los errores del catálogo.
Autenticación
3 operacionesInicio de sesión, revalidación y cambio de contraseña. Pantalla 1.
▸POST/api/auth/loginInicia sesión y devuelve token y permisos efectivos
permisos viene ya resuelto: rol más excepciones de UsuarioPermiso,
aplanado en una lista de códigos. El frontend no reconstruye la precedencia
entre rol y excepción (system-design §2.4).
Cuerpo de la petición req
application/json → PeticionLogin
Respuestas
Sesión iniciada
VALIDACION · El nombre en campos[].campo debe coincidir con el del
cuerpo de la petición: así el frontend marca el control automáticamente, sin
mantener un mapa de traducción.
CREDENCIALES_INVALIDAS · «Usuario o contraseña incorrectos»
USUARIO_INACTIVO · Mensaje distinto al de credenciales: el operador necesita distinguir «me equivoqué de contraseña» de «me deshabilitaron».
▸GET/api/auth/sesionRevalida la sesión y devuelve permisos vigentes
El frontend lo llama al recargar la página en lugar de confiar en lo que guardó: si al usuario le cambiaron permisos o lo desactivaron, se entera de inmediato (system-design §6.1).
Respuestas
Sesión vigente
TOKEN_EXPIRADO · El frontend redirige a login conservando la ruta de retorno
▸POST/api/auth/passwordCambia la contraseña del usuario autenticado
Obligatorio en el primer acceso tras la migración (Anexo A §A.2). Mientras
requiereCambioPassword sea true, el frontend bloquea la navegación.
Cuerpo de la petición req
application/json → PeticionCambioPassword
Respuestas
Contraseña actualizada
VALIDACION · El nombre en campos[].campo debe coincidir con el del
cuerpo de la petición: así el frontend marca el control automáticamente, sin
mantener un mapa de traducción.
TOKEN_EXPIRADO · El frontend redirige a login conservando la ruta de retorno
Usuarios y seguridad
8 operacionesUsuarios, roles y permisos. Pantalla 2 · Anexo A §A.2.
▸GET/api/usuariosLista los usuarios del sistema
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| page | query | integer | opc | Índice de página, base 0 |
| size | query | integer | opc | |
| orden | query | string | opc | Campo y dirección: |
Respuestas
Página de usuarios
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
▸POST/api/usuariosDa de alta un usuario
Cuerpo de la petición req
application/json → PeticionUsuario
Respuestas
Usuario creado
VALIDACION · El nombre en campos[].campo debe coincidir con el del
cuerpo de la petición: así el frontend marca el control automáticamente, sin
mantener un mapa de traducción.
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
▸PUT/api/usuarios/{id}Edita un usuario
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Cuerpo de la petición req
application/json → PeticionUsuario
Respuestas
Usuario actualizado
VALIDACION · El nombre en campos[].campo debe coincidir con el del
cuerpo de la petición: así el frontend marca el control automáticamente, sin
mantener un mapa de traducción.
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
NO_ENCONTRADO · Estado vacío con retorno al listado
▸PATCH/api/usuarios/{id}/estadoActiva o desactiva un usuario
activo es estado del usuario, no permiso (Anexo A §A.2). Un usuario
inactivo recibe 403 USUARIO_INACTIVO al intentar iniciar sesión, con un
mensaje distinto al de credenciales incorrectas.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Cuerpo de la petición req
application/json → object
Respuestas
Estado actualizado
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
NO_ENCONTRADO · Estado vacío con retorno al listado
▸PUT/api/usuarios/{id}/rolesReemplaza los roles asignados a un usuario
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Cuerpo de la petición req
application/json → object
Respuestas
Roles actualizados
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
NO_ENCONTRADO · Estado vacío con retorno al listado
▸PUT/api/usuarios/{id}/permisosDefine excepciones de permiso para un usuario
Solo se expone en la interfaz si el cliente confirma que lo necesita
(Anexo A §A.2, punto abierto #5; Anexo C §C.2). La tabla UsuarioPermiso
existe en el modelo; la pantalla puede no existir.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Cuerpo de la petición req
application/json → object
Respuestas
Excepciones actualizadas
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
▸GET/api/rolesCatálogo de roles
Respuestas
Roles
▸GET/api/permisosCatálogo de los 5 permisos del Anexo A §A.2
Respuestas
Permisos
Catálogos
9 operacionesRutas y enumeraciones servidas por el backend para no duplicarlas en el frontend.
▸GET/api/rutasCatálogo de rutas de cobranza
En el sistema Access la ruta es texto libre; aquí es catálogo administrable (Anexo A §A.5).
REGLA ABIERTA #10 — falta confirmar si GRUPOS, QUINCENAL y
LIQUIDACION son rutas equivalentes a las numeradas o clasificaciones de
otra naturaleza. Si no son rutas, el filtro de la pantalla 3 necesita una
dimensión más.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| soloActivas | query | boolean | opc |
Respuestas
Rutas
▸POST/api/rutasCrea una ruta
Cuerpo de la petición req
application/json → PeticionRuta
Respuestas
Ruta creada
VALIDACION · El nombre en campos[].campo debe coincidir con el del
cuerpo de la petición: así el frontend marca el control automáticamente, sin
mantener un mapa de traducción.
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
▸PUT/api/rutas/{id}Edita una ruta
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Cuerpo de la petición req
application/json → PeticionRuta
Respuestas
Ruta actualizada
VALIDACION · El nombre en campos[].campo debe coincidir con el del
cuerpo de la petición: así el frontend marca el control automáticamente, sin
mantener un mapa de traducción.
NO_ENCONTRADO · Estado vacío con retorno al listado
▸PATCH/api/rutas/{id}/estadoActiva o desactiva una ruta
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Cuerpo de la petición req
application/json → object
Respuestas
Estado actualizado
NO_ENCONTRADO · Estado vacío con retorno al listado
La ruta tiene créditos asignados y no puede desactivarse
▸GET/api/catalogos/periodicidadesPeriodicidades, acotadas por ámbito
ambito=ALTA devuelve las tres que se ofrecen al registrar un crédito nuevo
(SEMANAL, QUINCENAL, MENSUAL). ambito=TODAS devuelve las seis del
modelo, necesarias para mostrar créditos históricos migrados (Anexo A §A.3).
Se pide al servidor en lugar de mantener una copia en el frontend, para que la regla no quede quemada en una lista que alguien olvide actualizar.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| ambito | query | string | opc |
Respuestas
Periodicidades
▸GET/api/catalogos/tipos-pagoTipos de pago
En la operación real de campo prácticamente todo es EFECTIVO; el frontend lo deja preseleccionado.
Respuestas
Tipos de pago
▸GET/api/catalogos/tipos-movimiento-cajaTipos de movimiento de caja, con su naturaleza y si son manuales
Respuestas
Tipos de movimiento de caja
▸GET/api/catalogos/tipos-documentoTipos de documento admitidos en el expediente
Coinciden exactamente con los «tipos admitidos» del Anexo C §C.1.1.
REGLA ABIERTA #13 — si el expediente incompleto bloquea el alta de
crédito, hay que marcar cuáles de estos son obligatorios. El campo
obligatorio está previsto para eso y hoy puede venir siempre en false.
Respuestas
Tipos de documento
▸GET/api/catalogos/tipos-identificacionTipos de identificación
[ABIERTO — acuerdo #17 del Apéndice 2] El modelo v1.1 declara
TipoIdentificacion como tipo pero no enumera sus valores. Propuesta del
frontend: INE, PASAPORTE, LICENCIA, CEDULA_PROFESIONAL, OTRO.
Respuestas
Tipos de identificación
Clientes
6 operacionesPantallas 3 y 4.
▸GET/api/clientesListado de clientes con búsqueda, filtro y paginación
busqueda es un solo parámetro que cubre nombre y número de
identificación: es como el operador busca en la práctica, sin elegir antes
por qué campo (system-design §5.2).
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| busqueda | query | string | opc | Texto libre sobre nombre completo y número de identificación |
| rutaId | query | integer · int64 | opc | |
| estado | query | ClienteStatus | opc | |
| page | query | integer | opc | Índice de página, base 0 |
| size | query | integer | opc | |
| orden | query | string | opc | Campo y dirección: |
Respuestas
Página de clientes
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
▸POST/api/clientesDa de alta un cliente
Cuerpo de la petición req
application/json → PeticionCliente
Respuestas
Cliente creado
VALIDACION · El nombre en campos[].campo debe coincidir con el del
cuerpo de la petición: así el frontend marca el control automáticamente, sin
mantener un mapa de traducción.
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
▸GET/api/clientes/{id}Ficha completa de un cliente
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Respuestas
Cliente
NO_ENCONTRADO · Estado vacío con retorno al listado
▸PUT/api/clientes/{id}Edita un cliente
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Cuerpo de la petición req
application/json → PeticionCliente
Respuestas
Cliente actualizado
VALIDACION · El nombre en campos[].campo debe coincidir con el del
cuerpo de la petición: así el frontend marca el control automáticamente, sin
mantener un mapa de traducción.
NO_ENCONTRADO · Estado vacío con retorno al listado
▸GET/api/clientes/{id}/prestamosCréditos de un cliente
Un cliente puede tener varios créditos sin duplicar su ficha. Es la separación cliente/crédito del Anexo A §A.8, y la columna que la hace visible en la interfaz.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Respuestas
Créditos del cliente
NO_ENCONTRADO · Estado vacío con retorno al listado
▸GET/api/clientes/{id}/estado-cuentaEstado de cuenta del cliente (entidad Movimiento)
No confundir con la caja del negocio. Movimiento es el estado de
cuenta del cliente; MovimientoCaja es el efectivo de la financiera. Son
dos contabilidades distintas y en la interfaz son dos pantallas que no se
suman entre sí (modelo v1.1 §2.17 y §2.18).
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req | |
| desde | query | string · date | opc | |
| hasta | query | string · date | opc | |
| page | query | integer | opc | Índice de página, base 0 |
| size | query | integer | opc |
Respuestas
Movimientos del estado de cuenta
NO_ENCONTRADO · Estado vacío con retorno al listado
Expediente digital
8 operacionesPantalla 5 · Anexo C §C.1.1. Único grupo que sube archivos.
▸GET/api/clientes/{id}/documentosDocumentos del expediente de un cliente
Por omisión devuelve solo los vigentes (activo = true).
incluirInactivos=true devuelve además los sustituidos y los eliminados
lógicamente, para poder ofrecer un historial de versiones del documento
(system-design §3.5, acuerdo #24).
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req | |
| tipoDocumento | query | TipoDocumento | opc | |
| incluirInactivos | query | boolean | opc |
Respuestas
Documentos
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
NO_ENCONTRADO · Estado vacío con retorno al listado
▸POST/api/clientes/{id}/documentosSube un documento al expediente
Un archivo por petición. Si el operador selecciona cinco, el frontend hace cinco llamadas con progreso independiente: es más simple del lado del backend y permite reintentar solo la que falló (system-design §5.2, acuerdo #21).
La validación de tipo y tamaño se hace también en el navegador para no subir 40 MB en vano, pero el servidor debe revalidar: la validación del cliente es cortesía, no control.
CRÍTICO (acuerdos #22 y #23): el límite de spring.servlet.multipart
debe coincidir con el de la REGLA ABIERTA #12, y el rechazo por tamaño debe
devolver 413 ARCHIVO_MUY_GRANDE con el sobre { codigo, mensaje }. Si sale
como página HTML del contenedor, el usuario ve un error incomprensible.
Esta operación queda fuera del mecanismo de idempotencia: un documento duplicado es molesto pero inofensivo, y el operador lo borra (§5.4).
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Cuerpo de la petición req
multipart/form-data → object
Respuestas
Documento cargado
ARCHIVO_VACIO · 0 bytes o multipart mal formado
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
ARCHIVO_MUY_GRANDE · Debe salir con el sobre { codigo, mensaje }
(acuerdo #23). Si Spring rechaza el multipart por exceder el límite del
contenedor, la respuesta suele salir con cuerpo HTML y sin sobre, y el
usuario ve un error incomprensible.
FORMATO_NO_ADMITIDO · La respuesta nombra los formatos válidos
▸GET/api/documentos/{id}Metadatos de un documento
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Respuestas
Documento
NO_ENCONTRADO · Estado vacío con retorno al listado
▸PUT/api/documentos/{id}Reclasifica un documento sin cambiar el archivo
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Cuerpo de la petición req
application/json → object
Respuestas
Documento actualizado
NO_ENCONTRADO · Estado vacío con retorno al listado
▸DELETE/api/documentos/{id}Baja lógica de un documento
Requiere permiso ELIMINAR. Es baja lógica (activo = false), no
borrado físico: el campo activo del modelo v1.1 existe justamente para eso.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Respuestas
Documento dado de baja
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
NO_ENCONTRADO · Estado vacío con retorno al listado
▸GET/api/documentos/{id}/contenidoDescarga o previsualiza el archivo, con la sesión validada
Nunca una URL pública adivinable (RNF-07). Este endpoint valida sesión y permiso en cada llamada, aunque la lista ya se haya cargado: el token puede haber expirado entre una cosa y otra.
Como <img src> y <iframe src> no envían el encabezado Authorization,
el frontend descarga los bytes y los convierte en blob: desde TypeScript
(opción (a) de system-design §2.4). Si en pruebas resulta pesado para archivos
de varios MB, se evaluará una URL firmada de vida corta.
Content-Disposition: attachment con el nombre original si descarga=true;
inline en caso contrario.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req | |
| descarga | query | boolean | opc |
Respuestas
Bytes del archivo
TOKEN_EXPIRADO · El frontend redirige a login conservando la ruta de retorno
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
NO_ENCONTRADO · Estado vacío con retorno al listado
▸POST/api/documentos/{id}/reemplazoReemplaza el archivo de un documento
Acuerdo #24: el documento anterior se conserva inactivo, no se
sustituye sin rastro. Aprovecha el campo activo del modelo y permite
ofrecer un historial de versiones en la pantalla 5.
La respuesta devuelve el documento nuevo; el anterior queda accesible
con ?incluirInactivos=true.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Cuerpo de la petición req
multipart/form-data → object
Respuestas
Documento reemplazado
ARCHIVO_VACIO · 0 bytes o multipart mal formado
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
ARCHIVO_MUY_GRANDE · Debe salir con el sobre { codigo, mensaje }
(acuerdo #23). Si Spring rechaza el multipart por exceder el límite del
contenedor, la respuesta suele salir con cuerpo HTML y sin sobre, y el
usuario ve un error incomprensible.
FORMATO_NO_ADMITIDO · La respuesta nombra los formatos válidos
▸GET/api/clientes/{id}/expediente/estadoEstado de completitud del expediente
REGLA ABIERTA #13 — solo tiene sentido si el cliente decide que un
expediente incompleto BLOQUEA el alta de crédito. Si se resuelve como
«solo advertir», este endpoint se retira y basta con totalDocumentos en la
ficha del cliente.
Mientras no se resuelva, el frontend maqueta la advertencia, que es lo reversible: convertir un aviso en un bloqueo es una línea; quitar un bloqueo mal puesto genera una discusión con el cliente.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Respuestas
Estado del expediente
NO_ENCONTRADO · Estado vacío con retorno al listado
Préstamos y amortización
7 operacionesPantallas 6 y 8.
▸POST/api/prestamos/simulacionSimula el plan de pagos sin persistir nada
Sostiene la vista previa en vivo del alta de crédito (pantalla 6) y es además el oráculo contra el que el frontend valida su motor espejo en TypeScript (system-design §2.2).
El frontend lo llama con debounce de 400 ms mientras el operador teclea, y pinta primero su cálculo local para respetar RNF-04 (< 200 ms). Si difieren, manda el backend y la discrepancia se registra en consola, nunca se le muestra al usuario como conflicto.
Fórmula documentada para CAPITAL_MAS_INTERES (Anexo A §A.3):
capitalPorCuota = capital / numeroCuotas ·
interesPorCuota = capital × tasaInteres ·
totalCuota = capitalPorCuota + interesPorCuota.
Interés simple sobre capital original, no sobre saldo insoluto.
Fechas: SEMANAL +7 días · QUINCENAL +15 días · MENSUAL REGLA ABIERTA #8 (+30 días como en Access, o mes calendario).
REGLA ABIERTA #3 — para SOLO_INTERES falta definir cómo y cuándo se
recupera el capital. Afecta a 6 créditos históricos y la vista previa no
puede mostrar el plan hasta cerrarlo.
Cuerpo de la petición req
application/json → PeticionSimulacion
Respuestas
Plan simulado
VALIDACION · El nombre en campos[].campo debe coincidir con el del
cuerpo de la petición: así el frontend marca el control automáticamente, sin
mantener un mapa de traducción.
MONTO_INVALIDO · Monto menor o igual a cero, o que excede lo permitido
▸GET/api/prestamosListado de créditos
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| clienteId | query | integer · int64 | opc | |
| rutaId | query | integer · int64 | opc | |
| status | query | PrestamoStatus | opc | |
| page | query | integer | opc | Índice de página, base 0 |
| size | query | integer | opc | |
| orden | query | string | opc | Campo y dirección: |
Respuestas
Página de créditos
▸POST/api/prestamosRegistra el crédito y genera el plan de pagos completo
En una sola transacción: crea el Prestamo con el snapshot de condiciones
(tasa, periodicidad, esquema y número de cuotas quedan congelados en el
crédito), genera todas las Cuota, registra el Movimiento de otorgamiento
y el MovimientoCaja de egreso (system-design §6.3).
Requiere Clave-Idempotencia (§5.4).
REGLA ABIERTA #13 — si se resuelve como bloqueo, este endpoint debe
revalidar el expediente y devolver 409 EXPEDIENTE_INCOMPLETO con la lista
de tipos faltantes en campos[].
Los avales NO se capturan aquí. Aval.prestamoId exige que el préstamo
exista primero, así que el flujo es en dos pasos (acuerdo #25). Si el backend
prefiere recibirlos como arreglo anidado, funciona igual y ahorra una
pantalla intermedia, pero el alta se vuelve una transacción más grande.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| Clave-Idempotencia | header | string · uuid | req | UUID generado por el frontend. Si llega una clave ya procesada, se devuelve el resultado original en lugar de crear un registro nuevo (§5.4). Un doble clic, una reconexión o un reintento del navegador no deben producir dos pagos: un pago duplicado ensucia cuotas, saldos y caja a la vez. Implementado en el backend como la entidad
|
Cuerpo de la petición req
application/json → PeticionPrestamo
Respuestas
Crédito creado con su plan de pagos
VALIDACION · El nombre en campos[].campo debe coincidir con el del
cuerpo de la petición: así el frontend marca el control automáticamente, sin
mantener un mapa de traducción.
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
EXPEDIENTE_INCOMPLETO (REGLA ABIERTA #13). Los tipos faltantes viajan en
campos[] para que el frontend pueda listarlos y ofrecer ir al expediente.
▸GET/api/prestamos/{id}Ficha de un crédito
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Respuestas
Crédito
NO_ENCONTRADO · Estado vacío con retorno al listado
▸GET/api/prestamos/{id}/amortizacionTabla de amortización con estado, saldo, atraso y mora al día
El estado de cada cuota es derivado, nunca almacenado (modelo v1.1
§2.14). Llega ya resuelto; el frontend solo pinta el badge.
VENCIDA tiene precedencia sobre PARCIAL (acuerdo #15): una cuota
vencida con abono parcial se pinta roja, porque el cobrador necesita verla
como pendiente de cobro, no como avanzada.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req | |
| estado | query | EstadoCuota | opc | Filtra por estado derivado. Sostiene las pestañas Todas / Pendientes / Vencidas |
Respuestas
Plan de pagos
NO_ENCONTRADO · Estado vacío con retorno al listado
PRESTAMO_SIN_PLAN · Crédito migrado sin cuotas generadas. Son 24 casos conocidos (Anexo A §A.9). El frontend muestra un aviso explicativo y no ofrece cobro.
▸GET/api/prestamos/{id}/historialCuotas y pagos aplicados, para la pantalla 9
Los pagos cancelados siguen apareciendo, marcados como tales, con su fecha de cancelación y el usuario que la hizo. Es requisito de auditoría (system-design §3.3): un pago cancelado no desaparece.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Respuestas
Historial
NO_ENCONTRADO · Estado vacío con retorno al listado
▸PATCH/api/prestamos/{id}/statusCambia el estado del crédito (cancelación)
Requiere permiso ELIMINAR. La interfaz de esta entrega opera únicamente
ACTIVO, LIQUIDADO, VENCIDO y CANCELADO; los demás valores del enum
existen en el modelo sin flujo asociado (Anexo C §C.2).
Acuerdo #13 pendiente: si VENCIDO es un estado real que alguien asigna
o una condición derivada («tiene al menos una cuota vencida»). El frontend
maquetó la segunda lectura, que es lo que hace el Access en la práctica.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Cuerpo de la petición req
application/json → object
Respuestas
Estado actualizado
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
NO_ENCONTRADO · Estado vacío con retorno al listado
Avales
4 operaciones▸GET/api/prestamos/{id}/avalesAvales de un crédito
El aval avala un crédito, no a una persona. Si el mismo avalista respalda dos créditos del mismo cliente, son dos registros. Por eso la pantalla 7 vive dentro del crédito y no dentro de la ficha del cliente (system-design §4.4).
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Respuestas
Avales
NO_ENCONTRADO · Estado vacío con retorno al listado
▸POST/api/prestamos/{id}/avalesAgrega un aval al crédito
Un crédito admite cero, uno o varios avales; ni el contrato ni la pantalla imponen un máximo. Si el negocio quiere un tope, es una validación de servidor y un mensaje, no un cambio de estructura.
Acuerdo #26: se pueden agregar y editar avales con el crédito ya
ACTIVO, porque los datos de contacto cambian y hay que poder corregirlos.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Cuerpo de la petición req
application/json → PeticionAval
Respuestas
Aval creado
VALIDACION · El nombre en campos[].campo debe coincidir con el del
cuerpo de la petición: así el frontend marca el control automáticamente, sin
mantener un mapa de traducción.
NO_ENCONTRADO · Estado vacío con retorno al listado
▸PUT/api/avales/{id}Edita un aval
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Cuerpo de la petición req
application/json → PeticionAval
Respuestas
Aval actualizado
VALIDACION · El nombre en campos[].campo debe coincidir con el del
cuerpo de la petición: así el frontend marca el control automáticamente, sin
mantener un mapa de traducción.
NO_ENCONTRADO · Estado vacío con retorno al listado
▸DELETE/api/avales/{id}Elimina un aval
Requiere permiso ELIMINAR.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Respuestas
Aval eliminado
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
NO_ENCONTRADO · Estado vacío con retorno al listado
Cobranza y pagos
8 operacionesPantallas 9 a 14. El grupo más delicado del contrato.
▸POST/api/prestamos/{id}/pagos/previsualizacionCalcula la distribución de un pago sin persistir nada
Es la pieza clave de la pantalla 10. El operador teclea el monto recibido y necesita ver, antes de confirmar, qué cuotas se liquidan y cuánto va a mora, interés y capital.
Sin este endpoint el frontend tendría que reimplementar la regla de distribución en TypeScript, que es exactamente lo que system-design §2.2 dice que no debe pasar. Si el backend prefiere resolverlo de otra forma, cualquier mecanismo que devuelva la distribución sin persistir sirve igual.
REGLA ABIERTA #2 — orden de aplicación. Se propone
mora → interés → capital → saldo a favor, conforme a la práctica habitual
del sector, pero debe confirmarlo el cliente.
REGLA ABIERTA #5 — condonarMora. Si la mora no es condonable por el
operador, el campo se retira del contrato y de la pantalla.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Cuerpo de la petición req
application/json → PeticionPrevisualizacionPago
Respuestas
Distribución simulada
VALIDACION · El nombre en campos[].campo debe coincidir con el del
cuerpo de la petición: así el frontend marca el control automáticamente, sin
mantener un mapa de traducción.
NO_ENCONTRADO · Estado vacío con retorno al listado
Conflicto de negocio al cobrar: MONTO_INVALIDO, PRESTAMO_LIQUIDADO,
PRESTAMO_SIN_PLAN o CONFLICTO_CONCURRENCIA. Se presenta como aviso
dentro del panel de cobro, que no se cierra.
CONFLICTO_CONCURRENCIA deja de ser hipotético: Prestamo, Cuota, Pago,
MovimientoCaja y ClienteDocumento llevan @Version, y el plan del backend
incluye «validar concurrencia de dos pagos simultáneos» en la semana 8. Con dos
personas cobrando la misma ruta es un caso real, no de laboratorio. El frontend
recarga el historial, vuelve a previsualizar y avisa de que la distribución
cambió; nunca reintenta en silencio con la misma clave de idempotencia.
▸GET/api/prestamos/{id}/pagosPagos registrados sobre un crédito
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req | |
| page | query | integer | opc | Índice de página, base 0 |
| size | query | integer | opc |
Respuestas
Página de pagos
▸POST/api/prestamos/{id}/pagosRegistra un pago y lo distribuye sobre las cuotas
El flujo central del sistema. Es donde se cruzan cuotas, saldos, estado
de cuenta y caja. Los 8 pasos van en una sola transacción
(system-design §6.4): crea Pago, distribuye, crea PagoAplicacion,
acumula en Cuota, actualiza saldos de Prestamo, registra Movimiento,
registra un único MovimientoCaja de tipo INGRESO_PAGO_PRESTAMO con
utilidad = interés + mora efectivamente cobrados, e incrementa el saldo a
favor si hubo excedente.
Si algo falla en el paso del movimiento de caja, no puede quedar el pago aplicado sin movimiento: el cuadre de la caja del día es lo primero que el cliente revisa.
La respuesta trae la distribución final, no solo el pagoId: con eso el
frontend pinta el historial actualizado y arma el recibo sin una segunda
consulta. Si difiere de la previsualización —porque otro usuario cobró en
medio— manda la respuesta, y el frontend avisa de que cambió.
No existe POST /api/cuotas/{id}/pago. El pago se registra contra el
crédito, no contra una cuota, porque el modelo permite que cubra varias. El
«abono parcial» de la pantalla 11 no es un endpoint distinto: es el
mismo pago con un monto menor al total de la cuota. Una operación, dos
pantallas.
Requiere Clave-Idempotencia. Un doble clic, una reconexión o un reintento
del navegador no deben producir dos pagos: es el riesgo más caro del sistema.
REGLA ABIERTA #4 — si el abono acumulado alcanza el monto de la cuota, ¿se cierra sola o la cierra el operador? REGLA ABIERTA #6 — ¿el saldo a favor se aplica solo al siguiente vencimiento o se conserva hasta que el operador decida?
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req | |
| Clave-Idempotencia | header | string · uuid | req | UUID generado por el frontend. Si llega una clave ya procesada, se devuelve el resultado original en lugar de crear un registro nuevo (§5.4). Un doble clic, una reconexión o un reintento del navegador no deben producir dos pagos: un pago duplicado ensucia cuotas, saldos y caja a la vez. Implementado en el backend como la entidad
|
Cuerpo de la petición req
application/json → PeticionPago
Respuestas
Pago registrado y aplicado
VALIDACION · El nombre en campos[].campo debe coincidir con el del
cuerpo de la petición: así el frontend marca el control automáticamente, sin
mantener un mapa de traducción.
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
Conflicto de negocio al cobrar: MONTO_INVALIDO, PRESTAMO_LIQUIDADO,
PRESTAMO_SIN_PLAN o CONFLICTO_CONCURRENCIA. Se presenta como aviso
dentro del panel de cobro, que no se cierra.
CONFLICTO_CONCURRENCIA deja de ser hipotético: Prestamo, Cuota, Pago,
MovimientoCaja y ClienteDocumento llevan @Version, y el plan del backend
incluye «validar concurrencia de dos pagos simultáneos» en la semana 8. Con dos
personas cobrando la misma ruta es un caso real, no de laboratorio. El frontend
recarga el historial, vuelve a previsualizar y avisa de que la distribución
cambió; nunca reintenta en silencio con la misma clave de idempotencia.
▸GET/api/pagos/{id}Detalle de un pago con sus aplicaciones
PagoAplicacion es lo que permite que el recibo impreso diga exactamente
cuánto fue a capital, interés y mora. Sin ese detalle, el recibo sería un
total opaco (system-design §4.4).
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Respuestas
Pago con detalle
NO_ENCONTRADO · Estado vacío con retorno al listado
▸POST/api/pagos/{id}/cancelacionCancela un pago registrado por error
En una sola transacción: revierte las PagoAplicacion, recalcula las
cuotas afectadas, ajusta los saldos del crédito, anula o compensa el
movimiento de caja y marca el pago como CANCELADO.
El pago cancelado se conserva y sigue visible en el historial: es requisito de auditoría.
REGLA ABIERTA #7 — qué perfil puede cancelar y con qué límite de tiempo.
Si la ventana ya pasó, se espera 409 CANCELACION_NO_PERMITIDA con un
mensaje que explique el motivo — no un 403 genérico. El operador necesita
entender por qué no puede.
Si la reversión genera un movimiento de caja compensatorio en lugar de anular el original, hay que avisarlo: cambia lo que muestra la pantalla 15.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req | |
| Clave-Idempotencia | header | string · uuid | req | UUID generado por el frontend. Si llega una clave ya procesada, se devuelve el resultado original en lugar de crear un registro nuevo (§5.4). Un doble clic, una reconexión o un reintento del navegador no deben producir dos pagos: un pago duplicado ensucia cuotas, saldos y caja a la vez. Implementado en el backend como la entidad
|
Cuerpo de la petición req
application/json → object
Respuestas
Pago cancelado
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
NO_ENCONTRADO · Estado vacío con retorno al listado
CANCELACION_NO_PERMITIDA o PAGO_YA_CANCELADO. Debe explicar el motivo —no un 403 genérico—: el operador necesita entender por qué no puede.
▸POST/api/prestamos/{id}/abonos-capitalAbono directo al capital, fuera del plan de cuotas
REGLA ABIERTA #1 — BLOQUEANTE. Determina el comportamiento del motor de
amortización y debe cerrarse antes de aprobar el Anexo A. Las tres opciones
están en el enum aplicacion:
REDUCE_CUOTAS— reduce el número de cuotas restantes, manteniendo el montoREDUCE_MONTO— reduce el monto de las cuotas restantes, manteniendo el númeroSOLO_CAJA— se registra en caja sin alterar el plan de pagos vigente
Dato del sistema actual: existen 5 movimientos de caja con el concepto
Abona capital <cliente>, lo que confirma que la operación genera un
registro en caja, pero no es concluyente respecto de su efecto sobre el
plan de pagos.
Cuando el cliente elija, queda un valor y los otros dos se retiran del contrato y de la pantalla 12.
La respuesta devuelve el plan resultante para que la pantalla pueda mostrar el antes y el después.
Requiere Clave-Idempotencia.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req | |
| Clave-Idempotencia | header | string · uuid | req | UUID generado por el frontend. Si llega una clave ya procesada, se devuelve el resultado original en lugar de crear un registro nuevo (§5.4). Un doble clic, una reconexión o un reintento del navegador no deben producir dos pagos: un pago duplicado ensucia cuotas, saldos y caja a la vez. Implementado en el backend como la entidad
|
Cuerpo de la petición req
application/json → PeticionAbonoCapital
Respuestas
Abono a capital registrado
VALIDACION · El nombre en campos[].campo debe coincidir con el del
cuerpo de la petición: así el frontend marca el control automáticamente, sin
mantener un mapa de traducción.
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
Conflicto de negocio al cobrar: MONTO_INVALIDO, PRESTAMO_LIQUIDADO,
PRESTAMO_SIN_PLAN o CONFLICTO_CONCURRENCIA. Se presenta como aviso
dentro del panel de cobro, que no se cierra.
CONFLICTO_CONCURRENCIA deja de ser hipotético: Prestamo, Cuota, Pago,
MovimientoCaja y ClienteDocumento llevan @Version, y el plan del backend
incluye «validar concurrencia de dos pagos simultáneos» en la semana 8. Con dos
personas cobrando la misma ruta es un caso real, no de laboratorio. El frontend
recarga el historial, vuelve a previsualizar y avisa de que la distribución
cambió; nunca reintenta en silencio con la misma clave de idempotencia.
▸GET/api/cobranza/vencidasCuotas con fecha de vencimiento cumplida y saldo pendiente
Pantalla 13 y base del formato impreso «Listado de ruta». Es la pantalla estrella del sistema y la lista de trabajo del cobrador.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| rutaId | query | integer · int64 | opc | |
| fecha | query | string · date | opc | Fecha de corte. Por omisión, hoy |
| clienteId | query | integer · int64 | opc | |
| page | query | integer | opc | Índice de página, base 0 |
| size | query | integer | opc | |
| orden | query | string | opc | Campo y dirección: |
Respuestas
Cuotas vencidas
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
▸GET/api/cobranza/proximasCuotas por vencer dentro de los días configurados
Pantalla 14. El valor por omisión de dias es
Configuracion.diasAnticipacionAviso (hoy 3, Anexo A §A.7). El frontend lo
muestra como cualquier otro filtro: visible y editable, nunca oculto.
Los recordatorios se derivan de las cuotas; no existe una tabla de recordatorios (modelo v1.1 §1.7).
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| rutaId | query | integer · int64 | opc | |
| dias | query | integer | opc | Días de anticipación. Por omisión, el valor de Configuracion |
| page | query | integer | opc | Índice de página, base 0 |
| size | query | integer | opc | |
| orden | query | string | opc | Campo y dirección: |
Respuestas
Cuotas próximas a vencer
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
Caja
4 operaciones▸GET/api/caja/movimientosMovimientos de caja del negocio
naturaleza viene derivada del prefijo de tipoMovimiento, para que el
frontend no tenga que parsear el nombre del enum para saber el signo
(system-design §4.2).
Cuando el movimiento nació de un pago, la respuesta trae clienteNombre y
prestamoId: es la trazabilidad que el Access no tenía, porque allí el
nombre viajaba concatenado dentro del texto del concepto.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| desde | query | string · date | opc | |
| hasta | query | string · date | opc | |
| tipo | query | TipoMovimientoCaja | opc | |
| soloManuales | query | boolean | opc | Solo los movimientos con generadoPorSistema = false |
| page | query | integer | opc | Índice de página, base 0 |
| size | query | integer | opc | |
| orden | query | string | opc | Campo y dirección: |
Respuestas
Página de movimientos de caja
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
▸POST/api/caja/movimientosRegistra un movimiento de caja manual
Solo movimientos manuales: aportaciones de capital, retiros y gastos operativos. El frontend no crea los movimientos derivados de pagos ni de altas de crédito: esos los genera el backend como efecto de la operación (Anexo A §A.4).
REGLA ABIERTA #9 — dónde se registran hoy los gastos operativos del negocio. En los 9 meses de la base entregada no existe ningún gasto operativo capturado, y eso define qué significa «utilidad» en 2 de los 6 indicadores del tablero.
Requiere Clave-Idempotencia.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| Clave-Idempotencia | header | string · uuid | req | UUID generado por el frontend. Si llega una clave ya procesada, se devuelve el resultado original en lugar de crear un registro nuevo (§5.4). Un doble clic, una reconexión o un reintento del navegador no deben producir dos pagos: un pago duplicado ensucia cuotas, saldos y caja a la vez. Implementado en el backend como la entidad
|
Cuerpo de la petición req
application/json → PeticionMovimientoCaja
Respuestas
Movimiento registrado
VALIDACION · El nombre en campos[].campo debe coincidir con el del
cuerpo de la petición: así el frontend marca el control automáticamente, sin
mantener un mapa de traducción.
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
▸PUT/api/caja/movimientos/{id}Edita un movimiento de caja manual
Solo si generadoPorSistema = false. Un movimiento generado por un pago
o por un alta de crédito no es editable: se corrige cancelando la operación
que lo originó. Se espera 409 si se intenta.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| id | path | integer · int64 | req |
Cuerpo de la petición req
application/json → PeticionMovimientoCaja
Respuestas
Movimiento actualizado
VALIDACION · El nombre en campos[].campo debe coincidir con el del
cuerpo de la petición: así el frontend marca el control automáticamente, sin
mantener un mapa de traducción.
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
NO_ENCONTRADO · Estado vacío con retorno al listado
El movimiento fue generado por el sistema y no es editable
▸GET/api/caja/resumenTotales de caja del periodo
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| desde | query | string · date | opc | |
| hasta | query | string · date | opc |
Respuestas
Resumen de caja
Reportes e impresión
5 operacionesAnexo B §B.1 (9 reportes) y §B.2 (7 formatos). Pantallas 16 y 17.
▸GET/api/reportes/{clave}Datos de un reporte, para pintar en pantalla
Un solo par de endpoints cubre los 9 reportes: cada reporte nuevo es configuración, no código, de los dos lados.
Los filtros aplicables dependen de clave y están en el Anexo B §B.1. Se
envían como parámetros de consulta libres; el backend valida los que aplican
a cada reporte y responde 400 VALIDACION con el nombre del filtro si
alguno no corresponde.
Los totales los calcula el backend y vienen en totales: el frontend no
suma columnas (system-design §2.2).
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| clave | path | string | req | Las 9 claves del Anexo B §B.1 |
| desde | query | string · date | opc | |
| hasta | query | string · date | opc | |
| clienteId | query | integer · int64 | opc | |
| prestamoId | query | integer · int64 | opc | |
| rutaId | query | integer · int64 | opc | |
| anio | query | integer | opc | |
| mes | query | integer | opc | |
| page | query | integer | opc | Índice de página, base 0 |
| size | query | integer | opc |
Respuestas
Datos del reporte
VALIDACION · El nombre en campos[].campo debe coincidir con el del
cuerpo de la petición: así el frontend marca el control automáticamente, sin
mantener un mapa de traducción.
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
NO_ENCONTRADO · Estado vacío con retorno al listado
▸GET/api/reportes/{clave}/pdfReporte en PDF, generado en el servidor
Requisito contractual (Anexo B §B.4): el PDF se genera en el servidor a partir de la misma plantilla que se imprime, de modo que el archivo descargado y el documento en papel son idénticos. No es preferencia.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| clave | path | string | req | Las 9 claves del Anexo B §B.1 |
| desde | query | string · date | opc | |
| hasta | query | string · date | opc | |
| clienteId | query | integer · int64 | opc | |
| prestamoId | query | integer · int64 | opc | |
| rutaId | query | integer · int64 | opc | |
| anio | query | integer | opc |
Respuestas
Documento PDF
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
NO_ENCONTRADO · Estado vacío con retorno al listado
▸GET/api/reportes/{clave}/excelReporte en Excel
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| clave | path | string | req | Las 9 claves del Anexo B §B.1 |
| desde | query | string · date | opc | |
| hasta | query | string · date | opc | |
| clienteId | query | integer · int64 | opc | |
| prestamoId | query | integer · int64 | opc | |
| rutaId | query | integer · int64 | opc | |
| anio | query | integer | opc |
Respuestas
Libro de Excel
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
▸GET/api/impresion/{formato}Datos de un formato de cobranza, para la vista previa en pantalla
La respuesta incluye la identidad del negocio (nombre, lema y logotipo de Datos del negocio, Anexo A Módulo 7), porque encabeza todos los formatos.
El frontend renderiza la vista previa con @media print para que se vea al
instante; el archivo lo genera el backend (Anexo B §B.4).
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| formato | path | string | req | Los 7 formatos del Anexo B §B.2 |
| rutaId | query | integer · int64 | opc | |
| fecha | query | string · date | opc | |
| prestamoId | query | integer · int64 | opc | |
| cuotaId | query | integer · int64 | opc | |
| pagoId | query | integer · int64 | opc |
Respuestas
Datos del formato
NO_ENCONTRADO · Estado vacío con retorno al listado
▸GET/api/impresion/{formato}/pdfFormato de cobranza en PDF
REGLA ABIERTA #11 — falta confirmar si los tickets se imprimen en
impresora térmica de rollo o en hoja carta. El parámetro presentacion
soporta ambos desde el primer día; la confirmación decide cuál es el valor
por omisión, no si se construye.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| formato | path | string | req | Los 7 formatos del Anexo B §B.2 |
| presentacion | query | string | opc | |
| rutaId | query | integer · int64 | opc | |
| fecha | query | string · date | opc | |
| prestamoId | query | integer · int64 | opc | |
| cuotaId | query | integer · int64 | opc | |
| pagoId | query | integer · int64 | opc |
Respuestas
Documento PDF
NO_ENCONTRADO · Estado vacío con retorno al listado
Tablero y configuración
6 operacionesAnexo B §B.3 y Módulo 7 del Anexo A.
▸GET/api/dashboardLos 6 indicadores del Anexo B §B.3, en una sola llamada
Una petición, no seis. Seis peticiones en paralelo para pintar seis tarjetas es la clase de decisión que se paga en percepción de lentitud desde el primer día (system-design §5.2).
Definiciones exactas del Anexo B §B.3:
totalEfectivo— suma algebraica de todos los movimientos de cajacarteraActiva— saldo pendiente de todos los créditos con cuotas por cobrarcobranzaPeriodo— pagos recibidos en el periodo seleccionadocuotasVencidas— número e importe de las cuotas vencidas con saldoproyeccionMes— cuotas cuyo vencimiento cae en el mes seleccionadoutilidadMes— intereses y mora efectivamente cobrados
Advertencia que el frontend rotula en la interfaz: «Utilidad del mes» no es ingresos menos gastos y «Total en efectivo» no descuenta gastos operativos, porque el negocio no los captura (Anexo A §A.9). Dos de los seis indicadores cambian de significado si se resuelve la REGLA ABIERTA #9. El tablero no es un estado de resultados y así se rotula.
Parámetros
| Nombre | En | Tipo | Descripción | |
|---|---|---|---|---|
| anio | query | integer | opc | |
| mes | query | integer | opc | |
| rutaId | query | integer · int64 | opc |
Respuestas
Indicadores del tablero
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
▸GET/api/configuracionConfiguración operativa
Respuestas
Configuración
▸PUT/api/configuracionActualiza la configuración operativa
Cuerpo de la petición req
application/json → Configuracion
Respuestas
Configuración actualizada
VALIDACION · El nombre en campos[].campo debe coincidir con el del
cuerpo de la petición: así el frontend marca el control automáticamente, sin
mantener un mapa de traducción.
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
▸GET/api/empresaDatos del negocio usados en los formatos impresos
Respuestas
Empresa
▸PUT/api/empresaActualiza los datos del negocio
Cuerpo de la petición req
application/json → PeticionEmpresa
Respuestas
Empresa actualizada
VALIDACION · El nombre en campos[].campo debe coincidir con el del
cuerpo de la petición: así el frontend marca el control automáticamente, sin
mantener un mapa de traducción.
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
▸POST/api/empresa/logoSube el logotipo del negocio
Es la única subida de archivo fuera del expediente digital. Se resuelve como caso único, no como componente del sistema de diseño (DS §10).
El logotipo debe imprimirse en monocromo a 12 mm de altura máxima en la hoja térmica; si no hay uno apto, los formatos usan solo el nombre (DS §6.3).
Cuerpo de la petición req
multipart/form-data → object
Respuestas
Logotipo actualizado
SIN_PERMISO · Ocultar un botón no es autorizar. Cada endpoint valida el permiso por su cuenta; si el frontend falla y muestra una acción indebida, la respuesta correcta es este 403, no que la operación pase.
ARCHIVO_MUY_GRANDE · Debe salir con el sobre { codigo, mensaje }
(acuerdo #23). Si Spring rechaza el multipart por exceder el límite del
contenedor, la respuesta suele salir con cuerpo HTML y sin sobre, y el
usuario ve un error incomprensible.
FORMATO_NO_ADMITIDO · La respuesta nombra los formatos válidos
11Catálogo de errores esperados
18 códigos estables. Todo error viaja en el mismo sobre; el frontend decide el texto visible a partir del
codigo con un diccionario único. Si llega un código desconocido, muestra el mensaje y registra el código.
{
"codigo": "VALIDACION",
"mensaje": "Revisa los datos capturados",
"campos": [ { "campo": "capital", "mensaje": "Debe ser mayor que cero" } ]
}
campo en campos[] debe coincidir con el del cuerpo de la
petición — así el frontend marca el control automáticamente, sin mapa de traducción. Y para EXPEDIENTE_INCOMPLETO,
los tipos faltantes viajan en campos[] para poder listarlos y ofrecer ir al expediente.
413 ARCHIVO_MUY_GRANDE con el mismo
formato que todo lo demás — y el límite del contenedor debe coincidir con el acordado en la regla abierta #12.
| Código | HTTP | Cuándo ocurre | Qué hace el frontend |
|---|---|---|---|
| CREDENCIALES_INVALIDAS | 401 | Usuario o contraseña incorrectos |
Mensaje: «Usuario o contraseña incorrectos» |
| USUARIO_INACTIVO | 403 | Usuario deshabilitado |
Mensaje distinto: «Usuario deshabilitado. Contacta al administrador» — el operador distingue «me equivoqué» de «me deshabilitaron» |
| TOKEN_EXPIRADO | 401 | Sesión vencida |
Redirige a login conservando la ruta de retorno |
| PASSWORD_REQUERIDO | 403 | Falta el cambio obligatorio de contraseña |
Fuerza la pantalla de cambio |
| SIN_PERMISO | 403 | Permiso insuficiente — ocultar no es autorizar |
«No tienes permiso para esta acción» |
| NO_ENCONTRADO | 404 | Recurso inexistente |
Estado vacío con retorno al listado |
| VALIDACION | 400 | Campos inválidos |
Marca cada campo con |
| MONTO_INVALIDO | 409 | Monto ≤ 0, o que excede lo permitido |
Marca el campo de importe |
| PRESTAMO_SIN_PLAN | 409 | Crédito migrado sin cuotas generadas — 24 casos conocidos (Anexo A §A.9) |
Aviso explicativo, sin ofrecer cobro |
| PRESTAMO_LIQUIDADO | 409 | Cobro sobre préstamo sin saldo |
«Este crédito ya está liquidado» |
| PAGO_YA_CANCELADO | 409 | Doble cancelación |
Refresca y avisa |
| CANCELACION_NO_PERMITIDA | 409 | Fuera de la ventana de tiempo (regla abierta #7) |
Explica el motivo — nunca un 403 genérico |
| ARCHIVO_MUY_GRANDE | 413 | Supera el límite de la regla abierta #12 |
«El archivo supera el máximo de N MB». Debe salir con el sobre, no como HTML del contenedor |
| FORMATO_NO_ADMITIDO | 415 | Extensión o MIME fuera de los permitidos |
Nombra los formatos válidos |
| ARCHIVO_VACIO | 400 | 0 bytes o multipart mal formado |
Pide volver a seleccionar |
| EXPEDIENTE_INCOMPLETO | 409 | Alta de crédito sin documentos obligatorios (regla abierta #13) |
Lista los tipos faltantes en |
| CONFLICTO_CONCURRENCIA | 409 | El recurso cambió desde que se cargó — @Version en Prestamo, Cuota, Pago, MovimientoCaja y ClienteDocumento |
Recarga, vuelve a previsualizar y avisa. Nunca reintentar en silencio con la misma clave de idempotencia |
| ERROR_INTERNO | 500 | Cualquier otro fallo |
Mensaje genérico + opción de reintentar |
12Enumeraciones
Los valores que viajan por la API en MAYÚSCULAS, tal cual. Los marcados como abiertos dependen de una decisión del cliente (ver reglas abiertas); cuando se cierren, se retira lo que sobre.
Periodicidad
Días que suma: DIARIO +1 · SEMANAL +7 · QUINCENAL +15 · MENSUAL +30 o mes calendario (REGLA ABIERTA #8)
TipoCalculo
CAPITAL_MAS_INTERES (533 créditos) · SOLO_INTERES (6 créditos).
SALDOS_INSOLUTOS queda fuera del alcance (Anexo C §C.3).
PrestamoStatus
La interfaz de esta entrega opera únicamente ACTIVO, LIQUIDADO, VENCIDO y CANCELADO. EN_REVISION, AUTORIZADO, RECHAZADO y REFINANCIADO existen en el modelo sin flujo asociado (Anexo C §C.2).
PagoStatus
TipoPago
EstadoCuota
Derivado, nunca almacenado (modelo v1.1 §2.14). VENCIDA tiene precedencia sobre PARCIAL: una cuota vencida con abono se pinta roja, porque el cobrador necesita verla como pendiente de cobro (acuerdo #15).
ClienteStatus
CERRADO (acuerdo #18). No estaba enumerado en el modelo v1.1; el backend lo implementa así en ClienteStatus.java
TipoIdentificacion
CERRADO (acuerdo #17). No estaba enumerado en el modelo v1.1; el backend lo implementa así en TipoIdentificacion.java
TipoDocumento
Coinciden exactamente con los tipos admitidos del Anexo C §C.1.1
EstadoExpediente
[ABIERTO] Solo si la REGLA ABIERTA #13 se resuelve como bloqueo
TipoMovimiento
Estado de cuenta del cliente (pantalla 9). No confundir con la caja del negocio. REFINANCIAMIENTO está definido sin operación que lo genere (C.2).
TipoMovimientoCaja
Caja del negocio.
CERRADO (acuerdo #16). El Anexo A §A.4 clasifica el alta de crédito como
EGRESO_PRESTAMO_OTORGADO, valor que no existía en el enum del modelo v1.1.
Son 2,107 movimientos, el 14 % de la caja. El backend lo incorporó en
TipoMovimientoCaja.java y su plan de 8 semanas lo lista como corrección
recomendada de la semana 1. El filtro de la pantalla 15 y la agrupación del
reporte 5 del Anexo B conservan la categoría.
NaturalezaMovimientoCaja
Campo derivado del prefijo de tipoMovimiento. Existe para que el
frontend no tenga que parsear el nombre del enum para saber el signo.
AplicacionAbonoCapital
REGLA ABIERTA #1 — bloqueante. Cuando el cliente elija, quedan uno y se retiran los otros dos
Presentacion
Parámetro de GET /api/impresion/{formato}/pdf. Cuál es el valor por omisión depende de la REGLA ABIERTA #11 (Anexo B §B.2).
AmbitoPeriodicidad
Parámetro de GET /api/catalogos/periodicidades: ALTA devuelve las tres del alta; TODAS, las seis del modelo para créditos históricos.
13Esquemas del contrato
Los 70 esquemas de financiera-api-v1.1.0.yaml. Los campos requeridos van marcados;
las descripciones conservan las referencias a anexos, reglas y acuerdos, enlazadas.
Errorobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| codigo | CodigoError | req | |
| mensaje | string | req | Respaldo en español operativo. El texto que ve el usuario lo decide el
frontend a partir del |
| campos | array<object> | opc |
CodigoErrorenum
Los 18 códigos estables de system-design §5.3
Paginaobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| pagina | integer | req | |
| tamano | integer | req | |
| totalElementos | integer | req | |
| totalPaginas | integer | req |
OpcionCatalogoobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| valor | string | req | |
| etiqueta | string | req | Etiqueta en español para mostrar. El valor técnico nunca se muestra |
| activo | boolean | opc |
Periodicidadenum
Días que suma: DIARIO +1 · SEMANAL +7 · QUINCENAL +15 · MENSUAL +30 o mes calendario (REGLA ABIERTA #8)
TipoCalculoenum
CAPITAL_MAS_INTERES (533 créditos) · SOLO_INTERES (6 créditos).
SALDOS_INSOLUTOS queda fuera del alcance (Anexo C §C.3).
PrestamoStatusenum
La interfaz de esta entrega opera únicamente ACTIVO, LIQUIDADO, VENCIDO y CANCELADO. EN_REVISION, AUTORIZADO, RECHAZADO y REFINANCIADO existen en el modelo sin flujo asociado (Anexo C §C.2).
PagoStatusenum
TipoPagoenum
EstadoCuotaenum
Derivado, nunca almacenado (modelo v1.1 §2.14). VENCIDA tiene precedencia sobre PARCIAL: una cuota vencida con abono se pinta roja, porque el cobrador necesita verla como pendiente de cobro (acuerdo #15).
ClienteStatusenum
CERRADO (acuerdo #18). No estaba enumerado en el modelo v1.1; el backend lo implementa así en ClienteStatus.java
TipoIdentificacionenum
CERRADO (acuerdo #17). No estaba enumerado en el modelo v1.1; el backend lo implementa así en TipoIdentificacion.java
TipoDocumentoenum
Coinciden exactamente con los tipos admitidos del Anexo C §C.1.1
EstadoExpedienteenum
[ABIERTO] Solo si la REGLA ABIERTA #13 se resuelve como bloqueo
TipoMovimientoenum
Estado de cuenta del cliente (pantalla 9). No confundir con la caja del negocio. REFINANCIAMIENTO está definido sin operación que lo genere (C.2).
TipoMovimientoCajaenum
Caja del negocio.
CERRADO (acuerdo #16). El Anexo A §A.4 clasifica el alta de crédito como
EGRESO_PRESTAMO_OTORGADO, valor que no existía en el enum del modelo v1.1.
Son 2,107 movimientos, el 14 % de la caja. El backend lo incorporó en
TipoMovimientoCaja.java y su plan de 8 semanas lo lista como corrección
recomendada de la semana 1. El filtro de la pantalla 15 y la agrupación del
reporte 5 del Anexo B conservan la categoría.
NaturalezaMovimientoCajaenum
Campo derivado del prefijo de tipoMovimiento. Existe para que el
frontend no tenga que parsear el nombre del enum para saber el signo.
AplicacionAbonoCapitalenum
REGLA ABIERTA #1 — bloqueante. Cuando el cliente elija, quedan uno y se retiran los otros dos
PeticionLoginobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| usuario | string | req | |
| password | string · password | req |
PeticionCambioPasswordobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| actual | string · password | req | |
| nueva | string · password | req |
RespuestaSesionobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| token | string | req | |
| expiraEn | string · date-time | req | ISO-8601 con offset |
| requiereCambioPassword | boolean | req | Obligatorio en el primer acceso tras la migración: las contraseñas
estaban en texto plano y se cifran con BCrypt (Anexo A §A.2). Mientras
sea |
| usuario | object | req | |
| roles | array<string> | req | |
| permisos | array<string> | req | Ya resuelto: rol más excepciones de |
| empresa | Empresa | opc |
Usuarioobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| id | integer · int64 | req | |
| username | string | req | |
| nombre | string · null | opc | |
| activo | boolean | req | Estado del usuario, no un permiso (Anexo A §A.2) |
| requiereCambioPassword | boolean | opc | Campo persistido ( |
| roles | array<Role> | opc | |
| permisosEfectivos | array<string> | opc | |
| fechaRegistro | string · date-time | opc | |
| fechaActualizacion | string · null | opc |
PeticionUsuarioobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| username | string | req | |
| nombre | string | opc | |
| password | string · null | opc | Solo en el alta. Nunca viaja de vuelta en ninguna respuesta |
| activo | boolean | req | |
| roleIds | array<integer · int64> | req |
Roleobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| id | integer · int64 | req | |
| authority | string | req | |
| descripcion | string · null | opc |
Permisoobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| id | integer · int64 | req | |
| codigo | string | req | Los 5 del Anexo A §A.2. Punto abierto (Ap.1 §2): los 5 permisos vienen del Access, que no
tenía expediente ni avales. Con el Anexo C hay dos superficies nuevas sin
permiso propio. El frontend asumió la opción (a): ver bajo
|
| descripcion | string · null | opc |
Rutaobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| id | integer · int64 | req | |
| codigo | string | req | |
| descripcion | string · null | opc | |
| activo | boolean | req | |
| totalCreditos | integer · null | opc | Para poder avisar antes de desactivar una ruta con cartera |
PeticionRutaobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| codigo | string | req | |
| descripcion | string | req | Obligatoria. |
OpcionTipoMovimientoCajaobjeto compuesto
Compone y extiende: OpcionCatalogo
| Campo | Tipo | Descripción | |
|---|---|---|---|
| naturaleza | NaturalezaMovimientoCaja | req | |
| manual | boolean | req | Si |
OpcionTipoDocumentoobjeto compuesto
Compone y extiende: OpcionCatalogo
| Campo | Tipo | Descripción | |
|---|---|---|---|
| obligatorio | boolean | opc | REGLA ABIERTA #13. Hoy puede venir siempre en false |
ClienteResumenobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| id | integer · int64 | req | |
| nombreCompleto | string | req | Se migra íntegro en un solo campo. No se separa automáticamente en nombre y apellidos: hacerlo sobre cientos de nombres capturados libremente produce errores (Anexo A §A.8). |
| curp | string · null | opc | |
| numeroIdentificacion | string · null | opc | |
| rutaId | integer · null | opc | |
| rutaNombre | string · null | opc | Junto al |
| status | ClienteStatus | req | |
| totalPrestamos | integer | opc | Columna que hace visible la separación cliente/crédito del Anexo A §A.8 |
| prestamosActivos | integer | opc | |
| saldoTotal | number | opc | Suma de saldos de sus créditos activos |
| totalDocumentos | integer | opc | Documentos vigentes del expediente |
| estadoExpediente | EstadoExpediente | null | opc | Solo si la REGLA ABIERTA #13 lo hace necesario |
| marcadoParaRevision | boolean | opc | Registro migrado con incidencia de calidad conocida: sin nombre (14 casos), sin fecha de préstamo, forma de pago o ruta (18–20 casos), o cliente marcador de movimientos huérfanos (60 casos). Anexo A §A.9. ABIERTO — acuerdo #30. |
Clienteobjeto compuesto
Compone y extiende: ClienteResumen
| Campo | Tipo | Descripción | |
|---|---|---|---|
| nombre | string · null | opc | |
| primerApellido | string · null | opc | |
| segundoApellido | string · null | opc | |
| fechaNacimiento | string · null | opc | |
| tipoIdentificacion | TipoIdentificacion | null | opc | |
| telefono | string · null | opc | ABIERTO — acuerdo #28. Ver la nota de |
| direccion | string · null | opc | ABIERTO — acuerdo #28. Llama la atención que |
| notas | string · null | opc | ABIERTO — acuerdo #28. Ver la nota de |
| fechaRegistro | string · date-time | opc | |
| fechaActualizacion | string · null | opc |
PeticionClienteobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| nombreCompleto | string | req | |
| nombre | string | opc | |
| primerApellido | string | opc | |
| segundoApellido | string | opc | |
| curp | string | opc | |
| fechaNacimiento | string · null | opc | |
| tipoIdentificacion | TipoIdentificacion | null | opc | |
| numeroIdentificacion | string | opc | |
| telefono | string | opc | |
| direccion | string | opc | |
| notas | string | opc | |
| rutaId | integer · null | opc | |
| status | ClienteStatus | opc |
ClienteDocumentoobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| id | integer · int64 | req | |
| clienteId | integer · int64 | req | |
| tipoDocumento | TipoDocumento | req | |
| nombreArchivo | string | req | |
| descripcion | string · null | opc | |
| contentType | string · null | opc | |
| tamanoBytes | integer | opc | Para mostrar «2.4 MB» en la lista sin descargar el archivo |
| fechaCarga | string · date-time | req | |
| usuarioCarga | string · null | opc | Quién lo subió. Útil en un expediente que revisan varias personas |
| activo | boolean | req |
|
| reemplazaAId | integer · null | opc | Documento al que este reemplaza. Corresponde a la FK Es lo que permite que la pantalla 5 ofrezca «Ver versiones anteriores» recorriendo la cadena, en lugar de mostrar una lista plana de inactivos. |
| version | integer · null | opc | Control de concurrencia optimista ( |
| urlContenido | string | req | Ruta relativa del endpoint de descarga, armada por el backend. El frontend no concatena rutas: si mañana el archivo se mueve a otro almacenamiento, no toca nada.
|
EstadoExpedienteRespuestaobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| estado | EstadoExpediente | req | |
| faltantes | array<TipoDocumento> | req |
Avalobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| id | integer · int64 | req | |
| prestamoId | integer · int64 | req | |
| nombreCompleto | string | req | Un solo campo, igual que en |
| telefono | string · null | opc | |
| direccion | string · null | opc | |
| curp | string · null | opc | |
| tipoIdentificacion | TipoIdentificacion | null | opc | |
| numeroIdentificacion | string · null | opc | |
| fechaRegistro | string · date-time | req | |
| fechaActualizacion | string · null | opc |
PeticionAvalobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| nombreCompleto | string | req | |
| telefono | string | opc | |
| direccion | string | opc | |
| curp | string | opc | |
| tipoIdentificacion | TipoIdentificacion | null | opc | |
| numeroIdentificacion | string | opc |
PeticionSimulacionobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| capital | number | req | |
| tasaInteres | number | req | Fracción por periodo, no porcentaje. |
| tasaInteresMoratorio | number | opc | De 557 créditos, uno solo tiene tasa de mora distinta de cero (0.01), y solo 17 de 17,927 movimientos registran mora aplicada. La funcionalidad se implementa completa, pero en la práctica el negocio no la usa. |
| numeroCuotas | integer | req | |
| periodicidad | Periodicidad | req | |
| tipoCalculo | TipoCalculo | req | |
| fechaPrimerPago | string · date | req |
ResumenSimulacionobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| capitalPorCuota | number | req | |
| interesPorCuota | number | req | |
| totalCuota | number | req | |
| interesTotal | number | req | |
| totalPagar | number | req |
CuotaSimuladaobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| numeroCuota | integer | req | |
| fechaVencimiento | string · date | req | |
| capital | number | req | |
| interes | number | req | |
| totalCuota | number | req | La última cuota absorbe el ajuste de redondeo (p. ej. 1,058.37 frente a 1,058.33) |
RespuestaSimulacionobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| resumen | ResumenSimulacion | req | |
| cuotas | array<CuotaSimulada> | req |
PeticionPrestamoobject
Sin rutaId. Prestamo.java no tiene relación con Ruta: la ruta vive en
Cliente. En Access cliente y crédito eran el mismo registro, así que la ruta
parecía del crédito; al separarlos (Anexo A §A.8), la ruta quedó donde
corresponde — es la zona de cobranza de una persona, no una condición del
préstamo. Los créditos de un cliente comparten su ruta.
PrestamoResumen.rutaId y rutaNombre viajan igualmente en las respuestas,
derivados del cliente, para no obligar a una consulta extra por fila. Y
GET /api/prestamos?rutaId= filtra por la ruta del cliente.
Consecuencia a vigilar: si la regla abierta #10 concluye que GRUPOS,
QUINCENAL y LIQUIDACION no son rutas de cobranza sino clasificaciones
del crédito, esta decisión se cae y hace falta una dimensión propia en
Prestamo. Es la razón por la que esa regla no es tan menor como parece.
| Campo | Tipo | Descripción | |
|---|---|---|---|
| clienteId | integer · int64 | req | |
| fechaSolicitud | string · null | opc | |
| fechaEntrega | string · null | opc | |
| fechaPrimerPago | string · date | req | |
| capital | number | req | |
| tasaInteres | number | req | |
| tasaInteresMoratorio | number | opc | |
| numeroCuotas | integer | req | |
| periodicidad | Periodicidad | req | |
| tipoCalculo | TipoCalculo | req | |
| observaciones | string | opc |
PrestamoResumenobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| id | integer · int64 | req | |
| clienteId | integer · int64 | req | |
| clienteNombre | string · null | opc | |
| rutaId | integer · null | opc | |
| rutaNombre | string · null | opc | |
| fechaPrestamo | string · null | opc | |
| capital | number | req | |
| tasaInteres | number | opc | |
| tasaInteresMoratorio | number | opc | |
| numeroCuotas | integer | req | |
| periodicidad | Periodicidad | req | |
| tipoCalculo | TipoCalculo | req | |
| status | PrestamoStatus | req | |
| saldoTotal | number | req | |
| saldoCapital | number | opc | |
| saldoIntereses | number | opc | |
| saldoMoratorio | number | opc | |
| saldoFavor | number | opc | |
| cuotasPagadas | integer | opc | Junto con |
| cuotasVencidas | integer | opc | Conteo, para el distintivo de riesgo |
| proximoVencimiento | string · null | opc | Fecha de la siguiente cuota no pagada |
| condicionVencido | boolean | opc | Derivado: «tiene al menos una cuota vencida». Es la lectura (b) del
acuerdo #13: un crédito con cuotas vencidas sigue siendo |
| totalAvales | integer | opc | Conteo, para mostrar el distintivo en la ficha sin traer la lista |
| tienePlanGenerado | boolean | opc |
|
| version | integer · null | opc | Control de concurrencia optimista ( |
Prestamoobjeto compuesto
Compone y extiende: PrestamoResumen
| Campo | Tipo | Descripción | |
|---|---|---|---|
| fechaSolicitud | string · null | opc | |
| fechaAutorizacion | string · null | opc | |
| fechaEntrega | string · null | opc | |
| fechaPrimerPago | string · null | opc | |
| interesTotal | number | opc | |
| totalPagar | number | opc | |
| prestamoOrigenId | integer · null | opc | Campo creado y nullable. Sin flujo de refinanciamiento (Anexo C §C.2) |
| observaciones | string · null | opc | |
| fechaRegistro | string · date-time | opc | |
| fechaActualizacion | string · null | opc |
Cuotaobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| id | integer · int64 | req | |
| prestamoId | integer · int64 | req | |
| numeroCuota | integer | req | |
| fechaVencimiento | string · date | req | |
| capital | number | opc | |
| interes | number | opc | |
| totalCuota | number | req | |
| capitalPagado | number | opc | |
| interesPagado | number | opc | |
| moratorioPagado | number | opc | |
| saldoCapitalDespues | number | opc | |
| fechaLiquidacion | string · null | opc | |
| estado | EstadoCuota | req | Derivado por el backend. El frontend solo pinta la marca |
| saldoPendiente | number | req |
|
| diasAtraso | integer | opc | Fórmula del Anexo A §A.4: si la cuota está pagada,
|
| moraAlDia | number | opc | Mora acumulada a la fecha de consulta, sin persistir.
|
| totalACobrarHoy | number | opc |
|
| version | integer · null | opc | Control de concurrencia optimista ( |
RespuestaAmortizacionobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| prestamo | PrestamoResumen | req | |
| cuotas | array<Cuota> | req | |
| totales | object | opc |
RespuestaHistorialobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| prestamo | PrestamoResumen | req | |
| cuotas | array<Cuota> | req | |
| pagos | array<PagoDetalle> | req | Incluye los pagos cancelados, marcados como tales. Requisito de auditoría |
CuotaCobranzaobjeto compuesto
Compone y extiende: Cuota
| Campo | Tipo | Descripción | |
|---|---|---|---|
| clienteId | integer · int64 | req | |
| clienteNombre | string | req | |
| rutaId | integer · null | opc | |
| rutaNombre | string · null | opc | |
| telefonoCliente | string · null | opc |
PaginaCuotasCobranzaobjeto compuesto
Compone y extiende: Pagina
| Campo | Tipo | Descripción | |
|---|---|---|---|
| contenido | array<CuotaCobranza> | req | |
| totales | object | req | Totales de la consulta completa, no de la página. Es lo que se muestra al pie de la tabla («23 cuotas vencidas · 41,320.00») y lo que el frontend nunca calcula sumando columnas. |
PeticionPrevisualizacionPagoobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| fechaPago | string · date | req | |
| monto | number | req | |
| condonarMora | boolean | opc | REGLA ABIERTA #5. Si la mora no es condonable, el campo se retira |
PeticionPagoobjeto compuesto
Compone y extiende: PeticionPrevisualizacionPago
| Campo | Tipo | Descripción | |
|---|---|---|---|
| tipoPago | TipoPago | req | |
| referencia | string | opc | |
| observaciones | string | opc |
AplicacionCuotaobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| cuotaId | integer · int64 | req | |
| numeroCuota | integer | req | |
| capitalAplicado | number | req | |
| interesAplicado | number | req | |
| moratorioAplicado | number | req | |
| saldoCuotaDespues | number | req | |
| estadoResultante | EstadoCuota | req |
TotalesDistribucionobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| capital | number | req | |
| interes | number | req | |
| mora | number | req | |
| saldoFavorGenerado | number | req |
RespuestaDistribucionPagoobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| distribucion | array<AplicacionCuota> | req | |
| totales | TotalesDistribucion | req | |
| saldoPrestamoDespues | number | req |
RespuestaPagoRegistradoobjeto compuesto
Compone y extiende: RespuestaDistribucionPago
| Campo | Tipo | Descripción | |
|---|---|---|---|
| pagoId | integer · int64 | req | |
| movimientoCajaId | integer · int64 | req | Un pago genera un solo movimiento de caja, por el monto total
recibido, con |
| prestamoLiquidado | boolean | opc | Si |
Pagoobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| id | integer · int64 | req | |
| prestamoId | integer · int64 | req | |
| clienteId | integer · int64 | req | |
| clienteNombre | string · null | opc | |
| fechaPago | string · date-time | req | |
| monto | number | req | |
| tipoPago | TipoPago | req | |
| referencia | string · null | opc | |
| observaciones | string · null | opc | |
| usuarioId | integer · int64 | opc | |
| usuarioNombre | string · null | opc | |
| status | PagoStatus | req | |
| fechaRegistro | string · date-time | opc | |
| fechaCancelacion | string · null | opc | |
| usuarioCancelacion | string · null | opc | |
| motivoCancelacion | string · null | opc | |
| version | integer · null | opc | Control de concurrencia optimista ( |
PagoAplicacionobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| id | integer · int64 | req | |
| pagoId | integer · int64 | req | |
| cuotaId | integer · int64 | req | |
| numeroCuota | integer | opc | |
| capitalAplicado | number | req | |
| interesAplicado | number | req | |
| moratorioAplicado | number | req | |
| saldoFavorGenerado | number | opc | |
| fechaRegistro | string · date-time | opc |
PagoDetalleobjeto compuesto
Compone y extiende: Pago
| Campo | Tipo | Descripción | |
|---|---|---|---|
| aplicaciones | array<PagoAplicacion> | opc | Es lo que permite que el recibo impreso diga exactamente cuánto fue a capital, interés y mora. Sin esto, el recibo sería un total opaco. |
| movimientoCajaId | integer · null | opc |
PeticionAbonoCapitalobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| fecha | string · date | req | |
| monto | number | req | |
| aplicacion | AplicacionAbonoCapital | req | |
| observaciones | string | opc |
RespuestaAbonoCapitalobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| pagoId | integer · null | opc | |
| movimientoCajaId | integer · int64 | req | |
| saldoPrestamoDespues | number | req | |
| planResultante | array<Cuota> | opc | Plan de pagos tras el abono, para que la pantalla 12 muestre el antes y el después |
Movimientoobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| id | integer · int64 | req | |
| clienteId | integer · int64 | req | |
| prestamoId | integer · null | opc | |
| tipoMovimiento | TipoMovimiento | req | |
| cargo | number | req | |
| abono | number | req | |
| saldo | number | req | |
| descripcion | string | opc | |
| referencia | string · null | opc | |
| fecha | string · date-time | req | |
| usuarioId | integer · null | opc |
MovimientoCajaobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| id | integer · int64 | req | |
| fecha | string · date-time | req | |
| tipoMovimiento | TipoMovimientoCaja | req | |
| naturaleza | NaturalezaMovimientoCaja | req | |
| concepto | string | req | |
| importe | number | req | |
| utilidad | number | opc | En un pago, |
| prestamoId | integer · null | opc | |
| pagoId | integer · null | opc | |
| clienteNombre | string · null | opc | Trazabilidad que el Access no tenía: allí el nombre viajaba dentro del texto del concepto |
| generadoPorSistema | boolean | req | Determina si la fila es editable desde la pantalla 15 |
| usuarioId | integer · null | opc | |
| referencia | string · null | opc | |
| fechaRegistro | string · date-time | opc | |
| marcadoParaRevision | boolean | opc | Movimiento migrado con importe de escala anómala (9 casos de hasta 9 dígitos, muy por encima del volumen de cartera). No afectan los saldos de los créditos, pero se marcan para revisión del cliente (Anexo A §A.9). ABIERTO — acuerdo #30. |
| version | integer · null | opc | Control de concurrencia optimista ( |
PeticionMovimientoCajaobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| fecha | string · date | req | |
| tipoMovimiento | TipoMovimientoCaja | req | Solo se admiten los tipos marcados como manuales en el catálogo |
| concepto | string | req | |
| importe | number | req | |
| referencia | string | opc |
ResumenCajaobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| totalEfectivo | number | req | |
| ingresos | number | req | |
| egresos | number | req | |
| utilidad | number | req |
RespuestaReporteobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| clave | string | req | |
| titulo | string | req | |
| filtrosAplicados | object | opc | Los filtros en claro, para imprimirlos como texto en la cabecera del formato |
| columnas | array<object> | req | |
| filas | array<object> | req | |
| totales | object | opc | Calculados por el backend. El frontend no suma columnas |
| pagina | integer · null | opc | |
| tamano | integer · null | opc | |
| totalElementos | integer · null | opc | |
| totalPaginas | integer · null | opc |
RespuestaImpresionobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| formato | string | req | |
| titulo | string | req | |
| subtitulo | string · null | opc | Ruta y fecha, cliente, o crédito |
| empresa | Empresa | req | |
| filtrosAplicados | object | opc | |
| columnas | array<object> | opc | |
| filas | array<object> | req | |
| totales | object | opc | |
| generadoEn | string · date-time | opc | Va al pie de todo formato junto con el usuario que imprimió. No es decorativo: un listado de ruta que circula en papel sin decir cuándo se generó induce a cobrar sobre datos viejos. |
| generadoPor | string | opc |
RespuestaTableroobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| periodo | object | opc | |
| totalEfectivo | number | req | |
| carteraActiva | number | req | |
| cobranzaPeriodo | number | req | |
| cuotasVencidas | object | req | |
| proyeccionMes | number | req | |
| utilidadMes | number | req | No es ingresos menos gastos. Es la ganancia financiera cobrada: intereses y mora efectivamente recibidos. No descuenta gastos operativos, porque en los 9 meses de la base entregada no existe ningún gasto operativo capturado (Anexo A §A.9, Anexo B §B.3). |
Configuracionobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| diasAnticipacionAviso | integer | req | Días de anticipación del aviso de vencimiento. Valor actual del negocio: 3. Atención a la siembra: |
| fechaActualizacion | string · null | opc |
Empresaobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| id | integer · null | opc | |
| nombre | string | req | |
| lema | string · null | opc | ABIERTO — acuerdo #29. Sin este campo, el encabezado impreso queda incompleto respecto de lo
aprobado. Alternativa si no se añade: reutilizar uno de los tres |
| rfc | string · null | opc | |
| logoUrl | string · null | opc | Ruta del endpoint que sirve el logotipo. Encabeza todos los formatos impresos |
| activo | boolean | opc |
PeticionEmpresaobject
| Campo | Tipo | Descripción | |
|---|---|---|---|
| nombre | string | req | |
| lema | string | opc | |
| rfc | string | opc |
PrestamoConPlanobject
14Reglas abiertas — pendientes del cliente
Consolidación de los puntos [POR CONFIRMAR] de los anexos y del modelo v1.1, ordenadas por lo que impiden
construir. Ninguna bloquea el arranque: donde la regla está abierta, el contrato la expone como enum o campo opcional.
Criterio: se maqueta lo reversible — convertir un aviso en bloqueo es una línea; quitar un bloqueo mal
puesto genera una discusión con el cliente.
| # | Regla abierta | Bloquea | Impacto en el frontend |
|---|---|---|---|
| 1 | Abono a capital: reduce cuotas / reduce monto / solo caja | POST /api/prestamos/{id}/abonos-capital | Bloqueante La pantalla 12 está maquetada con las 3 variantes; aplicacion es enum mientras tanto |
| 2 | Orden de aplicación del pago (se propone mora → interés → capital → saldo a favor) | POST /api/prestamos/{id}/pagos y su previsualización | El desglose sale del backend; si cambia el orden, cambian los números, no la pantalla |
| 3 | Esquema SOLO_INTERÉS: cómo y cuándo se recupera el capital | POST /api/prestamos/simulacion | Afecta 6 créditos históricos; la vista previa no puede mostrar el plan hasta cerrarlo |
| 4 | Abono parcial completado: ¿la cuota se cierra sola o la cierra el operador? | Distribución del pago | Cambia si la pantalla 11 necesita una acción explícita de cierre |
| 5 | Mora: automática o condonable por el operador | POST …/pagos (campo condonarMora) | El campo está en el contrato; si no es condonable, se retira |
| 6 | Saldo a favor: se aplica solo al siguiente vencimiento o se conserva | Distribución del pago | Cambia si hay que mostrar y accionar el saldo a favor en el historial |
| 7 | Reverso de pagos: quién puede y con qué límite de tiempo | POST /api/pagos/{id}/cancelacion | Define si la acción se oculta por permiso, por antigüedad, o ambas |
| 8 | Mensual: ¿+30 días o mes calendario? | Generación de cuotas | Cosmético para el frontend, crítico para la conciliación con el Access |
| 9 | Gastos operativos: dónde se registran hoy | GET /api/dashboard | Define qué significa «utilidad» en 2 de los 6 indicadores |
| 10 | Rutas GRUPOS / QUINCENAL / LIQUIDACIÓN: ¿rutas o clasificaciones? | Catálogo de rutas · y la ruta en Cliente | Si no son rutas, el filtro de la pantalla 3 necesita una dimensión más — y la regla sube de importancia |
| 11 | Formato del ticket: rollo térmico o carta | GET /api/impresion/{formato}/pdf | Define el valor por omisión de presentacion y 4 de los 7 formatos |
| 12 | Tamaño máximo y formatos del expediente (propuesto: PDF, JPG, PNG hasta 10 MB) | POST /api/clientes/{id}/documentos | Define la validación previa y el mensaje al usuario; cerrar antes de la pantalla 5 |
| 13 | ¿El expediente incompleto impide el alta de crédito o solo advierte? | POST /api/prestamos y la pantalla 6 | Alto Si bloquea, hay que definir qué documentos son obligatorios |
15Acuerdos frontend–backend (Apéndice 2)
Los 26 acuerdos a cerrar entre los dos equipos, ordenados por lo que bloquean. Los cerrados con los modelos JPA del backend quedan confirmados.
Frontera general
| # | Punto | Propuesta |
|---|---|---|
| 1 | Nombres de campo en JSON | camelCase — cerrado |
| 2 | Nombre del recurso de préstamos | /api/prestamos — cerrado |
| 3 | Endpoint de previsualización de pago | Que exista; sostiene la pantalla 10 |
| 4 | Endpoint de simulación de amortización | Que exista; sostiene la pantalla 6 |
| 5 | Campos derivados en las respuestas | La lista de §05 · campos derivados |
| 6 | Permisos efectivos resueltos en el login | Lista plana de códigos |
| 7 | Catálogo de códigos de error | La tabla de §11, o la del backend si prefiere otra |
| 8 | Mecanismo de idempotencia | Encabezado Clave-Idempotencia + entidad IdempotenciaOperacion — cerrado |
| 9 | Refresh token | Sin refresh: token largo y relogin manual |
| 10 | Claves de reportes y formatos | La lista del contrato (9 claves, 7 formatos) |
| 11 | Aviso de endpoints estables por módulo | Un mensaje por grupo, aunque los datos estén incompletos |
| 12 | Casos de prueba compartidos de amortización | amortizacion-casos.json en el repositorio |
Modelo y estados
| # | Punto | Propuesta |
|---|---|---|
| 13 | Naturaleza del estado VENCIDO | Condición derivada, no estado asignado — cerrado |
| 14 | ¿Varios préstamos activos por cliente? | Sí, por el Anexo A §A.8 |
| 15 | Precedencia VENCIDA sobre PARCIAL | Un solo campo estado, con VENCIDA ganando |
| 16 | EGRESO_PRESTAMO_OTORGADO no existía en el enum | Agregado — cerrado. Son 2,107 movimientos, el 14 % de la caja |
| 17 | Valores de TipoIdentificacion | INE, PASAPORTE, LICENCIA, CEDULA_PROFESIONAL, OTRO — cerrado |
| 18 | Valores de ClienteStatus | ACTIVO, INACTIVO — cerrado |
Expediente digital y avales — nuevos con el Anexo C
| # | Punto | Propuesta |
|---|---|---|
| 19 | Entrega de archivos al navegador | Endpoint autenticado + blob:. Si los PDFs pesan, URL firmada |
| 20 | urlContenido armada por el backend | Sin exponer rutaArchivo |
| 21 | Un archivo por petición | multipart/form-data; cinco archivos son cinco llamadas con progreso independiente |
| 22 | Límite del contenedor = regla abierta #12 | Configurar Spring al mismo valor acordado con el cliente |
| 23 | Error de tamaño con el sobre { codigo, mensaje } | Capturar el rechazo del contenedor → 413 ARCHIVO_MUY_GRANDE |
| 24 | ¿El reemplazo conserva el documento anterior? | Sí, inactivo — cerrado (reemplazaAId) |
| 25 | ¿Avales dentro del POST /api/prestamos o después? | Después, en dos pasos: Aval.prestamoId exige el préstamo existente |
| 26 | ¿Avales editables con el préstamo activo? | Sí; los datos de contacto cambian y hay que poder corregirlos |
Cerrados y abiertos con los modelos JPA del backend
| # | Punto | Estado |
|---|---|---|
| 27 | Control de concurrencia optimista: @Version en Prestamo, Cuota, Pago, MovimientoCaja y ClienteDocumento | cerrado — version expuesto en las 5 entidades |
| 28 | Cliente.telefono, direccion y notas — los Anexos los exigen (pantalla 4) y el JPA no los tiene; Aval sí tiene teléfono y dirección | abierto — el contrato ya los expone |
| 29 | Empresa.lema — encabeza los 7 formatos del Anexo B §B.2 y el JPA no lo tiene | abierto — el contrato ya lo expone |
| 30 | marcadoParaRevision en Cliente y MovimientoCaja — para los 14 clientes sin nombre, los 60 movimientos huérfanos y los 9 importes anómalos del Anexo A §A.9 | abierto — el contrato ya lo expone |
GESTIONAR_EXPEDIENTE y GESTIONAR_AVALES. Lo decide el cliente.
16Anexos y documentos fuente
Cada referencia de esta página — «Anexo A §A.4», «modelo v1.1 §2.14», «REGLA ABIERTA #n» — es un enlace al punto exacto del documento correspondiente. Orden de prioridad cuando dos documentos discrepan: Modelo v1.1 > system-design > Anexos A/B/C > design system > arquitectura.
- A.1 Módulos y pantallas · A.2 Perfiles y permisos
- A.3 Esquemas de amortización · A.4 Reglas de pago y mora
- A.8 Tablas a migrar · A.9 Volumen y calidad de los datos
- B.1 Reportes analíticos (pantalla, PDF, Excel)
- B.2 Formatos de impresión de cobranza
- B.3 Los 6 indicadores del tablero, definidos con exactitud
- C.1 Expediente digital (C.1.1) y avales (C.1.2) — aprobados
- C.2 Entidades preparadas sin funcionalidad contratada
- C.3 Fuera de alcance explícito, incorporable sin rehacer
- §2 Las entidades, una por una · §3 Enums recomendados
- §5 Flujo de un pago (8 pasos)
- §11 Reglas que todavía deben cerrarse con el cliente
Archivos fuente en el repositorio
docs/contratos/financiera-api-v1.1.0.yaml— la frontera HTTP: 54 rutas, 68 operaciones, 70 esquemasdocs/system-design-financiero.md— el documento de coordinación v1.2docs/ANEXO_A_INVENTARIO_ALCANCE_2.md·docs/ANEXO_B_REPORTES_2.md·docs/ANEXO_C_ENTIDADES_Y_EXCLUSIONES.mddocs/Modelo_Datos_Financiero_Reconciliado.md