API · 01La frontera en una mirada
Convenciones vinculantes de financiera-api-v1.1.0.yaml y de system-design §4.3 y §5.1.
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.
API · 02Convenciones
| 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 pestaña Errores |
| Archivos | multipart/form-data, un archivo por petición; tamaños en bytes |
API · 03Seguridad
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.
API · 04Idempotencia 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.
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
Ejemplos
{
"usuario": "reyna.janeth",
"password": "********"
}{
"token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIzIn0.kK7mQ2vL",
"expiraEn": "2026-09-02T20:00:00-06:00",
"requiereCambioPassword": false,
"usuario": {
"id": 3,
"username": "reyna.janeth",
"nombre": "Reyna Janeth"
},
"roles": [
"ROLE_CAJA"
],
"permisos": [
"EDITAR_HISTORIAL",
"LISTAR_CLIENTES",
"BUSCAR_HISTORIAL"
],
"empresa": {
"id": 1,
"nombre": "Creciendo Juntos Financiera",
"lema": "Creciendo juntos, mano a mano",
"rfc": "CJF210416XYA",
"logoUrl": "/api/empresa/logo",
"activo": true
}
}Request
{
"usuario": "reyna.janeth",
"password": "********"
}
Response
{
"token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIzIn0.kK7mQ2vL",
"expiraEn": "2026-09-02T20:00:00-06:00",
"requiereCambioPassword": false,
"usuario": {
"id": 3,
"username": "reyna.janeth",
"nombre": "Reyna Janeth"
},
"roles": [
"ROLE_CAJA"
],
"permisos": [
"EDITAR_HISTORIAL",
"LISTAR_CLIENTES",
"BUSCAR_HISTORIAL"
],
"empresa": {
"id": 1,
"nombre": "Creciendo Juntos Financiera",
"lema": "Creciendo juntos, mano a mano",
"rfc": "CJF210416XYA",
"logoUrl": "/api/empresa/logo",
"activo": true
}
}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).
Ejemplos
{
"token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIzIn0.kK7mQ2vL",
"expiraEn": "2026-09-02T14:30:00-06:00",
"requiereCambioPassword": true,
"usuario": {
"id": 305,
"username": "reyna.janeth",
"nombre": "María"
},
"roles": [
"ROLE_CAJA"
],
"permisos": [
"EDITAR_HISTORIAL",
"LISTAR_CLIENTES"
],
"empresa": {
"id": 305,
"nombre": "María",
"lema": "Creciendo juntos, mano a mano",
"rfc": "CJF210416XYA",
"logoUrl": "Texto de logo url",
"activo": true
}
}Request
Response
{
"token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIzIn0.kK7mQ2vL",
"expiraEn": "2026-09-02T14:30:00-06:00",
"requiereCambioPassword": true,
"usuario": {
"id": 305,
"username": "reyna.janeth",
"nombre": "María"
},
"roles": [
"ROLE_CAJA"
],
"permisos": [
"EDITAR_HISTORIAL",
"LISTAR_CLIENTES"
],
"empresa": {
"id": 305,
"nombre": "María",
"lema": "Creciendo juntos, mano a mano",
"rfc": "CJF210416XYA",
"logoUrl": "Texto de logo url",
"activo": true
}
}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
Ejemplos
{
"actual": "********",
"nueva": "Clave#2026"
}Request
{
"actual": "********",
"nueva": "Clave#2026"
}
Response
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: |
Ejemplos
{
"pagina": 0,
"tamano": 50,
"totalElementos": 23,
"totalPaginas": 1,
"contenido": [
{
"id": 305,
"username": "reyna.janeth",
"nombre": "María",
"activo": true,
"requiereCambioPassword": true,
"roles": [
null
],
"permisosEfectivos": [
"Texto de permisos efectivos"
],
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}
]
}Request
Response
{
"pagina": 0,
"tamano": 50,
"totalElementos": 23,
"totalPaginas": 1,
"contenido": [
{
"id": 305,
"username": "reyna.janeth",
"nombre": "María",
"activo": true,
"requiereCambioPassword": true,
"roles": [
null
],
"permisosEfectivos": [
"Texto de permisos efectivos"
],
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}
]
}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
Ejemplos
{
"username": "reyna.janeth",
"nombre": "María",
"password": "********",
"activo": true,
"roleIds": [
2
]
}{
"id": 305,
"username": "reyna.janeth",
"nombre": "María",
"activo": true,
"requiereCambioPassword": true,
"roles": [
{
"id": 305,
"authority": "ROLE_CAJA",
"descripcion": "Cobranza de lunes por la colonia Centro"
}
],
"permisosEfectivos": [
"Texto de permisos efectivos"
],
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}Request
{
"username": "reyna.janeth",
"nombre": "María",
"password": "********",
"activo": true,
"roleIds": [
2
]
}
Response
{
"id": 305,
"username": "reyna.janeth",
"nombre": "María",
"activo": true,
"requiereCambioPassword": true,
"roles": [
{
"id": 305,
"authority": "ROLE_CAJA",
"descripcion": "Cobranza de lunes por la colonia Centro"
}
],
"permisosEfectivos": [
"Texto de permisos efectivos"
],
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}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
Ejemplos
{
"username": "reyna.janeth",
"nombre": "María",
"password": "********",
"activo": true,
"roleIds": [
2
]
}{
"id": 305,
"username": "reyna.janeth",
"nombre": "María",
"activo": true,
"requiereCambioPassword": true,
"roles": [
{
"id": 305,
"authority": "ROLE_CAJA",
"descripcion": "Cobranza de lunes por la colonia Centro"
}
],
"permisosEfectivos": [
"Texto de permisos efectivos"
],
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}Request
{
"username": "reyna.janeth",
"nombre": "María",
"password": "********",
"activo": true,
"roleIds": [
2
]
}
Response
{
"id": 305,
"username": "reyna.janeth",
"nombre": "María",
"activo": true,
"requiereCambioPassword": true,
"roles": [
{
"id": 305,
"authority": "ROLE_CAJA",
"descripcion": "Cobranza de lunes por la colonia Centro"
}
],
"permisosEfectivos": [
"Texto de permisos efectivos"
],
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}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
Ejemplos
{
"activo": true
}{
"id": 305,
"username": "reyna.janeth",
"nombre": "María",
"activo": true,
"requiereCambioPassword": true,
"roles": [
{
"id": 305,
"authority": "ROLE_CAJA",
"descripcion": "Cobranza de lunes por la colonia Centro"
}
],
"permisosEfectivos": [
"Texto de permisos efectivos"
],
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}Request
{
"activo": true
}
Response
{
"id": 305,
"username": "reyna.janeth",
"nombre": "María",
"activo": true,
"requiereCambioPassword": true,
"roles": [
{
"id": 305,
"authority": "ROLE_CAJA",
"descripcion": "Cobranza de lunes por la colonia Centro"
}
],
"permisosEfectivos": [
"Texto de permisos efectivos"
],
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}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
Ejemplos
{
"roleIds": [
2
]
}{
"id": 305,
"username": "reyna.janeth",
"nombre": "María",
"activo": true,
"requiereCambioPassword": true,
"roles": [
{
"id": 305,
"authority": "ROLE_CAJA",
"descripcion": "Cobranza de lunes por la colonia Centro"
}
],
"permisosEfectivos": [
"Texto de permisos efectivos"
],
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}Request
{
"roleIds": [
2
]
}
Response
{
"id": 305,
"username": "reyna.janeth",
"nombre": "María",
"activo": true,
"requiereCambioPassword": true,
"roles": [
{
"id": 305,
"authority": "ROLE_CAJA",
"descripcion": "Cobranza de lunes por la colonia Centro"
}
],
"permisosEfectivos": [
"Texto de permisos efectivos"
],
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}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
Ejemplos
{
"excepciones": [
{
"permisoId": 240,
"concedido": true
}
]
}{
"id": 305,
"username": "reyna.janeth",
"nombre": "María",
"activo": true,
"requiereCambioPassword": true,
"roles": [
{
"id": 305,
"authority": "ROLE_CAJA",
"descripcion": "Cobranza de lunes por la colonia Centro"
}
],
"permisosEfectivos": [
"Texto de permisos efectivos"
],
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}Request
{
"excepciones": [
{
"permisoId": 240,
"concedido": true
}
]
}
Response
{
"id": 305,
"username": "reyna.janeth",
"nombre": "María",
"activo": true,
"requiereCambioPassword": true,
"roles": [
{
"id": 305,
"authority": "ROLE_CAJA",
"descripcion": "Cobranza de lunes por la colonia Centro"
}
],
"permisosEfectivos": [
"Texto de permisos efectivos"
],
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}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
Ejemplos
[
{
"id": 305,
"authority": "ROLE_CAJA",
"descripcion": "Cobranza de lunes por la colonia Centro"
}
]Request
Response
[
{
"id": 305,
"authority": "ROLE_CAJA",
"descripcion": "Cobranza de lunes por la colonia Centro"
}
]Respuestas
Roles
▸GET/api/permisosCatálogo de los 5 permisos del Anexo A §A.2
Ejemplos
[
{
"id": 305,
"codigo": "LISTAR_CLIENTES",
"descripcion": "Cobranza de lunes por la colonia Centro"
}
]Request
Response
[
{
"id": 305,
"codigo": "LISTAR_CLIENTES",
"descripcion": "Cobranza de lunes por la colonia Centro"
}
]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 |
Ejemplos
[
{
"id": 305,
"codigo": "Ruta 1 LUNES",
"descripcion": "Cobranza de lunes por la colonia Centro",
"activo": true,
"totalCreditos": 2
}
]Request
Response
[
{
"id": 305,
"codigo": "Ruta 1 LUNES",
"descripcion": "Cobranza de lunes por la colonia Centro",
"activo": true,
"totalCreditos": 2
}
]Respuestas
Rutas
▸POST/api/rutasCrea una ruta
Cuerpo de la petición req
application/json → PeticionRuta
Ejemplos
{
"codigo": "Ruta 1 LUNES",
"descripcion": "Cobranza de lunes por la colonia Centro"
}{
"id": 305,
"codigo": "Ruta 1 LUNES",
"descripcion": "Cobranza de lunes por la colonia Centro",
"activo": true,
"totalCreditos": 2
}Request
{
"codigo": "Ruta 1 LUNES",
"descripcion": "Cobranza de lunes por la colonia Centro"
}
Response
{
"id": 305,
"codigo": "Ruta 1 LUNES",
"descripcion": "Cobranza de lunes por la colonia Centro",
"activo": true,
"totalCreditos": 2
}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
Ejemplos
{
"codigo": "Ruta 1 LUNES",
"descripcion": "Cobranza de lunes por la colonia Centro"
}{
"id": 305,
"codigo": "Ruta 1 LUNES",
"descripcion": "Cobranza de lunes por la colonia Centro",
"activo": true,
"totalCreditos": 2
}Request
{
"codigo": "Ruta 1 LUNES",
"descripcion": "Cobranza de lunes por la colonia Centro"
}
Response
{
"id": 305,
"codigo": "Ruta 1 LUNES",
"descripcion": "Cobranza de lunes por la colonia Centro",
"activo": true,
"totalCreditos": 2
}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
Ejemplos
{
"activo": true
}{
"id": 305,
"codigo": "Ruta 1 LUNES",
"descripcion": "Cobranza de lunes por la colonia Centro",
"activo": true,
"totalCreditos": 2
}Request
{
"activo": true
}
Response
{
"id": 305,
"codigo": "Ruta 1 LUNES",
"descripcion": "Cobranza de lunes por la colonia Centro",
"activo": true,
"totalCreditos": 2
}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 |
Ejemplos
[
{
"valor": "Texto de valor",
"etiqueta": "Texto de etiqueta",
"activo": true
}
]Request
Response
[
{
"valor": "Texto de valor",
"etiqueta": "Texto de etiqueta",
"activo": true
}
]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.
Ejemplos
[
{
"valor": "Texto de valor",
"etiqueta": "Texto de etiqueta",
"activo": true
}
]Request
Response
[
{
"valor": "Texto de valor",
"etiqueta": "Texto de etiqueta",
"activo": true
}
]Respuestas
Tipos de pago
▸GET/api/catalogos/tipos-movimiento-cajaTipos de movimiento de caja, con su naturaleza y si son manuales
Ejemplos
[
{
"valor": "Texto de valor",
"etiqueta": "Texto de etiqueta",
"activo": true,
"naturaleza": "INGRESO",
"manual": false
}
]Request
Response
[
{
"valor": "Texto de valor",
"etiqueta": "Texto de etiqueta",
"activo": true,
"naturaleza": "INGRESO",
"manual": false
}
]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.
Ejemplos
[
{
"valor": "Texto de valor",
"etiqueta": "Texto de etiqueta",
"activo": true,
"obligatorio": true
}
]Request
Response
[
{
"valor": "Texto de valor",
"etiqueta": "Texto de etiqueta",
"activo": true,
"obligatorio": true
}
]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.
Ejemplos
[
{
"valor": "Texto de valor",
"etiqueta": "Texto de etiqueta",
"activo": true
}
]Request
Response
[
{
"valor": "Texto de valor",
"etiqueta": "Texto de etiqueta",
"activo": true
}
]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: |
Ejemplos
{
"pagina": 0,
"tamano": 50,
"totalElementos": 23,
"totalPaginas": 1,
"contenido": [
{
"id": 305,
"nombreCompleto": "María López Rivera",
"curp": "LORM800214MDFRNS09",
"numeroIdentificacion": "4123 4567 8901",
"rutaId": 717,
"rutaNombre": "Ruta 1 LUNES",
"status": "ACTIVO",
"totalPrestamos": 3,
"prestamosActivos": 1,
"saldoTotal": 6500,
"totalDocumentos": 4,
"estadoExpediente": null,
"marcadoParaRevision": false
}
]
}Request
Response
{
"pagina": 0,
"tamano": 50,
"totalElementos": 23,
"totalPaginas": 1,
"contenido": [
{
"id": 305,
"nombreCompleto": "María López Rivera",
"curp": "LORM800214MDFRNS09",
"numeroIdentificacion": "4123 4567 8901",
"rutaId": 717,
"rutaNombre": "Ruta 1 LUNES",
"status": "ACTIVO",
"totalPrestamos": 3,
"prestamosActivos": 1,
"saldoTotal": 6500,
"totalDocumentos": 4,
"estadoExpediente": null,
"marcadoParaRevision": false
}
]
}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
Ejemplos
{
"nombreCompleto": "María López Rivera",
"nombre": "María",
"primerApellido": "López",
"segundoApellido": "Rivera",
"curp": "LORM800214MDFRNS09",
"fechaNacimiento": "1980-02-14",
"tipoIdentificacion": "INE",
"numeroIdentificacion": "4123 4567 8901",
"telefono": "55 1234 5678",
"direccion": "Av. Reforma 42, Col. Centro",
"notas": "Cliente de la ruta 1 desde 2024",
"rutaId": 1,
"status": "ACTIVO"
}{
"id": 305,
"nombreCompleto": "María López Rivera",
"curp": "LORM800214MDFRNS09",
"numeroIdentificacion": "4123 4567 8901",
"rutaId": 717,
"rutaNombre": "Ruta 1 LUNES",
"status": "ACTIVO",
"totalPrestamos": 3,
"prestamosActivos": 1,
"saldoTotal": 6500,
"totalDocumentos": 4,
"estadoExpediente": "COMPLETO",
"marcadoParaRevision": false,
"nombre": "María",
"primerApellido": "López",
"segundoApellido": "Rivera",
"fechaNacimiento": "1980-02-14",
"tipoIdentificacion": "INE",
"telefono": "55 1234 5678",
"direccion": "Av. Reforma 42, Col. Centro",
"notas": "Texto de notas",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}Request
{
"nombreCompleto": "María López Rivera",
"nombre": "María",
"primerApellido": "López",
"segundoApellido": "Rivera",
"curp": "LORM800214MDFRNS09",
"fechaNacimiento": "1980-02-14",
"tipoIdentificacion": "INE",
"numeroIdentificacion": "4123 4567 8901",
"telefono": "55 1234 5678",
"direccion": "Av. Reforma 42, Col. Centro",
"notas": "Cliente de la ruta 1 desde 2024",
"rutaId": 1,
"status": "ACTIVO"
}
Response
{
"id": 305,
"nombreCompleto": "María López Rivera",
"curp": "LORM800214MDFRNS09",
"numeroIdentificacion": "4123 4567 8901",
"rutaId": 717,
"rutaNombre": "Ruta 1 LUNES",
"status": "ACTIVO",
"totalPrestamos": 3,
"prestamosActivos": 1,
"saldoTotal": 6500,
"totalDocumentos": 4,
"estadoExpediente": "COMPLETO",
"marcadoParaRevision": false,
"nombre": "María",
"primerApellido": "López",
"segundoApellido": "Rivera",
"fechaNacimiento": "1980-02-14",
"tipoIdentificacion": "INE",
"telefono": "55 1234 5678",
"direccion": "Av. Reforma 42, Col. Centro",
"notas": "Texto de notas",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}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 |
Ejemplos
{
"id": 305,
"nombreCompleto": "María López Rivera",
"curp": "LORM800214MDFRNS09",
"numeroIdentificacion": "4123 4567 8901",
"rutaId": 717,
"rutaNombre": "Ruta 1 LUNES",
"status": "ACTIVO",
"totalPrestamos": 3,
"prestamosActivos": 1,
"saldoTotal": 6500,
"totalDocumentos": 4,
"estadoExpediente": "COMPLETO",
"marcadoParaRevision": false,
"nombre": "María",
"primerApellido": "López",
"segundoApellido": "Rivera",
"fechaNacimiento": "1980-02-14",
"tipoIdentificacion": "INE",
"telefono": "55 1234 5678",
"direccion": "Av. Reforma 42, Col. Centro",
"notas": "Texto de notas",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}Request
Response
{
"id": 305,
"nombreCompleto": "María López Rivera",
"curp": "LORM800214MDFRNS09",
"numeroIdentificacion": "4123 4567 8901",
"rutaId": 717,
"rutaNombre": "Ruta 1 LUNES",
"status": "ACTIVO",
"totalPrestamos": 3,
"prestamosActivos": 1,
"saldoTotal": 6500,
"totalDocumentos": 4,
"estadoExpediente": "COMPLETO",
"marcadoParaRevision": false,
"nombre": "María",
"primerApellido": "López",
"segundoApellido": "Rivera",
"fechaNacimiento": "1980-02-14",
"tipoIdentificacion": "INE",
"telefono": "55 1234 5678",
"direccion": "Av. Reforma 42, Col. Centro",
"notas": "Texto de notas",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}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
Ejemplos
{
"nombreCompleto": "María López Rivera",
"nombre": "María",
"primerApellido": "López",
"segundoApellido": "Rivera",
"curp": "LORM800214MDFRNS09",
"fechaNacimiento": "1980-02-14",
"tipoIdentificacion": "INE",
"numeroIdentificacion": "4123 4567 8901",
"telefono": "55 1234 5678",
"direccion": "Av. Reforma 42, Col. Centro",
"notas": "Texto de notas",
"rutaId": 717,
"status": "ACTIVO"
}{
"id": 305,
"nombreCompleto": "María López Rivera",
"curp": "LORM800214MDFRNS09",
"numeroIdentificacion": "4123 4567 8901",
"rutaId": 717,
"rutaNombre": "Ruta 1 LUNES",
"status": "ACTIVO",
"totalPrestamos": 3,
"prestamosActivos": 1,
"saldoTotal": 6500,
"totalDocumentos": 4,
"estadoExpediente": "COMPLETO",
"marcadoParaRevision": false,
"nombre": "María",
"primerApellido": "López",
"segundoApellido": "Rivera",
"fechaNacimiento": "1980-02-14",
"tipoIdentificacion": "INE",
"telefono": "55 1234 5678",
"direccion": "Av. Reforma 42, Col. Centro",
"notas": "Texto de notas",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}Request
{
"nombreCompleto": "María López Rivera",
"nombre": "María",
"primerApellido": "López",
"segundoApellido": "Rivera",
"curp": "LORM800214MDFRNS09",
"fechaNacimiento": "1980-02-14",
"tipoIdentificacion": "INE",
"numeroIdentificacion": "4123 4567 8901",
"telefono": "55 1234 5678",
"direccion": "Av. Reforma 42, Col. Centro",
"notas": "Texto de notas",
"rutaId": 717,
"status": "ACTIVO"
}
Response
{
"id": 305,
"nombreCompleto": "María López Rivera",
"curp": "LORM800214MDFRNS09",
"numeroIdentificacion": "4123 4567 8901",
"rutaId": 717,
"rutaNombre": "Ruta 1 LUNES",
"status": "ACTIVO",
"totalPrestamos": 3,
"prestamosActivos": 1,
"saldoTotal": 6500,
"totalDocumentos": 4,
"estadoExpediente": "COMPLETO",
"marcadoParaRevision": false,
"nombre": "María",
"primerApellido": "López",
"segundoApellido": "Rivera",
"fechaNacimiento": "1980-02-14",
"tipoIdentificacion": "INE",
"telefono": "55 1234 5678",
"direccion": "Av. Reforma 42, Col. Centro",
"notas": "Texto de notas",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}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 |
Ejemplos
[
{
"id": 305,
"clienteId": 213,
"clienteNombre": "María López Rivera",
"rutaId": 717,
"rutaNombre": "Ruta 1 LUNES",
"fechaPrestamo": "2026-09-02",
"capital": 10000,
"tasaInteres": 0.0225,
"tasaInteresMoratorio": 0.01,
"numeroCuotas": 12,
"periodicidad": "SEMANAL",
"tipoCalculo": "CAPITAL_MAS_INTERES",
"status": "ACTIVO",
"saldoTotal": 6500,
"saldoCapital": 6041.67,
"saldoIntereses": 450,
"saldoMoratorio": 0,
"saldoFavor": 0,
"cuotasPagadas": 5,
"cuotasVencidas": 2,
"proximoVencimiento": "2026-09-02",
"condicionVencido": true,
"totalAvales": 1,
"tienePlanGenerado": true,
"version": 3
}
]Request
Response
[
{
"id": 305,
"clienteId": 213,
"clienteNombre": "María López Rivera",
"rutaId": 717,
"rutaNombre": "Ruta 1 LUNES",
"fechaPrestamo": "2026-09-02",
"capital": 10000,
"tasaInteres": 0.0225,
"tasaInteresMoratorio": 0.01,
"numeroCuotas": 12,
"periodicidad": "SEMANAL",
"tipoCalculo": "CAPITAL_MAS_INTERES",
"status": "ACTIVO",
"saldoTotal": 6500,
"saldoCapital": 6041.67,
"saldoIntereses": 450,
"saldoMoratorio": 0,
"saldoFavor": 0,
"cuotasPagadas": 5,
"cuotasVencidas": 2,
"proximoVencimiento": "2026-09-02",
"condicionVencido": true,
"totalAvales": 1,
"tienePlanGenerado": true,
"version": 3
}
]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 |
Ejemplos
{
"pagina": 0,
"tamano": 50,
"totalElementos": 23,
"totalPaginas": 1,
"contenido": [
{
"id": 305,
"clienteId": 213,
"prestamoId": 348,
"tipoMovimiento": "PAGO",
"cargo": 2,
"abono": 2,
"saldo": 2,
"descripcion": "Cobranza de lunes por la colonia Centro",
"referencia": "TRANSFER-8841",
"fecha": "2026-09-02T14:30:00-06:00",
"usuarioId": 249
}
]
}Request
Response
{
"pagina": 0,
"tamano": 50,
"totalElementos": 23,
"totalPaginas": 1,
"contenido": [
{
"id": 305,
"clienteId": 213,
"prestamoId": 348,
"tipoMovimiento": "PAGO",
"cargo": 2,
"abono": 2,
"saldo": 2,
"descripcion": "Cobranza de lunes por la colonia Centro",
"referencia": "TRANSFER-8841",
"fecha": "2026-09-02T14:30:00-06:00",
"usuarioId": 249
}
]
}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 |
Ejemplos
[
{
"id": 48,
"clienteId": 12,
"tipoDocumento": "IDENTIFICACION_FRENTE",
"nombreArchivo": "ine-frente.jpg",
"descripcion": "INE por el frente",
"contentType": "image/jpeg",
"tamanoBytes": 245760,
"fechaCarga": "2026-09-02T14:30:00-06:00",
"usuarioCarga": "reyna.janeth",
"activo": true,
"reemplazaAId": null,
"version": 1,
"urlContenido": "/api/documentos/48/contenido"
}
]Request
Response
[
{
"id": 48,
"clienteId": 12,
"tipoDocumento": "IDENTIFICACION_FRENTE",
"nombreArchivo": "ine-frente.jpg",
"descripcion": "INE por el frente",
"contentType": "image/jpeg",
"tamanoBytes": 245760,
"fechaCarga": "2026-09-02T14:30:00-06:00",
"usuarioCarga": "reyna.janeth",
"activo": true,
"reemplazaAId": null,
"version": 1,
"urlContenido": "/api/documentos/48/contenido"
}
]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
Ejemplos
Content-Type: multipart/form-data
archivo ine-frente.jpg (binario)
tipoDocumento IDENTIFICACION_FRENTE
descripcion Cobranza de lunes por la colonia Centro{
"id": 305,
"clienteId": 213,
"tipoDocumento": "IDENTIFICACION_FRENTE",
"nombreArchivo": "ine-frente.jpg",
"descripcion": "Cobranza de lunes por la colonia Centro",
"contentType": "image/jpeg",
"tamanoBytes": 245760,
"fechaCarga": "2026-09-02T14:30:00-06:00",
"usuarioCarga": "reyna.janeth",
"activo": true,
"reemplazaAId": 499,
"version": 3,
"urlContenido": "/api/documentos/48/contenido"
}Request
Content-Type: multipart/form-data
archivo ine-frente.jpg (binario)
tipoDocumento IDENTIFICACION_FRENTE
descripcion Cobranza de lunes por la colonia Centro
Response
{
"id": 305,
"clienteId": 213,
"tipoDocumento": "IDENTIFICACION_FRENTE",
"nombreArchivo": "ine-frente.jpg",
"descripcion": "Cobranza de lunes por la colonia Centro",
"contentType": "image/jpeg",
"tamanoBytes": 245760,
"fechaCarga": "2026-09-02T14:30:00-06:00",
"usuarioCarga": "reyna.janeth",
"activo": true,
"reemplazaAId": 499,
"version": 3,
"urlContenido": "/api/documentos/48/contenido"
}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 |
Ejemplos
{
"id": 305,
"clienteId": 213,
"tipoDocumento": "IDENTIFICACION_FRENTE",
"nombreArchivo": "ine-frente.jpg",
"descripcion": "Cobranza de lunes por la colonia Centro",
"contentType": "image/jpeg",
"tamanoBytes": 245760,
"fechaCarga": "2026-09-02T14:30:00-06:00",
"usuarioCarga": "reyna.janeth",
"activo": true,
"reemplazaAId": 499,
"version": 3,
"urlContenido": "/api/documentos/48/contenido"
}Request
Response
{
"id": 305,
"clienteId": 213,
"tipoDocumento": "IDENTIFICACION_FRENTE",
"nombreArchivo": "ine-frente.jpg",
"descripcion": "Cobranza de lunes por la colonia Centro",
"contentType": "image/jpeg",
"tamanoBytes": 245760,
"fechaCarga": "2026-09-02T14:30:00-06:00",
"usuarioCarga": "reyna.janeth",
"activo": true,
"reemplazaAId": 499,
"version": 3,
"urlContenido": "/api/documentos/48/contenido"
}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
Ejemplos
{
"tipoDocumento": "IDENTIFICACION_FRENTE",
"descripcion": "Cobranza de lunes por la colonia Centro"
}{
"id": 305,
"clienteId": 213,
"tipoDocumento": "IDENTIFICACION_FRENTE",
"nombreArchivo": "ine-frente.jpg",
"descripcion": "Cobranza de lunes por la colonia Centro",
"contentType": "image/jpeg",
"tamanoBytes": 245760,
"fechaCarga": "2026-09-02T14:30:00-06:00",
"usuarioCarga": "reyna.janeth",
"activo": true,
"reemplazaAId": 499,
"version": 3,
"urlContenido": "/api/documentos/48/contenido"
}Request
{
"tipoDocumento": "IDENTIFICACION_FRENTE",
"descripcion": "Cobranza de lunes por la colonia Centro"
}
Response
{
"id": 305,
"clienteId": 213,
"tipoDocumento": "IDENTIFICACION_FRENTE",
"nombreArchivo": "ine-frente.jpg",
"descripcion": "Cobranza de lunes por la colonia Centro",
"contentType": "image/jpeg",
"tamanoBytes": 245760,
"fechaCarga": "2026-09-02T14:30:00-06:00",
"usuarioCarga": "reyna.janeth",
"activo": true,
"reemplazaAId": 499,
"version": 3,
"urlContenido": "/api/documentos/48/contenido"
}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 |
Ejemplos
Request
Response
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 |
Ejemplos
Request
Response
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
Ejemplos
Content-Type: multipart/form-data
archivo ine-frente.jpg (binario)
descripcion Cobranza de lunes por la colonia Centro{
"id": 305,
"clienteId": 213,
"tipoDocumento": "IDENTIFICACION_FRENTE",
"nombreArchivo": "ine-frente.jpg",
"descripcion": "Cobranza de lunes por la colonia Centro",
"contentType": "image/jpeg",
"tamanoBytes": 245760,
"fechaCarga": "2026-09-02T14:30:00-06:00",
"usuarioCarga": "reyna.janeth",
"activo": true,
"reemplazaAId": 499,
"version": 3,
"urlContenido": "/api/documentos/48/contenido"
}Request
Content-Type: multipart/form-data
archivo ine-frente.jpg (binario)
descripcion Cobranza de lunes por la colonia Centro
Response
{
"id": 305,
"clienteId": 213,
"tipoDocumento": "IDENTIFICACION_FRENTE",
"nombreArchivo": "ine-frente.jpg",
"descripcion": "Cobranza de lunes por la colonia Centro",
"contentType": "image/jpeg",
"tamanoBytes": 245760,
"fechaCarga": "2026-09-02T14:30:00-06:00",
"usuarioCarga": "reyna.janeth",
"activo": true,
"reemplazaAId": 499,
"version": 3,
"urlContenido": "/api/documentos/48/contenido"
}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 |
Ejemplos
{
"estado": "INCOMPLETO",
"faltantes": [
"COMPROBANTE_DOMICILIO",
"COMPROBANTE_INGRESOS"
]
}Request
Response
{
"estado": "INCOMPLETO",
"faltantes": [
"COMPROBANTE_DOMICILIO",
"COMPROBANTE_INGRESOS"
]
}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
Ejemplos
{
"capital": 10000,
"tasaInteres": 0.0225,
"tasaInteresMoratorio": 0.01,
"numeroCuotas": 12,
"periodicidad": "SEMANAL",
"tipoCalculo": "CAPITAL_MAS_INTERES",
"fechaPrimerPago": "2026-09-07"
}{
"resumen": {
"capitalPorCuota": 833.33,
"interesPorCuota": 225,
"totalCuota": 1058.33,
"interesTotal": 2700,
"totalPagar": 12700
},
"cuotas": [
{
"numeroCuota": 1,
"fechaVencimiento": "2026-09-07",
"capital": 833.33,
"interes": 225,
"totalCuota": 1058.33
},
{
"numeroCuota": 2,
"fechaVencimiento": "2026-09-14",
"capital": 833.33,
"interes": 225,
"totalCuota": 1058.33
},
{
"numeroCuota": 12,
"fechaVencimiento": "2026-11-23",
"capital": 833.37,
"interes": 225,
"totalCuota": 1058.37
}
]
}Request
{
"capital": 10000,
"tasaInteres": 0.0225,
"tasaInteresMoratorio": 0.01,
"numeroCuotas": 12,
"periodicidad": "SEMANAL",
"tipoCalculo": "CAPITAL_MAS_INTERES",
"fechaPrimerPago": "2026-09-07"
}
Response
{
"resumen": {
"capitalPorCuota": 833.33,
"interesPorCuota": 225,
"totalCuota": 1058.33,
"interesTotal": 2700,
"totalPagar": 12700
},
"cuotas": [
{
"numeroCuota": 1,
"fechaVencimiento": "2026-09-07",
"capital": 833.33,
"interes": 225,
"totalCuota": 1058.33
},
{
"numeroCuota": 2,
"fechaVencimiento": "2026-09-14",
"capital": 833.33,
"interes": 225,
"totalCuota": 1058.33
},
{
"numeroCuota": 12,
"fechaVencimiento": "2026-11-23",
"capital": 833.37,
"interes": 225,
"totalCuota": 1058.37
}
]
}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: |
Ejemplos
{
"pagina": 0,
"tamano": 50,
"totalElementos": 23,
"totalPaginas": 1,
"contenido": [
{
"id": 305,
"clienteId": 213,
"clienteNombre": "María López Rivera",
"rutaId": 717,
"rutaNombre": "Ruta 1 LUNES",
"fechaPrestamo": "2026-09-02",
"capital": 10000,
"tasaInteres": 0.0225,
"tasaInteresMoratorio": 0.01,
"numeroCuotas": 12,
"periodicidad": "SEMANAL",
"tipoCalculo": "CAPITAL_MAS_INTERES",
"status": "ACTIVO",
"saldoTotal": 6500,
"saldoCapital": 6041.67,
"saldoIntereses": 450,
"saldoMoratorio": 0,
"saldoFavor": 0,
"cuotasPagadas": 5,
"cuotasVencidas": 2,
"proximoVencimiento": "2026-09-02",
"condicionVencido": true,
"totalAvales": 1,
"tienePlanGenerado": true,
"version": 3
}
]
}Request
Response
{
"pagina": 0,
"tamano": 50,
"totalElementos": 23,
"totalPaginas": 1,
"contenido": [
{
"id": 305,
"clienteId": 213,
"clienteNombre": "María López Rivera",
"rutaId": 717,
"rutaNombre": "Ruta 1 LUNES",
"fechaPrestamo": "2026-09-02",
"capital": 10000,
"tasaInteres": 0.0225,
"tasaInteresMoratorio": 0.01,
"numeroCuotas": 12,
"periodicidad": "SEMANAL",
"tipoCalculo": "CAPITAL_MAS_INTERES",
"status": "ACTIVO",
"saldoTotal": 6500,
"saldoCapital": 6041.67,
"saldoIntereses": 450,
"saldoMoratorio": 0,
"saldoFavor": 0,
"cuotasPagadas": 5,
"cuotasVencidas": 2,
"proximoVencimiento": "2026-09-02",
"condicionVencido": true,
"totalAvales": 1,
"tienePlanGenerado": true,
"version": 3
}
]
}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
Ejemplos
{
"clienteId": 12,
"fechaSolicitud": "2026-09-01",
"fechaEntrega": "2026-09-02",
"fechaPrimerPago": "2026-09-07",
"capital": 10000,
"tasaInteres": 0.0225,
"tasaInteresMoratorio": 0.01,
"numeroCuotas": 12,
"periodicidad": "SEMANAL",
"tipoCalculo": "CAPITAL_MAS_INTERES",
"observaciones": "Renovación puntual"
}{
"prestamo": {
"id": 42,
"clienteId": 12,
"clienteNombre": "María López Rivera",
"rutaId": 1,
"rutaNombre": "Ruta 1 LUNES",
"fechaPrestamo": "2026-09-02",
"capital": 10000,
"tasaInteres": 0.0225,
"tasaInteresMoratorio": 0.01,
"numeroCuotas": 12,
"periodicidad": "SEMANAL",
"tipoCalculo": "CAPITAL_MAS_INTERES",
"status": "ACTIVO",
"saldoTotal": 12700,
"saldoCapital": 10000,
"saldoIntereses": 2700,
"saldoMoratorio": 0,
"saldoFavor": 0,
"cuotasPagadas": 0,
"cuotasVencidas": 0,
"proximoVencimiento": "2026-09-07",
"condicionVencido": false,
"totalAvales": 0,
"tienePlanGenerado": true,
"version": 1,
"fechaSolicitud": "2026-09-02",
"fechaAutorizacion": "2026-09-02",
"fechaEntrega": "2026-09-02",
"fechaPrimerPago": "2026-09-02",
"interesTotal": 2700,
"totalPagar": 12700,
"prestamoOrigenId": 160,
"observaciones": "El cliente pidió adelantar la siguiente cuota",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
},
"cuotas": [
{
"id": 51229,
"prestamoId": 42,
"numeroCuota": 1,
"fechaVencimiento": "2026-09-07",
"capital": 833.33,
"interes": 225,
"totalCuota": 1058.33,
"capitalPagado": 0,
"interesPagado": 0,
"moratorioPagado": 0,
"estado": "PENDIENTE",
"saldoPendiente": 1058.33,
"diasAtraso": 0,
"moraAlDia": 0,
"totalACobrarHoy": 1058.33,
"version": 0
},
{
"id": 51230,
"prestamoId": 42,
"numeroCuota": 2,
"fechaVencimiento": "2026-09-14",
"capital": 833.33,
"interes": 225,
"totalCuota": 1058.33,
"capitalPagado": 0,
"interesPagado": 0,
"moratorioPagado": 0,
"estado": "PENDIENTE",
"saldoPendiente": 1058.33,
"diasAtraso": 0,
"moraAlDia": 0,
"totalACobrarHoy": 1058.33,
"version": 0
}
],
"movimientoCajaId": 22110
}Request
{
"clienteId": 12,
"fechaSolicitud": "2026-09-01",
"fechaEntrega": "2026-09-02",
"fechaPrimerPago": "2026-09-07",
"capital": 10000,
"tasaInteres": 0.0225,
"tasaInteresMoratorio": 0.01,
"numeroCuotas": 12,
"periodicidad": "SEMANAL",
"tipoCalculo": "CAPITAL_MAS_INTERES",
"observaciones": "Renovación puntual"
}
Response
{
"prestamo": {
"id": 42,
"clienteId": 12,
"clienteNombre": "María López Rivera",
"rutaId": 1,
"rutaNombre": "Ruta 1 LUNES",
"fechaPrestamo": "2026-09-02",
"capital": 10000,
"tasaInteres": 0.0225,
"tasaInteresMoratorio": 0.01,
"numeroCuotas": 12,
"periodicidad": "SEMANAL",
"tipoCalculo": "CAPITAL_MAS_INTERES",
"status": "ACTIVO",
"saldoTotal": 12700,
"saldoCapital": 10000,
"saldoIntereses": 2700,
"saldoMoratorio": 0,
"saldoFavor": 0,
"cuotasPagadas": 0,
"cuotasVencidas": 0,
"proximoVencimiento": "2026-09-07",
"condicionVencido": false,
"totalAvales": 0,
"tienePlanGenerado": true,
"interesTotal": 2700,
"totalPagar": 12700,
"version": 1
},
"cuotas": [
{
"id": 51229,
"prestamoId": 42,
"numeroCuota": 1,
"fechaVencimiento": "2026-09-07",
"capital": 833.33,
"interes": 225,
"totalCuota": 1058.33,
"capitalPagado": 0,
"interesPagado": 0,
"moratorioPagado": 0,
"estado": "PENDIENTE",
"saldoPendiente": 1058.33,
"diasAtraso": 0,
"moraAlDia": 0,
"totalACobrarHoy": 1058.33,
"version": 0
},
{
"id": 51230,
"prestamoId": 42,
"numeroCuota": 2,
"fechaVencimiento": "2026-09-14",
"capital": 833.33,
"interes": 225,
"totalCuota": 1058.33,
"capitalPagado": 0,
"interesPagado": 0,
"moratorioPagado": 0,
"estado": "PENDIENTE",
"saldoPendiente": 1058.33,
"diasAtraso": 0,
"moraAlDia": 0,
"totalACobrarHoy": 1058.33,
"version": 0
}
],
"movimientoCajaId": 22110
}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 |
Ejemplos
{
"id": 305,
"clienteId": 213,
"clienteNombre": "María López Rivera",
"rutaId": 717,
"rutaNombre": "Ruta 1 LUNES",
"fechaPrestamo": "2026-09-02",
"capital": 10000,
"tasaInteres": 0.0225,
"tasaInteresMoratorio": 0.01,
"numeroCuotas": 12,
"periodicidad": "SEMANAL",
"tipoCalculo": "CAPITAL_MAS_INTERES",
"status": "ACTIVO",
"saldoTotal": 6500,
"saldoCapital": 6041.67,
"saldoIntereses": 450,
"saldoMoratorio": 0,
"saldoFavor": 0,
"cuotasPagadas": 5,
"cuotasVencidas": 2,
"proximoVencimiento": "2026-09-02",
"condicionVencido": true,
"totalAvales": 1,
"tienePlanGenerado": true,
"version": 3,
"fechaSolicitud": "2026-09-02",
"fechaAutorizacion": "2026-09-02",
"fechaEntrega": "2026-09-02",
"fechaPrimerPago": "2026-09-02",
"interesTotal": 2700,
"totalPagar": 12700,
"prestamoOrigenId": 160,
"observaciones": "El cliente pidió adelantar la siguiente cuota",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}Request
Response
{
"id": 305,
"clienteId": 213,
"clienteNombre": "María López Rivera",
"rutaId": 717,
"rutaNombre": "Ruta 1 LUNES",
"fechaPrestamo": "2026-09-02",
"capital": 10000,
"tasaInteres": 0.0225,
"tasaInteresMoratorio": 0.01,
"numeroCuotas": 12,
"periodicidad": "SEMANAL",
"tipoCalculo": "CAPITAL_MAS_INTERES",
"status": "ACTIVO",
"saldoTotal": 6500,
"saldoCapital": 6041.67,
"saldoIntereses": 450,
"saldoMoratorio": 0,
"saldoFavor": 0,
"cuotasPagadas": 5,
"cuotasVencidas": 2,
"proximoVencimiento": "2026-09-02",
"condicionVencido": true,
"totalAvales": 1,
"tienePlanGenerado": true,
"version": 3,
"fechaSolicitud": "2026-09-02",
"fechaAutorizacion": "2026-09-02",
"fechaEntrega": "2026-09-02",
"fechaPrimerPago": "2026-09-02",
"interesTotal": 2700,
"totalPagar": 12700,
"prestamoOrigenId": 160,
"observaciones": "El cliente pidió adelantar la siguiente cuota",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}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 |
Ejemplos
{
"prestamo": {
"id": 42,
"clienteId": 12,
"clienteNombre": "María López Rivera",
"rutaId": 1,
"rutaNombre": "Ruta 1 LUNES",
"fechaPrestamo": "2026-09-02",
"capital": 10000,
"tasaInteres": 0.0225,
"tasaInteresMoratorio": 0.01,
"numeroCuotas": 12,
"periodicidad": "SEMANAL",
"tipoCalculo": "CAPITAL_MAS_INTERES",
"status": "ACTIVO",
"saldoTotal": 6500,
"saldoCapital": 6041.67,
"saldoIntereses": 450,
"saldoMoratorio": 0,
"saldoFavor": 8.34,
"cuotasPagadas": 5,
"cuotasVencidas": 1,
"proximoVencimiento": "2026-09-07",
"condicionVencido": true,
"totalAvales": 1,
"tienePlanGenerado": true,
"version": 3
},
"cuotas": [
{
"id": 51233,
"prestamoId": 42,
"numeroCuota": 6,
"fechaVencimiento": "2026-08-31",
"capital": 833.33,
"interes": 225,
"totalCuota": 1058.33,
"capitalPagado": 0,
"interesPagado": 0,
"moratorioPagado": 0,
"estado": "VENCIDA",
"saldoPendiente": 1058.33,
"diasAtraso": 2,
"moraAlDia": 16.67,
"totalACobrarHoy": 1075
},
{
"id": 51234,
"prestamoId": 42,
"numeroCuota": 7,
"fechaVencimiento": "2026-09-07",
"capital": 833.33,
"interes": 225,
"totalCuota": 1058.33,
"capitalPagado": 0,
"interesPagado": 0,
"moratorioPagado": 0,
"estado": "PENDIENTE",
"saldoPendiente": 1058.33,
"diasAtraso": 0,
"moraAlDia": 0,
"totalACobrarHoy": 1058.33
}
],
"totales": {
"capital": 10000,
"interes": 2700,
"total": 12700,
"pagado": 5416.66,
"pendiente": 7283.34
}
}Request
Response
{
"prestamo": {
"id": 42,
"clienteId": 12,
"clienteNombre": "María López Rivera",
"rutaId": 1,
"rutaNombre": "Ruta 1 LUNES",
"capital": 10000,
"tasaInteres": 0.0225,
"numeroCuotas": 12,
"periodicidad": "SEMANAL",
"tipoCalculo": "CAPITAL_MAS_INTERES",
"status": "ACTIVO",
"saldoTotal": 6500,
"saldoCapital": 6041.67,
"saldoIntereses": 450,
"saldoMoratorio": 0,
"saldoFavor": 8.34,
"cuotasPagadas": 5,
"cuotasVencidas": 1,
"proximoVencimiento": "2026-09-07",
"condicionVencido": true,
"totalAvales": 1,
"tienePlanGenerado": true
},
"cuotas": [
{
"id": 51233,
"prestamoId": 42,
"numeroCuota": 6,
"fechaVencimiento": "2026-08-31",
"capital": 833.33,
"interes": 225,
"totalCuota": 1058.33,
"capitalPagado": 0,
"interesPagado": 0,
"moratorioPagado": 0,
"estado": "VENCIDA",
"saldoPendiente": 1058.33,
"diasAtraso": 2,
"moraAlDia": 16.67,
"totalACobrarHoy": 1075
},
{
"id": 51234,
"prestamoId": 42,
"numeroCuota": 7,
"fechaVencimiento": "2026-09-07",
"capital": 833.33,
"interes": 225,
"totalCuota": 1058.33,
"capitalPagado": 0,
"interesPagado": 0,
"moratorioPagado": 0,
"estado": "PENDIENTE",
"saldoPendiente": 1058.33,
"diasAtraso": 0,
"moraAlDia": 0,
"totalACobrarHoy": 1058.33
}
],
"totales": {
"capital": 10000,
"interes": 2700,
"total": 12700,
"pagado": 5416.66,
"pendiente": 7283.34
}
}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 |
Ejemplos
{
"prestamo": {
"id": 305,
"clienteId": 213,
"clienteNombre": "María López Rivera",
"rutaId": 717,
"rutaNombre": "Ruta 1 LUNES",
"fechaPrestamo": "2026-09-02",
"capital": 10000,
"tasaInteres": 0.0225,
"tasaInteresMoratorio": 0.01,
"numeroCuotas": 12,
"periodicidad": "SEMANAL",
"tipoCalculo": "CAPITAL_MAS_INTERES",
"status": "ACTIVO",
"saldoTotal": 6500,
"saldoCapital": 6041.67,
"saldoIntereses": 450,
"saldoMoratorio": 0,
"saldoFavor": 0,
"cuotasPagadas": 5,
"cuotasVencidas": 2,
"proximoVencimiento": "2026-09-02",
"condicionVencido": true,
"totalAvales": 1,
"tienePlanGenerado": true,
"version": 3
},
"cuotas": [
{
"id": 305,
"prestamoId": 348,
"numeroCuota": 7,
"fechaVencimiento": "2026-09-02",
"capital": 10000,
"interes": 2,
"totalCuota": 1058.33,
"capitalPagado": 1666.67,
"interesPagado": 1125,
"moratorioPagado": 0,
"saldoCapitalDespues": 5208.34,
"fechaLiquidacion": "2026-09-02T14:30:00-06:00",
"estado": {},
"saldoPendiente": 933.33,
"diasAtraso": 12,
"moraAlDia": 100,
"totalACobrarHoy": 1033.33,
"version": 3
}
],
"pagos": [
{
"id": null,
"prestamoId": null,
"clienteId": null,
"clienteNombre": null,
"fechaPago": null,
"monto": null,
"tipoPago": null,
"referencia": null,
"observaciones": null,
"usuarioId": null,
"usuarioNombre": null,
"status": null,
"fechaRegistro": null,
"fechaCancelacion": null,
"usuarioCancelacion": null,
"motivoCancelacion": null,
"version": null,
"aplicaciones": [
null
],
"movimientoCajaId": 135
}
]
}Request
Response
{
"prestamo": {
"id": 305,
"clienteId": 213,
"clienteNombre": "María López Rivera",
"rutaId": 717,
"rutaNombre": "Ruta 1 LUNES",
"fechaPrestamo": "2026-09-02",
"capital": 10000,
"tasaInteres": 0.0225,
"tasaInteresMoratorio": 0.01,
"numeroCuotas": 12,
"periodicidad": "SEMANAL",
"tipoCalculo": "CAPITAL_MAS_INTERES",
"status": "ACTIVO",
"saldoTotal": 6500,
"saldoCapital": 6041.67,
"saldoIntereses": 450,
"saldoMoratorio": 0,
"saldoFavor": 0,
"cuotasPagadas": 5,
"cuotasVencidas": 2,
"proximoVencimiento": "2026-09-02",
"condicionVencido": true,
"totalAvales": 1,
"tienePlanGenerado": true,
"version": 3
},
"cuotas": [
{
"id": 305,
"prestamoId": 348,
"numeroCuota": 7,
"fechaVencimiento": "2026-09-02",
"capital": 10000,
"interes": 2,
"totalCuota": 1058.33,
"capitalPagado": 1666.67,
"interesPagado": 1125,
"moratorioPagado": 0,
"saldoCapitalDespues": 5208.34,
"fechaLiquidacion": "2026-09-02T14:30:00-06:00",
"estado": {},
"saldoPendiente": 933.33,
"diasAtraso": 12,
"moraAlDia": 100,
"totalACobrarHoy": 1033.33,
"version": 3
}
],
"pagos": [
{
"id": null,
"prestamoId": null,
"clienteId": null,
"clienteNombre": null,
"fechaPago": null,
"monto": null,
"tipoPago": null,
"referencia": null,
"observaciones": null,
"usuarioId": null,
"usuarioNombre": null,
"status": null,
"fechaRegistro": null,
"fechaCancelacion": null,
"usuarioCancelacion": null,
"motivoCancelacion": null,
"version": null,
"aplicaciones": [
null
],
"movimientoCajaId": 135
}
]
}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
Ejemplos
{
"status": "CANCELADO",
"motivo": "Crédito cancelado por duplicado en el alta"
}{
"id": 305,
"clienteId": 213,
"clienteNombre": "María López Rivera",
"rutaId": 717,
"rutaNombre": "Ruta 1 LUNES",
"fechaPrestamo": "2026-09-02",
"capital": 10000,
"tasaInteres": 0.0225,
"tasaInteresMoratorio": 0.01,
"numeroCuotas": 12,
"periodicidad": "SEMANAL",
"tipoCalculo": "CAPITAL_MAS_INTERES",
"status": "ACTIVO",
"saldoTotal": 6500,
"saldoCapital": 6041.67,
"saldoIntereses": 450,
"saldoMoratorio": 0,
"saldoFavor": 0,
"cuotasPagadas": 5,
"cuotasVencidas": 2,
"proximoVencimiento": "2026-09-02",
"condicionVencido": true,
"totalAvales": 1,
"tienePlanGenerado": true,
"version": 3,
"fechaSolicitud": "2026-09-02",
"fechaAutorizacion": "2026-09-02",
"fechaEntrega": "2026-09-02",
"fechaPrimerPago": "2026-09-02",
"interesTotal": 2700,
"totalPagar": 12700,
"prestamoOrigenId": 160,
"observaciones": "El cliente pidió adelantar la siguiente cuota",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}Request
{
"status": "CANCELADO",
"motivo": "Crédito cancelado por duplicado en el alta"
}
Response
{
"id": 305,
"clienteId": 213,
"clienteNombre": "María López Rivera",
"rutaId": 717,
"rutaNombre": "Ruta 1 LUNES",
"fechaPrestamo": "2026-09-02",
"capital": 10000,
"tasaInteres": 0.0225,
"tasaInteresMoratorio": 0.01,
"numeroCuotas": 12,
"periodicidad": "SEMANAL",
"tipoCalculo": "CAPITAL_MAS_INTERES",
"status": "ACTIVO",
"saldoTotal": 6500,
"saldoCapital": 6041.67,
"saldoIntereses": 450,
"saldoMoratorio": 0,
"saldoFavor": 0,
"cuotasPagadas": 5,
"cuotasVencidas": 2,
"proximoVencimiento": "2026-09-02",
"condicionVencido": true,
"totalAvales": 1,
"tienePlanGenerado": true,
"version": 3,
"fechaSolicitud": "2026-09-02",
"fechaAutorizacion": "2026-09-02",
"fechaEntrega": "2026-09-02",
"fechaPrimerPago": "2026-09-02",
"interesTotal": 2700,
"totalPagar": 12700,
"prestamoOrigenId": 160,
"observaciones": "El cliente pidió adelantar la siguiente cuota",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}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 operacionesPantalla 7 · Anexo C §C.1.2.
▸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 |
Ejemplos
[
{
"id": 305,
"prestamoId": 348,
"nombreCompleto": "María López Rivera",
"telefono": "55 1234 5678",
"direccion": "Av. Reforma 42, Col. Centro",
"curp": "LORM800214MDFRNS09",
"tipoIdentificacion": "INE",
"numeroIdentificacion": "4123 4567 8901",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}
]Request
Response
[
{
"id": 305,
"prestamoId": 348,
"nombreCompleto": "María López Rivera",
"telefono": "55 1234 5678",
"direccion": "Av. Reforma 42, Col. Centro",
"curp": "LORM800214MDFRNS09",
"tipoIdentificacion": "INE",
"numeroIdentificacion": "4123 4567 8901",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}
]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
Ejemplos
{
"nombreCompleto": "Jorge Méndez Salas",
"telefono": "55 8765 4321",
"direccion": "C. Hidalgo 17, Col. Centro",
"curp": "MESJ750612HDFRRN08",
"tipoIdentificacion": "LICENCIA",
"numeroIdentificacion": "2045871"
}{
"id": 305,
"prestamoId": 348,
"nombreCompleto": "María López Rivera",
"telefono": "55 1234 5678",
"direccion": "Av. Reforma 42, Col. Centro",
"curp": "LORM800214MDFRNS09",
"tipoIdentificacion": "INE",
"numeroIdentificacion": "4123 4567 8901",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}Request
{
"nombreCompleto": "Jorge Méndez Salas",
"telefono": "55 8765 4321",
"direccion": "C. Hidalgo 17, Col. Centro",
"curp": "MESJ750612HDFRRN08",
"tipoIdentificacion": "LICENCIA",
"numeroIdentificacion": "2045871"
}
Response
{
"id": 305,
"prestamoId": 348,
"nombreCompleto": "María López Rivera",
"telefono": "55 1234 5678",
"direccion": "Av. Reforma 42, Col. Centro",
"curp": "LORM800214MDFRNS09",
"tipoIdentificacion": "INE",
"numeroIdentificacion": "4123 4567 8901",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}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
Ejemplos
{
"nombreCompleto": "María López Rivera",
"telefono": "55 1234 5678",
"direccion": "Av. Reforma 42, Col. Centro",
"curp": "LORM800214MDFRNS09",
"tipoIdentificacion": "INE",
"numeroIdentificacion": "4123 4567 8901"
}{
"id": 305,
"prestamoId": 348,
"nombreCompleto": "María López Rivera",
"telefono": "55 1234 5678",
"direccion": "Av. Reforma 42, Col. Centro",
"curp": "LORM800214MDFRNS09",
"tipoIdentificacion": "INE",
"numeroIdentificacion": "4123 4567 8901",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}Request
{
"nombreCompleto": "María López Rivera",
"telefono": "55 1234 5678",
"direccion": "Av. Reforma 42, Col. Centro",
"curp": "LORM800214MDFRNS09",
"tipoIdentificacion": "INE",
"numeroIdentificacion": "4123 4567 8901"
}
Response
{
"id": 305,
"prestamoId": 348,
"nombreCompleto": "María López Rivera",
"telefono": "55 1234 5678",
"direccion": "Av. Reforma 42, Col. Centro",
"curp": "LORM800214MDFRNS09",
"tipoIdentificacion": "INE",
"numeroIdentificacion": "4123 4567 8901",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}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 |
Ejemplos
Request
Response
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
Ejemplos
{
"fechaPago": "2026-09-02",
"monto": 2500,
"condonarMora": false
}{
"distribucion": [
{
"cuotaId": 51234,
"numeroCuota": 7,
"capitalAplicado": 2000,
"interesAplicado": 225,
"moratorioAplicado": 100,
"saldoCuotaDespues": 0,
"estadoResultante": "PAGADA"
},
{
"cuotaId": 51235,
"numeroCuota": 8,
"capitalAplicado": 0,
"interesAplicado": 0,
"moratorioAplicado": 0,
"saldoCuotaDespues": 1058.33,
"estadoResultante": "PENDIENTE"
}
],
"totales": {
"capital": 2000,
"interes": 225,
"mora": 100,
"saldoFavorGenerado": 175
},
"saldoPrestamoDespues": 4523.34
}Request
{
"fechaPago": "2026-09-02",
"monto": 2500,
"condonarMora": false
}
Response
{
"distribucion": [
{
"cuotaId": 51234,
"numeroCuota": 7,
"capitalAplicado": 2000,
"interesAplicado": 225,
"moratorioAplicado": 100,
"saldoCuotaDespues": 0,
"estadoResultante": "PAGADA"
},
{
"cuotaId": 51235,
"numeroCuota": 8,
"capitalAplicado": 0,
"interesAplicado": 0,
"moratorioAplicado": 0,
"saldoCuotaDespues": 1058.33,
"estadoResultante": "PENDIENTE"
}
],
"totales": {
"capital": 2000,
"interes": 225,
"mora": 100,
"saldoFavorGenerado": 175
},
"saldoPrestamoDespues": 4523.34
}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 |
Ejemplos
{
"pagina": 0,
"tamano": 50,
"totalElementos": 23,
"totalPaginas": 1,
"contenido": [
{
"id": 305,
"prestamoId": 348,
"clienteId": 213,
"clienteNombre": "María López Rivera",
"fechaPago": "2026-09-02T14:30:00-06:00",
"monto": 2500,
"tipoPago": "EFECTIVO",
"referencia": "TRANSFER-8841",
"observaciones": "El cliente pidió adelantar la siguiente cuota",
"usuarioId": 249,
"usuarioNombre": "reyna.janeth",
"status": "APLICADO",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaCancelacion": "2026-09-02T14:30:00-06:00",
"usuarioCancelacion": "reyna.janeth",
"motivoCancelacion": "Duplicado del pago 8840",
"version": 3
}
]
}Request
Response
{
"pagina": 0,
"tamano": 50,
"totalElementos": 23,
"totalPaginas": 1,
"contenido": [
{
"id": 305,
"prestamoId": 348,
"clienteId": 213,
"clienteNombre": "María López Rivera",
"fechaPago": "2026-09-02T14:30:00-06:00",
"monto": 2500,
"tipoPago": "EFECTIVO",
"referencia": "TRANSFER-8841",
"observaciones": "El cliente pidió adelantar la siguiente cuota",
"usuarioId": 249,
"usuarioNombre": "reyna.janeth",
"status": "APLICADO",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaCancelacion": "2026-09-02T14:30:00-06:00",
"usuarioCancelacion": "reyna.janeth",
"motivoCancelacion": "Duplicado del pago 8840",
"version": 3
}
]
}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
Ejemplos
{
"fechaPago": "2026-09-02",
"monto": 2500,
"condonarMora": false,
"tipoPago": "EFECTIVO",
"referencia": "RUTA-1-0209",
"observaciones": "Pago en puerta"
}{
"distribucion": [
{
"cuotaId": 51234,
"numeroCuota": 7,
"capitalAplicado": 2000,
"interesAplicado": 225,
"moratorioAplicado": 100,
"saldoCuotaDespues": 0,
"estadoResultante": "PAGADA"
},
{
"cuotaId": 51235,
"numeroCuota": 8,
"capitalAplicado": 175,
"interesAplicado": 0,
"moratorioAplicado": 0,
"saldoCuotaDespues": 883.33,
"estadoResultante": "PARCIAL"
}
],
"totales": {
"capital": 2175,
"interes": 225,
"mora": 100,
"saldoFavorGenerado": 0
},
"saldoPrestamoDespues": 4348.34,
"pagoId": 8841,
"movimientoCajaId": 22114,
"prestamoLiquidado": false
}Request
{
"fechaPago": "2026-09-02",
"monto": 2500,
"condonarMora": false,
"tipoPago": "EFECTIVO",
"referencia": "RUTA-1-0209",
"observaciones": "Pago en puerta"
}
Response
{
"pagoId": 8841,
"distribucion": [
{
"cuotaId": 51234,
"numeroCuota": 7,
"capitalAplicado": 2000,
"interesAplicado": 225,
"moratorioAplicado": 100,
"saldoCuotaDespues": 0,
"estadoResultante": "PAGADA"
},
{
"cuotaId": 51235,
"numeroCuota": 8,
"capitalAplicado": 175,
"interesAplicado": 0,
"moratorioAplicado": 0,
"saldoCuotaDespues": 883.33,
"estadoResultante": "PARCIAL"
}
],
"totales": {
"capital": 2175,
"interes": 225,
"mora": 100,
"saldoFavorGenerado": 0
},
"saldoPrestamoDespues": 4348.34,
"movimientoCajaId": 22114,
"prestamoLiquidado": false
}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 |
Ejemplos
{
"id": 305,
"prestamoId": 348,
"clienteId": 213,
"clienteNombre": "María López Rivera",
"fechaPago": "2026-09-02T14:30:00-06:00",
"monto": 2500,
"tipoPago": "EFECTIVO",
"referencia": "TRANSFER-8841",
"observaciones": "El cliente pidió adelantar la siguiente cuota",
"usuarioId": 249,
"usuarioNombre": "reyna.janeth",
"status": "APLICADO",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaCancelacion": "2026-09-02T14:30:00-06:00",
"usuarioCancelacion": "reyna.janeth",
"motivoCancelacion": "Duplicado del pago 8840",
"version": 3,
"aplicaciones": [
{
"id": 305,
"pagoId": 696,
"cuotaId": 813,
"numeroCuota": 7,
"capitalAplicado": 2000,
"interesAplicado": 225,
"moratorioAplicado": 100,
"saldoFavorGenerado": 0,
"fechaRegistro": "2026-09-02T14:30:00-06:00"
}
],
"movimientoCajaId": 135
}Request
Response
{
"id": 305,
"prestamoId": 348,
"clienteId": 213,
"clienteNombre": "María López Rivera",
"fechaPago": "2026-09-02T14:30:00-06:00",
"monto": 2500,
"tipoPago": "EFECTIVO",
"referencia": "TRANSFER-8841",
"observaciones": "El cliente pidió adelantar la siguiente cuota",
"usuarioId": 249,
"usuarioNombre": "reyna.janeth",
"status": "APLICADO",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaCancelacion": "2026-09-02T14:30:00-06:00",
"usuarioCancelacion": "reyna.janeth",
"motivoCancelacion": "Duplicado del pago 8840",
"version": 3,
"aplicaciones": [
{
"id": 305,
"pagoId": 696,
"cuotaId": 813,
"numeroCuota": 7,
"capitalAplicado": 2000,
"interesAplicado": 225,
"moratorioAplicado": 100,
"saldoFavorGenerado": 0,
"fechaRegistro": "2026-09-02T14:30:00-06:00"
}
],
"movimientoCajaId": 135
}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
Ejemplos
{
"motivo": "Pago registrado dos veces por error de captura"
}{
"id": 8840,
"prestamoId": 42,
"clienteId": 12,
"clienteNombre": "María López Rivera",
"fechaPago": "2026-09-02T10:12:00-06:00",
"monto": 2500,
"tipoPago": "EFECTIVO",
"referencia": "TRANSFER-8841",
"observaciones": "El cliente pidió adelantar la siguiente cuota",
"usuarioId": 249,
"usuarioNombre": "reyna.janeth",
"status": "CANCELADO",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"fechaCancelacion": "2026-09-02T16:45:00-06:00",
"usuarioCancelacion": "reyna.janeth",
"motivoCancelacion": "Pago registrado dos veces por error de captura",
"version": 3,
"aplicaciones": [
{
"id": 9101,
"pagoId": 8840,
"cuotaId": 51234,
"numeroCuota": 7,
"capitalAplicado": 2000,
"interesAplicado": 225,
"moratorioAplicado": 100,
"saldoFavorGenerado": 0,
"fechaRegistro": "2026-09-02T10:12:00-06:00"
}
],
"movimientoCajaId": 22113
}Request
{
"motivo": "Pago registrado dos veces por error de captura"
}
Response
{
"id": 8840,
"prestamoId": 42,
"clienteId": 12,
"clienteNombre": "María López Rivera",
"fechaPago": "2026-09-02T10:12:00-06:00",
"monto": 2500,
"tipoPago": "EFECTIVO",
"status": "CANCELADO",
"usuarioNombre": "reyna.janeth",
"fechaCancelacion": "2026-09-02T16:45:00-06:00",
"usuarioCancelacion": "reyna.janeth",
"motivoCancelacion": "Pago registrado dos veces por error de captura",
"aplicaciones": [
{
"id": 9101,
"pagoId": 8840,
"cuotaId": 51234,
"numeroCuota": 7,
"capitalAplicado": 2000,
"interesAplicado": 225,
"moratorioAplicado": 100,
"saldoFavorGenerado": 0,
"fechaRegistro": "2026-09-02T10:12:00-06:00"
}
],
"movimientoCajaId": 22113
}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
Ejemplos
{
"fecha": "2026-09-02",
"monto": 3000,
"aplicacion": "REDUCE_CUOTAS",
"observaciones": "Abono acordado"
}{
"pagoId": 8842,
"movimientoCajaId": 22115,
"saldoPrestamoDespues": 3041.67,
"planResultante": [
{
"id": 51236,
"prestamoId": 42,
"numeroCuota": 9,
"fechaVencimiento": "2026-10-26",
"capital": 833.33,
"interes": 225,
"totalCuota": 1058.33,
"estado": "PENDIENTE",
"saldoPendiente": 1058.33
},
{
"id": 51237,
"prestamoId": 42,
"numeroCuota": 10,
"fechaVencimiento": "2026-11-02",
"capital": 833.33,
"interes": 225,
"totalCuota": 1058.33,
"estado": "PENDIENTE",
"saldoPendiente": 1058.33
}
]
}Request
{
"fecha": "2026-09-02",
"monto": 3000,
"aplicacion": "REDUCE_CUOTAS",
"observaciones": "Abono acordado"
}
Response
{
"pagoId": 8842,
"movimientoCajaId": 22115,
"saldoPrestamoDespues": 3041.67,
"planResultante": [
{
"id": 51236,
"prestamoId": 42,
"numeroCuota": 9,
"fechaVencimiento": "2026-10-26",
"capital": 833.33,
"interes": 225,
"totalCuota": 1058.33,
"estado": "PENDIENTE",
"saldoPendiente": 1058.33
},
{
"id": 51237,
"prestamoId": 42,
"numeroCuota": 10,
"fechaVencimiento": "2026-11-02",
"capital": 833.33,
"interes": 225,
"totalCuota": 1058.33,
"estado": "PENDIENTE",
"saldoPendiente": 1058.33
}
]
}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: |
Ejemplos
{
"pagina": 0,
"tamano": 50,
"totalElementos": 23,
"totalPaginas": 1,
"contenido": [
{
"id": 51233,
"prestamoId": 42,
"numeroCuota": 6,
"fechaVencimiento": "2026-08-31",
"capital": 833.33,
"interes": 225,
"totalCuota": 1058.33,
"estado": "VENCIDA",
"saldoPendiente": 1058.33,
"diasAtraso": 2,
"moraAlDia": 16.67,
"totalACobrarHoy": 1075,
"clienteId": 12,
"clienteNombre": "María López Rivera",
"rutaId": 1,
"rutaNombre": "Ruta 1 LUNES",
"telefonoCliente": "55 1234 5678"
}
],
"totales": {
"numero": 23,
"importe": 41320,
"mora": 1240
}
}Request
Response
{
"pagina": 0,
"tamano": 50,
"totalElementos": 23,
"totalPaginas": 1,
"contenido": [
{
"id": 51233,
"prestamoId": 42,
"numeroCuota": 6,
"fechaVencimiento": "2026-08-31",
"capital": 833.33,
"interes": 225,
"totalCuota": 1058.33,
"estado": "VENCIDA",
"saldoPendiente": 1058.33,
"diasAtraso": 2,
"moraAlDia": 16.67,
"totalACobrarHoy": 1075,
"clienteId": 12,
"clienteNombre": "María López Rivera",
"rutaId": 1,
"rutaNombre": "Ruta 1 LUNES",
"telefonoCliente": "55 1234 5678"
}
],
"totales": {
"numero": 23,
"importe": 41320,
"mora": 1240
}
}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: |
Ejemplos
{
"pagina": 0,
"tamano": 50,
"totalElementos": 23,
"totalPaginas": 1,
"contenido": [
{
"clienteId": null,
"clienteNombre": null,
"rutaId": null,
"rutaNombre": null,
"telefonoCliente": null
}
],
"totales": {
"numero": 23,
"importe": 8500,
"mora": 2
}
}Request
Response
{
"pagina": 0,
"tamano": 50,
"totalElementos": 23,
"totalPaginas": 1,
"contenido": [
{
"clienteId": null,
"clienteNombre": null,
"rutaId": null,
"rutaNombre": null,
"telefonoCliente": null
}
],
"totales": {
"numero": 23,
"importe": 8500,
"mora": 2
}
}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 operacionesPantalla 15 · Anexo A §A.4.
▸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: |
Ejemplos
{
"pagina": 0,
"tamano": 50,
"totalElementos": 23,
"totalPaginas": 1,
"contenido": [
{
"id": 305,
"fecha": "2026-09-02T14:30:00-06:00",
"tipoMovimiento": "INGRESO_PAGO_PRESTAMO",
"naturaleza": "INGRESO",
"concepto": "Aportación de capital del negocio",
"importe": 8500,
"utilidad": 500,
"prestamoId": 348,
"pagoId": 696,
"clienteNombre": "María López Rivera",
"generadoPorSistema": false,
"usuarioId": 249,
"referencia": "TRANSFER-8841",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"marcadoParaRevision": false,
"version": 3
}
]
}Request
Response
{
"pagina": 0,
"tamano": 50,
"totalElementos": 23,
"totalPaginas": 1,
"contenido": [
{
"id": 305,
"fecha": "2026-09-02T14:30:00-06:00",
"tipoMovimiento": "INGRESO_PAGO_PRESTAMO",
"naturaleza": "INGRESO",
"concepto": "Aportación de capital del negocio",
"importe": 8500,
"utilidad": 500,
"prestamoId": 348,
"pagoId": 696,
"clienteNombre": "María López Rivera",
"generadoPorSistema": false,
"usuarioId": 249,
"referencia": "TRANSFER-8841",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"marcadoParaRevision": false,
"version": 3
}
]
}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
Ejemplos
{
"fecha": "2026-09-02",
"tipoMovimiento": "EGRESO_GASTO_OPERATIVO",
"concepto": "Renta de septiembre del local",
"importe": 8500,
"referencia": "RECIBO-2026-09"
}{
"id": 305,
"fecha": "2026-09-02T14:30:00-06:00",
"tipoMovimiento": "INGRESO_PAGO_PRESTAMO",
"naturaleza": "INGRESO",
"concepto": "Aportación de capital del negocio",
"importe": 8500,
"utilidad": 500,
"prestamoId": 348,
"pagoId": 696,
"clienteNombre": "María López Rivera",
"generadoPorSistema": false,
"usuarioId": 249,
"referencia": "TRANSFER-8841",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"marcadoParaRevision": false,
"version": 3
}Request
{
"fecha": "2026-09-02",
"tipoMovimiento": "EGRESO_GASTO_OPERATIVO",
"concepto": "Renta de septiembre del local",
"importe": 8500,
"referencia": "RECIBO-2026-09"
}
Response
{
"id": 305,
"fecha": "2026-09-02T14:30:00-06:00",
"tipoMovimiento": "INGRESO_PAGO_PRESTAMO",
"naturaleza": "INGRESO",
"concepto": "Aportación de capital del negocio",
"importe": 8500,
"utilidad": 500,
"prestamoId": 348,
"pagoId": 696,
"clienteNombre": "María López Rivera",
"generadoPorSistema": false,
"usuarioId": 249,
"referencia": "TRANSFER-8841",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"marcadoParaRevision": false,
"version": 3
}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
Ejemplos
{
"fecha": "2026-09-02",
"tipoMovimiento": {},
"concepto": "Aportación de capital del negocio",
"importe": 8500,
"referencia": "TRANSFER-8841"
}{
"id": 305,
"fecha": "2026-09-02T14:30:00-06:00",
"tipoMovimiento": "INGRESO_PAGO_PRESTAMO",
"naturaleza": "INGRESO",
"concepto": "Aportación de capital del negocio",
"importe": 8500,
"utilidad": 500,
"prestamoId": 348,
"pagoId": 696,
"clienteNombre": "María López Rivera",
"generadoPorSistema": false,
"usuarioId": 249,
"referencia": "TRANSFER-8841",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"marcadoParaRevision": false,
"version": 3
}Request
{
"fecha": "2026-09-02",
"tipoMovimiento": {},
"concepto": "Aportación de capital del negocio",
"importe": 8500,
"referencia": "TRANSFER-8841"
}
Response
{
"id": 305,
"fecha": "2026-09-02T14:30:00-06:00",
"tipoMovimiento": "INGRESO_PAGO_PRESTAMO",
"naturaleza": "INGRESO",
"concepto": "Aportación de capital del negocio",
"importe": 8500,
"utilidad": 500,
"prestamoId": 348,
"pagoId": 696,
"clienteNombre": "María López Rivera",
"generadoPorSistema": false,
"usuarioId": 249,
"referencia": "TRANSFER-8841",
"fechaRegistro": "2026-09-02T14:30:00-06:00",
"marcadoParaRevision": false,
"version": 3
}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 |
Ejemplos
{
"totalEfectivo": 184320.5,
"ingresos": 412300,
"egresos": 227979.5,
"utilidad": 18275.4
}Request
Response
{
"totalEfectivo": 184320.5,
"ingresos": 412300,
"egresos": 227979.5,
"utilidad": 18275.4
}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 |
Ejemplos
{
"clave": "cuotas-vencidas",
"titulo": "Cuotas vencidas",
"filtrosAplicados": {
"rutaId": 1,
"fecha": "2026-09-02"
},
"columnas": [
{
"clave": "clienteNombre",
"titulo": "Cliente",
"tipo": "texto"
},
{
"clave": "numeroCuota",
"titulo": "Cuota",
"tipo": "numero"
},
{
"clave": "diasAtraso",
"titulo": "Días de atraso",
"tipo": "numero"
},
{
"clave": "totalACobrarHoy",
"titulo": "Total a cobrar hoy",
"tipo": "dinero"
}
],
"filas": [
{
"clienteNombre": "María López Rivera",
"numeroCuota": 6,
"diasAtraso": 2,
"totalACobrarHoy": 1075
},
{
"clienteNombre": "Jorge Méndez Salas",
"numeroCuota": 11,
"diasAtraso": 9,
"totalACobrarHoy": 1145.5
}
],
"totales": {
"numero": 23,
"importe": 41320,
"mora": 1240
},
"pagina": 0,
"tamano": 50,
"totalElementos": 23,
"totalPaginas": 1
}Request
Response
{
"clave": "cuotas-vencidas",
"titulo": "Cuotas vencidas",
"filtrosAplicados": {
"rutaId": 1,
"fecha": "2026-09-02"
},
"columnas": [
{
"clave": "clienteNombre",
"titulo": "Cliente",
"tipo": "texto"
},
{
"clave": "numeroCuota",
"titulo": "Cuota",
"tipo": "numero"
},
{
"clave": "diasAtraso",
"titulo": "Días de atraso",
"tipo": "numero"
},
{
"clave": "totalACobrarHoy",
"titulo": "Total a cobrar hoy",
"tipo": "dinero"
}
],
"filas": [
{
"clienteNombre": "María López Rivera",
"numeroCuota": 6,
"diasAtraso": 2,
"totalACobrarHoy": 1075
},
{
"clienteNombre": "Jorge Méndez Salas",
"numeroCuota": 11,
"diasAtraso": 9,
"totalACobrarHoy": 1145.5
}
],
"totales": {
"numero": 23,
"importe": 41320,
"mora": 1240
},
"pagina": 0,
"tamano": 50,
"totalElementos": 23,
"totalPaginas": 1
}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 |
Ejemplos
Request
Response
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 |
Ejemplos
Request
Response
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 |
Ejemplos
{
"formato": "Texto de formato",
"titulo": "Cuotas vencidas",
"subtitulo": "Ruta 1 LUNES · 02/09/2026",
"empresa": {
"id": 305,
"nombre": "María",
"lema": "Creciendo juntos, mano a mano",
"rfc": "CJF210416XYA",
"logoUrl": "Texto de logo url",
"activo": true
},
"filtrosAplicados": {},
"columnas": [
{}
],
"filas": [
{}
],
"totales": {},
"generadoEn": "2026-09-02T14:30:00-06:00",
"generadoPor": "reyna.janeth"
}Request
Response
{
"formato": "Texto de formato",
"titulo": "Cuotas vencidas",
"subtitulo": "Ruta 1 LUNES · 02/09/2026",
"empresa": {
"id": 305,
"nombre": "María",
"lema": "Creciendo juntos, mano a mano",
"rfc": "CJF210416XYA",
"logoUrl": "Texto de logo url",
"activo": true
},
"filtrosAplicados": {},
"columnas": [
{}
],
"filas": [
{}
],
"totales": {},
"generadoEn": "2026-09-02T14:30:00-06:00",
"generadoPor": "reyna.janeth"
}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 |
Ejemplos
Request
Response
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 |
Ejemplos
{
"periodo": {
"anio": 2026,
"mes": 9
},
"totalEfectivo": 184320.5,
"carteraActiva": 1245800,
"cobranzaPeriodo": 98450,
"cuotasVencidas": {
"numero": 23,
"importe": 41320
},
"proyeccionMes": 210940,
"utilidadMes": 18275.4
}Request
Response
{
"periodo": {
"anio": 2026,
"mes": 9
},
"totalEfectivo": 184320.5,
"carteraActiva": 1245800,
"cobranzaPeriodo": 98450,
"cuotasVencidas": {
"numero": 23,
"importe": 41320
},
"proyeccionMes": 210940,
"utilidadMes": 18275.4
}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
Ejemplos
{
"diasAnticipacionAviso": 3,
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}Request
Response
{
"diasAnticipacionAviso": 3,
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}Respuestas
Configuración
▸PUT/api/configuracionActualiza la configuración operativa
Cuerpo de la petición req
application/json → Configuracion
Ejemplos
{
"diasAnticipacionAviso": 3,
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}{
"diasAnticipacionAviso": 3,
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}Request
{
"diasAnticipacionAviso": 3,
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}
Response
{
"diasAnticipacionAviso": 3,
"fechaActualizacion": "2026-09-02T14:30:00-06:00"
}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
Ejemplos
{
"id": 305,
"nombre": "María",
"lema": "Creciendo juntos, mano a mano",
"rfc": "CJF210416XYA",
"logoUrl": "Texto de logo url",
"activo": true
}Request
Response
{
"id": 305,
"nombre": "María",
"lema": "Creciendo juntos, mano a mano",
"rfc": "CJF210416XYA",
"logoUrl": "Texto de logo url",
"activo": true
}Respuestas
Empresa
▸PUT/api/empresaActualiza los datos del negocio
Cuerpo de la petición req
application/json → PeticionEmpresa
Ejemplos
{
"nombre": "María",
"lema": "Creciendo juntos, mano a mano",
"rfc": "CJF210416XYA"
}{
"id": 305,
"nombre": "María",
"lema": "Creciendo juntos, mano a mano",
"rfc": "CJF210416XYA",
"logoUrl": "Texto de logo url",
"activo": true
}Request
{
"nombre": "María",
"lema": "Creciendo juntos, mano a mano",
"rfc": "CJF210416XYA"
}
Response
{
"id": 305,
"nombre": "María",
"lema": "Creciendo juntos, mano a mano",
"rfc": "CJF210416XYA",
"logoUrl": "Texto de logo url",
"activo": true
}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
Ejemplos
Content-Type: multipart/form-data
archivo ine-frente.jpg (binario){
"id": 305,
"nombre": "María",
"lema": "Creciendo juntos, mano a mano",
"rfc": "CJF210416XYA",
"logoUrl": "Texto de logo url",
"activo": true
}Request
Content-Type: multipart/form-data
archivo ine-frente.jpg (binario)
Response
{
"id": 305,
"nombre": "María",
"lema": "Creciendo juntos, mano a mano",
"rfc": "CJF210416XYA",
"logoUrl": "Texto de logo url",
"activo": true
}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.