Royal Clean CRM · Documentación
Calidad

Estado del sistema

Qué está hecho, qué falta y qué encontró la auditoría de calidad.

Última actualización: 31 de agosto de 2026.


Completado

Fundación

  • Next.js 16 · React 19 · TypeScript estricto · Tailwind 4
  • Prisma 7 con PostgreSQL 17: 18 modelos, migraciones versionadas
  • Invariantes de dominio como restricciones CHECK e índices parciales
  • Validación de entorno con Zod, perezosa (el build no necesita secretos)
  • docker compose con PostgreSQL y MinIO para desarrollo
  • Semilla de desarrollo con datos realistas de Guanacaste

Autenticación y autorización

  • Sesiones opacas en base de datos con revocación inmediata
  • Argon2id, política de contraseñas, contraseñas temporales
  • Rate limiting por usuario y por IP, en base de datos
  • Hash señuelo para no filtrar qué usuarios existen
  • Guardas centrales; un recurso ajeno devuelve 404
  • Cambio forzado de contraseña tras un restablecimiento

Directorio

  • Clientes: alta, edición, archivado en cascada
  • Propiedades: alta, edición, información de acceso protegida, archivado
  • Plantillas de tareas reordenables
  • Empleados: alta con contraseña temporal, tarifa propia, desactivación
  • Búsqueda, filtros y paginación en todos los listados del directorio, y búsqueda global que cruza clientes, propiedades, personal y servicios

Servicios

  • Creación con snapshot de tareas y congelado de precio
  • Máquina de estados centralizada
  • Asignación múltiple con congelado de tarifa y aviso de cruces de agenda
  • Listado con filtros en la URL, calendario mensual y semanal, y tablero Kanban
  • Detalle administrativo con tiempos, evidencia, incidentes, insumos y cifras
  • Horario por día, semana y mes, descargable como imagen y como PDF
  • Corrección de horarios con motivo obligatorio y auditoría
  • Cancelación con motivo y marcado para revisión si había trabajo, por cualquiera de las dos vías (ficha y tablero)

Flujo del personal de campo

  • Pantallas mobile-first: hoy, próximos, historial, perfil
  • Detalle de servicio con cronómetro en vivo, sin escribir en la base
  • Datos de acceso protegidos por estado y tapados hasta pulsar «Mostrar»
  • Tareas con evidencia antes y después, notas y reapertura
  • Iniciar, finalizar participación, finalizar servicio y retomar trabajo
  • Reporte de incidentes

Evidencia

  • Compresión y borrado de EXIF en el navegador
  • Subida directa a R2 con URL firmada; confirmación verificada contra el bucket
  • Visualización mediante ruta autorizada que redirige a URL firmada
  • Retención configurable y limpieza de subidas abandonadas
  • Progreso, reintento y cancelación por fotografía

Insumos

  • Gestión completa desde el detalle de servicio: crear, editar, eliminar
  • Aprobación y rechazo de los registrados por el personal
  • Pantalla del personal que anota qué y cuánto repuso, sin ver importes
  • Los pendientes no cuentan en las cifras hasta aprobarse

Finanzas

  • Aritmética monetaria en céntimos enteros
  • Rentabilidad por servicio, propiedad, cliente y período
  • Cifras parciales explícitas cuando faltan costos
  • Nómina con desglose por servicio y tarifa congelada
  • Pagos con desglose congelado y reversión auditada
  • Indicadores de gestión en el panel: GOP, labor cost ratio y productivity index
  • El GOP se compone desde Configuración: qué costos entran, costo o margen, y su nombre
  • Ranking de tareas del mes: mayor y menor facturación, y mayor margen
  • Costo de insumos estimado por tarea del catálogo, congelado en cada servicio

