Saltar a contenido

27 de junio de 2026

Día corto pero ordenador: junté en un solo lugar todo lo que venía dejando "para más adelante", disperso entre el PRD, el RFC y la bitácora.

Un tablero único de pendientes

Revisando el trabajo de los últimos días me di cuenta de algo incómodo: las cosas que iba dejando para resolver después estaban registradas —eso sí, fiel a la costumbre de anotar todo—, pero repartidas en cuatro lugares distintos. La aprobación manual del despliegue que pide plan pago estaba en la entrada de integración continua; el ejecutor on-premise que todavía no existe, también ahí; el origen del remito de ingreso a confirmar con el cliente vivía en el PRD; y el cache de sesión que asume un solo backend estaba metido en la prosa del RFC. Cada cosa en su contexto, sí, pero ninguna vista que me dijera de un saque "esto es lo que falta".

Así que armé esa vista. Una página nueva, pendientes.md, que funciona como tablero: cada ítem con su casilla para tachar, agrupado por tema —despliegue e infraestructura, integración con QUICK POS, decisiones de negocio por confirmar, deuda técnica y escala—. No me conformé con listar títulos: a cada pendiente le puse tres cosas, qué es, por qué quedó para después y, sobre todo, qué lo destraba, que es el dato que de verdad importa cuando uno vuelve a mirar la lista semanas más tarde y no se acuerda por qué algo seguía abierto.

Me importa aclarar que esta página no reemplaza el registro original. Los pendientes siguen naciendo donde corresponde —el día que aparecen, en la bitácora; las limitaciones asumidas, en el RFC; lo que queda fuera de alcance, en el PRD—. El tablero solo los reúne. Pensado como documento vivo: cuando algo se resuelve lo marco con la fecha, y si deja de tener sentido lo saco y lo cuento acá.

La enganché al hub como una sección más, entre el plan y la bitácora, y le dejé su tarjeta en la portada. Verifiqué que el sitio siga construyendo en modo estricto —sin enlaces rotos ni páginas sueltas— antes de dar la tarea por cerrada, que para eso me impuse esa verificación.

Afinando el filtro de la integración continua

Al subir este mismo tablero me topé con una inconsistencia que el propio ejercicio dejó a la vista. Tiempo atrás había hecho que la rutina de integración del código ignore los cambios que tocan solo la carpeta de documentación, para no gastar corridas de calidad y build en cosas que no son código. Pero el archivo de configuración del hub y sus dependencias viven en la raíz del repositorio, no adentro de esa carpeta, así que un cambio que solo movía la navegación del sitio seguía disparando la rutina del código igual —una corrida entera para revisar un código que ni se había tocado—.

Lo corregí extendiendo el filtro: ahora la rutina del código también ignora la configuración del hub, sus dependencias, el archivo que fija la versión de Python y el propio flujo de documentación. Todo eso es herramienta de la documentación y ya tiene su propia rutina, la que arma el sitio en modo estricto, que lo vigila aparte. Cada flujo se ocupa de lo suyo y los minutos de ejecución quedan para cuando de verdad cambia el código. Es un retoque chico, pero va en la misma línea que el filtro original: que la automatización no trabaje al pedo.

El modelo de datos toma forma: Prisma, tiendas y usuarios (Task 2)

Hoy escribí la segunda tarea del milestone, la que pone los cimientos de datos sobre los que se va a apoyar todo lo demás: el esquema con Prisma y las dos primeras tablas, tiendas y usuarios. Son las que necesita el Milestone 1 —administración y acceso—; el resto del modelo del RFC entra después, cada milestone con su propia migración, para no traer de golpe tablas que todavía no uso.

Apenas arranqué me llevé una sorpresa que me obligó a frenar y decidir. Instalé Prisma y resultó que la versión nueva, la 7, cambió de raíz cómo se configura: la dirección de la base de datos ya no va en el esquema, y el cliente ahora se conecta a través de un "adaptador" de driver en vez de hacerlo por su cuenta. No es un detalle menor —cambia qué dependencias uso y agrega un archivo de configuración aparte—, así que en lugar de asumirlo lo puse sobre la mesa: o me quedaba en la versión 6, más simple y la que el plan daba por sentada, o adoptaba la 7 con su esquema nuevo. Elegí la 7, la última, y la dejé andando con el adaptador de Postgres. Vale tenerlo anotado porque es de esas decisiones de stack que después hay que poder explicar.

