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.

Servidor local · http://localhost:8080 QA · https://qa.creciendojuntos.local (pendiente) Bearer JWT — todo salvo /api/auth/login Clave-Idempotencia en POST de dinero

API · 02Convenciones

TemaRegla
Prefijo y recursos/api · plural, español, minúsculas: /api/clientes, /api/prestamos (no /creditos)
Campos JSONcamelCase, igual en Java y TypeScript
EnumsTexto, MAYÚSCULAS, español — exactamente los valores del modelo v1.1
ImportesNúmero JSON con 2 decimales (1058.33), nunca cadena; el backend calcula con BigDecimal
TasasFracción por periodo: 0.0225 = 2.25 %
Fechas de negocioaaaa-mm-dd, sin hora ni zona — evita el desplazamiento de un día
Marcas de tiempoISO-8601 con offset: 2026-09-02T14:30:00-06:00
NulosCampo presente con null, nunca campo ausente
Paginación?page=0&size=50{ contenido, pagina, tamano, totalElementos, totalPaginas } — siempre de servidor
Orden?orden=campo,asc
ErroresSobre { codigo, mensaje, campos? } con el HTTP que corresponda — ver pestaña Errores
Archivosmultipart/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.hashSolicitud detecta 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_CONCURRENCIA con 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.

Servidor local · http://localhost:8080 QA · https://qa.creciendojuntos.local (pendiente) Bearer JWT — todo salvo /api/auth/login Clave-Idempotencia en POST de dinero

Autenticación

3 operaciones

Inicio de sesión, revalidación y cambio de contraseña. Pantalla 1.

POST/api/auth/loginInicia sesión y devuelve token y permisos efectivos
iniciarSesionpública · sin token

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

POST /api/auth/login
{
  "usuario": "reyna.janeth",
  "password": "********"
}
HTTP/1.1 200 application/json
{
  "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
  }
}
Reyna (ROLE_CAJA) inicia sesión: el token trae los permisos ya resueltos y la empresa para el encabezado de formatos.
Request
POST /api/auth/login
{
  "usuario": "reyna.janeth",
  "password": "********"
}
Response
HTTP/1.1 200 application/json
{
  "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

200

Sesión iniciada

400

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.

VALIDACION
401

CREDENCIALES_INVALIDAS · «Usuario o contraseña incorrectos»

CREDENCIALES_INVALIDAS
403

USUARIO_INACTIVO · Mensaje distinto al de credenciales: el operador necesita distinguir «me equivoqué de contraseña» de «me deshabilitaron».

USUARIO_INACTIVO
GET/api/auth/sesionRevalida la sesión y devuelve permisos vigentes
obtenerSesionVigente

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

GET /api/auth/sesion Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "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
  }
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/auth/sesion Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "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

200

Sesión vigente

401

TOKEN_EXPIRADO · El frontend redirige a login conservando la ruta de retorno

TOKEN_EXPIRADO
POST/api/auth/passwordCambia la contraseña del usuario autenticado
cambiarPassword

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

POST /api/auth/password Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "actual": "********",
  "nueva": "Clave#2026"
}
HTTP/1.1 204 No Content — sin cuerpo
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
POST /api/auth/password Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "actual": "********",
  "nueva": "Clave#2026"
}
Response
HTTP/1.1 204 No Content — sin cuerpo

Respuestas

204

Contraseña actualizada

400

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.

VALIDACION
401

TOKEN_EXPIRADO · El frontend redirige a login conservando la ruta de retorno

TOKEN_EXPIRADO

Usuarios y seguridad

8 operaciones

Usuarios, roles y permisos. Pantalla 2 · Anexo A §A.2.

GET/api/usuariosLista los usuarios del sistema
listarUsuarios

Parámetros

NombreEnTipoDescripción
pagequeryintegeropc

Índice de página, base 0

sizequeryintegeropc
ordenquerystringopc

Campo y dirección: fechaVencimiento,asc

Ejemplos

GET /api/usuarios?page=2&size=2&orden=Texto%20de%20orden Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "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"
    }
  ]
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/usuarios?page=2&size=2&orden=Texto%20de%20orden Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "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

200

Página de usuarios

403

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.

SIN_PERMISO
POST/api/usuariosDa de alta un usuario
crearUsuario

Cuerpo de la petición req

application/json  → PeticionUsuario

Ejemplos

POST /api/usuarios Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "username": "reyna.janeth",
  "nombre": "María",
  "password": "********",
  "activo": true,
  "roleIds": [
    2
  ]
}
HTTP/1.1 201 application/json
{
  "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"
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
POST /api/usuarios Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "username": "reyna.janeth",
  "nombre": "María",
  "password": "********",
  "activo": true,
  "roleIds": [
    2
  ]
}
Response
HTTP/1.1 201 application/json
{
  "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

201

Usuario creado

400

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.

VALIDACION
403

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.

SIN_PERMISO
PUT/api/usuarios/{id}Edita un usuario
actualizarUsuario

Parámetros

NombreEnTipoDescripción
idpathinteger · int64req

Cuerpo de la petición req

application/json  → PeticionUsuario

Ejemplos

PUT /api/usuarios/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "username": "reyna.janeth",
  "nombre": "María",
  "password": "********",
  "activo": true,
  "roleIds": [
    2
  ]
}
HTTP/1.1 200 application/json
{
  "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"
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
PUT /api/usuarios/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "username": "reyna.janeth",
  "nombre": "María",
  "password": "********",
  "activo": true,
  "roleIds": [
    2
  ]
}
Response
HTTP/1.1 200 application/json
{
  "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

200

Usuario actualizado

400

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.

VALIDACION
403

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.

SIN_PERMISO
404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
PATCH/api/usuarios/{id}/estadoActiva o desactiva un usuario
cambiarEstadoUsuario

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

NombreEnTipoDescripción
idpathinteger · int64req

Cuerpo de la petición req

application/json  → object

Ejemplos

PATCH /api/usuarios/42/estado Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "activo": true
}
HTTP/1.1 200 application/json
{
  "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"
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
PATCH /api/usuarios/42/estado Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "activo": true
}
Response
HTTP/1.1 200 application/json
{
  "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

200

Estado actualizado

403

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.

SIN_PERMISO
404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
PUT/api/usuarios/{id}/rolesReemplaza los roles asignados a un usuario
asignarRolesUsuario

Parámetros

NombreEnTipoDescripción
idpathinteger · int64req

Cuerpo de la petición req

application/json  → object

Ejemplos

PUT /api/usuarios/42/roles Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "roleIds": [
    2
  ]
}
HTTP/1.1 200 application/json
{
  "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"
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
PUT /api/usuarios/42/roles Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "roleIds": [
    2
  ]
}
Response
HTTP/1.1 200 application/json
{
  "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

200

Roles actualizados

403

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.

SIN_PERMISO
404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
PUT/api/usuarios/{id}/permisosDefine excepciones de permiso para un usuario
definirExcepcionesPermiso

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

NombreEnTipoDescripción
idpathinteger · int64req

Cuerpo de la petición req

application/json  → object

Ejemplos

PUT /api/usuarios/42/permisos Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "excepciones": [
    {
      "permisoId": 240,
      "concedido": true
    }
  ]
}
HTTP/1.1 200 application/json
{
  "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"
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
PUT /api/usuarios/42/permisos Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "excepciones": [
    {
      "permisoId": 240,
      "concedido": true
    }
  ]
}
Response
HTTP/1.1 200 application/json
{
  "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

200

Excepciones actualizadas

403

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.

SIN_PERMISO
GET/api/rolesCatálogo de roles
listarRoles

Ejemplos

GET /api/roles Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
[
  {
    "id": 305,
    "authority": "ROLE_CAJA",
    "descripcion": "Cobranza de lunes por la colonia Centro"
  }
]
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/roles Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
[
  {
    "id": 305,
    "authority": "ROLE_CAJA",
    "descripcion": "Cobranza de lunes por la colonia Centro"
  }
]

Respuestas

200

Roles

GET/api/permisosCatálogo de los 5 permisos del Anexo A §A.2
listarPermisos

Ejemplos

GET /api/permisos Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
[
  {
    "id": 305,
    "codigo": "LISTAR_CLIENTES",
    "descripcion": "Cobranza de lunes por la colonia Centro"
  }
]
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/permisos Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
[
  {
    "id": 305,
    "codigo": "LISTAR_CLIENTES",
    "descripcion": "Cobranza de lunes por la colonia Centro"
  }
]

Respuestas

200

Permisos

Catálogos

9 operaciones

Rutas y enumeraciones servidas por el backend para no duplicarlas en el frontend.

GET/api/rutasCatálogo de rutas de cobranza
listarRutas

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

NombreEnTipoDescripción
soloActivasquerybooleanopc

Ejemplos

GET /api/rutas?soloActivas=true Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
[
  {
    "id": 305,
    "codigo": "Ruta 1 LUNES",
    "descripcion": "Cobranza de lunes por la colonia Centro",
    "activo": true,
    "totalCreditos": 2
  }
]
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/rutas?soloActivas=true Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
[
  {
    "id": 305,
    "codigo": "Ruta 1 LUNES",
    "descripcion": "Cobranza de lunes por la colonia Centro",
    "activo": true,
    "totalCreditos": 2
  }
]

Respuestas

200

Rutas

POST/api/rutasCrea una ruta
crearRuta

Cuerpo de la petición req

application/json  → PeticionRuta

Ejemplos

POST /api/rutas Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "codigo": "Ruta 1 LUNES",
  "descripcion": "Cobranza de lunes por la colonia Centro"
}
HTTP/1.1 201 application/json
{
  "id": 305,
  "codigo": "Ruta 1 LUNES",
  "descripcion": "Cobranza de lunes por la colonia Centro",
  "activo": true,
  "totalCreditos": 2
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
POST /api/rutas Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "codigo": "Ruta 1 LUNES",
  "descripcion": "Cobranza de lunes por la colonia Centro"
}
Response
HTTP/1.1 201 application/json
{
  "id": 305,
  "codigo": "Ruta 1 LUNES",
  "descripcion": "Cobranza de lunes por la colonia Centro",
  "activo": true,
  "totalCreditos": 2
}

Respuestas

201

Ruta creada

400

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.

VALIDACION
403

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.

SIN_PERMISO
PUT/api/rutas/{id}Edita una ruta
actualizarRuta

Parámetros

NombreEnTipoDescripción
idpathinteger · int64req

Cuerpo de la petición req

application/json  → PeticionRuta

Ejemplos

PUT /api/rutas/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "codigo": "Ruta 1 LUNES",
  "descripcion": "Cobranza de lunes por la colonia Centro"
}
HTTP/1.1 200 application/json
{
  "id": 305,
  "codigo": "Ruta 1 LUNES",
  "descripcion": "Cobranza de lunes por la colonia Centro",
  "activo": true,
  "totalCreditos": 2
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
PUT /api/rutas/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "codigo": "Ruta 1 LUNES",
  "descripcion": "Cobranza de lunes por la colonia Centro"
}
Response
HTTP/1.1 200 application/json
{
  "id": 305,
  "codigo": "Ruta 1 LUNES",
  "descripcion": "Cobranza de lunes por la colonia Centro",
  "activo": true,
  "totalCreditos": 2
}

Respuestas

200

Ruta actualizada

400

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.

VALIDACION
404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
PATCH/api/rutas/{id}/estadoActiva o desactiva una ruta
cambiarEstadoRuta

Parámetros

NombreEnTipoDescripción
idpathinteger · int64req

Cuerpo de la petición req

application/json  → object

Ejemplos

PATCH /api/rutas/42/estado Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "activo": true
}
HTTP/1.1 200 application/json
{
  "id": 305,
  "codigo": "Ruta 1 LUNES",
  "descripcion": "Cobranza de lunes por la colonia Centro",
  "activo": true,
  "totalCreditos": 2
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
PATCH /api/rutas/42/estado Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "activo": true
}
Response
HTTP/1.1 200 application/json
{
  "id": 305,
  "codigo": "Ruta 1 LUNES",
  "descripcion": "Cobranza de lunes por la colonia Centro",
  "activo": true,
  "totalCreditos": 2
}

Respuestas

200

Estado actualizado

404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
409

La ruta tiene créditos asignados y no puede desactivarse

GET/api/catalogos/periodicidadesPeriodicidades, acotadas por ámbito
listarPeriodicidades

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

NombreEnTipoDescripción
ambitoquerystringopc

Ejemplos

GET /api/catalogos/periodicidades?ambito=ALTA Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
[
  {
    "valor": "Texto de valor",
    "etiqueta": "Texto de etiqueta",
    "activo": true
  }
]
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/catalogos/periodicidades?ambito=ALTA Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
[
  {
    "valor": "Texto de valor",
    "etiqueta": "Texto de etiqueta",
    "activo": true
  }
]

Respuestas

200

Periodicidades

GET/api/catalogos/tipos-pagoTipos de pago
listarTiposPago

En la operación real de campo prácticamente todo es EFECTIVO; el frontend lo deja preseleccionado.

Ejemplos

GET /api/catalogos/tipos-pago Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
[
  {
    "valor": "Texto de valor",
    "etiqueta": "Texto de etiqueta",
    "activo": true
  }
]
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/catalogos/tipos-pago Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
[
  {
    "valor": "Texto de valor",
    "etiqueta": "Texto de etiqueta",
    "activo": true
  }
]

Respuestas

200

Tipos de pago

GET/api/catalogos/tipos-movimiento-cajaTipos de movimiento de caja, con su naturaleza y si son manuales
listarTiposMovimientoCaja

Ejemplos

GET /api/catalogos/tipos-movimiento-caja Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
[
  {
    "valor": "Texto de valor",
    "etiqueta": "Texto de etiqueta",
    "activo": true,
    "naturaleza": "INGRESO",
    "manual": false
  }
]
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/catalogos/tipos-movimiento-caja Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
[
  {
    "valor": "Texto de valor",
    "etiqueta": "Texto de etiqueta",
    "activo": true,
    "naturaleza": "INGRESO",
    "manual": false
  }
]

Respuestas

200

Tipos de movimiento de caja

GET/api/catalogos/tipos-documentoTipos de documento admitidos en el expediente
listarTiposDocumento

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

GET /api/catalogos/tipos-documento Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
[
  {
    "valor": "Texto de valor",
    "etiqueta": "Texto de etiqueta",
    "activo": true,
    "obligatorio": true
  }
]
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/catalogos/tipos-documento Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
[
  {
    "valor": "Texto de valor",
    "etiqueta": "Texto de etiqueta",
    "activo": true,
    "obligatorio": true
  }
]

Respuestas

200

Tipos de documento

GET/api/catalogos/tipos-identificacionTipos de identificación
listarTiposIdentificacion

[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

GET /api/catalogos/tipos-identificacion Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
[
  {
    "valor": "Texto de valor",
    "etiqueta": "Texto de etiqueta",
    "activo": true
  }
]
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/catalogos/tipos-identificacion Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
[
  {
    "valor": "Texto de valor",
    "etiqueta": "Texto de etiqueta",
    "activo": true
  }
]

Respuestas

200

Tipos de identificación

Clientes

6 operaciones

Pantallas 3 y 4.

GET/api/clientesListado de clientes con búsqueda, filtro y paginación
listarClientes

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

NombreEnTipoDescripción
busquedaquerystringopc

Texto libre sobre nombre completo y número de identificación

rutaIdqueryinteger · int64opc
estadoqueryClienteStatusopc
pagequeryintegeropc

Índice de página, base 0

sizequeryintegeropc
ordenquerystringopc

Campo y dirección: fechaVencimiento,asc

Ejemplos

GET /api/clientes?busqueda=Texto%20de%20busqueda&rutaId=717&estado=ACTIVO&page=2&size=2 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "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
    }
  ]
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/clientes?busqueda=Texto%20de%20busqueda&rutaId=717&estado=ACTIVO&page=2&size=2 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "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

200

Página de clientes

403

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.

SIN_PERMISO
POST/api/clientesDa de alta un cliente
crearCliente

Cuerpo de la petición req

application/json  → PeticionCliente

Ejemplos