Cotizaciones

  • Moneda (colones o dólares) e idioma (español o inglés) por cotización
  • Tipo de cambio escrito a mano y congelado en el documento
  • El cliente se elige de la lista o se escribe: si no existe, se registra al guardar
  • Líneas desde el catálogo de tareas o escritas a mano, con cantidad y precio
  • IVA configurable (13 % por defecto) aplicado sobre el subtotal, no por línea
  • Importes derivados siempre en el servidor y congelados en la cotización
  • Estados borrador, enviada, aceptada y rechazada, con auditoría
  • Una cotización ya respondida no se puede modificar
  • Envío por correo con Resend: PDF adjunto, confirmación y registro de a quién y cuándo

Reportes

  • Reporte PDF con marca, evidencia incrustada y paginación real
  • Test que verifica que no filtra tarifas ni márgenes
  • Reportes internos de rentabilidad por propiedad y cliente

Sistema

  • Incidentes: reporte, bandeja, resolución con notificación
  • Notificaciones internas con contador
  • Auditoría legible campo a campo, con filtros
  • Configuración global con advertencias de efecto histórico
  • Apariencia clara, oscura o automática, guardada por dispositivo
  • Línea de tiempo derivada de la auditoría

Infraestructura

  • Dockerfile multi-etapa, usuario sin privilegios, healthcheck (~640 MB)
  • Migraciones aplicadas al arrancar; el contenedor no arranca si fallan
  • Trabajo de mantenimiento idempotente, por CLI o endpoint con token
  • GitHub Actions: typecheck, lint, tests, integración, build y arranque
  • PWA instalable con manifiesto e iconos

Pruebas

  • 198 tests unitarios sobre los módulos de cálculo
  • 110 tests de integración contra PostgreSQL y almacenamiento reales
  • 30 recorridos de extremo a extremo con Playwright, sobre la aplicación compilada, en escritorio y en móvil

Auditoría de calidad (31 de agosto de 2026)

Cinco revisiones independientes —seguridad, corrección de negocio, interfaz, cobertura de especificación y calidad de código— sobre el sistema terminado. Resultado: 38 defectos reales corregidos en cuatro entregas.

Lo que encontró

Seguridad. La evidencia se autorizaba contra un servicio que elegía el cliente: un empleado podía borrar una foto de otro servicio suyo ya cerrado. La portada del panel era la única página sin guarda propia. x-forwarded-for se leía por el extremo que escribe el cliente, lo que permitía evadir el límite de intentos de acceso. El personal podía fijar el precio de cobro de un insumo.

Dinero. Lo pendiente de nómina se contaba por identidad de servicio y no por importe, así que el trabajo añadido con «Retomar trabajo» a un servicio ya pagado era imposible de cobrar. Un servicio compartido perdía del panel al empleado no pagado. Un pago podía registrarse dos veces. Y reasignar a alguien recongelaba su tarifa, reescribiendo el valor de horas ya trabajadas.

Estado. El tablero cancelaba sin motivo, sin marcar para revisión y sin avisar al personal; y reabrir dejaba puesta la fecha de finalización, de la que depende la ventana de acceso a los códigos de la propiedad.

Interfaz. No existía ninguna pantalla de error: un fallo al renderizar mostraba la de Next.js, en inglés. El buscador entraba en un bucle de navegación con un espacio al final. La marca «Requiere revisión» no se podía quitar nunca. El cajón de navegación era el único modal sin trampa de foco.

Cobertura. Faltaba la búsqueda global. Las advertencias de archivado se calculaban y se descartaban. La paginación de la auditoría perdía los filtros.

Lo que confirmó

  • Ninguna consulta del personal de campo arrastra tarifas, salarios, precios ni márgenes: se comprobó campo a campo sobre los nueve campos monetarios.
  • Las 60 Server Actions siguen el mismo patrón, sin una sola excepción, y ninguna puede propagar un error crudo de Prisma.
  • Cero any, cero @ts-ignore, cero TODO sin clasificar en todo el código escrito a mano.
  • Cero colores fuera del sistema de tokens y cero textos en inglés.
  • El PDF del cliente no selecciona siquiera los campos que no debe mostrar.

Verificación sobre la imagen de producción

Comprobado el 1 de septiembre de 2026 contra la imagen real, con PostgreSQL 17 en su propia red de Docker y NODE_ENV=production.