Una cosa en la que me puse firme: los CHECK del modelo. El RFC dice, y con razón, que los constraints son parte del contrato y no decoración —que el puerto del QUICK POS esté en rango, que el correo sea del dominio de la empresa, que el rol sea uno de los dos válidos—. Prisma no expresa esas reglas en su esquema, así que en vez de dejarlas libradas a la confianza del código las grabé a mano en el SQL de la migración. Quedan en la base: aunque alguien algún día inserte un registro por fuera de la aplicación, la base lo rechaza. Lo verifiqué consultando los constraints directamente sobre Postgres, no me quedé con que "debería estar".

El cliente de Prisma lo armé como una instancia única para toda la app, con una precaución para desarrollo: como Next recarga los módulos en caliente cada vez que guardo, sin cuidado se crearía una conexión nueva en cada recarga hasta agotar la base. Guardándolo en una variable global eso se evita. Y aunque es código de cableado más que de lógica, le escribí su prueba igual —primero el test—, cubriendo las dos ramas: cuándo crea el cliente y cuándo reutiliza el que ya hay, en desarrollo y en producción. Quedó con la cobertura completa.

Cerré pasando toda la batería antes de dar nada por bueno: la migración aplica limpia contra la base de desarrollo, el esquema valida, los tipos compilan, el linter no chista, las pruebas pasan con cobertura total y el build de producción sale entero. De paso, corriendo el linter en mi máquina apareció un ruido que no tenía que ver con esta tarea: revisaba archivos de adentro del entorno de Python del hub de documentación, un montón de código ajeno y minificado. Le indiqué que ignore esa carpeta y la del sitio construido, igual que ya ignora las demás. En la nube nunca había molestado —ahí el entorno ni existe—, pero en local ensuciaba el resultado, y prefiero que correr el linter sea igual de limpio en los dos lados.

Desgloso el plan en subtasks más finas

Con las dos primeras tareas ya cerradas, antes de seguir me paré a mirar cómo venía dividido el resto del plan y me hice una pregunta honesta: ¿no estaban algunas tareas demasiado grandes para entrar en un solo commit? Porque una cosa que me importa cuidar es que el historial respire como avance sostenido y no como volcados gigantes. Revisé tarea por tarea, de la 3 a la 10, y la respuesta fue que sí, pero no parejo.

Tres tareas estaban claramente sub-dimensionadas, escondiendo varias capas adentro de una sola viñeta. La de autenticación con Entra ID era la peor: mezclaba el provider, el callback que consulta la whitelist, los callbacks que arman la sesión con el rol, y la vinculación del identificador de Microsoft en el primer login —cuatro cosas distintas, cada una con su prueba, apretadas en un commit—. La partí en tres. Lo mismo con los dos ABM, el de tiendas y el de usuarios: cada uno es en realidad dos capas, primero la API probada contra la base y después la pantalla, así que van en dos commits cada uno, que además es el orden natural de TDD. Y el empaque de Docker tenía adentro el endpoint de salud, que es chico y autónomo, así que lo saqué adelante para que vaya solo.

Las chicas las dejé casi enteras a propósito. La lógica de autorización son tres funciones puras y cortas; trocearla en tres commits se leería robótico, no como trabajo real, así que va en uno (con la puerta abierta a un segundo si la parte de matching crece). Los schemas de validación, igual: store y usuario son separables, pero juntos son "la capa de validación" y entran como unidad. El seed del primer admin es atómico y ni lo toqué.

El criterio, en el fondo, fue el mismo de siempre: ni un commit por archivo ni medio milestone de una. Partir donde de verdad hay capas separadas y dejar quieto lo que ya es una unidad lógica. Con eso las tareas 3 a 10 pasan de unos ocho commits a trece o quince. Bajé todo esto al plan, cada subtask con su propio criterio de validación y marcado de en qué commit cae, y actualicé el estado del documento: ya no es un borrador esperando luz verde, está en ejecución.