POST /api/clientes Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "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"
}
HTTP/1.1 201 application/json
{
  "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"
}
Alta de una cliente nueva en la ruta 1 con su identificación INE.
Request
POST /api/clientes Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "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
HTTP/1.1 201 application/json
{
  "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

201

Cliente creado

400

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.

VALIDACION
403

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.

SIN_PERMISO
GET/api/clientes/{id}Ficha completa de un cliente
obtenerCliente

Parámetros

NombreEnTipoDescripción
idpathinteger · int64req

Ejemplos

GET /api/clientes/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "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"
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/clientes/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "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

200

Cliente

404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
PUT/api/clientes/{id}Edita un cliente
actualizarCliente

Parámetros

NombreEnTipoDescripción
idpathinteger · int64req

Cuerpo de la petición req

application/json  → PeticionCliente

Ejemplos

PUT /api/clientes/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "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"
}
HTTP/1.1 200 application/json
{
  "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"
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
PUT /api/clientes/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "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
HTTP/1.1 200 application/json
{
  "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

200

Cliente actualizado

400

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.

VALIDACION
404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
GET/api/clientes/{id}/prestamosCréditos de un cliente
listarPrestamosDeCliente

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

NombreEnTipoDescripción
idpathinteger · int64req

Ejemplos

GET /api/clientes/42/prestamos Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
[
  {
    "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
  }
]
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/clientes/42/prestamos Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
[
  {
    "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

200

Créditos del cliente

404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
GET/api/clientes/{id}/estado-cuentaEstado de cuenta del cliente (entidad Movimiento)
obtenerEstadoCuentaCliente

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

NombreEnTipoDescripción
idpathinteger · int64req
desdequerystring · dateopc
hastaquerystring · dateopc
pagequeryintegeropc

Índice de página, base 0

sizequeryintegeropc

Ejemplos

GET /api/clientes/42/estado-cuenta?desde=2026-09-02&hasta=2026-09-02&page=2&size=2 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "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
    }
  ]
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/clientes/42/estado-cuenta?desde=2026-09-02&hasta=2026-09-02&page=2&size=2 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "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

200

Movimientos del estado de cuenta

404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO

Expediente digital

8 operaciones

Pantalla 5 · Anexo C §C.1.1. Único grupo que sube archivos.

GET/api/clientes/{id}/documentosDocumentos del expediente de un cliente
listarDocumentosCliente

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

NombreEnTipoDescripción
idpathinteger · int64req
tipoDocumentoqueryTipoDocumentoopc
incluirInactivosquerybooleanopc

Ejemplos

GET /api/clientes/42/documentos?tipoDocumento=IDENTIFICACION_FRENTE&incluirInactivos=false Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
[
  {
    "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"
  }
]
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/clientes/42/documentos?tipoDocumento=IDENTIFICACION_FRENTE&incluirInactivos=false Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
[
  {
    "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

200

Documentos

403

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.

SIN_PERMISO
404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
POST/api/clientes/{id}/documentosSube un documento al expediente
subirDocumentoClientemultipart

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

NombreEnTipoDescripción
idpathinteger · int64req

Cuerpo de la petición req

multipart/form-data  → object

Ejemplos

POST /api/clientes/42/documentos Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Content-Type: multipart/form-data

archivo         ine-frente.jpg (binario)
tipoDocumento   IDENTIFICACION_FRENTE
descripcion     Cobranza de lunes por la colonia Centro
HTTP/1.1 201 application/json
{
  "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"
}
La cobradora sube el INE frontal de María (JPG de 240 KB). Un archivo por petición; la validación previa la hace el navegador y el servidor revalida.
Request
POST /api/clientes/42/documentos Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Content-Type: multipart/form-data

archivo         ine-frente.jpg (binario)
tipoDocumento   IDENTIFICACION_FRENTE
descripcion     Cobranza de lunes por la colonia Centro
Response
HTTP/1.1 201 application/json
{
  "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

201

Documento cargado

400

ARCHIVO_VACIO · 0 bytes o multipart mal formado

ARCHIVO_VACIO
403

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.

SIN_PERMISO
413

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.

ARCHIVO_MUY_GRANDE
415

FORMATO_NO_ADMITIDO · La respuesta nombra los formatos válidos

FORMATO_NO_ADMITIDO
GET/api/documentos/{id}Metadatos de un documento
obtenerDocumento

Parámetros

NombreEnTipoDescripción
idpathinteger · int64req

Ejemplos

GET /api/documentos/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "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"
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/documentos/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "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

200

Documento

404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
PUT/api/documentos/{id}Reclasifica un documento sin cambiar el archivo
actualizarDocumento

Parámetros

NombreEnTipoDescripción
idpathinteger · int64req

Cuerpo de la petición req

application/json  → object

Ejemplos

PUT /api/documentos/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "tipoDocumento": "IDENTIFICACION_FRENTE",
  "descripcion": "Cobranza de lunes por la colonia Centro"
}
HTTP/1.1 200 application/json
{
  "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"
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
PUT /api/documentos/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "tipoDocumento": "IDENTIFICACION_FRENTE",
  "descripcion": "Cobranza de lunes por la colonia Centro"
}
Response
HTTP/1.1 200 application/json
{
  "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

200

Documento actualizado

404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
DELETE/api/documentos/{id}Baja lógica de un documento
eliminarDocumento

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

NombreEnTipoDescripción
idpathinteger · int64req

Ejemplos

DELETE /api/documentos/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 204 No Content — sin cuerpo
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
DELETE /api/documentos/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 204 No Content — sin cuerpo

Respuestas

204

Documento dado de baja

403

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.

SIN_PERMISO
404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
GET/api/documentos/{id}/contenidoDescarga o previsualiza el archivo, con la sesión validada
obtenerContenidoDocumento

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

NombreEnTipoDescripción
idpathinteger · int64req
descargaquerybooleanopc

Ejemplos

GET /api/documentos/42/contenido?descarga=false Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 · application/pdf — binario generado por el servidor
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/documentos/42/contenido?descarga=false Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 · application/pdf — binario generado por el servidor

Respuestas

200

Bytes del archivo

401

TOKEN_EXPIRADO · El frontend redirige a login conservando la ruta de retorno

TOKEN_EXPIRADO
403

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.

SIN_PERMISO
404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
POST/api/documentos/{id}/reemplazoReemplaza el archivo de un documento
reemplazarDocumentomultipart

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

NombreEnTipoDescripción
idpathinteger · int64req

Cuerpo de la petición req

multipart/form-data  → object

Ejemplos

POST /api/documentos/42/reemplazo Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Content-Type: multipart/form-data

archivo         ine-frente.jpg (binario)
descripcion     Cobranza de lunes por la colonia Centro
HTTP/1.1 201 application/json
{
  "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"
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
POST /api/documentos/42/reemplazo Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Content-Type: multipart/form-data

archivo         ine-frente.jpg (binario)
descripcion     Cobranza de lunes por la colonia Centro
Response
HTTP/1.1 201 application/json
{
  "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

201

Documento reemplazado

400

ARCHIVO_VACIO · 0 bytes o multipart mal formado

ARCHIVO_VACIO
403

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.

SIN_PERMISO
413

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.

ARCHIVO_MUY_GRANDE
415

FORMATO_NO_ADMITIDO · La respuesta nombra los formatos válidos

FORMATO_NO_ADMITIDO
GET/api/clientes/{id}/expediente/estadoEstado de completitud del expediente
obtenerEstadoExpediente

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

NombreEnTipoDescripción
idpathinteger · int64req

Ejemplos

GET /api/clientes/42/expediente/estado Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "estado": "INCOMPLETO",
  "faltantes": [
    "COMPROBANTE_DOMICILIO",
    "COMPROBANTE_INGRESOS"
  ]
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/clientes/42/expediente/estado Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "estado": "INCOMPLETO",
  "faltantes": [
    "COMPROBANTE_DOMICILIO",
    "COMPROBANTE_INGRESOS"
  ]
}

Respuestas

200

Estado del expediente

404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO

Préstamos y amortización

7 operaciones

Pantallas 6 y 8.

POST/api/prestamos/simulacionSimula el plan de pagos sin persistir nada
simularAmortizacion

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

POST /api/prestamos/simulacion Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "capital": 10000,
  "tasaInteres": 0.0225,
  "tasaInteresMoratorio": 0.01,
  "numeroCuotas": 12,
  "periodicidad": "SEMANAL",
  "tipoCalculo": "CAPITAL_MAS_INTERES",
  "fechaPrimerPago": "2026-09-07"
}
HTTP/1.1 200 application/json
{
  "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
    }
  ]
}
Vista previa de la pantalla 6: $10,000 a 12 semanas con tasa de 2.25 %. Se muestran 3 de las 12 cuotas. No persiste nada.
Request
POST /api/prestamos/simulacion Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "capital": 10000,
  "tasaInteres": 0.0225,
  "tasaInteresMoratorio": 0.01,
  "numeroCuotas": 12,
  "periodicidad": "SEMANAL",
  "tipoCalculo": "CAPITAL_MAS_INTERES",
  "fechaPrimerPago": "2026-09-07"
}
Response
HTTP/1.1 200 application/json
{
  "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

200

Plan simulado

400

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.

VALIDACION
409

MONTO_INVALIDO · Monto menor o igual a cero, o que excede lo permitido

MONTO_INVALIDO
GET/api/prestamosListado de créditos
listarPrestamos

Parámetros

NombreEnTipoDescripción
clienteIdqueryinteger · int64opc
rutaIdqueryinteger · int64opc
statusqueryPrestamoStatusopc
pagequeryintegeropc

Índice de página, base 0

sizequeryintegeropc
ordenquerystringopc

Campo y dirección: fechaVencimiento,asc

Ejemplos

GET /api/prestamos?clienteId=213&rutaId=717&status=ACTIVO&page=2&size=2 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "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
    }
  ]
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/prestamos?clienteId=213&rutaId=717&status=ACTIVO&page=2&size=2 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "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

200

Página de créditos

POST/api/prestamosRegistra el crédito y genera el plan de pagos completo
crearPrestamoClave-Idempotencia

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

NombreEnTipoDescripción
Clave-Idempotenciaheaderstring · uuidreq

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 IdempotenciaOperacion (clave única, hashSolicitud, httpStatus, respuestaJson, expiraEn). Dos consecuencias que el frontend necesita acordadas:

  • hashSolicitud implica que la misma clave con un cuerpo distinto es un caso detectable. Regla del frontend: una clave por operación, no por pantalla. Si el operador corrige el monto tras un fallo, es una operación nueva y genera clave nueva; solo el reintento del mismo envío reutiliza la clave. Falta acordar qué responde el servidor ante clave repetida con cuerpo distinto — se propone 409 CONFLICTO_CONCURRENCIA con mensaje explícito, no un 400 genérico.
  • expiraEn implica una ventana de retención. Falta acordar su duración. Debe superar con holgura el tiempo que un operador tarda en reintentar tras perder la red; se propone 24 horas.

Cuerpo de la petición req

application/json  → PeticionPrestamo

Ejemplos

POST /api/prestamos Authorization: Bearer eyJhbGciOiJIUzI1NiJ9… Clave-Idempotencia: 7f9c24e5-1b3d-4a2f-9e8c-05d1a6b7c3f9
{
  "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"
}
HTTP/1.1 201 application/json
{
  "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
}
Alta del crédito de María López en una sola transacción: snapshot de condiciones, 12 cuotas generadas, movimiento de estado de cuenta y egreso de caja. Se muestran 2 de las 12 cuotas.
Request
POST /api/prestamos Authorization: Bearer eyJhbGciOiJIUzI1NiJ9… Clave-Idempotencia: 7f9c24e5-1b3d-4a2f-9e8c-05d1a6b7c3f9
{
  "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
HTTP/1.1 200 application/json
{
  "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

201

Crédito creado con su plan de pagos

400

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.

VALIDACION
403

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.

SIN_PERMISO
409

EXPEDIENTE_INCOMPLETO (REGLA ABIERTA #13). Los tipos faltantes viajan en campos[] para que el frontend pueda listarlos y ofrecer ir al expediente.

EXPEDIENTE_INCOMPLETO
GET/api/prestamos/{id}Ficha de un crédito
obtenerPrestamo

Parámetros

NombreEnTipoDescripción
idpathinteger · int64req

Ejemplos

GET /api/prestamos/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "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"
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/prestamos/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "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

200

Crédito

404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
GET/api/prestamos/{id}/amortizacionTabla de amortización con estado, saldo, atraso y mora al día
obtenerAmortizacion

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

NombreEnTipoDescripción
idpathinteger · int64req
estadoqueryEstadoCuotaopc

Filtra por estado derivado. Sostiene las pestañas Todas / Pendientes / Vencidas

Ejemplos

GET /api/prestamos/42/amortizacion?estado=VENCIDA Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "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
  }
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/prestamos/42/amortizacion?estado=VENCIDA Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "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

200

Plan de pagos

404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
409

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.

PRESTAMO_SIN_PLAN
GET/api/prestamos/{id}/historialCuotas y pagos aplicados, para la pantalla 9
obtenerHistorialPrestamo

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

NombreEnTipoDescripción
idpathinteger · int64req

Ejemplos

GET /api/prestamos/42/historial Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "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
    }
  ]
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/prestamos/42/historial Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "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

200

Historial

404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
PATCH/api/prestamos/{id}/statusCambia el estado del crédito (cancelación)
cambiarStatusPrestamo

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

NombreEnTipoDescripción
idpathinteger · int64req

Cuerpo de la petición req

application/json  → object

Ejemplos

PATCH /api/prestamos/42/status Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "status": "CANCELADO",
  "motivo": "Crédito cancelado por duplicado en el alta"
}
HTTP/1.1 200 application/json
{
  "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"
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
PATCH /api/prestamos/42/status Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "status": "CANCELADO",
  "motivo": "Crédito cancelado por duplicado en el alta"
}
Response
HTTP/1.1 200 application/json
{
  "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

200

Estado actualizado

403

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.

SIN_PERMISO
404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO

Avales

4 operaciones

Pantalla 7 · Anexo C §C.1.2.

GET/api/prestamos/{id}/avalesAvales de un crédito
listarAvales

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

NombreEnTipoDescripción
idpathinteger · int64req

Ejemplos

GET /api/prestamos/42/avales Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
[
  {
    "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"
  }
]
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/prestamos/42/avales Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
[
  {
    "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

200

Avales

404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
POST/api/prestamos/{id}/avalesAgrega un aval al crédito
crearAval

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

NombreEnTipoDescripción
idpathinteger · int64req

Cuerpo de la petición req

application/json  → PeticionAval

Ejemplos

POST /api/prestamos/42/avales Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "nombreCompleto": "Jorge Méndez Salas",
  "telefono": "55 8765 4321",
  "direccion": "C. Hidalgo 17, Col. Centro",
  "curp": "MESJ750612HDFRRN08",
  "tipoIdentificacion": "LICENCIA",
  "numeroIdentificacion": "2045871"
}
HTTP/1.1 201 application/json
{
  "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"
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
POST /api/prestamos/42/avales Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "nombreCompleto": "Jorge Méndez Salas",
  "telefono": "55 8765 4321",
  "direccion": "C. Hidalgo 17, Col. Centro",
  "curp": "MESJ750612HDFRRN08",
  "tipoIdentificacion": "LICENCIA",
  "numeroIdentificacion": "2045871"
}
Response
HTTP/1.1 201 application/json
{
  "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

201

Aval creado

400

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.

VALIDACION
404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
PUT/api/avales/{id}Edita un aval
actualizarAval

Parámetros

NombreEnTipoDescripción
idpathinteger · int64req

Cuerpo de la petición req

application/json  → PeticionAval

Ejemplos

PUT /api/avales/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "nombreCompleto": "María López Rivera",
  "telefono": "55 1234 5678",
  "direccion": "Av. Reforma 42, Col. Centro",
  "curp": "LORM800214MDFRNS09",
  "tipoIdentificacion": "INE",
  "numeroIdentificacion": "4123 4567 8901"
}
HTTP/1.1 200 application/json
{
  "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"
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
PUT /api/avales/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "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
HTTP/1.1 200 application/json
{
  "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

200

Aval actualizado

400

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.

VALIDACION
404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
DELETE/api/avales/{id}Elimina un aval
eliminarAval

Requiere permiso ELIMINAR.

Parámetros

NombreEnTipoDescripción
idpathinteger · int64req

Ejemplos

DELETE /api/avales/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 204 No Content — sin cuerpo
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
DELETE /api/avales/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 204 No Content — sin cuerpo

Respuestas

204

Aval eliminado

403

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.

SIN_PERMISO
404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO

Cobranza y pagos

8 operaciones

Pantallas 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
previsualizarPago

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 #5condonarMora. Si la mora no es condonable por el operador, el campo se retira del contrato y de la pantalla.

Parámetros

NombreEnTipoDescripción
idpathinteger · int64req

Cuerpo de la petición req

application/json  → PeticionPrevisualizacionPago

Ejemplos

POST /api/prestamos/42/pagos/previsualizacion Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "fechaPago": "2026-09-02",
  "monto": 2500,
  "condonarMora": false
}
HTTP/1.1 200 application/json
{
  "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
}
La cobradora captura $2,500 recibidos y ve la distribución antes de confirmar: mora, interés, capital y el saldo que queda en la cuota 7. No persiste nada.
Request
POST /api/prestamos/42/pagos/previsualizacion Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "fechaPago": "2026-09-02",
  "monto": 2500,
  "condonarMora": false
}
Response
HTTP/1.1 200 application/json
{
  "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

200

Distribución simulada

400

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.

VALIDACION
404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
409

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.

MONTO_INVALIDOPRESTAMO_LIQUIDADOPRESTAMO_SIN_PLANCONFLICTO_CONCURRENCIA
GET/api/prestamos/{id}/pagosPagos registrados sobre un crédito
listarPagosPrestamo

Parámetros

NombreEnTipoDescripción
idpathinteger · int64req
pagequeryintegeropc

Índice de página, base 0

sizequeryintegeropc

Ejemplos

GET /api/prestamos/42/pagos?page=2&size=2 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "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
    }
  ]
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/prestamos/42/pagos?page=2&size=2 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "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

200

Página de pagos

POST/api/prestamos/{id}/pagosRegistra un pago y lo distribuye sobre las cuotas
registrarPagoClave-Idempotencia

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

NombreEnTipoDescripción
idpathinteger · int64req
Clave-Idempotenciaheaderstring · uuidreq

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 IdempotenciaOperacion (clave única, hashSolicitud, httpStatus, respuestaJson, expiraEn). Dos consecuencias que el frontend necesita acordadas:

  • hashSolicitud implica que la misma clave con un cuerpo distinto es un caso detectable. Regla del frontend: una clave por operación, no por pantalla. Si el operador corrige el monto tras un fallo, es una operación nueva y genera clave nueva; solo el reintento del mismo envío reutiliza la clave. Falta acordar qué responde el servidor ante clave repetida con cuerpo distinto — se propone 409 CONFLICTO_CONCURRENCIA con mensaje explícito, no un 400 genérico.
  • expiraEn implica una ventana de retención. Falta acordar su duración. Debe superar con holgura el tiempo que un operador tarda en reintentar tras perder la red; se propone 24 horas.

Cuerpo de la petición req

application/json  → PeticionPago

Ejemplos

POST /api/prestamos/42/pagos Authorization: Bearer eyJhbGciOiJIUzI1NiJ9… Clave-Idempotencia: 7f9c24e5-1b3d-4a2f-9e8c-05d1a6b7c3f9
{
  "fechaPago": "2026-09-02",
  "monto": 2500,
  "condonarMora": false,
  "tipoPago": "EFECTIVO",
  "referencia": "RUTA-1-0209",
  "observaciones": "Pago en puerta"
}
HTTP/1.1 201 application/json
{
  "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
}
Se confirma el pago de $2,500: una transacción crea el pago, lo distribuye y genera un único movimiento de caja con utilidad de $325 (interés + mora cobrados).
Request
POST /api/prestamos/42/pagos Authorization: Bearer eyJhbGciOiJIUzI1NiJ9… Clave-Idempotencia: 7f9c24e5-1b3d-4a2f-9e8c-05d1a6b7c3f9
{
  "fechaPago": "2026-09-02",
  "monto": 2500,
  "condonarMora": false,
  "tipoPago": "EFECTIVO",
  "referencia": "RUTA-1-0209",
  "observaciones": "Pago en puerta"
}
Response
HTTP/1.1 200 application/json
{
  "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

201

Pago registrado y aplicado

400

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.

VALIDACION
403

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.

SIN_PERMISO
409

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.

MONTO_INVALIDOPRESTAMO_LIQUIDADOPRESTAMO_SIN_PLANCONFLICTO_CONCURRENCIA
GET/api/pagos/{id}Detalle de un pago con sus aplicaciones
obtenerPago

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

NombreEnTipoDescripción
idpathinteger · int64req

Ejemplos

GET /api/pagos/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "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
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/pagos/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "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

200

Pago con detalle

404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
POST/api/pagos/{id}/cancelacionCancela un pago registrado por error
cancelarPagoClave-Idempotencia

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

NombreEnTipoDescripción
idpathinteger · int64req
Clave-Idempotenciaheaderstring · uuidreq

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 IdempotenciaOperacion (clave única, hashSolicitud, httpStatus, respuestaJson, expiraEn). Dos consecuencias que el frontend necesita acordadas:

  • hashSolicitud implica que la misma clave con un cuerpo distinto es un caso detectable. Regla del frontend: una clave por operación, no por pantalla. Si el operador corrige el monto tras un fallo, es una operación nueva y genera clave nueva; solo el reintento del mismo envío reutiliza la clave. Falta acordar qué responde el servidor ante clave repetida con cuerpo distinto — se propone 409 CONFLICTO_CONCURRENCIA con mensaje explícito, no un 400 genérico.
  • expiraEn implica una ventana de retención. Falta acordar su duración. Debe superar con holgura el tiempo que un operador tarda en reintentar tras perder la red; se propone 24 horas.

Cuerpo de la petición req

application/json  → object

Ejemplos

POST /api/pagos/42/cancelacion Authorization: Bearer eyJhbGciOiJIUzI1NiJ9… Clave-Idempotencia: 7f9c24e5-1b3d-4a2f-9e8c-05d1a6b7c3f9
{
  "motivo": "Pago registrado dos veces por error de captura"
}
HTTP/1.1 200 application/json
{
  "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
}
Se cancela el pago 8840 por duplicado: la reversión recalcula cuotas y saldos; el pago sigue visible en el historial, marcado.
Request
POST /api/pagos/42/cancelacion Authorization: Bearer eyJhbGciOiJIUzI1NiJ9… Clave-Idempotencia: 7f9c24e5-1b3d-4a2f-9e8c-05d1a6b7c3f9
{
  "motivo": "Pago registrado dos veces por error de captura"
}
Response
HTTP/1.1 200 application/json
{
  "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

200

Pago cancelado

403

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.

SIN_PERMISO
404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
409

CANCELACION_NO_PERMITIDA o PAGO_YA_CANCELADO. Debe explicar el motivo —no un 403 genérico—: el operador necesita entender por qué no puede.

CANCELACION_NO_PERMITIDAPAGO_YA_CANCELADO
POST/api/prestamos/{id}/abonos-capitalAbono directo al capital, fuera del plan de cuotas
registrarAbonoCapitalClave-Idempotencia

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 monto
  • REDUCE_MONTO — reduce el monto de las cuotas restantes, manteniendo el número
  • SOLO_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

NombreEnTipoDescripción
idpathinteger · int64req
Clave-Idempotenciaheaderstring · uuidreq

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 IdempotenciaOperacion (clave única, hashSolicitud, httpStatus, respuestaJson, expiraEn). Dos consecuencias que el frontend necesita acordadas:

  • hashSolicitud implica que la misma clave con un cuerpo distinto es un caso detectable. Regla del frontend: una clave por operación, no por pantalla. Si el operador corrige el monto tras un fallo, es una operación nueva y genera clave nueva; solo el reintento del mismo envío reutiliza la clave. Falta acordar qué responde el servidor ante clave repetida con cuerpo distinto — se propone 409 CONFLICTO_CONCURRENCIA con mensaje explícito, no un 400 genérico.
  • expiraEn implica una ventana de retención. Falta acordar su duración. Debe superar con holgura el tiempo que un operador tarda en reintentar tras perder la red; se propone 24 horas.

Cuerpo de la petición req

application/json  → PeticionAbonoCapital

Ejemplos

POST /api/prestamos/42/abonos-capital Authorization: Bearer eyJhbGciOiJIUzI1NiJ9… Clave-Idempotencia: 7f9c24e5-1b3d-4a2f-9e8c-05d1a6b7c3f9
{
  "fecha": "2026-09-02",
  "monto": 3000,
  "aplicacion": "REDUCE_CUOTAS",
  "observaciones": "Abono acordado"
}
HTTP/1.1 201 application/json
{
  "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
    }
  ]
}
Abono de $3,000 a capital con la variante REDUCE_CUOTAS (regla abierta #1 aún abierta): la respuesta trae el plan resultante para el antes y el después.
Request
POST /api/prestamos/42/abonos-capital Authorization: Bearer eyJhbGciOiJIUzI1NiJ9… Clave-Idempotencia: 7f9c24e5-1b3d-4a2f-9e8c-05d1a6b7c3f9
{
  "fecha": "2026-09-02",
  "monto": 3000,
  "aplicacion": "REDUCE_CUOTAS",
  "observaciones": "Abono acordado"
}
Response
HTTP/1.1 200 application/json
{
  "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

201

Abono a capital registrado

400

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.

VALIDACION
403

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.

SIN_PERMISO
409

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.

MONTO_INVALIDOPRESTAMO_LIQUIDADOPRESTAMO_SIN_PLANCONFLICTO_CONCURRENCIA
GET/api/cobranza/vencidasCuotas con fecha de vencimiento cumplida y saldo pendiente
listarCuotasVencidas

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

NombreEnTipoDescripción
rutaIdqueryinteger · int64opc
fechaquerystring · dateopc

Fecha de corte. Por omisión, hoy

clienteIdqueryinteger · int64opc
pagequeryintegeropc

Índice de página, base 0

sizequeryintegeropc
ordenquerystringopc

Campo y dirección: fechaVencimiento,asc

Ejemplos

GET /api/cobranza/vencidas?rutaId=717&fecha=2026-09-02&clienteId=213&page=2&size=2 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "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
  }
}
La lista de trabajo del cobrador de la Ruta 1 al 02/09/2026: 23 cuotas vencidas por $41,320, con $1,240 de mora al día. Totales de la consulta completa, no de la página.
Request
GET /api/cobranza/vencidas?rutaId=717&fecha=2026-09-02&clienteId=213&page=2&size=2 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "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

200

Cuotas vencidas

403

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.

SIN_PERMISO
GET/api/cobranza/proximasCuotas por vencer dentro de los días configurados
listarCuotasProximas

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

NombreEnTipoDescripción
rutaIdqueryinteger · int64opc
diasqueryintegeropc

Días de anticipación. Por omisión, el valor de Configuracion

pagequeryintegeropc

Índice de página, base 0

sizequeryintegeropc
ordenquerystringopc

Campo y dirección: fechaVencimiento,asc

Ejemplos

GET /api/cobranza/proximas?rutaId=717&dias=3&page=2&size=2&orden=Texto%20de%20orden Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "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
  }
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/cobranza/proximas?rutaId=717&dias=3&page=2&size=2&orden=Texto%20de%20orden Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "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