ComprobaciónResultado
Arranca y aplica migraciones6 segundos
/api/health{"status":"ok","database":"ok"}
/admin sin sesión307 al inicio de sesión
Ruta inventada307, no filtra si existe
POST /api/jobs/run sin token401
Con token incorrecto401
Con token correcto200 con el recuento del mantenimiento
Sin JOBS_SECRET configurado503, no queda abierto
Usuario del contenedoruid=1001(nextjs), no root
Cabeceras de seguridadX-Frame-Options, X-Content-Type-Options, Referrer-Policy
X-Powered-ByAusente
Secretos en los registrosNinguno
Primer administrador desde el contenedorCreado, con cambio de contraseña forzado

Corregido el 17 de septiembre de 2026

Las tareas de un servicio no se podían editar desde ninguna parte

Síntoma. Royal Clean preguntó «¿cómo edito las tareas de un servicio?». La ficha listaba el checklist y el botón «Editar» sólo abría fecha, hora y precio.

Causa. No era una carencia del dominio: addServiceTaskAction, updateServiceTaskAction y deleteServiceTaskAction existían, con sus transacciones y su auditoría. Lo que faltaba era el botón que las llamara.

Arreglo. «Editar tareas» en la ficha del servicio, que abre el diálogo para añadir, renombrar y eliminar. El diálogo dice expresamente que afecta sólo a ese servicio y enlaza a la plantilla de la propiedad para los siguientes, porque confundir las dos cosas es fácil (§19). Una tarea completada no se borra: tiene evidencia detrás y hay que reabrirla primero.

El lápiz de una propiedad no llevaba a sus tareas

Editar una propiedad abría sólo sus datos y el acceso. Las tareas vivían en la ficha, a la que se llega por el otro botón de la fila, así que quien iba a «editar la propiedad» no las encontraba. Ahora la pantalla de edición incluye la plantilla de tareas.

«Hora esperada» no decía de qué

Ni en el formulario de alta ni en el de edición quedaba claro si era la hora de llegada o la de finalización. Ahora es «Hora esperada de inicio», también en la auditoría.

Ninguna propiedad se podía editar

Síntoma. Al guardar cambios en una propiedad, el formulario se revertía y aparecía «Invalid input: expected string, received null». No se podía cambiar nada: ni la dirección, ni los códigos de acceso. Lo reportó Royal Clean, que había estado archivando propiedades y volviéndolas a crear.

Causa. En modo edición el selector de cliente se deshabilita a propósito —el cliente de una propiedad no se cambia—, y un control deshabilitado no se incluye en el FormData: lo manda la especificación de HTML. Así que clientId llegaba nulo al servidor y Zod rechazaba el formulario entero, no sólo ese campo.

Arreglo. Un campo oculto con el valor, junto al selector deshabilitado. El selector sigue bloqueado, que es lo que se quiere; lo que se recupera es el valor. Es el único sitio del sistema con ese patrón: se comprobó.

Qué enseña. Un campo deshabilitado dentro de un formulario que se envía entero necesita siempre un acompañante oculto, o el servidor recibe un hueco.

El tema oscuro y la preferencia del sistema se contradecían

Las utilidades dark: seguían al sistema operativo mientras el resto del tema seguía al interruptor. Ver DEC-024.

Las imágenes de MinIO dejaron de poder descargarse, dos veces

Primero MinIO retiró sus imágenes públicas de Docker Hub y se pasó a quay.io. Después cerró también el acceso anónimo allí: quay.io/minio/minio y quay.io/minio/mc responden 401 para todas sus etiquetas, incluidas las que llevaban meses fijadas. docker compose up fallaba con un error que parece de credenciales propias y no lo es, y CI dejó de pasar en todas las ramas.

Se usa bitnamilegacy/minio, el archivo público de Bitnami. Es MinIO de verdad y no un simulador de S3: los tests de evidencia ejercitan URLs firmadas, HeadObject y borrado por lotes, y un sustituto parcial los dejaría pasando en falso. El bucket lo crea la propia imagen con MINIO_DEFAULT_BUCKETS, así que desapareció el contenedor storage-init y la dependencia del cliente mc.