La primera pieza de dominio: decidir quién entra (Task 3a)

Y de ahí pasé a escribir la primera tarea con lógica de verdad: la autorización. La empecé por su parte más chica, la función que resuelve si alguien puede entrar y con qué rol. Acá no hay base de datos ni mecánica de login todavía —eso lo cablea el borde más adelante—; esto es pura decisión. Le pasás lo que ya se sabe del usuario, su fila en la whitelist o nada si no figura, y devuelve un veredicto: entra con tal rol, o no entra y por qué.

Fui por el test primero, como me obligué desde el arranque del proyecto. Escribí los casos antes que la función —el habilitado y activo entra con su rol, el dado de baja no entra aunque figure en la lista, el que no está tampoco— y recién después la implementé. Corrí la prueba con la función todavía sin existir y, claro, falló: ese rojo inicial es parte del rito, es lo que me confirma que el test prueba algo y no se pasa solo. Ahí sí escribí lo mínimo para que pasara y volví a correr. Verde, las cinco.

Hubo una decisión de diseño chica que igual quiero dejar anotada, porque es de las que después conviene poder explicar. En vez de confiar en el rol tal como viene escrito en la base, la función lo contrasta contra la lista de roles válidos y lo devuelve ya tipado. ¿Para qué el rodeo? Para que si algún día aparece una fila con un rol que no reconozco —uno corrupto, uno viejo de una versión anterior, lo que sea— el sistema la niegue en lugar de dejarla pasar con un rol que no significa nada. Sumé un motivo de rechazo más para ese caso. Es defensa de más, lo sé, pero sale barata y me deja la decisión cerrada por todos lados.

Cobertura completa sobre el módulo, los tipos compilan y el linter no chistó. Una última sobre el ritmo: en la entrada de arriba había dado por hecho que las tres funciones de autorización entrarían juntas en un mismo commit. Lo repensé recién ahora, con la primera ya andando. Esta función está probada, funciona y deja el repo estable, así que no tiene sentido dejarla colgada esperando a las otras dos: la cierro y la commiteo sola. Las que faltan —normalizar el correo y vincular el identificador de Microsoft en el primer login— van por su cuenta cuando les toque.

Normalizar el correo, ese detalle que comparten acceso y validación (Task 3b)

La segunda pieza de la autorización es mínima en líneas, pero la traté con el mismo cuidado que el resto: una función que lleva un correo a su forma canónica, todo en minúsculas y sin espacios sobrantes en las puntas. Suena a nada, y sin embargo es justo lo que evita un error silencioso más adelante —que Diana@Zavidoro.Com.Py y diana@zavidoro.com.py terminen tratados como dos personas distintas cuando son la misma—.

Antes de escribirla me frené a pensar dónde iba a vivir. Lo fácil era meterla adentro del módulo de la decisión de acceso, pero caí en que la van a usar dos lados distintos: el emparejamiento contra la whitelist en el primer login y, más adelante, la validación de los usuarios que cargue el admin. Si la dejaba junto a la autorización, la validación terminaría dependiendo de ese módulo solo para pedirle prestado este ayudante, y eso enreda las dependencias sin necesidad. Así que le di su propio archivo, chiquito y con una sola responsabilidad. Cada cosa en su lugar y nadie arrastra de más.

El resto fue el ciclo de siempre: primero los casos —mayúsculas que bajan, espacios que se recortan, las dos cosas mezcladas que tienen que caer en el mismo valor—, después el rojo de rigor con la función todavía sin existir, y recién ahí la implementación, que son dos operaciones encadenadas y nada más. Verde, cobertura completa, tipos y linter sin una queja. La dejé a propósito así de tonta: normaliza y punto. Decir si el correo es del dominio de la empresa es harina de otro costal, de la capa de validación que entra en la tarea cuatro.

Una auditoría de arquitectura con ojos de afuera