200

Cuotas próximas a vencer

403

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.

SIN_PERMISO

Caja

4 operaciones

Pantalla 15 · Anexo A §A.4.

GET/api/caja/movimientosMovimientos de caja del negocio
listarMovimientosCaja

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

NombreEnTipoDescripción
desdequerystring · dateopc
hastaquerystring · dateopc
tipoqueryTipoMovimientoCajaopc
soloManualesquerybooleanopc

Solo los movimientos con generadoPorSistema = false

pagequeryintegeropc

Índice de página, base 0

sizequeryintegeropc
ordenquerystringopc

Campo y dirección: fechaVencimiento,asc

Ejemplos

GET /api/caja/movimientos?desde=2026-09-02&hasta=2026-09-02&tipo=INGRESO_PAGO_PRESTAMO&soloManuales=false&page=2 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "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
    }
  ]
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/caja/movimientos?desde=2026-09-02&hasta=2026-09-02&tipo=INGRESO_PAGO_PRESTAMO&soloManuales=false&page=2 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "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

200

Página de movimientos de caja

403

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.

SIN_PERMISO
POST/api/caja/movimientosRegistra un movimiento de caja manual
crearMovimientoCajaClave-Idempotencia

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

NombreEnTipoDescripción
Clave-Idempotenciaheaderstring · uuidreq

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 IdempotenciaOperacion (clave única, hashSolicitud, httpStatus, respuestaJson, expiraEn). Dos consecuencias que el frontend necesita acordadas:

  • hashSolicitud implica que la misma clave con un cuerpo distinto es un caso detectable. Regla del frontend: una clave por operación, no por pantalla. Si el operador corrige el monto tras un fallo, es una operación nueva y genera clave nueva; solo el reintento del mismo envío reutiliza la clave. Falta acordar qué responde el servidor ante clave repetida con cuerpo distinto — se propone 409 CONFLICTO_CONCURRENCIA con mensaje explícito, no un 400 genérico.
  • expiraEn implica una ventana de retención. Falta acordar su duración. Debe superar con holgura el tiempo que un operador tarda en reintentar tras perder la red; se propone 24 horas.

Cuerpo de la petición req

application/json  → PeticionMovimientoCaja

Ejemplos

POST /api/caja/movimientos Authorization: Bearer eyJhbGciOiJIUzI1NiJ9… Clave-Idempotencia: 7f9c24e5-1b3d-4a2f-9e8c-05d1a6b7c3f9
{
  "fecha": "2026-09-02",
  "tipoMovimiento": "EGRESO_GASTO_OPERATIVO",
  "concepto": "Renta de septiembre del local",
  "importe": 8500,
  "referencia": "RECIBO-2026-09"
}
HTTP/1.1 201 application/json
{
  "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
}
La caja registra la renta de septiembre: un movimiento manual de egreso. Los movimientos de pagos no se crean aquí: los genera el backend.
Request
POST /api/caja/movimientos Authorization: Bearer eyJhbGciOiJIUzI1NiJ9… Clave-Idempotencia: 7f9c24e5-1b3d-4a2f-9e8c-05d1a6b7c3f9
{
  "fecha": "2026-09-02",
  "tipoMovimiento": "EGRESO_GASTO_OPERATIVO",
  "concepto": "Renta de septiembre del local",
  "importe": 8500,
  "referencia": "RECIBO-2026-09"
}
Response
HTTP/1.1 201 application/json
{
  "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

201

Movimiento registrado

400

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.

VALIDACION
403

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.

SIN_PERMISO
PUT/api/caja/movimientos/{id}Edita un movimiento de caja manual
actualizarMovimientoCaja

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

NombreEnTipoDescripción
idpathinteger · int64req

Cuerpo de la petición req

application/json  → PeticionMovimientoCaja

Ejemplos

PUT /api/caja/movimientos/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "fecha": "2026-09-02",
  "tipoMovimiento": {},
  "concepto": "Aportación de capital del negocio",
  "importe": 8500,
  "referencia": "TRANSFER-8841"
}
HTTP/1.1 200 application/json
{
  "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
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
PUT /api/caja/movimientos/42 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "fecha": "2026-09-02",
  "tipoMovimiento": {},
  "concepto": "Aportación de capital del negocio",
  "importe": 8500,
  "referencia": "TRANSFER-8841"
}
Response
HTTP/1.1 200 application/json
{
  "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

200

Movimiento actualizado

400

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.

VALIDACION
403

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.

SIN_PERMISO
404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
409

El movimiento fue generado por el sistema y no es editable

GET/api/caja/resumenTotales de caja del periodo
obtenerResumenCaja

Parámetros

NombreEnTipoDescripción
desdequerystring · dateopc
hastaquerystring · dateopc

Ejemplos

GET /api/caja/resumen?desde=2026-09-02&hasta=2026-09-02 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "totalEfectivo": 184320.5,
  "ingresos": 412300,
  "egresos": 227979.5,
  "utilidad": 18275.4
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/caja/resumen?desde=2026-09-02&hasta=2026-09-02 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "totalEfectivo": 184320.5,
  "ingresos": 412300,
  "egresos": 227979.5,
  "utilidad": 18275.4
}

Respuestas

200

Resumen de caja

Reportes e impresión

5 operaciones

Anexo 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
obtenerReporte

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

NombreEnTipoDescripción
clavepathstringreq

Las 9 claves del Anexo B §B.1

desdequerystring · dateopc
hastaquerystring · dateopc
clienteIdqueryinteger · int64opc
prestamoIdqueryinteger · int64opc
rutaIdqueryinteger · int64opc
anioqueryintegeropc
mesqueryintegeropc
pagequeryintegeropc

Índice de página, base 0

sizequeryintegeropc

Ejemplos

GET /api/reportes/cuotas-vencidas?desde=2026-09-02&hasta=2026-09-02&clienteId=213&prestamoId=348&rutaId=717 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "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
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/reportes/cuotas-vencidas?desde=2026-09-02&hasta=2026-09-02&clienteId=213&prestamoId=348&rutaId=717 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "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

200

Datos del reporte

400

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.

VALIDACION
403

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.

SIN_PERMISO
404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
GET/api/reportes/{clave}/pdfReporte en PDF, generado en el servidor
obtenerReportePdf

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

NombreEnTipoDescripción
clavepathstringreq

Las 9 claves del Anexo B §B.1

desdequerystring · dateopc
hastaquerystring · dateopc
clienteIdqueryinteger · int64opc
prestamoIdqueryinteger · int64opc
rutaIdqueryinteger · int64opc
anioqueryintegeropc

Ejemplos

GET /api/reportes/cuotas-vencidas/pdf?desde=2026-09-02&hasta=2026-09-02&clienteId=213&prestamoId=348&rutaId=717 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 · application/pdf — binario generado por el servidor
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/reportes/cuotas-vencidas/pdf?desde=2026-09-02&hasta=2026-09-02&clienteId=213&prestamoId=348&rutaId=717 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 · application/pdf — binario generado por el servidor

Respuestas

200

Documento PDF

403

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.

SIN_PERMISO
404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
GET/api/reportes/{clave}/excelReporte en Excel
obtenerReporteExcel

Parámetros

NombreEnTipoDescripción
clavepathstringreq

Las 9 claves del Anexo B §B.1

desdequerystring · dateopc
hastaquerystring · dateopc
clienteIdqueryinteger · int64opc
prestamoIdqueryinteger · int64opc
rutaIdqueryinteger · int64opc
anioqueryintegeropc

Ejemplos

GET /api/reportes/cuotas-vencidas/excel?desde=2026-09-02&hasta=2026-09-02&clienteId=213&prestamoId=348&rutaId=717 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 · application/vnd.openxmlformats-officedocument.spreadsheetml.sheet — binario generado por el servidor
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/reportes/cuotas-vencidas/excel?desde=2026-09-02&hasta=2026-09-02&clienteId=213&prestamoId=348&rutaId=717 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 · application/vnd.openxmlformats-officedocument.spreadsheetml.sheet — binario generado por el servidor

Respuestas

200

Libro de Excel

403

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.

SIN_PERMISO
GET/api/impresion/{formato}Datos de un formato de cobranza, para la vista previa en pantalla
obtenerDatosImpresion

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

NombreEnTipoDescripción
formatopathstringreq

Los 7 formatos del Anexo B §B.2

rutaIdqueryinteger · int64opc
fechaquerystring · dateopc
prestamoIdqueryinteger · int64opc
cuotaIdqueryinteger · int64opc
pagoIdqueryinteger · int64opc

Ejemplos

GET /api/impresion/listado-ruta?rutaId=717&fecha=2026-09-02&prestamoId=348&cuotaId=813&pagoId=696 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "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"
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/impresion/listado-ruta?rutaId=717&fecha=2026-09-02&prestamoId=348&cuotaId=813&pagoId=696 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "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

200

Datos del formato

404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO
GET/api/impresion/{formato}/pdfFormato de cobranza en PDF
obtenerImpresionPdf

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

NombreEnTipoDescripción
formatopathstringreq

Los 7 formatos del Anexo B §B.2

presentacionquerystringopc
rutaIdqueryinteger · int64opc
fechaquerystring · dateopc
prestamoIdqueryinteger · int64opc
cuotaIdqueryinteger · int64opc
pagoIdqueryinteger · int64opc

Ejemplos

GET /api/impresion/listado-ruta/pdf?presentacion=CARTA&rutaId=717&fecha=2026-09-02&prestamoId=348&cuotaId=813 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 · application/pdf — binario generado por el servidor
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/impresion/listado-ruta/pdf?presentacion=CARTA&rutaId=717&fecha=2026-09-02&prestamoId=348&cuotaId=813 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 · application/pdf — binario generado por el servidor

Respuestas

200

Documento PDF

404

NO_ENCONTRADO · Estado vacío con retorno al listado

NO_ENCONTRADO

Tablero y configuración

6 operaciones

Anexo B §B.3 y Módulo 7 del Anexo A.

GET/api/dashboardLos 6 indicadores del Anexo B §B.3, en una sola llamada
obtenerTablero

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 caja
  • carteraActiva — saldo pendiente de todos los créditos con cuotas por cobrar
  • cobranzaPeriodo — pagos recibidos en el periodo seleccionado
  • cuotasVencidas — número e importe de las cuotas vencidas con saldo
  • proyeccionMes — cuotas cuyo vencimiento cae en el mes seleccionado
  • utilidadMes — 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

NombreEnTipoDescripción
anioqueryintegeropc
mesqueryintegeropc
rutaIdqueryinteger · int64opc

Ejemplos

GET /api/dashboard?anio=2026&mes=9&rutaId=717 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "periodo": {
    "anio": 2026,
    "mes": 9
  },
  "totalEfectivo": 184320.5,
  "carteraActiva": 1245800,
  "cobranzaPeriodo": 98450,
  "cuotasVencidas": {
    "numero": 23,
    "importe": 41320
  },
  "proyeccionMes": 210940,
  "utilidadMes": 18275.4
}
El tablero de septiembre en una sola llamada. «Utilidad del mes» no es ingresos menos gastos: es interés + mora efectivamente cobrados (Anexo B §B.3).
Request
GET /api/dashboard?anio=2026&mes=9&rutaId=717 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "periodo": {
    "anio": 2026,
    "mes": 9
  },
  "totalEfectivo": 184320.5,
  "carteraActiva": 1245800,
  "cobranzaPeriodo": 98450,
  "cuotasVencidas": {
    "numero": 23,
    "importe": 41320
  },
  "proyeccionMes": 210940,
  "utilidadMes": 18275.4
}

Respuestas

200

Indicadores del tablero

403

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.

SIN_PERMISO
GET/api/configuracionConfiguración operativa
obtenerConfiguracion

Ejemplos

GET /api/configuracion Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "diasAnticipacionAviso": 3,
  "fechaActualizacion": "2026-09-02T14:30:00-06:00"
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/configuracion Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "diasAnticipacionAviso": 3,
  "fechaActualizacion": "2026-09-02T14:30:00-06:00"
}

Respuestas

200

Configuración

PUT/api/configuracionActualiza la configuración operativa
actualizarConfiguracion

Cuerpo de la petición req

application/json  → Configuracion

Ejemplos

PUT /api/configuracion Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "diasAnticipacionAviso": 3,
  "fechaActualizacion": "2026-09-02T14:30:00-06:00"
}
HTTP/1.1 200 application/json
{
  "diasAnticipacionAviso": 3,
  "fechaActualizacion": "2026-09-02T14:30:00-06:00"
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
PUT /api/configuracion Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "diasAnticipacionAviso": 3,
  "fechaActualizacion": "2026-09-02T14:30:00-06:00"
}
Response
HTTP/1.1 200 application/json
{
  "diasAnticipacionAviso": 3,
  "fechaActualizacion": "2026-09-02T14:30:00-06:00"
}

Respuestas

200

Configuración actualizada

400

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.

VALIDACION
403

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.

SIN_PERMISO
GET/api/empresaDatos del negocio usados en los formatos impresos
obtenerEmpresa

Ejemplos

GET /api/empresa Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
HTTP/1.1 200 application/json
{
  "id": 305,
  "nombre": "María",
  "lema": "Creciendo juntos, mano a mano",
  "rfc": "CJF210416XYA",
  "logoUrl": "Texto de logo url",
  "activo": true
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
GET /api/empresa Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Response
HTTP/1.1 200 application/json
{
  "id": 305,
  "nombre": "María",
  "lema": "Creciendo juntos, mano a mano",
  "rfc": "CJF210416XYA",
  "logoUrl": "Texto de logo url",
  "activo": true
}

Respuestas

200

Empresa

PUT/api/empresaActualiza los datos del negocio
actualizarEmpresa

Cuerpo de la petición req

application/json  → PeticionEmpresa

Ejemplos

PUT /api/empresa Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "nombre": "María",
  "lema": "Creciendo juntos, mano a mano",
  "rfc": "CJF210416XYA"
}
HTTP/1.1 200 application/json
{
  "id": 305,
  "nombre": "María",
  "lema": "Creciendo juntos, mano a mano",
  "rfc": "CJF210416XYA",
  "logoUrl": "Texto de logo url",
  "activo": true
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
PUT /api/empresa Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
{
  "nombre": "María",
  "lema": "Creciendo juntos, mano a mano",
  "rfc": "CJF210416XYA"
}
Response
HTTP/1.1 200 application/json
{
  "id": 305,
  "nombre": "María",
  "lema": "Creciendo juntos, mano a mano",
  "rfc": "CJF210416XYA",
  "logoUrl": "Texto de logo url",
  "activo": true
}

Respuestas

200

Empresa actualizada

400

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.

VALIDACION
403

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.

SIN_PERMISO
POST/api/empresa/logoSube el logotipo del negocio
subirLogoEmpresamultipart

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

POST /api/empresa/logo Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Content-Type: multipart/form-data

archivo         ine-frente.jpg (binario)
HTTP/1.1 201 application/json
{
  "id": 305,
  "nombre": "María",
  "lema": "Creciendo juntos, mano a mano",
  "rfc": "CJF210416XYA",
  "logoUrl": "Texto de logo url",
  "activo": true
}
Escenario simulado con datos de desarrollo; los importes, saldos y estados reales los calcula siempre el backend.
Request
POST /api/empresa/logo Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
Content-Type: multipart/form-data

archivo         ine-frente.jpg (binario)
Response
HTTP/1.1 201 application/json
{
  "id": 305,
  "nombre": "María",
  "lema": "Creciendo juntos, mano a mano",
  "rfc": "CJF210416XYA",
  "logoUrl": "Texto de logo url",
  "activo": true
}

Respuestas

201

Logotipo actualizado

403

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.

SIN_PERMISO
413

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.

ARCHIVO_MUY_GRANDE
415

FORMATO_NO_ADMITIDO · La respuesta nombra los formatos válidos

FORMATO_NO_ADMITIDO

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" } ]
}
Contrato del nombre del campo: el 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.
Caso especial de archivos: si Spring rechaza un multipart por exceder el límite del contenedor, la respuesta suele salir como página HTML sin el sobre. Debe capturarse para devolver 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ódigoHTTPCuándo ocurreQué 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 campos[].campo y campos[].mensaje

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 campos[] y ofrece ir al expediente

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

DIARIOSEMANALQUINCENALMENSUALTRIMESTRALANUAL