Pendiente

Recorrido de extremo a extremo de una cotización

Las cotizaciones tienen tests unitarios de su aritmética y se verificaron a mano en el navegador —crear, IVA, guardar, ver el detalle en escritorio y en móvil—, pero todavía no hay un recorrido de Playwright que las cubra.

Impacto: medio. Es lo primero que conviene añadir.

Subida real de fotografías desde el navegador

Los recorridos comprueban que no se puede completar una tarea sin evidencia, pero no suben una fotografía de verdad desde el navegador. El ciclo completo —firmar, subir, confirmar— sí está cubierto a nivel de integración contra el almacenamiento real.

Impacto: bajo.

Vista de servicio para el personal con el servicio archivado

Menor: un servicio archivado no aparece en las listas del personal, pero su URL directa muestra la pantalla normal.

Correo electrónico

Sin integración. La aplicación funciona íntegramente sin correo, según lo acordado. Ver PENDING_BUSINESS_RULES.md #6.

Vídeo como evidencia

El modelo lo soporta con límites definidos, pero la interfaz sólo ofrece fotografías. Ver PENDING_BUSINESS_RULES.md #8.

Logotipo real

El logotipo es un SVG propio en src/components/brand/logo.tsx. El repositorio no contenía assets de marca. Sustituirlo es editar ese archivo y regenerar los iconos con node scripts/generate-icons.mjs.


Problemas conocidos

La tipografía depende de la plataforma

Decisión deliberada para no depender de Google Fonts al compilar (DECISIONS.md DEC-015). La aplicación se ve ligeramente distinta en Windows, macOS y Android.

La compresión depende del navegador

Navegadores antiguos sin soporte de WebP caen a JPEG. HEIC de iPhone se decodifica con el respaldo de <img>, que funciona en Safari reciente pero puede fallar en versiones viejas.

El panel no se actualiza solo

Por diseño (DECISIONS.md DEC-014). Los datos pueden tener unos segundos de antigüedad.


Decisiones registradas

20 decisiones técnicas documentadas en DECISIONS.md, incluidas tres que sólo aparecieron al construir la imagen de producción:

  • DEC-016 — El cliente Prisma se creaba al importar el módulo, lo que hacía que next build exigiera un DATABASE_URL real.
  • DEC-017 — La CLI de Prisma quedaba sin sus dependencias transitivas por los enlaces simbólicos de pnpm.
  • DEC-015 — El build descargaba la tipografía de Google Fonts, lo que rompe cualquier compilación sin salida a Internet.

Y dos que destaparon tests de integración:

  • DEC-018 — El SDK de S3 usaba direccionamiento virtual-host, incompatible con el endpoint de R2 y con MinIO.
  • El saneado de auditoría recorría campo a campo los Decimal de Prisma y alcanzaba sus métodos, con lo que Prisma rechazaba la escritura y tumbaba la operación auditada entera. Ahora las instancias de clase se convierten a texto, con un test que lo fija.

Preguntas abiertas para Royal Clean

Detalle en PENDING_BUSINESS_RULES.md.

  1. ¿Se redondean las horas de la nómina?
  2. ¿Existen horas extra con tarifa distinta?
  3. ¿Hay recargo por domingo, feriado o nocturnidad?
  4. ¿Es correcto el umbral de 6 horas para la alerta de servicio abierto?
  5. ¿Debe el sistema enviar correos?
  6. ¿Puede el personal registrar insumos, o sólo administración? (implementado con revisión obligatoria y un interruptor para desactivarlo)
  7. ¿Se admitirá vídeo como evidencia?
  8. ¿Qué pasa con la evidencia al expirar: se archiva o se elimina?
  9. ¿Con qué frecuencia y hacia dónde se respalda?

En todos los casos se implementó la opción más conservadora y el sistema quedó preparado para cambiarla sin reescribir nada.

On this page