Los estantes ya se pueden tocar
Racoonity ya no solo puede abrirse y dibujar su catálogo: ahora puede administrar usuarios, clientes y ubicaciones desde una experiencia de producto real.
Los estantes ya se pueden tocar
En la iteración anterior Racoonity dejó de ser una ciudad sin rostro.
El catálogo apareció en pantalla, Painter dejó de ser una demostración aislada y Productos y Categorías comenzaron a sentirse como partes reales del producto.
Pero todavía faltaba algo importante.
Podíamos entrar a la ciudad. Podíamos verla. Podíamos recorrer una parte de ella.
Todavía no podíamos prepararla para trabajar.
La tercera iteración del Segundo MVP, productize-customers-and-operational-administration, tuvo un objetivo menos espectacular a primera vista, pero mucho más cercano a la operación cotidiana:
hacer administrables las piezas que tienen que existir antes de mover inventario o vender.
Usuarios.
Clientes.
Ubicaciones.
Configuración operativa básica.
Los estantes ya se pueden tocar.
Antes de mover mercancía
Es tentador brincar directamente a las partes más vistosas de un sistema de negocio.
Inventario.
Punto de venta.
Tickets.
Ventas.
Reportes.
Pero una tienda no empieza cuando alguien escanea un producto.
Antes de eso alguien tuvo que definir quién puede entrar, quién puede administrar, quiénes son los clientes y en qué lugares ocurre la operación.
Por eso esta iteración se concentró en tres recorridos:
administrar usuarios
administrar clientes
administrar ubicaciones
No como endpoints aislados.
No como tres páginas dibujadas porque sí.
Sino como recorridos completos desde la interfaz hasta las reglas que ya existen detrás de ella.
Ese principio sigue siendo importante para el Segundo MVP:
una iteración no termina porque la pantalla se vea bien, ni porque el endpoint responda.
Tiene que cruzar todo el camino.
Usuarios: administrar sin convertir el frontend en autoridad
La administración de usuarios parece sencilla hasta que deja de serlo.
Listar.
Buscar.
Crear.
Editar.
Cambiar roles.
Restablecer contraseñas.
Archivar.
Reactivar.
Todo eso existe ahora como parte visible del producto.
Pero había una regla especialmente importante:
último administrador activo
→ intento de archivarlo o degradarlo
→ rechazo
La interfaz puede ocultar acciones cuando sabe que el usuario no tiene permiso para ejecutarlas.
Lo que no puede hacer es decidir por sí sola que una operación es válida.
Ese límite se mantuvo deliberadamente.
El frontend pregunta:
can(capability)
El backend decide.
Esto permite que la experiencia sea amable sin convertir la seguridad en una colección de if role === "administrator" dispersos por React.
Los roles existen como vocabulario de negocio.
Las capacidades siguen siendo la fuente de autorización.
Permisos que no son acciones
Esta iteración también encontró una pequeña grieta arquitectónica que terminó siendo bastante útil.
Algunas capacidades existen únicamente para autorizar.
Por ejemplo:
customers.view_archived
inventory.view_archived_locations
No son acciones.
No son consultas.
No deberían tener un handler falso solo para encajar en una clasificación que ya no era suficiente.
La solución fue introducir una categoría explícita para ese tipo de capacidad: una permission declarativa.
Eso parece un detalle pequeño, pero conserva una propiedad importante del sistema:
si una cosa existe solo para autorizar, no hay que fingir que ejecuta algo.
Más adelante, cuando probamos el sistema contra una base PostgreSQL real desde cero, esa decisión reveló otro problema: la aplicación ya entendía este nuevo tipo de capacidad, pero una restricción antigua de la base de datos todavía aceptaba únicamente action y query.
La aplicación tenía razón.
El esquema se había quedado atrás.
Una nueva migración alineó ambas partes.
Ese fallo no apareció en una prueba superficial. Apareció al levantar una instalación completa desde una base vacía.
Y esa es exactamente la clase de problema que queríamos empezar a encontrar ahora.
Clientes: Painter empieza a justificar su existencia
Clientes fue la prueba más interesante para Painter desde la iteración anterior.
Productos y Categorías fueron el primer caso real.
Ahora queríamos comprobar si el mismo mecanismo podía representar otro dominio sin aprender reglas específicas de él.
El recorrido quedó así:
metadata de Customer
→ Atlas
→ contrato de presentación
→ Painter
→ List / Form / Detail
Painter no sabe qué es un cliente.
No sabe cuáles campos son “core”.
No sabe cuáles son personalizados.
No tiene una rama especial que diga:
if model === "customer"
Recibe una definición y la interpreta.
Eso nos permitió construir lista, formulario y detalle de clientes sin convertir el renderer en una colección creciente de excepciones.
Y esa frontera sobrevivió intacta: al terminar la iteración no hubo cambios productivos dentro de Painter.
Un pequeño problema con los campos personalizados
La iteración también dejó una corrección interesante.
Los campos personalizados publicados para Customer ya podían viajar por el mismo camino de metadata y renderizarse como cualquier otro campo.
Eso era bueno.
Pero había una diferencia importante:
el backend de Customer todavía no tiene un contrato para persistir valores personalizados.
Mostrar un campo editable en el formulario habría comunicado algo falso:
puedes editar esto
cuando en realidad el valor se habría descartado.
La solución no fue enseñarle a Painter qué campos pertenecen a Customer.
Tampoco ampliar silenciosamente el alcance de Artifier.
La propia feature de Customers marca esos campos como solo lectura en el formulario.
Así:
Detail
→ el campo personalizado se muestra normalmente
Form
→ el campo se muestra, pero no promete una escritura que aún no existe
Es una corrección pequeña, pero representa una regla que queremos conservar:
la interfaz no debe prometer capacidades que el contrato real todavía no tiene.
Ubicaciones: ya existe un lugar donde ocurren las cosas
La tercera pieza fue Locations.
Ahora Racoonity puede administrar ubicaciones con tipos explícitos:
internal
sales
damaged
Una ubicación puede crearse, editarse, archivarse y reactivarse.
También puede definirse cuál ubicación de tipo sales es la ubicación de venta.
Y aquí nuevamente la interfaz ayuda, pero no decide.
No ofrece ubicaciones inválidas como candidatas.
Pide confirmación para cambiar la ubicación de venta.
No cambia el estado de forma optimista antes de que el backend confirme la operación.
Pero si alguien intenta saltarse la interfaz y enviar una operación inválida, el backend vuelve a comprobarla.
Esa separación es importante.
El frontend mejora la experiencia.
El backend conserva la autoridad.
Settings no se convirtió en un dominio
También apareció por primera vez una agrupación de Settings.
Eso podía convertirse muy fácilmente en el clásico cajón de sastre donde con el tiempo termina viviendo medio sistema.
Decidimos evitarlo desde ahora.
Settings no posee datos de negocio.
No tiene su propio repositorio.
No tiene Actions particulares.
No existe una enorme SettingsPage acumulando responsabilidades.
Es una agrupación de experiencia.
Dentro de ella aparecen superficies como Usuarios, Ubicaciones, Company y Branch, cada una consumiendo los contratos del dominio al que realmente pertenecen.
En otras palabras:
Settings organiza acceso. No se apropia del negocio.
Una navegación que ya empieza a parecer producto
Al terminar la iteración, la navegación principal puede expresarse aproximadamente así:
Catalog
Customers
Settings
├── Users
├── Locations
├── Company
└── Branch
Settings es un grupo.
No es una página vacía que existe solo porque había que tener un enlace.
Cada elemento publicable tiene una superficie real detrás.
Y las rutas protegidas consumen el mismo conjunto de capacidades resuelto durante el bootstrap.
No existe una segunda interpretación de permisos escondida dentro de cada feature.
Cuando por fin usamos PostgreSQL de verdad
Una de las partes más valiosas de esta iteración no fue una pantalla.
Fue una prueba.
Durante varias iteraciones, algunas suites que requerían PostgreSQL real habían quedado sin ejecutar en ciertos entornos.
Esta vez conseguimos cerrar esa deuda.
La prueba final arrancó desde una base completamente vacía:
fresh PostgreSQL
→ migrations
→ composition
→ setup
→ login
→ frontend
→ navegación
→ journeys reales
Y fue ahí donde apareció el problema de la restricción de permisos que había quedado desactualizada.
Después de corregirlo, los tres recorridos completos del Segundo MVP volvieron a ejecutarse:
I0 — entrada al producto
I1 — catálogo + Painter
I2 — administración operativa
Los tres pasaron contra PostgreSQL, Vite y Chromium reales.
Eso importa más que el número de tests.
Pero, por curiosidad, el frontend terminó la iteración con:
97 archivos de prueba
930 tests
930 passed
0 failed
Y el backend cerró su suite completa sin fallos ni suites PostgreSQL omitidas.
Por primera vez en este tramo del proyecto pudimos eliminar esa deuda ambiental de la lista.
Las pruebas también envejecen
Probar todo contra infraestructura real encontró además algo menos dramático y bastante saludable: algunos tests viejos estaban equivocados.
Uno asumía que la navegación productiva siempre tendría cuatro elementos.
Otro par asumía que la migración de una iteración histórica siempre sería la última migración disponible.
Eso funcionó durante meses.
Hasta que el sistema creció.
Los tests fueron corregidos para expresar la propiedad que realmente debían proteger:
qué elementos de navegación deben existir
en lugar de un número mágico, y:
probar una migración histórica específica
en lugar de asumir que siempre seguirá estando al final de la historia.
Es una buena advertencia:
una prueba también puede convertirse en deuda cuando termina verificando un accidente del presente en lugar de una regla del sistema.
Lo que deliberadamente no hicimos
Esta iteración prepara el terreno para Inventory.
No lo implementa.
Todavía no construimos:
- movimientos de inventario;
- entradas o salidas;
- ajustes;
- transferencias;
- POS;
- ventas;
- sesiones de caja;
- pagos;
- multiempresa o multisucursal real;
- constructor de roles;
- Artifier para Customers;
- reporting;
- packaging.
Incluso cuando la interfaz puede mostrar un rechazo relacionado con stock, eso no significa que esta iteración haya construido stock.
Mantener esa frontera importa.
El siguiente paso tendrá su propia tesis.
Cerrando la iteración
productize-customers-and-operational-administration terminó con:
38 requirements
123 scenarios
0 unmapped
El cambio fue archivado después de validar los recorridos completos y consolidar sus especificaciones.
La arquitectura también salió con varias respuestas importantes:
Painter conoce core/custom?
→ no
Painter necesitó lógica Customer?
→ no
el frontend autoriza por role?
→ no
Settings se convirtió en dominio?
→ no
el Shell conoce internals de features?
→ no
las features conocen internals de Painter?
→ no
el backend sigue siendo autoridad?
→ sí
Ese conjunto de “no” es tan importante como las nuevas pantallas.
Porque Racoonity está creciendo.
Y cada iteración que añade capacidad sin borrar las fronteras anteriores hace un poco más creíble que la arquitectura pueda sobrevivir a lo que viene.
Lo que sigue
La siguiente iteración es:
I3 — La mercancía ya se puede mover.
Hasta ahora hemos preparado el espacio.
Ya podemos entrar.
Ya podemos ver el catálogo.
Ya podemos administrar personas, clientes y lugares.
Ahora toca que algo ocurra dentro de esos lugares.
consultar stock
→ registrar inventario
→ entrada / salida
→ ajustar
→ transferir
→ consultar movimientos
Ese será otro problema.
Por ahora, la ciudad acaba de ganar algo menos espectacular, pero indispensable:
los estantes ya se pueden tocar.