Días que suma: DIARIO +1 · SEMANAL +7 · QUINCENAL +15 · MENSUAL +30 o mes calendario (REGLA ABIERTA #8)

TipoCalculo

CAPITAL_MAS_INTERESSOLO_INTERES

CAPITAL_MAS_INTERES (533 créditos) · SOLO_INTERES (6 créditos). SALDOS_INSOLUTOS queda fuera del alcance (Anexo C §C.3).

PrestamoStatus

EN_REVISIONAUTORIZADORECHAZADOACTIVOLIQUIDADOVENCIDOREFINANCIADOCANCELADO

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

APLICADOCANCELADO

TipoPago

EFECTIVOTRANSFERENCIADEPOSITOOTRO

EstadoCuota

PENDIENTEPARCIALVENCIDAPAGADA

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

ACTIVOINACTIVO

CERRADO (acuerdo #18). No estaba enumerado en el modelo v1.1; el backend lo implementa así en ClienteStatus.java

TipoIdentificacion

INEPASAPORTELICENCIACEDULA_PROFESIONALOTRO

CERRADO (acuerdo #17). No estaba enumerado en el modelo v1.1; el backend lo implementa así en TipoIdentificacion.java

TipoDocumento

IDENTIFICACION_FRENTEIDENTIFICACION_ATRASCOMPROBANTE_DOMICILIOCOMPROBANTE_INGRESOSCURPOTRO

Coinciden exactamente con los tipos admitidos del Anexo C §C.1.1

EstadoExpediente

SIN_DOCUMENTOSINCOMPLETOCOMPLETO

[ABIERTO] Solo si la REGLA ABIERTA #13 se resuelve como bloqueo

TipoMovimiento

OTORGAMIENTO_PRESTAMOPAGOINTERESMORATORIOAJUSTEREFINANCIAMIENTOCANCELACION

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

INGRESO_PAGO_PRESTAMOINGRESO_CAPITALINGRESO_OTROEGRESO_PRESTAMO_OTORGADOEGRESO_GASTO_OPERATIVOEGRESO_RETIRO_CAPITALEGRESO_OTROAJUSTE_INGRESOAJUSTE_EGRESO

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

INGRESOEGRESOAJUSTE

Campo derivado del prefijo de tipoMovimiento. Existe para que el frontend no tenga que parsear el nombre del enum para saber el signo.

AplicacionAbonoCapital

REDUCE_CUOTASREDUCE_MONTOSOLO_CAJA

REGLA ABIERTA #1 — bloqueante. Cuando el cliente elija, quedan uno y se retiran los otros dos

Presentacion

CARTATERMICO_58TERMICO_80

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

ALTATODAS

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
CampoTipoDescripción
CodigoErrorreq
stringreq

Respaldo en español operativo. El texto que ve el usuario lo decide el frontend a partir del codigo, con un diccionario único. Si llega un código desconocido se muestra este mensaje y se registra el código.

array<object>opc
CodigoErrorenum

Los 18 códigos estables de system-design §5.3

CREDENCIALES_INVALIDASUSUARIO_INACTIVOTOKEN_EXPIRADOPASSWORD_REQUERIDOSIN_PERMISONO_ENCONTRADOVALIDACIONMONTO_INVALIDOPRESTAMO_SIN_PLANPRESTAMO_LIQUIDADOPAGO_YA_CANCELADOCANCELACION_NO_PERMITIDAARCHIVO_MUY_GRANDEFORMATO_NO_ADMITIDOARCHIVO_VACIOEXPEDIENTE_INCOMPLETOCONFLICTO_CONCURRENCIAERROR_INTERNO
Paginaobject
CampoTipoDescripción
integerreq
integerreq
integerreq
integerreq
Periodicidadenum

Días que suma: DIARIO +1 · SEMANAL +7 · QUINCENAL +15 · MENSUAL +30 o mes calendario (REGLA ABIERTA #8)

DIARIOSEMANALQUINCENALMENSUALTRIMESTRALANUAL
TipoCalculoenum

CAPITAL_MAS_INTERES (533 créditos) · SOLO_INTERES (6 créditos). SALDOS_INSOLUTOS queda fuera del alcance (Anexo C §C.3).

CAPITAL_MAS_INTERESSOLO_INTERES
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).

EN_REVISIONAUTORIZADORECHAZADOACTIVOLIQUIDADOVENCIDOREFINANCIADOCANCELADO
PagoStatusenum
APLICADOCANCELADO
TipoPagoenum
EFECTIVOTRANSFERENCIADEPOSITOOTRO
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).

PENDIENTEPARCIALVENCIDAPAGADA
ClienteStatusenum

CERRADO (acuerdo #18). No estaba enumerado en el modelo v1.1; el backend lo implementa así en ClienteStatus.java

ACTIVOINACTIVO
TipoIdentificacionenum

CERRADO (acuerdo #17). No estaba enumerado en el modelo v1.1; el backend lo implementa así en TipoIdentificacion.java

INEPASAPORTELICENCIACEDULA_PROFESIONALOTRO
TipoDocumentoenum

Coinciden exactamente con los tipos admitidos del Anexo C §C.1.1

IDENTIFICACION_FRENTEIDENTIFICACION_ATRASCOMPROBANTE_DOMICILIOCOMPROBANTE_INGRESOSCURPOTRO
EstadoExpedienteenum

[ABIERTO] Solo si la REGLA ABIERTA #13 se resuelve como bloqueo

SIN_DOCUMENTOSINCOMPLETOCOMPLETO
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).

OTORGAMIENTO_PRESTAMOPAGOINTERESMORATORIOAJUSTEREFINANCIAMIENTOCANCELACION
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.

INGRESO_PAGO_PRESTAMOINGRESO_CAPITALINGRESO_OTROEGRESO_PRESTAMO_OTORGADOEGRESO_GASTO_OPERATIVOEGRESO_RETIRO_CAPITALEGRESO_OTROAJUSTE_INGRESOAJUSTE_EGRESO
NaturalezaMovimientoCajaenum

Campo derivado del prefijo de tipoMovimiento. Existe para que el frontend no tenga que parsear el nombre del enum para saber el signo.

INGRESOEGRESOAJUSTE
AplicacionAbonoCapitalenum

REGLA ABIERTA #1 — bloqueante. Cuando el cliente elija, quedan uno y se retiran los otros dos

REDUCE_CUOTASREDUCE_MONTOSOLO_CAJA
PeticionLoginobject
CampoTipoDescripción
stringreq
string · passwordreq
PeticionCambioPasswordobject
CampoTipoDescripción
string · passwordreq
string · passwordreq
RespuestaSesionobject
CampoTipoDescripción
stringreq
string · date-timereq

ISO-8601 con offset

booleanreq

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 true, el frontend bloquea la navegación.

objectreq
array<string>req
array<string>req

Ya resuelto: rol más excepciones de UsuarioPermiso, aplanado. El frontend no reconstruye la precedencia entre rol y excepción.

Empresaopc
Usuarioobject
CampoTipoDescripción
integer · int64req
stringreq
string · nullopc
booleanreq

Estado del usuario, no un permiso (Anexo A §A.2)

booleanopc

Campo persistido (Usuario.java). Se pone en true para los 6 usuarios migrados, cuyas contraseñas estaban en texto plano y se cifran con BCrypt (Anexo A §A.2). La pantalla 2 lo muestra como distintivo y permite forzarlo.

array<Role>opc
array<string>opc
string · date-timeopc
string · nullopc
PeticionUsuarioobject
CampoTipoDescripción
stringreq
stringopc
string · nullopc

Solo en el alta. Nunca viaja de vuelta en ninguna respuesta

booleanreq
array<integer · int64>req
Roleobject
CampoTipoDescripción
integer · int64req
stringreq
string · nullopc
Permisoobject
CampoTipoDescripción
integer · int64req
stringreq

Los 5 del Anexo A §A.2. ELIMINAR cubre también la baja de un documento del expediente y la de un aval.

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 LISTAR_CLIENTES, modificar bajo EDITAR_HISTORIAL. Consecuencia a mirar de frente: un cobrador podría ver identificaciones y comprobantes de domicilio de todos los clientes. Si eso preocupa al negocio, hacen falta GESTIONAR_EXPEDIENTE y GESTIONAR_AVALES. Lo decide el cliente, no el equipo técnico.

string · nullopc
Rutaobject
CampoTipoDescripción
integer · int64req
stringreq
string · nullopc
booleanreq
integer · nullopc

Para poder avisar antes de desactivar una ruta con cartera

PeticionRutaobject
CampoTipoDescripción
stringreq
stringreq

Obligatoria. Ruta.descripcion es nullable = false en Ruta.java

OpcionTipoMovimientoCajaobjeto compuesto

Compone y extiende: OpcionCatalogo

CampoTipoDescripción
NaturalezaMovimientoCajareq
booleanreq

Si false, el tipo solo lo genera el sistema y no se ofrece en el alta manual

OpcionTipoDocumentoobjeto compuesto

Compone y extiende: OpcionCatalogo

CampoTipoDescripción
booleanopc

REGLA ABIERTA #13. Hoy puede venir siempre en false

ClienteResumenobject
CampoTipoDescripción
integer · int64req
stringreq

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).

string · nullopc
string · nullopc
integer · nullopc
string · nullopc

Junto al rutaId, para no resolver el catálogo en cada fila

ClienteStatusreq
integeropc

Columna que hace visible la separación cliente/crédito del Anexo A §A.8

integeropc
numberopc

Suma de saldos de sus créditos activos

integeropc

Documentos vigentes del expediente

EstadoExpediente | nullopc

Solo si la REGLA ABIERTA #13 lo hace necesario

booleanopc

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. Cliente.java no tiene este campo. Sin él, los 14 clientes sin nombre y los 60 movimientos huérfanos se ven como filas defectuosas en lugar de como pendientes de revisión identificados, y el cliente no tiene forma de encontrarlos para corregirlos.

Clienteobjeto compuesto

Compone y extiende: ClienteResumen

CampoTipoDescripción
string · nullopc
string · nullopc
string · nullopc
string · nullopc
TipoIdentificacion | nullopc
string · nullopc

ABIERTO — acuerdo #28. Ver la nota de direccion

string · nullopc

ABIERTO — acuerdo #28. Cliente.java no tiene telefono, direccion ni notas, pero el Anexo A §A.1 define la pantalla 4 como «Datos personales, documento de identificación, contacto, dirección, ruta y notas». Los tres son alcance contratado.

Llama la atención que Aval.java tiene telefono y direccion: el avalista es localizable y el deudor no. Para una financiera de cobranza en campo, es al revés.

string · nullopc

ABIERTO — acuerdo #28. Ver la nota de direccion

string · date-timeopc
string · nullopc
PeticionClienteobject
CampoTipoDescripción
stringreq
stringopc
stringopc
stringopc
stringopc
string · nullopc
TipoIdentificacion | nullopc
stringopc
stringopc
stringopc
stringopc
integer · nullopc
ClienteStatusopc
ClienteDocumentoobject
CampoTipoDescripción
integer · int64req
integer · int64req
TipoDocumentoreq
stringreq
string · nullopc
string · nullopc
integeropc

Para mostrar «2.4 MB» en la lista sin descargar el archivo

string · date-timereq
string · nullopc

Quién lo subió. Útil en un expediente que revisan varias personas

booleanreq

false = sustituido por un reemplazo o dado de baja lógica

integer · nullopc

Documento al que este reemplaza. Corresponde a la FK reemplaza_a_id de ClienteDocumento.java y confirma el acuerdo #24: el reemplazo conserva el anterior inactivo en lugar de sustituirlo sin rastro.

Es lo que permite que la pantalla 5 ofrezca «Ver versiones anteriores» recorriendo la cadena, en lugar de mostrar una lista plana de inactivos.

integer · nullopc

Control de concurrencia optimista (@Version). Ver acuerdo #27

stringreq

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.

rutaArchivo del modelo NO debe salir en la respuesta: es una ruta interna del servidor y no le sirve de nada al navegador.

EstadoExpedienteRespuestaobject
CampoTipoDescripción
EstadoExpedientereq
array<TipoDocumento>req
Avalobject
CampoTipoDescripción
integer · int64req
integer · int64req
stringreq

Un solo campo, igual que en Cliente y por la misma razón

string · nullopc
string · nullopc
string · nullopc
TipoIdentificacion | nullopc
string · nullopc
string · date-timereq
string · nullopc
PeticionAvalobject
CampoTipoDescripción
stringreq
stringopc
stringopc
stringopc
TipoIdentificacion | nullopc
stringopc
PeticionSimulacionobject
CampoTipoDescripción
numberreq
numberreq

Fracción por periodo, no porcentaje. 0.0225 = 2.25 %

numberopc

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.

integerreq
Periodicidadreq
TipoCalculoreq
string · datereq
ResumenSimulacionobject
CampoTipoDescripción
numberreq
numberreq
numberreq
numberreq
numberreq
CuotaSimuladaobject
CampoTipoDescripción
integerreq
string · datereq
numberreq
numberreq
numberreq

La última cuota absorbe el ajuste de redondeo (p. ej. 1,058.37 frente a 1,058.33)

RespuestaSimulacionobject
CampoTipoDescripción
ResumenSimulacionreq
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.

CampoTipoDescripción
integer · int64req
string · nullopc
string · nullopc
string · datereq
numberreq
numberreq
numberopc
integerreq
Periodicidadreq
TipoCalculoreq
stringopc
PrestamoResumenobject
CampoTipoDescripción
integer · int64req
integer · int64req
string · nullopc
integer · nullopc
string · nullopc
string · nullopc
numberreq
numberopc
numberopc
integerreq
Periodicidadreq
TipoCalculoreq
PrestamoStatusreq
numberreq
numberopc
numberopc
numberopc
numberopc
integeropc

Junto con numeroCuotas, sostiene el «5 de 12» que aparece en toda la interfaz

integeropc

Conteo, para el distintivo de riesgo

string · nullopc

Fecha de la siguiente cuota no pagada

booleanopc

Derivado: «tiene al menos una cuota vencida». Es la lectura (b) del acuerdo #13: un crédito con cuotas vencidas sigue siendo ACTIVO; la mora es una condición de sus cuotas, no un cambio de estado del crédito.

integeropc

Conteo, para mostrar el distintivo en la ficha sin traer la lista

booleanopc

false en los 24 créditos migrados sin movimientos (Anexo A §A.9)

integer · nullopc

Control de concurrencia optimista (@Version). Ver acuerdo #27

Prestamoobjeto compuesto

Compone y extiende: PrestamoResumen

CampoTipoDescripción
string · nullopc
string · nullopc
string · nullopc
string · nullopc
numberopc
numberopc
integer · nullopc

Campo creado y nullable. Sin flujo de refinanciamiento (Anexo C §C.2)

string · nullopc
string · date-timeopc
string · nullopc
Cuotaobject
CampoTipoDescripción
integer · int64req
integer · int64req
integerreq
string · datereq
numberopc
numberopc
numberreq
numberopc
numberopc
numberopc
numberopc
string · nullopc
EstadoCuotareq

Derivado por el backend. El frontend solo pinta la marca

numberreq

totalCuota − (capitalPagado + interesPagado + moratorioPagado)

integeropc

Fórmula del Anexo A §A.4: si la cuota está pagada, fechaMovimiento − fechaPago; si está pendiente, fechaActual − fechaPago. Si el resultado es negativo, 0.

numberopc

Mora acumulada a la fecha de consulta, sin persistir. CAPITAL_MAS_INTERES: retornoCapital × tasaMora × diasAtraso. SOLO_INTERES: valorInteres × tasaMora × diasAtraso.

numberopc

saldoPendiente + moraAlDia. Es lo que el panel de cobro muestra como total

integer · nullopc

Control de concurrencia optimista (@Version). Ver acuerdo #27

RespuestaAmortizacionobject
CampoTipoDescripción
PrestamoResumenreq
array<Cuota>req
objectopc
RespuestaHistorialobject
CampoTipoDescripción
PrestamoResumenreq
array<Cuota>req
array<PagoDetalle>req

Incluye los pagos cancelados, marcados como tales. Requisito de auditoría

CuotaCobranzaobjeto compuesto

Compone y extiende: Cuota

CampoTipoDescripción
integer · int64req
stringreq
integer · nullopc
string · nullopc
string · nullopc
PaginaCuotasCobranzaobjeto compuesto

Compone y extiende: Pagina

CampoTipoDescripción
array<CuotaCobranza>req
objectreq

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
CampoTipoDescripción
string · datereq
numberreq
booleanopc

REGLA ABIERTA #5. Si la mora no es condonable, el campo se retira

PeticionPagoobjeto compuesto

Compone y extiende: PeticionPrevisualizacionPago

CampoTipoDescripción
TipoPagoreq
stringopc
stringopc
AplicacionCuotaobject
CampoTipoDescripción
integer · int64req
integerreq
numberreq
numberreq
numberreq
numberreq
EstadoCuotareq
TotalesDistribucionobject
CampoTipoDescripción
numberreq
numberreq
numberreq
numberreq
RespuestaDistribucionPagoobject
CampoTipoDescripción
array<AplicacionCuota>req
TotalesDistribucionreq
numberreq
RespuestaPagoRegistradoobjeto compuesto

Compone y extiende: RespuestaDistribucionPago

CampoTipoDescripción
integer · int64req
integer · int64req

Un pago genera un solo movimiento de caja, por el monto total recibido, con utilidad = interés + mora efectivamente cobrados. No se genera un movimiento aparte por la ganancia, para no duplicar el ingreso (Anexo A §A.4, modelo v1.1 §2.18).

booleanopc

Si true, el frontend prensa el sello LIQUIDADO sobre la tabla de amortización. Ocurre una vez por crédito en toda su vida.

Pagoobject
CampoTipoDescripción
integer · int64req
integer · int64req
integer · int64req
string · nullopc
string · date-timereq
numberreq
TipoPagoreq
string · nullopc
string · nullopc
integer · int64opc
string · nullopc
PagoStatusreq
string · date-timeopc
string · nullopc
string · nullopc
string · nullopc
integer · nullopc

Control de concurrencia optimista (@Version). Ver acuerdo #27

PagoAplicacionobject
CampoTipoDescripción
integer · int64req
integer · int64req
integer · int64req
integeropc
numberreq
numberreq
numberreq
numberopc
string · date-timeopc
PagoDetalleobjeto compuesto

Compone y extiende: Pago

CampoTipoDescripción
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.

integer · nullopc
PeticionAbonoCapitalobject
CampoTipoDescripción
string · datereq
numberreq
AplicacionAbonoCapitalreq
stringopc
RespuestaAbonoCapitalobject
CampoTipoDescripción
integer · nullopc
integer · int64req
numberreq
array<Cuota>opc

Plan de pagos tras el abono, para que la pantalla 12 muestre el antes y el después

Movimientoobject
CampoTipoDescripción
integer · int64req
integer · int64req
integer · nullopc
TipoMovimientoreq
numberreq
numberreq
numberreq
stringopc
string · nullopc
string · date-timereq
integer · nullopc
MovimientoCajaobject
CampoTipoDescripción
integer · int64req
string · date-timereq
TipoMovimientoCajareq
NaturalezaMovimientoCajareq
stringreq
numberreq
numberopc

En un pago, interés + mora efectivamente cobrados. Se muestra siempre atenuada, nunca destacada: no es un ingreso adicional, es un desglose del mismo importe. Resaltarla induce a leerla como un segundo ingreso, que es justo el error que la regla de utilidad del Anexo A §A.4 quiere evitar.

integer · nullopc
integer · nullopc
string · nullopc

Trazabilidad que el Access no tenía: allí el nombre viajaba dentro del texto del concepto

booleanreq

Determina si la fila es editable desde la pantalla 15

integer · nullopc
string · nullopc
string · date-timeopc
booleanopc

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. MovimientoCaja.java no tiene este campo. El Anexo A §A.9 sí exige que estos 9 registros queden «marcados para revisión del cliente». Si el backend no lo añade, el frontend no puede señalarlos y el compromiso del anexo queda sin superficie.

integer · nullopc

Control de concurrencia optimista (@Version). Ver acuerdo #27

PeticionMovimientoCajaobject
CampoTipoDescripción
string · datereq
TipoMovimientoCajareq

Solo se admiten los tipos marcados como manuales en el catálogo

stringreq
numberreq
stringopc
ResumenCajaobject
CampoTipoDescripción
numberreq
numberreq
numberreq
numberreq
RespuestaReporteobject
CampoTipoDescripción
stringreq
stringreq
objectopc

Los filtros en claro, para imprimirlos como texto en la cabecera del formato

array<object>req
array<object>req
objectopc

Calculados por el backend. El frontend no suma columnas

integer · nullopc
integer · nullopc
integer · nullopc
integer · nullopc
RespuestaImpresionobject
CampoTipoDescripción
stringreq
stringreq
string · nullopc

Ruta y fecha, cliente, o crédito

Empresareq
objectopc
array<object>opc
array<object>req
objectopc
string · date-timeopc

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.

stringopc
RespuestaTableroobject
CampoTipoDescripción
objectopc
numberreq
numberreq
numberreq
objectreq
numberreq
numberreq

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
CampoTipoDescripción
integerreq

Días de anticipación del aviso de vencimiento. Valor actual del negocio: 3.

Atención a la siembra: Configuracion.java declara @Builder.Default private Integer diasAnticipacionAviso = 0. Un 0 hace que «Próximos a vencer» salga vacía y parezca rota. El dato migrado desde la tabla Recordatorio de Access debe ser 3; conviene verificarlo en el seed inicial y no dejarlo al default de la entidad. Sustituye la idea de persistir recordatorios individuales: los avisos se derivan de las cuotas (modelo v1.1 §1.7 y §2.2).

string · nullopc
Empresaobject
CampoTipoDescripción
integer · nullopc
stringreq
string · nullopc

ABIERTO — acuerdo #29. Empresa.java tiene nombre, rfc, logo, logo2 y logo3, pero no lema. El Anexo A Módulo 7 define Datos del negocio como «Nombre, lema y logotipo utilizados en los formatos impresos», y el encabezado de los 7 formatos del Anexo B §B.2 lo incluye.

Sin este campo, el encabezado impreso queda incompleto respecto de lo aprobado. Alternativa si no se añade: reutilizar uno de los tres logo*, que es peor.

string · nullopc
string · nullopc

Ruta del endpoint que sirve el logotipo. Encabeza todos los formatos impresos

booleanopc
PeticionEmpresaobject
CampoTipoDescripción
stringreq
stringopc
stringopc
PrestamoConPlanobject
CampoTipoDescripción
Prestamoreq
array<Cuota>req

El plan definitivo generado por el backend. En producción manda éste siempre: si no coincide con el que la vista previa mostró, el que se guarda y se muestra es el del backend, sin excepción.

integer · nullopc

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 abiertaBloqueaImpacto en el frontend
1Abono a capital: reduce cuotas / reduce monto / solo cajaPOST /api/prestamos/{id}/abonos-capitalBloqueante La pantalla 12 está maquetada con las 3 variantes; aplicacion es enum mientras tanto
2Orden de aplicación del pago (se propone mora → interés → capital → saldo a favor)POST /api/prestamos/{id}/pagos y su previsualizaciónEl desglose sale del backend; si cambia el orden, cambian los números, no la pantalla
3Esquema SOLO_INTERÉS: cómo y cuándo se recupera el capitalPOST /api/prestamos/simulacionAfecta 6 créditos históricos; la vista previa no puede mostrar el plan hasta cerrarlo
4Abono parcial completado: ¿la cuota se cierra sola o la cierra el operador?Distribución del pagoCambia si la pantalla 11 necesita una acción explícita de cierre
5Mora: automática o condonable por el operadorPOST …/pagos (campo condonarMora)El campo está en el contrato; si no es condonable, se retira
6Saldo a favor: se aplica solo al siguiente vencimiento o se conservaDistribución del pagoCambia si hay que mostrar y accionar el saldo a favor en el historial
7Reverso de pagos: quién puede y con qué límite de tiempoPOST /api/pagos/{id}/cancelacionDefine si la acción se oculta por permiso, por antigüedad, o ambas
8Mensual: ¿+30 días o mes calendario?Generación de cuotasCosmético para el frontend, crítico para la conciliación con el Access
9Gastos operativos: dónde se registran hoyGET /api/dashboardDefine qué significa «utilidad» en 2 de los 6 indicadores
10Rutas GRUPOS / QUINCENAL / LIQUIDACIÓN: ¿rutas o clasificaciones?Catálogo de rutas · y la ruta en ClienteSi no son rutas, el filtro de la pantalla 3 necesita una dimensión más — y la regla sube de importancia
11Formato del ticket: rollo térmico o cartaGET /api/impresion/{formato}/pdfDefine el valor por omisión de presentacion y 4 de los 7 formatos
12Tamaño máximo y formatos del expediente (propuesto: PDF, JPG, PNG hasta 10 MB)POST /api/clientes/{id}/documentosDefine 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 6Alto 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

#PuntoPropuesta
1Nombres de campo en JSONcamelCasecerrado
2Nombre del recurso de préstamos/api/prestamoscerrado
3Endpoint de previsualización de pagoQue exista; sostiene la pantalla 10
4Endpoint de simulación de amortizaciónQue exista; sostiene la pantalla 6
5Campos derivados en las respuestasLa lista de §05 · campos derivados
6Permisos efectivos resueltos en el loginLista plana de códigos
7Catálogo de códigos de errorLa tabla de §11, o la del backend si prefiere otra
8Mecanismo de idempotenciaEncabezado Clave-Idempotencia + entidad IdempotenciaOperacioncerrado
9Refresh tokenSin refresh: token largo y relogin manual
10Claves de reportes y formatosLa lista del contrato (9 claves, 7 formatos)
11Aviso de endpoints estables por móduloUn mensaje por grupo, aunque los datos estén incompletos
12Casos de prueba compartidos de amortizaciónamortizacion-casos.json en el repositorio

Modelo y estados

#PuntoPropuesta
13Naturaleza del estado VENCIDOCondición derivada, no estado asignado — cerrado
14¿Varios préstamos activos por cliente?Sí, por el Anexo A §A.8
15Precedencia VENCIDA sobre PARCIALUn solo campo estado, con VENCIDA ganando
16EGRESO_PRESTAMO_OTORGADO no existía en el enumAgregado — cerrado. Son 2,107 movimientos, el 14 % de la caja
17Valores de TipoIdentificacionINE, PASAPORTE, LICENCIA, CEDULA_PROFESIONAL, OTROcerrado
18Valores de ClienteStatusACTIVO, INACTIVOcerrado

Expediente digital y avales — nuevos con el Anexo C

#PuntoPropuesta
19Entrega de archivos al navegadorEndpoint autenticado + blob:. Si los PDFs pesan, URL firmada
20urlContenido armada por el backendSin exponer rutaArchivo
21Un archivo por peticiónmultipart/form-data; cinco archivos son cinco llamadas con progreso independiente
22Límite del contenedor = regla abierta #12Configurar Spring al mismo valor acordado con el cliente
23Error 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

#PuntoEstado
27Control de concurrencia optimista: @Version en Prestamo, Cuota, Pago, MovimientoCaja y ClienteDocumentocerradoversion expuesto en las 5 entidades
28Cliente.telefono, direccion y notas — los Anexos los exigen (pantalla 4) y el JPA no los tiene; Aval sí tiene teléfono y direcciónabierto — el contrato ya los expone
29Empresa.lema — encabeza los 7 formatos del Anexo B §B.2 y el JPA no lo tieneabierto — el contrato ya lo expone
30marcadoParaRevision 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.9abierto — el contrato ya lo expone
Lo que hay que preguntarle al cliente, no resolver entre nosotros: regla abierta #13 (¿bloquea el expediente incompleto? ¿qué tipos son obligatorios?), regla abierta #12 (tamaño y formatos), y los permisos del expediente (Ap.1 §2): con los 5 permisos actuales, un cobrador puede ver identificaciones de todos los clientes. Si eso no es aceptable, hacen falta GESTIONAR_EXPEDIENTE y GESTIONAR_AVALES. Lo decide el cliente.
System design · v1.2 · 2 de septiembre de 2026

System Design — «Creciendo Juntos Financiera»

Angular · Spring Boot · PostgreSQL · Escenario A. Documento de coordinación escrito desde el frontend; su frontera se documenta en el Contrato API.

SD · 01Qué es este documento

Documento de coordinación escrito desde el frontend: define qué necesita consumir la aplicación Angular para que las pantallas del Anexo A y los formatos del Anexo B funcionen. No define cómo se construye el backend. El objetivo práctico: congelar la frontera antes de la semana 3, para que frontend y backend avancen en paralelo sin bloquearse.

El modelo de datos v1.1 se toma como dado

Es del backend y está cerrado; tiene prioridad sobre los anexos. Aquí se describe qué proyección necesita ver cada pantalla.

Las decisiones internas no se tocan

Capas, servicios, transacciones, JPA, índices: criterio del backend. Solo aparece lo que cruza la frontera HTTP.

Los contratos son propuesta, no ley

Salen del prototipo que corre contra mock. La referencia completa está en el documento «Contrato API».

SD · 02Las 17 pantallas en 7 módulos

Alcance cerrado en el Anexo A. El Anexo C incorporó las pantallas 5 y 7 (expediente digital y avales), desplazando la numeración del Anexo A §A.1. Esta numeración es la vigente en todo el contrato: cada endpoint indica a qué pantalla sostiene.

#PantallaMóduloOrigen
1Inicio de sesiónSeguridadAnexo A §A.1
2Gestión de usuariosSeguridadAnexo A §A.1
3Listado de clientesClientesAnexo A §A.1
4Ficha de clienteClientesAnexo A §A.1
5Expediente digital del clienteClientesNueva · Anexo C §C.1.1
6Alta de créditoCréditosAnexo A §A.1
7Avales del créditoCréditosNueva · Anexo C §C.1.2
8Tabla de amortizaciónCréditosAnexo A §A.1
9Historial del clienteCobranzaAnexo A §A.1
10Registro de pago de cuotaCobranzaAnexo A §A.1
11Abono parcialCobranzaAnexo A §A.1
12Abono a capitalCobranzaAnexo A §A.1
13Cuotas vencidasCobranzaAnexo A §A.1
14Próximos a vencerCobranzaAnexo A §A.1
15Ingresos y egresos (caja)CajaAnexo A §A.1
16Consultas por rango de fechasConsultasAnexo A §A.1
17Impresión de listados y recibosImpresiónAnexo A §A.1 · Anexo B §B.2

SD · 03Arquitectura de alto nivel

Sin caché distribuida, sin colas de mensajes, sin servicios adicionales. Con 6 usuarios y ~18 mil cuotas, cualquier pieza extra cuesta más de lo que resuelve.

NAVEGADOR — SPA Angular

Presentación (17 pantallas) · Dominio de UI: formateo, validación de captura, motor de amortización espejo (solo vista previa) · Capa de acceso: interfaz FinancieraApiMockApiService (semanas 1–2 y tests) / HttpApiService (desde la semana 3)

Spring Boot

API REST /api · Seguridad: autenticación + autorización por permiso · Reglas financieras: amortización, distribución de pagos, mora, saldos, caja · Expediente digital: recepción, validación y entrega autenticada de archivos · Reportes y renderizado de PDF / Excel · Migración Access → PostgreSQL (proceso aparte, una vez)

PostgreSQL — modelo v1.1

Metadatos del expediente · logotipo del negocio

Almacenamiento de archivos

Documentos del cliente · ubicación a criterio del backend, siempre con entrega autenticada

Decisiones transversales y sus contrapartidas

DecisiónSe ganaSe pagaCuándo revisarla
API sin versionado/api/…, nunca /api/v1/…URLs limpias, cero ceremoniaFrontend y backend se despliegan juntos siempreSi aparece una app móvil de cobradores — no antes
Estado de cuota derivado, no almacenadoNunca se desincroniza; sin proceso nocturnoSe calcula en cada consultaSi la cartera supera ~100 mil cuotas. Hoy son 18 mil
PDF en servidor (Anexo B §B.4)Papel y archivo idénticosEl frontend no puede generar PDF sin backendNo se revisa: requisito contractual
Archivos fuera de la base, metadatos dentroLa base no crece con binarios; respaldo simpleDos cosas que respaldar y desincronizarSi aparece un archivo huérfano en producción
Descarga autenticada por endpointEl expediente no se filtra por URLPrevisualización vía blob:Si los PDFs grandes se sienten lentos → URL firmada
Tablero en una sola llamadaUna petición, no seisUn endpoint que agrega seis consultasSi algún indicador se vuelve costoso, se separa
Sin caché ni colaMenos piezas que operarLos reportes pesados se recalculan cada vezSi un reporte anual supera 3 s
Un solo consumidor del APIContratos ajustados a las pantallasEndpoints específicos, poco reutilizablesSolo si aparece un segundo consumidor

Estrategia de integración — módulo por módulo

El frontend arranca contra una capa mock que implementa esta misma frontera y se conecta al backend real conforme los grupos de endpoints quedan estables. No hay una integración única al final.

Semana 1–2Todos los módulos contra mock
Semana 3Seguridad + Clientes → backend real · resto → mock
Semana 4+ Expediente digital y préstamos
Semana 5+ Avales y amortización
Semana 6–7+ Cobranza y pagos
Semana 8+ Caja
Semana 9+ Reportes, impresión y tablero
El expediente digital conviene conectarlo temprano: es lo único del sistema que sube archivos, y las sorpresas de multipart, límites de tamaño y CORS aparecen en la primera prueba real, no en la revisión de contrato.

SD · 04Reparto de responsabilidades

La línea que importa acordar: todo cálculo financiero con consecuencia contable vive en el backend. El frontend calcula únicamente para previsualizar mientras el usuario teclea, y nunca envía un resultado calculado por él como dato a persistir. En producción manda el backend siempre.

ResponsabilidadBackendFrontend
Generación del plan de cuotasAutoridadEspejo, solo para vista previa
Distribución de un pago sobre cuotasAutoridadMuestra lo que devuelve la previsualización
Cálculo de moraAutoridadSolo muestra el importe y la explicación
Estado de cada cuotaAutoridad (derivado)Solo pinta el badge
Saldos del préstamoAutoridadSolo muestra
Movimiento de caja de un pagoAutoridad (automático)No lo envía ni lo compone
Permisos efectivos del usuarioAutoridadOculta la UI según los recibe
Totales de reportes y del tableroAutoridadNo suma columnas en el cliente
Validación de tipo y tamaño de archivoAutoridad (revalida siempre)Primera línea, para no subir 40 MB en vano
Almacenamiento y entrega de archivosAutoridadSolo sube y descarga
Estado del expediente (completo / incompleto)Autoridad, si la regla abierta #13 lo exigeSolo muestra el distintivo
Formato de moneda, fecha y númeroAutoridad
Validación de captura (obligatorios, rangos, formato)Revalida siemprePrimera línea, para respuesta inmediata
Paginación, orden y filtrosAutoridadEnvía los parámetros
Estilos de impresión en pantallaAutoridad
Generación del archivo PDFAutoridad (Anexo B §B.4)Solo descarga
Motor de amortización duplicado: el frontend implementa la fórmula en TypeScript para que la vista previa responda al teclear. Dos implementaciones de la misma fórmula divergen: la mitigación acordada es un archivo amortizacion-casos.json con pares entrada → salida, versionado y consumido por los tests de ambos lados. Si divergen más de una vez, se elimina el motor local y la vista previa llama a POST /api/prestamos/simulacion con debounce.

SD · 05Máquinas de estado

Cinco máquinas relevantes para la interfaz. El frontend las pinta sin ambigüedad: los estados llegan ya resueltos desde el backend y nunca se recalculan en el cliente.

Cuota — derivado, nunca almacenado

Se resuelve al consultar; no requiere proceso nocturno. VENCIDA tiene precedencia sobre PARCIAL: una cuota vencida con abono parcial se pinta roja — el cobrador debe verla como pendiente de cobro.

PENDIENTE—abono menor→ PARCIAL—vence con saldo→ VENCIDA—pago del saldo→ PAGADA
PENDIENTE—pago total / vence y paga→ PAGADA
PAGADA—cancelación de pago→ PENDIENTE · PARCIAL · VENCIDA

Por cuota el frontend recibe, además: saldoPendiente, diasAtraso y moraAlDia — no puede derivarlos sin reimplementar la fórmula de mora.

Préstamo — la interfaz opera 4 de 8 estados

ACTIVO—último pago, saldo = 0→ LIQUIDADO
ACTIVO—cancelación (permiso ELIMINAR)→ CANCELADO

VENCIDO: lectura acordada (b) — condición derivada («tiene al menos una cuota vencida») sobre un préstamo ACTIVO, no un estado asignado. EN_REVISION, AUTORIZADO, RECHAZADO y REFINANCIADO existen en el modelo sin flujo (Anexo C §C.2).

Pago

APLICADO—cancelación (ventana de tiempo, regla abierta #7)→ CANCELADO→ terminal, no se reactiva

La cancelación revierte aplicaciones, recalcula cuotas, ajusta saldos y anula o compensa el movimiento de caja — en una sola transacción. El pago cancelado se conserva visible en el historial, marcado, con fecha y usuario de cancelación: requisito de auditoría.

Usuario

INACTIVO ACTIVO
ACTIVO—primer acceso tras migración→ ACTIVO · requiereCambioPassword

activo es estado, no permiso. Un usuario inactivo recibe 403 USUARIO_INACTIVO en el login, con mensaje distinto al de credenciales incorrectas.

Documento del expediente

VIGENTE—reemplazo→ SUSTITUIDO
VIGENTE—eliminación (permiso ELIMINAR)→ ELIMINADO

Baja lógica (activo = false), nunca borrado físico. El reemplazo conserva el documento anterior (acuerdo #24): reemplazaAId permite ofrecer «Ver versiones anteriores» en la pantalla 5.

Expediente completo — sujeto a la regla abierta #13

SIN_DOCUMENTOS INCOMPLETO COMPLETO

Solo existe si el cliente decide que el expediente incompleto bloquea el alta de crédito. Mientras no se resuelva, el frontend maqueta la advertencia, que es lo reversible.

SD · 06Modelo de datos v1.1

El modelo es del backend y tiene prioridad sobre los anexos cuando algo no coincida. El detalle completo entidad por entidad está en el documento Modelo de Datos Financiero Reconciliado.

Entidades de primer nivel

EmpresaIdentidad del negocio: nombre, lema, RFC, logotipo para los formatos impresos
ConfiguracionDías de anticipación del aviso de vencimiento (hoy: 3)
UsuarioAutenticación; password con BCrypt; requiereCambioPassword
Role / PermisoRoles agrupando los 5 permisos heredados del Access
UsuarioRole / RolePermisoAsignaciones rol ↔ usuario y permiso ↔ rol
UsuarioPermisoExcepciones puntuales por usuario; solo se expone si el cliente lo confirma
RutaCatálogo administrable de rutas de cobranza en campo
ClienteFicha única; nombreCompleto íntegro; la ruta vive aquí, no en el préstamo
ClienteDocumentoExpediente digital; baja lógica; reemplazo con historial
AvalEl aval avala un crédito, no a una persona; 0..n por préstamo
PrestamoSnapshot de condiciones: tasa, periodicidad, esquema y cuotas congelados al alta
CuotaRenglón de amortización; estado derivado, saldos y pagos acumulados
PagoEl dinero recibido; puede cubrir varias cuotas y dejar saldo a favor
PagoAplicacionDetalle de distribución: cuánto de cada pago fue a capital, interés y mora
MovimientoEstado de cuenta del cliente — no confundir con la caja
MovimientoCajaEfectivo real del negocio; un pago genera uno solo, con utilidad = interés + mora

Relaciones principales

Empresa ── Configuracion

Usuario ── UsuarioRole ── Role ── RolePermiso ── Permiso
   └────── UsuarioPermiso (excepciones opcionales)

Ruta ── Cliente ── ClienteDocumento
            └── Prestamo ── Aval
                    ├── Cuota ── PagoAplicacion ── Pago
                    ├── Movimiento
                    └── MovimientoCaja (cuando el movimiento de caja está relacionado)

Cinco decisiones del modelo que la interfaz hace visibles

Pago separado de Cuota

Un pago puede cubrir varias cuotas y dejar saldo a favor. El panel de cobro captura un monto recibido y muestra cómo se reparte — un cambio de mentalidad respecto del Access.

PagoAplicacion como detalle

Es lo que permite que el recibo impreso diga exactamente cuánto fue a capital, interés y mora. Sin eso, el recibo sería un total opaco.

Movimiento ≠ MovimientoCaja

El estado de cuenta del cliente y la caja del negocio son dos contabilidades: dos pantallas (9 y 15) que no se suman entre sí.

ClienteDocumento como tabla

Un cliente puede tener tres comprobantes de ingresos sin que nadie toque el esquema — lista con carga múltiple, no casillas fijas.

Aval colgado de Prestamo

Si el mismo avalista respalda dos créditos del mismo cliente, son dos registros. La pantalla 7 vive dentro del crédito.

Snapshot de condiciones

Tasa, periodicidad, esquema y número de cuotas quedan congelados en el crédito: cambiar tasas del negocio no altera créditos históricos.

Campos derivados que el frontend espera recibir calculados

Ninguno se deriva en el cliente. Llegan ya resueltos en cada respuesta.

RecursoCampoDefinición
CuotaPENDIENTE · PARCIAL · VENCIDA · PAGADA — derivado (§3.1)
totalCuota − (capitalPagado + interesPagado + moratorioPagado)
Fórmula del Anexo A §A.4; 0 si no aplica
Mora acumulada a la fecha de consulta, sin persistir
saldoPendiente + moraAlDia — el total del panel de cobro
PrestamoEl «5 de 12» que aparece en toda la interfaz
Conteo, para el distintivo de riesgo
Fecha de la siguiente cuota no pagada
Booleano derivado, lectura (b) de VENCIDO
Conteo para el distintivo, sin traer la lista
false en los 24 créditos migrados sin movimientos
ClienteLa columna que hace visible la separación cliente/crédito
Conteo
Suma de saldos de sus créditos activos
Junto a rutaId, para no resolver el catálogo en cada fila
Documentos vigentes del expediente
Solo si la regla abierta #13 lo hace necesario
ClienteDocumentoPara mostrar «2.4 MB» sin descargar el archivo
Ruta relativa del endpoint de descarga, armada por el backend. rutaArchivo no debe salir jamás
Quién lo subió — expediente que revisan varias personas
MovimientoCajaINGRESO · EGRESO · AJUSTE, derivada del prefijo del tipo
Trazabilidad cuando el movimiento nació de un pago
Determina si la fila es editable en la pantalla 15

Presente en el modelo, sin superficie en esta entrega

Anexo C §C.2: la presencia de una estructura en la base de datos no implica funcionalidad contratada.

Elemento del modeloEstado en la entrega
Campo creado y nullable. Sin flujo de refinanciamiento
Definidos. La interfaz opera los cuatro de §3.2
Para migrar créditos históricos. No se ofrecen en el alta
Tabla creada. Se expone solo si el cliente lo confirma
Valor definido. Sin operación que lo genere
Anexo C §C.3 prevé además cinco temas de revisión futura — refinanciamiento, módulo de ahorro, catálogo de productos, flujo de solicitud/autorización e interés sobre saldos insolutos — cada uno incorporable después sin rehacer lo ya construido. Es previsión, no backlog.

SD · 07Flujos clave

Los recorridos que concentran el riesgo. Cada uno indica dónde vive cada responsabilidad.

Alta de crédito con vista previa — pantalla 6

1. Motor espejo TS pinta el plan al teclear (respuesta instantánea, sin red) · 2. POST /api/prestamos/simulacion con debounce de 400 ms — si difieren, manda el backend y la discrepancia se registra en consola, nunca se muestra al usuario · 3. POST /api/prestamos + Clave-Idempotencia: una transacción que crea el Prestamo con snapshot, genera todas las Cuota, registra el Movimiento de otorgamiento y el MovimientoCaja EGRESO_PRESTAMO_OTORGADO · 4. Los avales se capturan después (pantalla 7): Aval.prestamoId exige que el préstamo exista.

Registro de un pago — pantallas 10 y 11

1. POST …/pagos/previsualizacion muestra qué cuotas se liquidan y cuánto va a mora, interés y capital — sin persistir · 2. POST …/pagos + Clave-Idempotencia: una sola transacción — crea Pago, distribuye, crea PagoAplicacion, acumula en Cuota, actualiza saldos, registra Movimiento, registra un único MovimientoCaja con utilidad = interés + mora cobrados, e incrementa el saldo a favor · 3. La respuesta trae la distribución final: con ella se pintan historial y recibo sin segunda consulta. Si difiere de la previsualización, manda la respuesta y se avisa del cambio. El abono parcial no es otro endpoint: es el mismo pago con monto menor.

Cancelación de un pago

Confirmación explícita (acción destructiva) → POST /api/pagos/{id}/cancelacion + idempotencia: revierte PagoAplicacion, recalcula cuotas, ajusta saldos, anula o compensa el movimiento de caja y marca CANCELADO. Si la ventana de tiempo pasó: 409 CANCELACION_NO_PERMITIDA con el motivo — nunca un 403 genérico.

Cobranza en ruta e impresión — pantallas 13, 14 y 17

GET /api/cobranza/vencidas es la lista de trabajo del cobrador · GET /api/impresion/{formato} trae datos + identidad del negocio para la vista previa con @media print · el PDF lo genera el backend con la misma plantilla que se imprime (Anexo B §B.4): archivo y papel idénticos.

Anexo A · documento fuente

Anexo A

Las referencias cruzadas de este documento abren en ventana superpuesta. Desde cualquier vista puedes volver al menú.

ANEXO A — INVENTARIO DE ALCANCE

Sistema de Gestión Financiera — CRECIENDO JUNTOS FINANCIERA

Escenario A — Implementación Completa

Versión: 1.3 Fecha de elaboración: 31 de agosto de 2026 Fecha de actualización: 2 de septiembre de 2026 Estado: Borrador para aprobación del cliente

Qué cambió en la versión 1.3: se incorporan al alcance el Expediente digital del cliente y los Avales del crédito (pantallas 5 y 7). El inventario pasa de 15 a 17 pantallas. Justificación en el Anexo C §C.1.

Qué cambió en la versión 1.2: las secciones de entidades preparadas y de exclusiones explícitas se trasladaron al Anexo C, que se aprueba junto con este anexo.

Qué cambió en la versión 1.1: se incorporó el modelo de datos reconciliado del equipo de backend (Modelo_Datos_Financiero_Reconciliado, v1.1). Los cambios afectan a las secciones A.2, A.3, A.4, A.8 y A.9, y la lista de puntos por confirmar pasó de 6 a 11. El inventario de pantallas (A.1) y los reportes del Anexo B no cambian: el alcance funcional contratado es el mismo.


Fuente y método

Este anexo se elaboró a partir del análisis directo de los archivos del sistema Access en operación, entregados por el cliente, y del modelo de datos definido para la nueva plataforma:

Archivo Contenido
APLICACION FINANCIERA CRECIENDO JUNTOS_be.accdb Back-end: 7 tablas de datos
APLICACION DE PRESTAMOS 2-estable v12_Backup.accdb Front-end: 17 formularios, 34 consultas, 8 reportes, 2 módulos
Modelo_Datos_Financiero_Reconciliado v1.1 Modelo de datos de la nueva plataforma en PostgreSQL

Todo lo listado en este anexo queda incluido en el alcance contratado. Todo lo que no aparezca en este anexo se considera desarrollo adicional y se cotiza por separado, conforme a la sección 2 de la propuesta.

Este anexo se complementa con el Anexo B (reportes y formatos incluidos) y el Anexo C (entidades preparadas y exclusiones explícitas). Los tres se aprueban de manera conjunta y forman una sola definición de alcance.

Los puntos marcados [POR CONFIRMAR] requieren definición expresa del cliente antes de la aprobación de este anexo. No pueden quedar abiertos al inicio del desarrollo.

A.1 Módulos y pantallas

Listado nominal cerrado. 17 pantallas, agrupadas en 7 módulos.

Quince de ellas reproducen funcionalidad existente en el sistema Access. Dos —Expediente digital del cliente y Avales del crédito— no existen en el sistema actual y se incorporan al alcance por acuerdo entre las partes; el detalle y su justificación están en el Anexo C §C.1.

Módulo 1 — Seguridad y acceso

# Pantalla Origen en Access Descripción
1 Inicio de sesión Pasword Autenticación por usuario y contraseña
2 Gestión de usuarios usuarios Alta, edición, activación y permisos por usuario

Módulo 2 — Clientes

# Pantalla Origen en Access Descripción
3 Listado de clientes Panelprincipal (lista) Consulta, búsqueda y filtro por ruta
4 Ficha de cliente Panelprincipal (alta/edición) Datos personales, documento de identificación, contacto, dirección, ruta y notas
5 Expediente digital del cliente (nueva) Carga, consulta y descarga de los documentos del cliente, clasificados por tipo

Módulo 3 — Créditos y amortización

# Pantalla Origen en Access Descripción
6 Alta de crédito Panelprincipal (capital, cuotas, tasa, forma de pago) Registro del préstamo y generación automática del plan de pagos
7 Avales del crédito (nueva) Alta, edición y consulta de los avales asociados a un crédito
8 Tabla de amortización Amortizacion, Amortizacion_2 Consulta del plan de pagos completo del crédito, con estado por cuota

Módulo 4 — Cobranza y pagos

# Pantalla Origen en Access Descripción
9 Historial del cliente Historial, Historial pagos Detalle de cuotas pagadas y pendientes, con mora calculada
10 Registro de pago de cuota Registro pago Aplicación de pago total de una cuota
11 Abono parcial Abono parcial Aplicación de un abono menor al monto de la cuota
12 Abono a capital Abona capital Abono directo al capital del crédito
13 Cuotas vencidas Vencidos, Cuotas vencidas Listado de cuotas con fecha de pago cumplida y sin registrar
14 Próximos a vencer Proximos_vencer Cuotas por vencer dentro del plazo configurado en Recordatorios

Módulo 5 — Caja

# Pantalla Origen en Access Descripción
15 Ingresos y egresos Ingresos_egresos Registro de movimientos de caja con concepto, valor y utilidad

Módulo 6 — Consultas e impresión

# Pantalla Origen en Access Descripción
16 Consultas por rango de fechas Buscar_historial, Reg_ingresos_egresos Búsqueda de movimientos e ingresos/egresos entre dos fechas
17 Impresión de listados y recibos por ruta Imprimir, Imprimir_recibos Selección de ruta y fecha para generar los formatos del Anexo B

Módulo 7 — Configuración

Incorporado como sección dentro de la pantalla de configuración, no como pantallas independientes:

Sección Origen en Access Descripción
Datos del negocio DatosNegocio Nombre, lema y logotipo utilizados en los formatos impresos
Recordatorios Recordatorio Días de anticipación del aviso de vencimiento
Catálogo de periodicidad tabla Tiempo Diario, Semanal, Quincenal, Mensual, Trimestral, Anual
Catálogo de rutas campo Ruta Ver A.5
Catálogo de tipos de documento (nuevo) Tipos admitidos en el expediente digital: identificación (frente y reverso), comprobante de domicilio, comprobante de ingresos, CURP y otros

No incluidos: el formulario Ayuda se sustituye por el manual de uso entregable. Los módulos VBA Boton y changecaracter son utilidades de interfaz de Access sin equivalente ni reglas de negocio; no se migran.


A.2 Perfiles y permisos

El sistema actual maneja permisos individuales por usuario mediante seis atributos booleanos. En la nueva plataforma se reproducen mediante un esquema de roles y permisos: los seis permisos se conservan íntegros como catálogo, y se agrupan en roles reutilizables.

Catálogo de permisos

Permiso Origen Descripción
ADMINISTRAR_USUARIOS admin Gestión de usuarios, roles y configuración del sistema
EDITAR_HISTORIAL editar_historial Registrar y modificar pagos y abonos
LISTAR_CLIENTES lista_clientes Consultar el listado de clientes
BUSCAR_HISTORIAL buscar_historial Consultas por rango de fechas
ELIMINAR eliminar Eliminar créditos y movimientos

El atributo activo del sistema actual no es un permiso: se conserva como estado del usuario, que habilita o deshabilita su acceso.

Roles

Rol Permisos Usuarios actuales
ROLE_ADMIN Todos SISTEMAS, SAMUEL ESAU
ROLE_CAJA Editar historial, Listar clientes, Buscar historial REYNA JANETH, BERNAL JASSIEL
ROLE_COBRANZA Editar historial, Listar clientes EVELIO ALEJANDRO, DONACIANO

Un usuario puede tener excepciones puntuales sobre su rol: se le concede o se le niega un permiso específico sin necesidad de crear un rol nuevo. La pantalla de usuarios permite asignar el rol; la administración de excepciones queda disponible en el modelo y se expone en la interfaz solo si el cliente confirma que la necesita.

[POR CONFIRMAR] Los nombres de los roles y su asignación a cada usuario. Los permisos individuales se conservan tal como están hoy en la tabla usuarios.

Nota de seguridad: las contraseñas se encuentran hoy almacenadas en texto plano. En la migración se cifran con algoritmo de hash irreversible (BCrypt) y se solicita cambio de contraseña en el primer acceso de cada usuario. Este cambio es obligatorio y no admite excepción.


A.3 Esquemas de amortización

El sistema actual opera dos esquemas, ambos incluidos en el alcance.

Esquema 1 — Capital + interés (533 créditos)

Interés simple calculado sobre el capital original. El interés es constante en todas las cuotas; no se recalcula sobre saldo insoluto.

capital_por_cuota = capital_prestado / número_de_cuotas
interes_por_cuota = capital_prestado × tasa_periodo
total_cuota       = capital_por_cuota + interes_por_cuota

Esquema 2 — Solo interés (6 créditos)

Se cobra únicamente el interés en cada periodo. El capital se liquida al final del plazo.

total_cuota = capital_prestado × tasa_periodo

[POR CONFIRMAR] La regla exacta de recuperación del capital en este esquema: si existe una cuota final que lo incluye, o si se registra como un evento aparte. Sin esta definición el esquema no puede codificarse.

Parámetros por crédito

Parámetro Origen Valores en operación
Capital prestado Capital prestado Monto libre
Tasa por periodo Interes Predominante 0.0225 (2.25%); también 0.045, 0.0175, 0.09, 0.05, 0.025, 0.02, 0.0155, 0.0125, 0.007875, 0.0009
Número de cuotas Cuotas 8, 10, 12, 16, 17, 19, 24, 29, 32, 36, 40, 42, 48, 49, 56, 72, 80, 94, 96, 120, 136, 144, 240, 244, 360, 370
Periodicidad Forma de pago Semanal (497), Quincenal (38), Mensual (4)
Esquema Paga cuotas Capital+interés (533), Solo interés (6)
Tasa de mora Interes_mora Ver A.4

Las condiciones del crédito (tasa, periodicidad, esquema y número de cuotas) quedan registradas en el propio crédito al momento de darlo de alta. Un cambio posterior de las tasas del negocio no altera los créditos ya generados.

La tasa y el número de cuotas son libres por crédito. El sistema no restringe a los valores listados; el listado documenta la operación observada.

Catálogo de periodicidad

El catálogo conserva los seis valores de la tabla Tiempo del sistema actual —Diario, Semanal, Quincenal, Mensual, Trimestral y Anual— para poder migrar sin pérdida cualquier crédito histórico. El alta de créditos nuevos ofrece únicamente Semanal, Quincenal y Mensual, que son las periodicidades en operación.

Generación del plan de pagos

Al registrar el crédito se generan por adelantado todas las cuotas con su fecha de vencimiento, calculada sumando los días de la periodicidad a partir de la fecha del préstamo:

Semanal   → +7 días
Quincenal → +15 días
Mensual   → +30 días

La periodicidad mensual suma 30 días, no un mes calendario, reproduciendo el comportamiento actual del sistema Access.

Estado de cada cuota

El estado no se almacena: se deriva en el momento de consultarlo, de modo que una cuota nunca queda con un estado desactualizado.

PAGADA    → no queda saldo pendiente
PARCIAL   → hay pago aplicado, pero queda saldo
VENCIDA   → la fecha de vencimiento ya pasó y queda saldo
PENDIENTE → la fecha de vencimiento no ha llegado

A.4 Reglas de pago

Pago de cuota

Se registra el monto recibido y la fecha real de pago. El sistema distribuye el monto sobre las cuotas pendientes y deja constancia de cuánto se aplicó a cada concepto, de modo que cada peso cobrado es rastreable.

Prioridad de aplicación

Un pago se aplica en este orden:

1. Mora
2. Interés
3. Capital
4. Saldo a favor

[POR CONFIRMAR] Confirmación de este orden. Se propone resuelto conforme a la práctica habitual del sector; si el negocio aplica otro orden, debe indicarlo antes de aprobar este anexo.

Mora

Fórmula reproducida del sistema actual:

días_atraso = si la cuota está pagada:    fecha_movimiento − fecha_pago
              si la cuota está pendiente: fecha_actual − fecha_pago
              (si el resultado es negativo, se toma 0)

mora = esquema Capital+interés:  retorno_capital × tasa_mora × días_atraso
       esquema Solo interés:     valor_interes   × tasa_mora × días_atraso

Observación: de 557 créditos, un solo crédito 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 utiliza.

[POR CONFIRMAR] Si la mora se aplica automáticamente al registrar el pago o si el operador puede condonarla. Hoy el campo es editable por crédito.

Abono parcial

Registro de un abono menor al monto de la cuota. La cuota queda como parcial hasta cubrirse. En la base actual existen 400 movimientos con abono parcial registrado.

[POR CONFIRMAR] Cuando el abono acumulado alcanza o supera el monto de la cuota: ¿la cuota se marca como pagada automáticamente y el excedente pasa a la siguiente, o requiere que el operador la cierre manualmente?

Saldo a favor

Un pago mayor al monto debido genera saldo a favor en el crédito.

[POR CONFIRMAR] Si el saldo a favor se aplica automáticamente al siguiente vencimiento o si se conserva hasta que el operador decida aplicarlo.

Abono a capital

Abono directo al capital del crédito, fuera del plan de cuotas.

[POR CONFIRMAR — bloqueante] Este punto debe definirse antes de aprobar el anexo, porque determina el comportamiento del motor de amortización. Opciones:

  • (a) Reduce el número de cuotas restantes, manteniendo el monto de cada una.
  • (b) Reduce el monto de las cuotas restantes, manteniendo el número.
  • (c) Se registra como movimiento de caja sin alterar el plan de pagos vigente.

Dato del sistema actual: en la base entregada existen 5 movimientos de caja con el concepto Abona capital <nombre del cliente>, lo que confirma que la operación genera un registro en caja. No es concluyente respecto de su efecto sobre el plan de pagos.

Cancelación y reverso de pagos

Un pago registrado por error puede cancelarse. La cancelación revierte la aplicación del pago sobre las cuotas y anula su movimiento de caja, conservando el registro del pago cancelado para efectos de auditoría.

[POR CONFIRMAR] Qué perfil puede cancelar un pago y si existe un límite de tiempo para hacerlo (por ejemplo, el mismo día, o hasta el cierre del periodo).

Registro en caja

Toda operación genera automáticamente un movimiento en la caja del negocio:

Operación Clasificación Naturaleza
Alta de crédito EGRESO_PRESTAMO_OTORGADO Egreso
Pago de cuota INGRESO_PAGO_PRESTAMO Ingreso
Abono parcial INGRESO_PAGO_PRESTAMO Ingreso
Abono a capital INGRESO_PAGO_PRESTAMO Ingreso
Aportación de capital al negocio INGRESO_CAPITAL Ingreso
Retiro de capital EGRESO_RETIRO_CAPITAL Egreso
Gasto operativo EGRESO_GASTO_OPERATIVO Egreso
Otros ingresos o egresos INGRESO_OTRO / EGRESO_OTRO Según el caso

Regla de utilidad: un pago genera un solo movimiento de caja, por el monto total recibido, con la utilidad registrada como interés + mora efectivamente cobrados. No se genera un movimiento aparte por la ganancia, para no duplicar el ingreso.

Ejemplo:

Pago recibido:      $2,500
  capital aplicado: $2,000
  interés aplicado:   $400
  mora aplicada:      $100

Movimiento de caja → importe $2,500 · utilidad $500

A.5 Rutas de cobranza

El sistema opera cobranza en campo organizada por rutas. Hoy el valor se captura como texto libre en el campo Ruta. En el nuevo sistema se convierte en catálogo administrable.

Ruta Créditos asignados
Ruta 1 LUNES 75
Ruta 2 MARTES 77
Ruta 3 MIERCOLES 79
Ruta 4 JUEVES 79
Ruta 5 VIERNES 77
Ruta 6 SABADO 53
GRUPOS 45
QUINCENAL 41
LIQUIDACION 13
Sin ruta asignada 18

[POR CONFIRMAR] Si GRUPOS, QUINCENAL y LIQUIDACION son rutas de cobranza equivalentes a las numeradas o clasificaciones de otra naturaleza.


A.6 Consultas y filtros

Consulta Filtros incluidos Origen
Listado de clientes Nombre, documento, ruta Lista clientes
Créditos activos Cliente, ruta Activos
Cuotas pendientes por crédito Crédito Pendientes
Cuotas vencidas Ruta, fecha Cuotas vencidas, Lista vencidos ruta
Próximos a vencer Días de anticipación configurados Proximos_vencer
Saldo por cliente Cliente Saldo
Historial de pagos por cliente Cliente Historial pagos
Historial de pagos por ruta y fecha Ruta, fecha Historial_pagos_rutas
Listado de abonos por ruta y fecha Ruta, fecha Lista abono rutas
Ingresos y egresos por rango de fechas Fecha inicial y final Reg_ingresos_egresos
Total en efectivo Total_efectivo
Proyección de cobranza por mes Año y mes Proyeccion
Utilidad histórica por mes Año y mes Utilidad_historial

Las 34 consultas del sistema actual se consolidan en estas 13. Las restantes son consultas de acción (inserción, actualización y borrado) que quedan implementadas como operaciones internas del sistema, sin pantalla propia.


A.7 Recordatorios

Tipo Condición que lo dispara Origen
Próximo a vencer Cuota pendiente cuya fecha programada está dentro de los N días configurados. Valor actual: 3 días tabla Recordatorio, formulario Proximos_vencer
Vencido Cuota pendiente cuya fecha programada ya pasó formulario Vencidos

Los recordatorios se muestran dentro del sistema. Conforme a la sección 5 de la propuesta, no se incluye envío por WhatsApp, SMS, correo electrónico ni ningún servicio de mensajería.



A.8 Tablas y campos a migrar

Se migra la información de las 7 tablas del sistema actual al nuevo modelo de datos.

Tabla origen Campos Registros Destino en PostgreSQL
Clientes 20 557 cliente + prestamo (se separa en dos entidades, ver nota)
Movimientos 16 17,927 cuota + pago + pago_aplicacion
Ingreso_egreso 5 15,030 movimiento_caja (se regenera, ver nota)
usuarios 10 6 usuario + role + permiso
Tiempo 3 6 catálogo de periodicidad (enum)
Recordatorio 2 1 configuracion
Datos negocio 4 1 empresa (incluye el logotipo)

Las entidades aval y cliente_documento no tienen origen en el sistema Access: se crean vacías y se alimentan a partir de la puesta en marcha.

Total en el origen: 33,528 registros.

Nota — separación cliente / crédito

En el sistema actual, cliente y crédito son el mismo registro. Un cliente con varios créditos aparece hoy duplicado, con sufijos "I", "II", "III" en el nombre. El nuevo modelo separa ambas entidades, lo que permite registrar varios créditos por cliente sin duplicar sus datos. La migración conserva íntegra la información existente: los registros que hoy están duplicados se migran tal como se encuentran en el sistema actual, cada uno con sus créditos y su historial completo.

Nota — nombre del cliente

El nombre se migra íntegro en un solo campo, tal como está en el sistema actual. No se separa automáticamente en nombre y apellidos: hacerlo sobre cientos de nombres capturados libremente produce errores. Los campos separados existen en el modelo y quedan disponibles para captura manual posterior, sin costo adicional.

Nota — regeneración de la caja

Los 15,030 registros de Ingreso_egreso se regeneran a partir de los movimientos de crédito, en lugar de migrarse fila por fila. Motivo: prácticamente la totalidad de esos registros son generados automáticamente por el sistema actual al otorgar un crédito o al recibir un pago, y la tabla no guarda ninguna referencia al cliente —el nombre viaja concatenado dentro del texto del concepto—. Regenerarlos desde su origen produce una caja con integridad referencial completa, en la que cada movimiento queda ligado a su crédito y a su cliente, cosa que hoy no ocurre.

Los 20 registros capturados manualmente (aportaciones de capital y un retiro de efectivo) se migran tal cual.

El resultado es equivalente en importes y superior en trazabilidad. Ver A.9 para el desglose.

Campos que no se migran

Corresponden a artefactos del sistema actual sin valor en el nuevo modelo:

  • Movimientos.Pago — contiene siempre el valor "Aceptar" en los 17,927 registros.
  • Clientes.Valor interes, Total interes, Retorno capital, Capital+interes — campos de cálculo que en la base actual están en cero; los valores se derivan del crédito.

A.9 Volumen y calidad de la información

Volumen

Concepto Cantidad
Clientes / créditos 557
Movimientos (cuotas del plan de pagos) 17,927
— cuotas pagadas 6,478
— cuotas pendientes 11,428
— con abono parcial 400
— con mora aplicada 17
— no legibles en la exportación 21
Movimientos de caja 15,030
Usuarios 6
Rango de fechas de préstamo febrero 2019 – marzo 2025
Rango de fechas de caja junio 2024 – marzo 2025

Composición de la caja

De los 15,030 movimientos de caja registrados en 9 meses de operación:

Origen Registros %
Generados automáticamente por pagos de cuota 11,138 74.1 %
Generados automáticamente por créditos otorgados 2,107 14.0 %
Generados automáticamente por abonos parciales 1,765 11.7 %
Capturados manualmente 20 0.1 %

Los 20 registros manuales se componen de 14 aportaciones de capital al negocio, 1 retiro de efectivo y 5 abonos a capital de clientes.

Consecuencia declarada: en los 9 meses registrados no existe ningún gasto operativo capturado —renta, sueldos, combustible, comisiones—. Por lo tanto, el indicador de utilidad del sistema refleja la ganancia por intereses y mora cobrados, no la utilidad neta del negocio. Esta definición se hace explícita en el Anexo B §B.3 para evitar que el tablero se interprete como un estado de resultados.

[POR CONFIRMAR] Dónde se registran hoy los gastos operativos del negocio y si deben incorporarse al sistema. La respuesta define qué significa "utilidad" en los reportes y en el tablero.

Incidencias de calidad detectadas

Conforme a la sección 10 de la propuesta, la reconstrucción manual de información inconsistente no está incluida en el alcance. Se documentan aquí para su tratamiento acordado:

Incidencia Volumen Tratamiento propuesto
Movimientos con identificador de cliente inexistente 60 Se migran a un cliente marcador identificable, para no perder el histórico
Clientes sin nombre registrado 14 Se migran tal cual, marcados para revisión del cliente
Clientes sin fecha de préstamo, forma de pago o ruta 18 a 20 Se migran sin plan de pagos recalculado; requieren captura manual del cliente
Créditos sin ningún movimiento 24 Se migran como créditos sin plan de pagos generado
Tasa de interés almacenada como 1E-135 en lugar de cero Varios Se normaliza a cero en la migración
Fechas inválidas (01/00/00) Algunas Se migran como fecha nula
Contraseñas en texto plano 6 Se cifran obligatoriamente (ver A.2)
Movimientos que no se exportan limpiamente (caracteres especiales o saltos de línea en campos de texto) 21 Se revisan y depuran uno a uno durante la migración
Movimientos de caja con importes de escala anómala (aportaciones y retiros de hasta 9 dígitos, muy por encima del volumen de cartera) 9 Se migran tal cual, marcados para revisión del cliente. No afectan los saldos de los créditos

[POR CONFIRMAR] Fecha de corte de la copia final de la base de datos. La copia entregada contiene información hasta marzo de 2025; la operación ha continuado desde entonces. Conforme a la sección 10, la migración se ejecuta una sola vez sobre la copia que el cliente entregue en la fecha acordada.


A.10 Puntos que requieren definición del cliente

Resumen de los once puntos marcados [POR CONFIRMAR] en este anexo. Todos deben quedar resueltos por escrito antes de la aprobación:

# Punto Sección Criticidad
1 Regla de aplicación del abono a capital A.4 Bloqueante — determina el motor de amortización
2 Cierre automático o manual de la cuota al completarse el abono parcial A.4 Alta
3 Aplicación automática o condonable de la mora A.4 Media
4 Naturaleza de las rutas GRUPOS, QUINCENAL y LIQUIDACION A.5 Media
5 Nombres de roles y asignación por usuario A.2 Baja
6 Fecha de corte de la copia final de la base de datos A.9 Alta — condiciona la ejecución de la migración
7 Orden de aplicación de un pago (se propone: mora → interés → capital → saldo a favor) A.4 Media
8 Saldo a favor: ¿se aplica automáticamente al siguiente vencimiento o se conserva? A.4 Media
9 Reverso de pagos: ¿qué perfil puede cancelar un pago y con qué límite de tiempo? A.4 Media
10 Esquema Solo interés: ¿cómo y cuándo se recupera el capital? A.3 Alta
11 Gastos operativos: ¿dónde se registran hoy y deben incorporarse al sistema? A.9 Alta — define el significado de "utilidad"

Aprobación

Este anexo, una vez firmado, define de manera cerrada el alcance contratado del Escenario A. Conforme a la sección 2 de la propuesta, toda funcionalidad, regla de negocio, cálculo, reporte o comportamiento que no aparezca en este documento se considera desarrollo adicional y se cotiza por separado.

Cliente Proveedor
Nombre
Cargo
Fecha
Firma
Anexo B · documento fuente

Anexo B

Las referencias cruzadas de este documento abren en ventana superpuesta. Desde cualquier vista puedes volver al menú.

ANEXO B — REPORTES Y FORMATOS INCLUIDOS

Sistema de Gestión Financiera — CRECIENDO JUNTOS FINANCIERA

Escenario A — Implementación Completa

Versión: 1.1 Fecha de elaboración: 31 de agosto de 2026 Fecha de actualización: 2 de septiembre de 2026 Estado: Borrador para aprobación del cliente

Qué cambió en la versión 1.1: se definieron con precisión los seis indicadores del tablero (§B.3), que antes solo estaban enunciados, y se añadió la nota sobre generación de PDF (§B.4). El listado de reportes y formatos no cambia.

Nota importante: este anexo sustituye al Anexo B preliminar de la propuesta del 27 de agosto de 2026. Del análisis del sistema Access se identificaron cinco formatos de impresión (recibos, tickets y listados de ruta) que no estaban contemplados en el listado preliminar y que constituyen la operación diaria de cobranza en campo. Se incorporan aquí para que formen parte del alcance contratado.


B.1 Reportes analíticos

Listado cerrado. Cualquier reporte no listado se cotiza por separado.

# Reporte Filtros Formatos Origen en Access
1 Movimientos por rango de fechas Fecha inicial y final Pantalla, PDF, Excel Buscar_historial
2 Estado de cuenta por cliente Cliente Pantalla, PDF, Excel Historial pagos
3 Pagos realizados por periodo Fecha inicial y final, ruta Pantalla, PDF, Excel Historial_pagos_rutas
4 Saldos pendientes Cliente, ruta Pantalla, PDF, Excel Saldo, Activos, Pendientes
5 Ingresos y egresos por periodo Fecha inicial y final Pantalla, PDF, Excel Reg_ingresos_egresos
6 Tabla de amortización por crédito Crédito Pantalla, PDF, Excel reporte Amortizacion
7 Cuotas vencidas Ruta, fecha Pantalla, PDF, Excel Cuotas vencidas
8 Proyección de cobranza por mes Año Pantalla, PDF, Excel Proyeccion
9 Utilidad por mes Año Pantalla, PDF, Excel Utilidad_historial

Nota al reporte 9: la definición de "utilidad" depende del punto por confirmar #11 del Anexo A (dónde se registran los gastos operativos del negocio). Con la información actual, el reporte muestra intereses y mora efectivamente cobrados. Ver §B.3.


B.2 Formatos de impresión de cobranza

Formatos operativos que el sistema actual imprime a diario para el trabajo en campo. Se reproducen conservando su estructura y los datos del negocio (nombre, lema y logotipo) tomados de la configuración.

# Formato Filtros Formatos Origen en Access
10 Listado de ruta Ruta, fecha Impresión, PDF reporte Lista_rutas
11 Listado de ruta con abonos Ruta, fecha Impresión, PDF reporte Lista_rutas_abonos
12 Recibo de pago Crédito, cuota Impresión, PDF reporte Recibo
13 Recibo de pago — esquema solo interés Crédito, cuota Impresión, PDF reporte Recibo_interes
14 Ticket de pago Crédito, cuota Impresión, PDF reporte Ticket
15 Ticket de pago — esquema solo interés Crédito, cuota Impresión, PDF reporte Ticket_interes
16 Ticket por ruta Ruta, fecha Impresión, PDF reporte Ticket_ruta

[POR CONFIRMAR] Si los tickets se imprimen en impresora térmica de rollo (formato angosto) o en hoja carta. El formato de salida condiciona el diseño y debe definirse antes de iniciar el módulo de impresión.


B.3 Tablero de indicadores

Incluido en el Escenario A. No incluido en el Escenario B. El tablero se compone de seis indicadores, cuya definición queda establecida a continuación:

# Indicador Definición exacta Origen
17.1 Total en efectivo Suma algebraica de todos los movimientos de caja: ingresos menos egresos registrados en el sistema Total_efectivo
17.2 Cartera activa Suma del saldo pendiente de todos los créditos con cuotas por cobrar Activos
17.3 Cobranza del periodo Suma de los pagos recibidos en el periodo seleccionado Historial pagos
17.4 Cuotas vencidas Número e importe de las cuotas con fecha de vencimiento cumplida y saldo pendiente Cuotas vencidas
17.5 Proyección del mes Suma de las cuotas cuyo vencimiento cae en el mes seleccionado Proyeccion
17.6 Utilidad del mes Suma de los intereses y la mora efectivamente cobrados en el periodo Utilidad_historial

Precisiones necesarias sobre estos indicadores

Se hacen explícitas para evitar que el tablero se interprete como un estado de resultados:

  • "Utilidad del mes" no es ingresos menos gastos. Es la ganancia financiera cobrada: intereses y mora efectivamente recibidos. No descuenta gastos operativos.
  • "Total en efectivo" no incluye gastos operativos si el negocio no los captura en el sistema. En los 9 meses de la base entregada no existe ningún gasto operativo registrado (ver Anexo A §A.9).
  • Ambos indicadores cambian de significado si el cliente decide registrar sus gastos operativos en el sistema. Esa decisión corresponde al punto por confirmar #11 del Anexo A y no altera el alcance contratado: los indicadores se calculan sobre la información que el negocio capture.

B.4 Condiciones

Los formatos, columnas y criterios de agrupación de cada reporte se definen y aprueban junto con este anexo, antes del inicio del proyecto.

Este anexo se aprueba de manera conjunta con el Anexo A (inventario de alcance) y el Anexo C (entidades preparadas y exclusiones explícitas).

Los reportes se generan sobre la información existente en el sistema. Reportes con lógica de cálculo distinta a la del sistema actual, o que requieran información no capturada hoy, se consideran desarrollo adicional.

Los archivos PDF se generan 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.


Aprobación

Cliente Proveedor
Nombre
Cargo
Fecha
Firma
Anexo C · documento fuente

Anexo C

Las referencias cruzadas de este documento abren en ventana superpuesta. Desde cualquier vista puedes volver al menú.

ANEXO C — ALCANCE COMPLEMENTARIO Y EXCLUSIONES

Sistema de Gestión Financiera — CRECIENDO JUNTOS FINANCIERA

Escenario A — Implementación Completa

Versión: 1.1 Fecha de elaboración: 2 de septiembre de 2026 Estado: Borrador para aprobación del cliente


Objeto de este anexo

Los Anexos A y B definen lo que se entrega. Este anexo cubre lo que queda en los bordes de esa definición, y lo hace en tres categorías que conviene no confundir:

  • C.1 — Funcionalidad aprobada e incorporada al alcance que no existe en el sistema Access actual.
  • C.2 — Estructuras que existirán en la base de datos sin funcionalidad disponible en la entrega.
  • C.3 — Funcionalidades expresamente fuera del alcance contratado.

Este anexo se elabora para beneficio de ambas partes. Deja constancia de que estos elementos fueron identificados, revisados y acordados antes de iniciar el desarrollo, evitando que durante las pruebas de aceptación se interprete como incumplimiento aquello que se acordó no incluir, o como cortesía aquello que se acordó incluir.

Se aprueba de manera conjunta con los Anexos A y B, y forma parte de la misma definición de alcance.


C.1 Funcionalidad aprobada, no existente en el sistema actual

Nota metodológica

El inventario de los Anexos A y B se levantó a partir del análisis directo del sistema Access en operación. Las dos funcionalidades de esta sección no aparecen en ese análisis porque el sistema actual no las gestiona: hoy el expediente del cliente y el registro de avales se llevan fuera del sistema, en papel.

Su ausencia en el sistema actual no significa que sean prescindibles. Al revisar el modelo de datos de la nueva plataforma se determinó que ambas son parte del proceso real de otorgamiento de crédito de cualquier financiera, y se acordó incorporarlas al alcance. Las entidades correspondientes ya están definidas en el modelo de datos aprobado (Modelo_Datos_Financiero_Reconciliado, v1.1).

Ambas funcionalidades quedan APROBADAS E INCLUIDAS en el alcance contratado.

C.1.1 Expediente digital del cliente

Carga, consulta y descarga de los documentos que integran el expediente del cliente, clasificados por tipo.

Concepto Definición
Pantalla Anexo A §A.1, pantalla 5
Entidad ClienteDocumento
Relación Un cliente puede tener cualquier cantidad de documentos, de uno o varios tipos
Tipos admitidos Identificación (frente y reverso), comprobante de domicilio, comprobante de ingresos, CURP y otros
Operaciones Cargar, reemplazar, consultar, descargar y eliminar un documento

Justificación: en una financiera no se aprueba un préstamo sin que el solicitante cumpla los requisitos documentales. El expediente no es un accesorio del proceso de crédito: es la condición para otorgarlo. Digitalizarlo elimina el manejo en papel y deja el respaldo asociado al cliente dentro del propio sistema.

C.1.2 Avales del crédito

Alta, edición y consulta de los avales asociados a un crédito.

Concepto Definición
Pantalla Anexo A §A.1, pantalla 7
Entidad Aval
Relación Un crédito puede tener cero, uno o varios avales
Datos por aval Nombre completo, teléfono, dirección, CURP y tipo y número de identificación

Justificación: el aval es parte de las condiciones bajo las que se otorga el crédito y de la información que el negocio necesita para la recuperación. La relación se modeló como uno a varios, de modo que el número de avales por crédito no queda limitado por la estructura de la base de datos.

C.1.3 Consideraciones de esta incorporación

  • Ninguna de las dos funcionalidades tiene información que migrar: el sistema Access no las gestiona. Ambas entidades se crean vacías y se alimentan a partir de la puesta en marcha.
  • El expediente digital implica almacenamiento de archivos. El espacio de almacenamiento forma parte de la infraestructura, conforme a la sección 13 de la propuesta.
  • [POR CONFIRMAR] Tamaño máximo por archivo y formatos admitidos (se propone PDF, JPG y PNG, hasta 10 MB por archivo).
  • [POR CONFIRMAR] Si el sistema debe impedir el alta de un crédito cuando el expediente del cliente esté incompleto, o si únicamente lo señala como advertencia.

C.2 Entidades preparadas sin funcionalidad contratada

El modelo de datos incluye estructuras que quedan creadas pero sin funcionalidad implementada. Se dejan preparadas para evitar tener que rehacer el modelo de datos si el cliente decide contratarlas más adelante. Es una previsión a favor del cliente, no un trabajo pendiente.

Ninguna de las siguientes está incluida en el alcance contratado. Ninguna tendrá pantalla ni operación disponible en la entrega:

Elemento Estado en la entrega Referencia
Refinanciamiento de créditos Campo de relación creado. Sin flujo de refinanciamiento C.3
Estados de crédito En revisión, Autorizado, Rechazado y Refinanciado Definidos en el modelo. La interfaz opera únicamente con Activo, Liquidado, Vencido y Cancelado C.3
Periodicidades Diario, Trimestral y Anual Definidas para permitir la migración de créditos históricos. No se ofrecen en el alta de créditos nuevos Anexo A §A.3
Excepciones de permisos por usuario Estructura creada. Se expone en la interfaz solo si el cliente lo confirma Anexo A §A.2

Criterio acordado: la presencia de una estructura en la base de datos no implica funcionalidad contratada. Habilitar cualquiera de estos elementos requiere desarrollo de interfaz y de reglas de operación.


C.3 Fuera de alcance explícito

Además de lo enunciado en la sección 5 de la propuesta, quedan expresamente fuera del alcance contratado. Cada uno puede incorporarse posteriormente, sin necesidad de rehacer lo ya construido:

Funcionalidad Descripción
Refinanciamiento de créditos Generación de un crédito nuevo a partir del saldo de uno existente, conservando la trazabilidad entre ambos
Módulo de ahorro Cuentas de ahorro de clientes con sus propios movimientos y saldos
Catálogo de productos de crédito Definición de productos con tasa, plazo y condiciones predefinidas, para no capturarlas crédito por crédito
Flujo de solicitud y autorización Proceso formal de solicitud, revisión, autorización o rechazo previo al otorgamiento del crédito, con sus estados y responsables
Cálculo de interés sobre saldos insolutos El sistema actual opera con interés sobre capital original. Un esquema de saldos insolutos es un método de cálculo distinto

Nota sobre el flujo de autorización: el expediente digital incorporado en C.1.1 permite registrar y consultar la documentación del cliente. No incluye el proceso formal de solicitud, revisión y autorización del crédito, que se mantiene fuera del alcance según esta sección.


C.4 Confirmación de lo ya excluido en la propuesta

Se reitera, sin modificación, lo establecido en la sección 5 de la propuesta:

  • Integraciones bancarias.
  • Integración con el SAT o timbrado de comprobantes fiscales.
  • Pasarelas de pago en línea.
  • Integración con WhatsApp, SMS o servicios de mensajería.
  • Aplicaciones móviles.
  • Nuevas reglas financieras que no operan actualmente en el sistema Access.
  • Módulos, reportes o funcionalidades que no consten en los Anexos A, B y C aprobados.
  • Reconstrucción manual de información histórica inconsistente o dañada.
  • Migración de fuentes de información distintas a la identificada en el Anexo A.

Aprobación

Al firmar este anexo, ambas partes reconocen que:

  • Las funcionalidades de C.1 quedan incorporadas al alcance contratado, con el detalle aquí descrito.
  • Los elementos de C.2 y C.3 fueron identificados y revisados antes del inicio del desarrollo, y su ausencia en la entrega corresponde al alcance acordado.
Cliente Proveedor
Nombre
Cargo
Fecha
Firma
Modelo v1.1 · documento fuente

Modelo v1.1

Las referencias cruzadas de este documento abren en ventana superpuesta. Desde cualquier vista puedes volver al menú.

Modelo de Datos Reconciliado — Sistema de Gestión Financiera

Versión propuesta: 1.1

Stack objetivo: Spring Boot + Angular + PostgreSQL

Objetivo: consolidar el modelo inicial de backend con la revisión del modelo existente en Access, conservando trazabilidad financiera, facilitando la migración y evitando entidades o reglas que no aporten valor al alcance actual.

1. Criterios de diseño adoptados

1. BigDecimal para todos los importes y tasas. No usar double ni float para dinero.

2. Enums legibles en lugar de números mágicos. En PostgreSQL se recomienda persistirlos como texto.

3. Snapshot de condiciones del préstamo. Las tasas, periodicidad, esquema de cálculo y número de cuotas se copian al préstamo al autorizarlo. Si después cambia una configuración general, los préstamos históricos no se modifican.

4. Pago separado de Cuota. Un pago puede liquidar una cuota, varias cuotas o dejar un saldo a favor.

5. PagoAplicacion como detalle de distribución. Permite conocer cuánto de cada pago se aplicó a capital, interés y mora.

6. Movimiento y MovimientoCaja son conceptos distintos. Movimiento representa el estado de cuenta del cliente/préstamo. MovimientoCaja representa el efectivo del negocio.

7. Los recordatorios de vencimiento se derivan de las cuotas. No se requiere crear una fila de recordatorio por cada cuota; basta una configuración de días de anticipación.

8. Periodicidad completa para migración. El enum soporta DIARIO, SEMANAL, QUINCENAL, MENSUAL, TRIMESTRAL y ANUAL. Para nuevos préstamos, la interfaz puede limitar inicialmente la selección a SEMANAL, QUINCENAL y MENSUAL.

9. Interés del sistema actual: tasa por periodo sobre capital original, no sobre saldo insoluto. Se conserva además el esquema SOLO_INTERES para los créditos históricos que lo requieren.

2. Modelo final recomendado

2.1 Empresa

Configuración e identidad de la financiera.

Empresa
-------
id: Long
nombre: String
rfc: String
logo: String nullable
logo2: String nullable
logo3: String nullable
activo: Boolean
fechaRegistro: LocalDateTime
fechaActualizacion: LocalDateTime

Uso: datos institucionales, encabezados, recibos y configuración general. Las tasas no deben depender únicamente de Empresa; las condiciones aplicables quedan congeladas en cada Préstamo.

2.2 Configuracion

Configuración operativa de un solo registro o por empresa.

Configuracion
-------------
id: Long
empresaId: Long
diasAnticipacionAviso: Integer
fechaActualizacion: LocalDateTime

Uso: define con cuántos días de anticipación aparecen las cuotas próximas a vencer. Sustituye la idea de persistir recordatorios individuales cuando el requerimiento es únicamente mostrar avisos calculados sobre las cuotas.

2.3 Usuario

Usuario
-------
id: Long
username: String UNIQUE
password: String
nombre: String nullable
activo: Boolean
fechaRegistro: LocalDateTime
fechaActualizacion: LocalDateTime

Uso: autenticación. password debe almacenarse con BCrypt u otro password encoder compatible con Spring Security.

2.4 Role

Role
----
id: Long
authority: String UNIQUE
descripcion: String

Ejemplos: ROLE_ADMIN, ROLE_CAJA, ROLE_COBRANZA, ROLE_CONSULTA.

2.5 Permiso

Permiso
-------
id: Long
codigo: String UNIQUE
descripcion: String

Ejemplos posibles: EDITAR_HISTORIAL, LISTAR_CLIENTES, BUSCAR_HISTORIAL, ELIMINAR, ADMINISTRAR_USUARIOS.

Justificación: el sistema anterior permite permisos individuales. Un modelo de roles puros puede perder esa granularidad.

2.6 UsuarioRole

UsuarioRole
-----------
id: Long
usuarioId: Long
roleId: Long
UNIQUE(usuarioId, roleId)

2.7 RolePermiso

RolePermiso
-----------
id: Long
roleId: Long
permisoId: Long
UNIQUE(roleId, permisoId)

Uso: un rol es una agrupación reutilizable de permisos.

2.8 UsuarioPermiso

UsuarioPermiso
--------------
id: Long
usuarioId: Long
permisoId: Long
concedido: Boolean
UNIQUE(usuarioId, permisoId)

Uso: permite conceder o negar una excepción particular a un usuario sin crear un nuevo rol. Si se decide simplificar el alcance, esta tabla puede omitirse inicialmente y quedarse solo con roles + permisos.

2.9 Ruta

Ruta
----
id: Long
codigo: String UNIQUE
descripcion: String
activo: Boolean

Uso: agrupación de clientes y cobranza por ruta o zona.

2.10 Cliente

Cliente
-------
id: Long
nombreCompleto: String
nombre: String nullable
primerApellido: String nullable
segundoApellido: String nullable
curp: String nullable
fechaNacimiento: LocalDate nullable
tipoIdentificacion: TipoIdentificacion nullable
numeroIdentificacion: String nullable
rutaId: Long nullable
status: ClienteStatus
fechaRegistro: LocalDateTime
fechaActualizacion: LocalDateTime

Decisión importante: nombreCompleto se conserva como campo obligatorio para migrar el dato de Access sin intentar separar automáticamente cientos de nombres. Los campos nombre, primerApellido y segundoApellido son opcionales y pueden completarse al editar al cliente.

No se recomienda guardar ahorro como un simple saldo en Cliente salvo que el ahorro sea parte real del alcance. Si se incorpora en el futuro, debe modelarse como un producto/movimiento financiero independiente.

2.11 ClienteDocumento

ClienteDocumento
----------------
id: Long
clienteId: Long
tipoDocumento: TipoDocumento
nombreArchivo: String
rutaArchivo: String
contentType: String nullable
fechaCarga: LocalDateTime
activo: Boolean

Tipos sugeridos: IDENTIFICACION_FRENTE, IDENTIFICACION_ATRAS, COMPROBANTE_DOMICILIO, COMPROBANTE_INGRESOS, CURP, OTRO.

Justificación: es preferible a columnas fijas como comprobanteIngresos1, comprobanteIngresos2, etc. Permite uno o muchos documentos del mismo tipo sin cambiar el esquema. Esta tabla debe considerarse parte del alcance solo si el expediente digital está efectivamente incluido.

2.12 Aval

Aval
----
id: Long
prestamoId: Long
nombreCompleto: String
telefono: String nullable
direccion: String nullable
curp: String nullable
tipoIdentificacion: TipoIdentificacion nullable
numeroIdentificacion: String nullable
fechaRegistro: LocalDateTime

Justificación: evita columnas aval1 y aval2. Si mañana se requieren 0, 1, 2 o más avales, no se modifica la tabla Préstamo. Si los avales no forman parte del alcance actual, esta entidad puede permanecer desactivada hasta una fase posterior.

2.13 Prestamo

Prestamo
--------
id: Long
clienteId: Long
fechaSolicitud: LocalDate
fechaAutorizacion: LocalDate nullable
fechaEntrega: LocalDate nullable
fechaPrimerPago: LocalDate nullable
capital: BigDecimal
tasaInteres: BigDecimal
tasaInteresMoratorio: BigDecimal
numeroCuotas: Integer
periodicidad: Periodicidad
tipoCalculo: TipoCalculo
interesTotal: BigDecimal
totalPagar: BigDecimal
saldoCapital: BigDecimal
saldoIntereses: BigDecimal
saldoMoratorio: BigDecimal
saldoFavor: BigDecimal
saldoTotal: BigDecimal
prestamoOrigenId: Long nullable
status: PrestamoStatus
fechaRegistro: LocalDateTime
fechaActualizacion: LocalDateTime

TipoCalculo

CAPITAL_MAS_INTERES
SOLO_INTERES

Se elimina SALDOS_INSOLUTOS del alcance actual porque el cálculo documentado utiliza interés fijo sobre capital original.

PrestamoStatus

EN_REVISION
AUTORIZADO
RECHAZADO
ACTIVO
LIQUIDADO
VENCIDO
REFINANCIADO
CANCELADO

prestamoOrigenId

Solo se utiliza si se habilita refinanciamiento. Permite mantener la relación entre el préstamo nuevo y el préstamo original. Debe permanecer nullable si el refinanciamiento todavía no está contratado o definido.

2.14 Cuota

Representa cada renglón de la tabla de amortización.

Cuota
-----
id: Long
prestamoId: Long
numeroCuota: Integer
fechaVencimiento: LocalDate
capital: BigDecimal
interes: BigDecimal
moratorioCalculado: BigDecimal
totalCuota: BigDecimal
capitalPagado: BigDecimal
interesPagado: BigDecimal
moratorioPagado: BigDecimal
saldoCapitalDespues: BigDecimal
fechaLiquidacion: LocalDateTime nullable

Estado de cuota

Se recomienda derivarlo en lugar de almacenarlo:

PAGADA -> total pendiente = 0
PARCIAL -> existe pago aplicado, pero queda saldo
VENCIDA -> fecha actual > fechaVencimiento y queda saldo
PENDIENTE -> fecha actual <= fechaVencimiento y no está pagada

Esto evita un proceso nocturno únicamente para cambiar estados de PENDIENTE a VENCIDA.

2.15 Pago

Representa el dinero recibido en una operación de cobro.

Pago
----
id: Long
prestamoId: Long
clienteId: Long
fechaPago: LocalDateTime
monto: BigDecimal
tipoPago: TipoPago
referencia: String nullable
observaciones: String nullable
usuarioId: Long
status: PagoStatus
fechaRegistro: LocalDateTime

Ejemplos de TipoPago: EFECTIVO, TRANSFERENCIA, DEPOSITO, OTRO.

PagoStatus: APLICADO, CANCELADO.

Uso: un pago es el evento de recepción. No se obliga a que corresponda a una sola cuota.

2.16 PagoAplicacion

PagoAplicacion
--------------
id: Long
pagoId: Long
cuotaId: Long
capitalAplicado: BigDecimal
interesAplicado: BigDecimal
moratorioAplicado: BigDecimal
saldoFavorGenerado: BigDecimal
fechaRegistro: LocalDateTime

Uso: indica exactamente cómo se distribuyó cada pago. Es la base de auditoría de pagos parciales, pagos de varias cuotas y excedentes.

Ejemplo: un pago de $4,500 puede liquidar una cuota vencida, su mora, parte de la siguiente cuota y dejar $200 a favor, sin perder trazabilidad.

2.17 Movimiento

Estado de cuenta financiero del cliente o préstamo.

Movimiento
----------
id: Long
clienteId: Long
prestamoId: Long nullable
tipoMovimiento: TipoMovimiento
cargo: BigDecimal
abono: BigDecimal
saldo: BigDecimal
descripcion: String
referencia: String nullable
fecha: LocalDateTime
usuarioId: Long nullable

Tipos sugeridos: OTORGAMIENTO_PRESTAMO, PAGO, INTERES, MORATORIO, AJUSTE, REFINANCIAMIENTO, CANCELACION.

Uso: permite reconstruir el estado de cuenta del cliente. No debe confundirse con la caja del negocio.

2.18 MovimientoCaja

Representa entradas y salidas reales de efectivo de la financiera. Esta entidad es independiente del estado de cuenta del cliente.

MovimientoCaja
--------------
id: Long
fecha: LocalDateTime
tipoMovimiento: TipoMovimientoCaja
concepto: String
importe: BigDecimal
utilidad: BigDecimal
prestamoId: Long nullable
pagoId: Long nullable
generadoPorSistema: Boolean
usuarioId: Long nullable
referencia: String nullable
fechaRegistro: LocalDateTime

Un solo enum para clasificar la caja

No se necesita un enum separado INGRESO/EGRESO y otro enum de concepto. Un único TipoMovimientoCaja puede definir la naturaleza del movimiento.

public enum TipoMovimientoCaja {
INGRESO_PAGO_PRESTAMO,
INGRESO_CAPITAL,
INGRESO_OTRO,
EGRESO_GASTO_OPERATIVO,
EGRESO_RETIRO_CAPITAL,
EGRESO_OTRO,
AJUSTE_INGRESO,
AJUSTE_EGRESO
}

Regla especial para pagos

Cuando el cliente realiza un pago, el sistema genera un solo movimiento de caja:

tipoMovimiento = INGRESO_PAGO_PRESTAMO
importe = monto total recibido
utilidad = interés aplicado + mora aplicada
pagoId = pago generado
prestamoId = préstamo correspondiente
generadoPorSistema = true

No se crea otro movimiento separado GANANCIA_INTERESES, porque eso duplicaría el ingreso en caja. La ganancia queda en el campo utilidad y se obtiene con total precisión desde PagoAplicacion.

Ejemplo:

Pago recibido: $2,500
Capital aplicado: $2,000
Interés aplicado: $400
Mora aplicada: $100

MovimientoCaja.importe = $2,500
MovimientoCaja.utilidad = $500
Tipo = INGRESO_PAGO_PRESTAMO

Esto permite calcular tanto el efectivo total como la utilidad del periodo sin duplicar movimientos.

3. Enums recomendados

Periodicidad

public enum Periodicidad {
DIARIO,
SEMANAL,
QUINCENAL,
MENSUAL,
TRIMESTRAL,
ANUAL
}

Para nuevos préstamos se puede exponer inicialmente solo SEMANAL, QUINCENAL y MENSUAL, pero mantener los seis valores evita errores al migrar créditos históricos.

TipoCalculo

public enum TipoCalculo {
CAPITAL_MAS_INTERES,
SOLO_INTERES
}

PrestamoStatus

public enum PrestamoStatus {
EN_REVISION,
AUTORIZADO,
RECHAZADO,
ACTIVO,
LIQUIDADO,
VENCIDO,
REFINANCIADO,
CANCELADO
}

TipoMovimientoCaja

public enum TipoMovimientoCaja {
INGRESO_PAGO_PRESTAMO,
INGRESO_CAPITAL,
INGRESO_OTRO,
EGRESO_GASTO_OPERATIVO,
EGRESO_RETIRO_CAPITAL,
EGRESO_OTRO,
AJUSTE_INGRESO,
AJUSTE_EGRESO
}

4. Tabla de amortización

Los datos necesarios para generar la amortización se encuentran en Prestamo:

capital
tasaInteres
numeroCuotas
periodicidad
fechaPrimerPago
tipoCalculo
tasaInteresMoratorio

Para el esquema documentado de capital + interés fijo por periodo:

capitalPorCuota = capital / numeroCuotas
interesPorCuota = capital * tasaInteres
totalCuota = capitalPorCuota + interesPorCuota

Fechas operativas conocidas:

SEMANAL -> +7 días
QUINCENAL -> +15 días
MENSUAL -> +1 mes (recomendado) o +30 días si se requiere reproducir exactamente el comportamiento histórico

Para SOLO_INTERES, la distribución de capital debe definirse exactamente con la regla del sistema actual antes de codificarla. La entidad ya soporta este esquema sin introducir saldos insolutos.

Importante: la generación de tablas de amortización no es minería de datos. Es lógica financiera determinística. Minería de datos sería, en una fase posterior, analizar patrones de morosidad, riesgo o comportamiento de clientes.

5. Flujo de un pago

1. Se registra Pago por el monto recibido.
2. PagoService consulta las cuotas pendientes/vencidas.
3. Se distribuye el monto según la prioridad acordada.
4. Se crean uno o varios PagoAplicacion.
5. Se actualizan los acumulados pagados de las Cuotas.
6. Se actualizan saldos monetarios del Prestamo.
7. Se genera Movimiento para el estado de cuenta.
8. Se genera un único MovimientoCaja tipo INGRESO_PAGO_PRESTAMO.
9. MovimientoCaja.utilidad = interés + mora efectivamente cobrados.
10. Si existe excedente, se incrementa Prestamo.saldoFavor.

La prioridad exacta de aplicación debe confirmarse con el negocio. Una opción típica es: mora -> interés -> capital -> saldo a favor, pero no debe asumirse sin validación.

6. Comparación: modelo inicial vs. revisión del compañero vs. decisión final

Tema Modelo inicial Revisión del compañero Decisión recomendada
PagoAplicacion Incluido Lo considera una mejora importante Se mantiene. Es clave para trazabilidad y pagos parciales.
Snapshot de tasas Incluido en Prestamo Lo aprueba Se mantiene. Evita alterar préstamos históricos.
Pago separado de Cuota Incluido Lo aprueba Se mantiene. Un pago puede cubrir varias cuotas.
Movimiento Estado de cuenta Correcto, pero no reemplaza caja Se mantiene.
MovimientoCaja Faltaba Detectado como hueco crítico Se agrega. Necesario para 15,030 registros de ingresos/egresos y reportes de caja.
Recordatorio persistido Se propuso como tareas Access usa configuración de días Se reemplaza por Configuracion para avisos de vencimiento. Un módulo de tareas puede quedar para fase futura.
Periodicidad 3 valores Access posee 6 Se amplía a 6 para migración; UI nueva puede limitarse a 3.
TipoCalculo Incluía saldos insolutos Access no usa saldos insolutos y sí usa solo interés CAPITAL_MAS_INTERES + SOLO_INTERES.
aportaCapital Campo adicional Considerado ambiguo/redundante Se elimina. El esquema se expresa con TipoCalculo.
Permisos Roles Access tiene permisos individuales Role + Permiso, con UsuarioPermiso opcional para excepciones.
Nombre del cliente Nombre + apellidos separados Migración riesgosa desde nombre único nombreCompleto obligatorio + campos separados opcionales.
Saldos de Prestamo Almacenados Acepta almacenarlos Se almacenan y PagoAplicacion permite auditarlos/reconstruirlos.
Cuota.status Almacenado Recomienda derivarlo Se deriva para evitar sincronización nocturna.
ClienteDocumento Incluido Fuera del Access original Se mantiene si el expediente digital forma parte del escenario de 90k. Su tabla está justificada por normalización y escalabilidad.
Aval Incluido Fuera del Access original Se mantiene solo si está contratado. Es mejor tabla independiente que aval1/aval2.
ProductoCredito Incluido No existe en Access Se elimina del núcleo actual. Las condiciones quedan directamente en Prestamo; puede agregarse después si existen productos estandarizados.
Refinanciamiento prestamoOrigenId Reglas aún no definidas Campo nullable, sin activar flujo hasta definir reglas de negocio.
Ahorro Campo en Cliente Fuera de alcance Se elimina del Cliente mientras no exista un módulo real de ahorro.

7. Justificación de las tablas adicionales respecto al modelo original

El aumento de tablas no significa complejidad innecesaria. Varias tablas existen para separar responsabilidades que en Access estaban mezcladas o representadas por columnas rígidas.

PagoAplicacion

Evita guardar un único acumulado opaco. Permite auditar exactamente cuánto de cada pago se aplicó a capital, interés y mora.

MovimientoCaja

Es indispensable porque el estado de cuenta de un cliente y la caja de la empresa son dos contabilidades diferentes. Un gasto de renta o una aportación de capital no pertenecen a ningún cliente, pero sí afectan la caja.

Permiso / RolePermiso / UsuarioPermiso

Normalizan los permisos existentes y evitan volver a columnas booleanas por usuario. Permiten que Spring Security maneje autorización de forma consistente.

ClienteDocumento

Evita columnas como comprobanteIngresos1, comprobanteIngresos2, comprobanteIngresos3. Un cliente puede tener cualquier cantidad de documentos sin modificar el esquema.

Aval

Evita aval1 y aval2. La relación uno-a-muchos es más limpia y no impone un límite artificial.

Configuracion

Representa correctamente los parámetros globales del sistema, como los días de anticipación del aviso, sin crear miles de recordatorios redundantes.

8. Entidades que se eliminan o dejan fuera del núcleo

ProductoCredito

No se considera necesario en la primera versión porque el sistema histórico maneja tasa, plazo y condiciones directamente por préstamo. Se puede incorporar posteriormente si la financiera formaliza productos como “Préstamo semanal”, “Préstamo quincenal”, etc.

Recordatorio como tarea persistida

No se requiere para los avisos de vencimiento existentes. Estos se calculan desde Cuota + Configuracion. Puede existir en una fase futura si se quiere un módulo CRM de seguimiento humano.

Cliente.ahorro

Un saldo aislado no es suficiente para modelar ahorro correctamente. Si se necesita, debe existir una cuenta de ahorro y sus propios movimientos, no solo un número mutable en Cliente.

SALDOS_INSOLUTOS

No se incluye porque contradice el cálculo observado en el sistema actual. Añadir métodos futuros es sencillo sin ensuciar el alcance actual.

9. Relaciones principales

Empresa
|
+-- Configuracion

Usuario -- UsuarioRole -- Role -- RolePermiso -- Permiso
|
+-- UsuarioPermiso (excepciones opcionales)

Ruta
|
+-- Cliente
|
+-- ClienteDocumento
|
+-- Prestamo
|
+-- Aval
+-- Cuota
| |
| +-- PagoAplicacion -- Pago
|
+-- Movimiento
+-- MovimientoCaja (cuando el movimiento de caja está relacionado)

10. Servicios sugeridos en Spring

UsuarioService
AutorizacionService
ClienteService
DocumentoClienteService
PrestamoService
AmortizacionService
CuotaService
PagoService
PagoAplicacionService
MoratorioService
MovimientoService
MovimientoCajaService
MigracionService
ConfiguracionService

Responsabilidades importantes:

  • AmortizacionService: genera cuotas y fechas.
  • PagoService: orquesta la recepción y aplicación de un pago.
  • PagoAplicacionService: distribuye y registra capital/interés/mora.
  • MovimientoService: registra el estado de cuenta.
  • MovimientoCajaService: registra ingresos y egresos reales, y evita duplicar ganancias por interés.
  • MoratorioService: calcula el moratorio conforme a la fórmula validada del negocio.
  • MigracionService: transforma Access a PostgreSQL manteniendo identificadores/referencias de origen cuando convenga.

11. Reglas que todavía deben cerrarse con el cliente

Antes de congelar PagoService y AmortizacionService, deben confirmarse por escrito:

1. Prioridad de aplicación de un pago: mora, interés, capital u otro orden.

2. Regla exacta de SOLO_INTERES: cuándo y cómo se recupera el capital.

3. Si mensual significa sumar 30 días exactos o avanzar un mes calendario.

4. Regla completa de interés moratorio y momento en que comienza a generarse.

5. Qué ocurre con un pago adelantado: reduce plazo, reduce cuota o solo cubre cuotas futuras.

6. Si el saldo a favor se aplica automáticamente al siguiente vencimiento.

7. Reglas de cancelación/reverso de pagos.

8. Si refinanciamiento forma parte de esta versión y, en su caso, qué deuda se transfiere al nuevo préstamo.

9. Si expediente digital y avales están incluidos contractualmente en el escenario de $90,000.

12. Conclusión

El modelo inicial tenía buenas decisiones técnicas —especialmente PagoAplicacion, separación entre Pago y Cuota, snapshots de tasas y saldos monetarios—, pero la reconciliación con Access evidencia que faltaba separar la caja del negocio del estado de cuenta del cliente, así como representar la configuración real de avisos y el esquema histórico SOLO_INTERES.

La propuesta final adopta esas correcciones y evita sobre-modelar funciones que todavía no están contratadas. Las tablas adicionales se mantienen únicamente cuando aportan trazabilidad, normalización o compatibilidad con la migración. En particular, MovimientoCaja se incorpora como entidad de primer nivel y los pagos generan un solo movimiento INGRESO_PAGO_PRESTAMO, usando utilidad para identificar la ganancia real de intereses y mora sin duplicar el efectivo recibido.

Referencia
Abrir en el documento completo →