Roles y permisos
Los cuatro roles
Sección titulada «Los cuatro roles»Estado: ✅ Confirmada — definidos por Aquazaku.
El sistema tiene exactamente cuatro roles. No hay más, y agregar uno nuevo es una decisión de negocio, no una comodidad de implementación.
| Rol | Quién es | Dónde opera | Qué hace |
|---|---|---|---|
admin |
Dueño / administración | Web (remoto, visita la planta cuando viaja) | Configuración, auditoría, ajustes sensibles |
seller |
Vendedor | Web responsiva | Contacta clientes y registra ventas a distancia |
pos |
Planta + mostrador | Web / terminal en la planta | Venta de mostrador, preparación de pedidos, despacho, cierre de producción |
contador |
Contador externo o interno | Web, solo lectura | Genera reportes PDF para temas impositivos y legales |
La diferencia entre seller y pos no es de jerarquía: es de contexto de
operación. Uno contacta clientes por canal remoto. El otro opera la planta y
atiende mostrador.
Por qué existe contador aparte de admin
Sección titulada «Por qué existe contador aparte de admin»El admin es el dueño, vive en otra ciudad y concentra los permisos sensibles
del sistema (anular ventas, ajustar stock, cambiar precios). Para temas
tributarios en Colombia — DIAN, retenciones, ICA, etc. — hace falta entregarle
reportes a un contador externo que no tiene por qué poder modificar nada.
Un admin “de solo lectura” como flag sobre el mismo rol no alcanza:
el contador necesita generar y descargar sus propios reportes sin heredar los
permisos de modificación. Es un rol aparte, no un subconjunto.
Multi-rol por usuario
Sección titulada «Multi-rol por usuario»Estado: ✅ Confirmada — cerrá la pregunta #13 de Qué falta preguntar.
Un mismo usuario puede tener N roles asignados a la vez. Esto cubre el caso futuro en que una persona ejerza varias funciones (ej. operador de planta que también venda en la calle).
usuario = { id: uuid, email: string, roles: ["pos", "seller"], // cualquiera de los cuatro; asignable por admin ...}Todos los roles asignados están activos simultáneamente. NO existe switch-role
ni active_role. Un usuario con ["pos", "seller"] ve y opera ambos módulos
sin elegir uno. La decisión de diseño completa está en
ADR-0003.
Todo se audita bajo el mismo user_id. La auditoría registra
rol_ejercido como array (los roles bajo los que se ejecutó la acción
específica), así que se puede responder “¿qué hizo este usuario como seller?”
o “¿qué hizo como pos?” filtrando el log.
Multi-rol por usuario ≠ UI multi-rol
Sección titulada «Multi-rol por usuario ≠ UI multi-rol»Que un usuario tenga permisos de pos y seller no significa que la app
muestre los módulos según un rol activo. Regla de UI:
- Web (desktop): muestra los módulos para los que el usuario tiene al menos un rol con permiso. Sin selector de rol, sin switch.
- App móvil (post-MVP): la lógica de UI mobile se define aparte, pero la regla sigue siendo “permisos del backend, UI mobile decide qué mostrar”.
El backend no conoce el device — autoriza por capacidad del rol, sin mirar si la petición viene desde mobile o web. Ocultar un botón no es seguridad (RN-ACC-02).
El error que casi todos cometen
Sección titulada «El error que casi todos cometen»Un permiso tiene dos ejes, no uno. La mayoría de los sistemas modela solo el primero y después parchea el segundo a mano por toda la aplicación.
| Eje | Pregunta | Ejemplo |
|---|---|---|
| Acción | ¿Qué puede hacer? | ventas:ver |
| Alcance | ¿Sobre qué datos? | Solo las suyas |
Un seller “puede ver ventas” — pero solo las suyas. Eso no es un flag
booleano: es un filtro de datos. Si tratás el alcance como un if suelto en cada
endpoint, tarde o temprano hay un endpoint donde te olvidaste, y un vendedor ve
la facturación completa de la empresa.
Alcances definidos
Sección titulada «Alcances definidos»| Alcance | Significa | Filtra datos |
|---|---|---|
todo |
Todos los registros del sistema | sin filtro |
propio |
Solo los registros que él mismo creó | sí |
ruta |
Solo los de la ruta que tiene abierta | sí |
BODEGA |
Solo lo que está físicamente en bodega | sí |
prep |
Solo los reportes de preparación | no — categoría |
operativos |
Solo los reportes operativos | no — categoría |
| — | Sin acceso | — |
Los cuatro primeros filtran filas de una tabla y se aplican con una cláusula
WHERE. prep y operativos no: acotan qué categoría de reporte se puede
ver o descargar, y eso lo resuelve el módulo de reportes (M13), no la capa de
datos.
La distinción importa: si un alcance categórico se colara en una consulta de
tabla, no habría columna por la cual filtrar. El código
(scopes.ts) falla en ese caso en vez de
devolver todo sin filtrar.
Nomenclatura de permisos
Sección titulada «Nomenclatura de permisos»ventas:anular │ └── acción └───────── recursoMinúsculas, sin acentos, recurso:accion. El mismo string se usa en el backend,
en el frontend y en esta documentación. Una sola fuente de verdad.
Matriz de permisos
Sección titulada «Matriz de permisos»Leyenda: ✅ alcance todo · 🟡 alcance limitado (se indica) · ❌ sin acceso
Ventas y cobros
Sección titulada «Ventas y cobros»| Permiso | admin |
seller |
pos |
contador |
|---|---|---|---|---|
ventas:ver |
✅ | 🟡 propio |
🟡 propio |
🟡 todo |
ventas:crear |
✅ | ✅ | ✅ | ❌ |
ventas:anular |
✅ todo |
🟡 propio + status=pendiente |
🟡 propio + status=pendiente |
❌ |
ventas:anular_verificada |
✅ todo (motivo obligatorio) |
❌ | ❌ | ❌ |
ventas:corregir |
✅ todo (motivo obligatorio) |
❌ | ❌ | ❌ |
ventas:verificar_pago |
✅ todo |
🟡 propio |
🟡 propio |
❌ |
ventas:gestionar_cuentas_pendientes |
✅ todo |
❌ | ❌ | ❌ |
cobros:ver |
✅ | 🟡 propio |
🟡 propio |
🟡 todo |
cobros:registrar |
✅ | ✅ | ✅ | ❌ |
Clientes
Sección titulada «Clientes»| Permiso | admin |
seller |
pos |
contador |
|---|---|---|---|---|
clientes:ver |
✅ | ✅ | ✅ | 🟡 todo |
clientes:crear |
✅ | ✅ | ✅ | ❌ |
clientes:verificar_documento |
✅ | ✅ | ✅ | ❌ |
clientes:editar |
✅ | ❌ | ❌ | ❌ |
clientes:habilitar_credito |
✅ | ❌ | ❌ | ❌ |
Stock de producto
Sección titulada «Stock de producto»| Permiso | admin |
seller |
pos |
contador |
|---|---|---|---|---|
stock:ver |
✅ | 🟡 todo |
🟡 todo |
🟡 todo |
stock:cargar_ruta |
✅ todo |
❌ | ✅ todo |
❌ |
stock:ajustar |
✅ | ❌ | 🟡 cantidades (con motivo) |
❌ |
stock:descartar |
✅ | ❌ | ✅ | ❌ |
insumos:ver |
✅ | ❌ | ✅ | 🟡 todo |
insumos:ajustar |
✅ | ❌ | 🟡 cantidades (con motivo) |
❌ |
Botellones — por cantidad
Sección titulada «Botellones — por cantidad»| Permiso | admin |
seller |
pos |
contador |
|---|---|---|---|---|
botellones:ver |
✅ | ✅ | ✅ | 🟡 todo |
botellones:entregar |
✅ | ❌ | ✅ | ❌ |
botellones:recibir_retorno |
✅ | ❌ | ✅ | ❌ |
botellones:registrar |
✅ | ❌ | ✅ | ❌ |
botellones:descartar |
✅ | ❌ | ✅ (con motivo) | ❌ |
Bases — por unidad identificada
Sección titulada «Bases — por unidad identificada»| Permiso | admin |
seller |
pos |
contador |
|---|---|---|---|---|
bases:ver |
✅ | ✅ | ✅ | 🟡 todo |
bases:prestar |
✅ todo |
❌ | ✅ con cliente verificado | ❌ |
bases:retirar |
✅ | ❌ | ✅ | ❌ |
bases:registrar |
✅ | ❌ | ✅ | ❌ |
bases:descartar |
✅ | ❌ | ✅ (con motivo) | ❌ |
Producción y agua
Sección titulada «Producción y agua»| Permiso | admin |
seller |
pos |
contador |
|---|---|---|---|---|
produccion:ver |
✅ | ❌ | ✅ | 🟡 todo |
produccion:registrar_cierre |
✅ | ❌ | ✅ | ❌ |
tanques:ver |
✅ | ❌ | ✅ | ❌ |
tanques:registrar_reposicion |
✅ | ❌ | ✅ | ❌ |
tanques:ajustar |
✅ | ❌ | ❌ | ❌ |
configuracion:equivalencias |
✅ | ❌ | ✅ | ❌ |
Proveedores y compras
Sección titulada «Proveedores y compras»| Permiso | admin |
seller |
pos |
contador |
|---|---|---|---|---|
proveedores:ver |
✅ | ❌ | 🟡 todo |
🟡 todo |
proveedores:crear |
✅ | ❌ | ❌ | ❌ |
proveedores:editar |
✅ | ❌ | ❌ | ❌ |
compras:crear |
✅ | ❌ | ✅ | ❌ |
compras:recibir |
✅ todo |
❌ | ✅ solo si compra=pendiente y proveedor=activo | ❌ |
| Permiso | admin |
seller |
pos |
contador |
|---|---|---|---|---|
rutas:ver |
✅ | 🟡 propio |
🟡 todo |
🟡 todo |
rutas:abrir |
✅ | ✅ | ❌ | ❌ |
rutas:rendir |
✅ | 🟡 propio |
❌ | ❌ |
rutas:cerrar_con_faltante |
✅ | ❌ | ❌ | ❌ |
Administración
Sección titulada «Administración»| Permiso | admin |
seller |
pos |
contador |
|---|---|---|---|---|
productos:ver |
✅ | ✅ | ✅ | 🟡 todo |
productos:crear |
✅ | ❌ | ❌ | ❌ |
productos:editar |
✅ | ❌ | ❌ | ❌ |
productos:editar_precios |
✅ | ❌ | ❌ | ❌ |
productos:desactivar |
✅ | ❌ | ❌ | ❌ |
usuarios:* |
✅ | ❌ | ❌ | ❌ |
auditoria:ver |
✅ todo |
❌ | ❌ | ✅ todo (read-only) |
reportes:operativos |
✅ | ❌ | 🟡 prep |
✅ |
reportes:financieros |
✅ | ❌ | ❌ | ✅ |
reportes:descargar_pdf |
✅ | ❌ | 🟡 operativos |
✅ |
configuracion:* |
✅ | ❌ | ❌ | ❌ |
La consecuencia de tener admin con super-poderes
Sección titulada «La consecuencia de tener admin con super-poderes»Con este modelo, admin concentra las funciones de control sensibles:
ajusta stock, anula ventas, cambia precios, descarta botellones y bases, y
administra usuarios. Ningún otro rol puede tocar nada de esto.
Eso es perfectamente razonable en una operación chica, sobre todo porque el
dueño es el único admin y vive en otra ciudad (viaja a Tasajera cada cierto
tiempo). Pero hay que decirlo en voz alta:
Reglas de acceso
Sección titulada «Reglas de acceso»RN-ACC-01 — Un usuario puede tener varios roles a la vez
Sección titulada «RN-ACC-01 — Un usuario puede tener varios roles a la vez»Estado: ✅ Confirmada — cerrá la pregunta #13 de Qué falta preguntar.
Un usuario puede tener N roles asignados a la vez, configurado por admin.
Hoy el operario de planta tiene solo pos; una futura persona híbrida podría
tener ["pos", "seller"].
Por qué: la operación futura de Aquazaku puede incluir personas que atiendan mostrador y salgan a ruta según el día. Prohibirlo en el modelo implica crear dos usuarios para la misma persona — feo y propenso a errores.
RN-ACC-02 — La UI oculta, la API prohíbe
Sección titulada «RN-ACC-02 — La UI oculta, la API prohíbe»Estado: ✅ Confirmada (decisión técnica)
El permiso se valida siempre en el servidor. Que el frontend no muestre el botón es comodidad para el usuario, no seguridad.
Por qué: ocultar un botón no impide llamar al endpoint. Toda validación que viva únicamente en el cliente ya está rota, solo que todavía no lo sabés.
RN-ACC-03 — El alcance se aplica en la capa de datos
Sección titulada «RN-ACC-03 — El alcance se aplica en la capa de datos»Estado: ✅ Confirmada (decisión técnica)
El filtro por alcance (propio, ruta, todo) se aplica en un único lugar del
acceso a datos, no repetido endpoint por endpoint.
Por qué: repetir el filtro garantiza que algún día falte en uno. Y ese uno va a ser el de reportes.
RN-ACC-04 — Toda acción sensible queda auditada
Sección titulada «RN-ACC-04 — Toda acción sensible queda auditada»Estado: ✅ Confirmada (sesión M0 del 19-ago-2026)
Anulaciones, ajustes de stock, bajas de botellones y bases, préstamo y retiro de bases, cambios de precio, habilitación de crédito y cierres con faltante registran quién, cuándo, bajo qué rol y por qué. La consulta de la auditoría es accesible desde la UI por admin (vista completa) y contador (read-only para temas impositivos/DIAN).
Por qué: con admin concentrando todo el poder de corrección, la auditoría
es el único mecanismo de control que queda. Todas las reglas de inmutabilidad
del dominio (RN-VEN-02, RN-RUT-04)
dependen de que exista. La consulta UI cierra el ciclo: sin forma de ver la
auditoría, no es control, es solo un log oculto.
Implementación: audit_log append-only (REVOKE + trigger en Postgres),
escritura automática desde authz/middleware.ts (allow/deny) y desde la capa
de servicio de cada módulo (acciones sensibles). La UI vive en
/admin/auditoria y /contador/auditoria con filtros por usuario, módulo,
acción y rango de fechas. Detalles en
ADR-0001 y
spec de M0.
Desde el 10-oct-2026, cada una de estas acciones escribe su fila dentro de
la transacción que hace el cambio, como decidió
ADR-0007: si la bitácora no se puede
escribir, la acción no ocurre. Lo vigila un guardián estructural
(audit/__tests__/auditoria-transaccional.test.ts) que falla cuando alguien
escribe la próxima acción sensible del lado equivocado.
RN-ACC-05 — Un usuario no se borra, se desactiva
Sección titulada «RN-ACC-05 — Un usuario no se borra, se desactiva»Estado: 🟡 Supuesto
Un usuario con historial se desactiva y pierde el acceso, pero sus registros mantienen la referencia a él.
Por qué: borrar al usuario deja huérfanas todas las ventas que registró.
RN-ACC-06 — Siempre tiene que quedar un administrador activo
Sección titulada «RN-ACC-06 — Siempre tiene que quedar un administrador activo»Estado: ✅ Confirmada (implementación de M0, 20-ago-2026)
El sistema rechaza cualquier operación que dejaría cero administradores activos:
- quitarle el rol
adminal único que lo tiene, - desactivar al único administrador activo.
Un administrador desactivado no cuenta como respaldo: si contara, el sistema quedaría en manos de una cuenta que no puede entrar.
Por qué: hoy el dueño es el único admin
(ver arriba). Sin esta
regla, un click de más en la pantalla de usuarios —la que existe justamente para
editar usuarios— lo deja sin el rol, y nadie queda con permiso para
devolvérselo. El sistema pasa a ser inadministrable y solo se recupera
metiendo mano directamente en la base de datos.
No es un escenario rebuscado. Es el error más fácil de cometer en esa pantalla.
Cómo se comporta: la API responde 409 con el código ULTIMO_ADMIN y un
mensaje que dice qué hacer (“asignale el rol admin a otra persona antes”). El
intento fallido queda auditado: alguien tratando de quitarse el rol admin es
exactamente el tipo de cosa que hay que poder ver después.
Qué NO hace: no impide que un admin se quite el rol a sí mismo si hay otro administrador activo. Delegar y salir es una operación legítima.
RN-ACC-07 — Un cambio de roles hace efecto en el acto
Sección titulada «RN-ACC-07 — Un cambio de roles hace efecto en el acto»Estado: ✅ Confirmada (implementación de M0, 20-ago-2026)
Cuando un admin le cambia los roles a alguien, el cambio rige desde el siguiente request, sin necesidad de que la persona vuelva a iniciar sesión. Y sin cerrarle la sesión: sigue trabajando, con los permisos nuevos.
Por qué: los roles se congelan dentro de la sesión al iniciar sesión
(RN-ACC-01), que es
lo que evita consultar la base en cada request. Sin actualizarlos al cambiarlos,
quitarle el rol admin a alguien lo dejaría administrando siete días más,
hasta que le venza la sesión.
Por qué no se cierra la sesión: cerrarla también resolvería el problema, pero echaría a la persona del sistema cada vez que un admin le toca un rol. Un trámite administrativo no debería interrumpirle el trabajo a nadie.
Excepción: desactivar a un usuario sí le cierra las sesiones. Ahí no hay trabajo que preservar.
RN-ACC-08 — Nadie puede ver la contraseña de nadie
Sección titulada «RN-ACC-08 — Nadie puede ver la contraseña de nadie»Estado: ✅ Confirmada (26-ago-2026)
Ni el administrador. Ni el rootadmin. No es una restricción de permisos: el sistema no tiene la contraseña de nadie.
accounts.password guarda un hash argon2id, que es una función de una sola
vía: sirve para verificar un intento, no para reconstruir el original. Mostrar
«la contraseña registrada» exigiría dejar de hashear y guardarlas de forma
recuperable.
Por qué no se haría aunque se pudiera, y es una razón de Aquazaku: si existe
alguien que puede leer esas claves, el audit_log deja de probar nada.
«pos@aquazaku.com anuló esta venta» pasaría a significar «alguien que sabía esa
clave lo hizo», y esa lista incluye a quien puede leerlas. Cualquiera podría
decir «yo no fui» y tendría razón.
El módulo de auditoría —la pieza más cara del sistema— quedaría decorativo.
Y hay algo práctico: la gente reusa contraseñas. Una filtración no expondría el acceso a Aquazaku, expondría el correo y el banco de ocho personas.
Lo que sí resuelve el problema real
Sección titulada «Lo que sí resuelve el problema real»El problema que motivó el pedido es legítimo: que nadie dependa del correo para volver a entrar. Son ocho personas en Campo de la Cruz y no todas tienen correo a mano.
Se resuelve con restablecimiento en persona: el admin genera una temporal, la
ve una sola vez en pantalla, se la dice de viva voz, y mustChangePassword
obliga a cambiarla al entrar. El admin nunca conoce la contraseña final, así
que la auditoría sigue valiendo.
La auditoría registra el hecho, no el valor: quién restableció, sobre quién y cuándo. Guardar la temporal en la bitácora la volvería permanente y legible por más gente que la que la necesita.
Preguntas abiertas
Sección titulada «Preguntas abiertas»- ¿
posvende contra el stock de bodega directamente, o el punto de venta tiene su propia ubicación de stock? - ¿Hay más de un punto de venta?
