Qué resuelve
Una mascota lleva una placa con un código. Quien la encuentra lo escanea con la cámara del teléfono, sin instalar nada y sin crear una cuenta, y obtiene lo necesario para devolverla.
Alrededor de esa idea hay una plataforma completa: identidad del animal, historial clínico, clínicas veterinarias con su agenda, adopciones, transferencia de tutor y un marketplace. Pero el problema de diseño que condiciona todo lo demás es el primero.
Para que la placa sirva, un desconocido tiene que poder ver el contacto del tutor. Para que sea segura, solo quien tiene la placa en la mano debe poder verlo — no cualquiera que pruebe direcciones.
Todo lo que sigue en las tres primeras secciones son las consecuencias de tomarse esa frase en serio.
El token de la placa
El código impreso en la placa es la credencial. No hay contraseña detrás ni segundo paso: quien lo tiene, ve la ficha.
Derivar el token de datos del perfil es la tentación obvia: sale determinista, no hay que guardarlo y se puede regenerar. Y rompe las dos propiedades que lo hacen útil.
Primero, permitiría reconstruir la placa de cualquier mascota a partir de datos que sí son públicos — el nombre del animal, por ejemplo. Segundo, haría inútil la revocación: si el token es función del perfil, revocarlo y emitir otro daría el mismo resultado, y una placa perdida seguiría abriendo la ficha para siempre.
La única puerta sin sesión
La ficha pública es la única superficie del sistema accesible sin haber iniciado sesión. Por eso está tratada como lo que es: la parte expuesta.
| Situación | Qué responde | Por qué |
|---|---|---|
| El token no existe | no encontrado | No distinguir los dos casos evita confirmarle a alguien que un token existió alguna vez |
| El token fue revocado | no encontrado | |
| El token está malformado | rechazo inmediato | No puede corresponder a ninguna placa: se descarta antes de contar contra el límite de peticiones y antes de tocar la base |
| Muchos intentos seguidos | límite de tasa | Corta el bucle que prueba códigos al azar buscando una placa válida |
El orden importa: descartar lo malformado antes del limitador significa que un atacante enviando basura no consume el presupuesto de peticiones de nadie, y que la base de datos no recibe consultas que no pueden dar resultado.
Este módulo está marcado en el código como módulo de referencia, con el patrón que los demás deben seguir: el router consulta la base, las reglas y la proyección de salida viven en el paquete de reglas, y el handler nunca entrega una fila tal como vino.
Es lo que impide el accidente clásico: alguien añade una columna a la tabla y esa columna aparece en la respuesta pública sin que nadie lo decida. Con una proyección explícita, el dato nuevo no sale hasta que alguien lo escribe.
La máquina de estados
Una mascota está en uno de cuatro estados, y las transiciones válidas están declaradas — no dispersas en condicionales.
| Decisión | Razonamiento |
|---|---|
| Adoptado se comporta como activo | La mascota sigue viva y bajo cuidado de un tutor. La diferencia es documental —cómo llegó— y no de ciclo de vida |
| Fallecido es terminal | El perfil se conserva para el historial clínico y para el historial de tutores, pero ya no admite cambios de estado |
Que fallecido conserve el perfil en vez de borrarlo no es sentimentalismo: el historial clínico de un animal es información veterinaria, y la cadena de tutores es lo que respalda una transferencia anterior.
Perdido y encontrado
El flujo de pérdida se apoya en la máquina de estados en lugar de repetirla. Lo que añade es la relación entre el estado de la mascota y el reporte abierto — que es justo lo que el router no debe decidir por su cuenta.
La tentación es restaurar lo que había antes. Pero si antes estaba adoptado, reponerlo tras una pérdida no aporta información nueva: adoptado describe cómo llegó a su tutor, no su situación actual. Volver a activo dice exactamente lo que pasó — la mascota está bien y con su tutor.
La fecha en que el tutor dice haber visto al animal por última vez se valida en la regla, no en el router, para que la app móvil y la web importen la misma validación en vez de copiarla cada una en su formulario.
Un núcleo, dos clientes
Hay una aplicación móvil y una web. Las dos consumen la misma API y, sobre todo, importan las mismas reglas.
Cuando una regla vive en el handler de la API, la app móvil tiene que reimplementarla para validar antes de enviar — y a partir de ese momento hay dos versiones que divergen. El usuario ve un formulario que acepta algo que el servidor después rechaza, o al revés.
Con la regla en un paquete compartido, la validación del formulario y la del servidor son literalmente la misma función. No pueden discrepar.
Arquitectura y verificación
api y las dos aplicaciones dependen de core, y core no depende de nadie. Por eso se puede probar entero sin levantar una base de datos.Dos niveles de prueba
| Nivel | Qué cubre |
|---|---|
| Reglas puras | Los veinte módulos del núcleo: transiciones de estado, acceso, precios, reservas, generación de token |
| Routers | El comportamiento contra una base real, incluidas las respuestas de la superficie pública |
49 archivos de prueba en total. La puerta de calidad levanta una base de datos, aplica las migraciones y corre el chequeo de tipos y las pruebas de todos los paquetes antes de permitir un despliegue.