Hoy pasó algo que no estaba en ningún plan y que para mí vale oro: un amigo arquitecto se tomó el trabajo de revisar la arquitectura del proyecto en frío, sin que yo le marcara hacia dónde mirar, y me devolvió un informe detallado. Lo agradecí de entrada, porque es justo la mirada que a una se le escapa cuando viene con la cabeza metida adentro del código desde hace semanas. La pregunta que ordenó toda su revisión fue una sola: ¿el RFC y lo que efectivamente hay en el repositorio cuentan la misma historia? Vista desde afuera, esa pregunta pega distinto.

Eso sí, tuve clara una cosa apenas empecé a leerlo: un informe de auditoría no se acepta de palabra, se contrasta contra el código, hallazgo por hallazgo. Así que me senté a verificar cada punto contra los archivos reales, y ahí la cosa se puso interesante, porque no todo se sostuvo.

Dos de sus observaciones se cayeron, y vale anotar por qué. Una era sobre la carpeta _fuentes/: la dio por "vacía y sin propósito declarado". Falso —el propósito está escrito adentro, en el encabezado del script que la usa, que la describe como la bandeja de entrada del pipeline de documentación—. Y lo más aleccionador no fue la conclusión equivocada sino cómo llegó a ella: afirmó una ausencia sin abrir el archivo que la desmentía, uno que ya tenía listado delante. La otra era sobre la capa de validación: la planteó como una "tensión arquitectónica" a resolver, cuando en realidad ya estaba resuelta por una razón concreta que él no vio —mido cobertura sobre src/lib/**, así que ubicar ahí los schemas no es una duda de diseño, es lo que los deja bajo esa red—. Cuando se lo señalé, lo reconoció sin atrincherarse, que es lo que uno espera de una buena revisión. De las dos cosas me llevo lo mismo: verificar antes de afirmar, y eso corre tanto para el que audita como para la que recibe la auditoría.

De ahí en más, lo que sí era cierto lo tomé y lo arreglé. El hallazgo de más peso —y el que más me importó— era que el diagrama de capas del RFC, en la sección 4.2, enumeraba lib/domain/ con las piezas del flujo de auditoría (reconciliación, SKU, prefijos) pero no con las que ya escribí para este Milestone 1: la decisión de acceso y la normalización de correo. No es que el código contradijera la regla del RFC —ambos módulos son puros, sin entrada ni salida, tal como manda—, pero un revisor de tribunal que pusiera el documento al lado del repositorio iba a notar el desfasaje al toque. Así que actualicé esa sección: sumé los dos módulos, los marqué como del Milestone 1 y dejé aclarado que la lista es ilustrativa y crece con cada milestone, no un inventario cerrado. De paso ratifiqué ahí dónde vive la capa de validación, que el RFC no nombraba.

Lo demás fueron retoques más chicos, pero dejan el repo más prolijo. El nombre de la aplicación estaba escrito en dos lugares —la portada lo leía de una constante, pero el metadata del documento lo tenía puesto a mano por su cuenta—; lo unifiqué para que haya una sola fuente y nadie tenga que acordarse de cambiarlo en dos lados. Y dos deudas de infraestructura, que igual corresponden a una tarea posterior, las dejé anotadas en el tablero de pendientes para que no se evaporen: que el archivo de ejemplo de variables de entorno tendrá que fijar el puerto de desarrollo, y que el flujo de integración ya nombra un archivo de orquestación de producción que todavía no existe y hay que cerrar antes de prender el despliegue.

Cerré como cierro siempre: corriendo todo antes de dar nada por bueno. Tipos, las catorce pruebas en verde, linter sin una queja y el sitio construyendo en modo estricto. Ninguno de estos cambios tocó la lógica de las tareas que tengo en curso —fue puro alinear el documento con la realidad del código y barrer un par de detalles—. Me quedo con la idea de fondo del informe, que comparto sin reservas: el RFC y el repositorio tienen que contar la misma historia, y mantener esa correspondencia al día, milestone a milestone, es parte de lo que el tribunal va a mirar.