Xirai Motors — Convertir el papel en una máquina de estados auditada
Gestión integral de talleres automotrices donde cada paso del vehículo deja una fila de auditoría. 44,725 líneas, 72 modelos y 585 métodos de test para reemplazar un cuaderno.
- Laravel 13
- PHP 8.3
- Pest 4
- MySQL
- Sanctum
- Docker
Restricciones
- Un taller real opera con papel. La trazabilidad no puede depender de que alguien recuerde.
- El técnico controla su propio reloj. Si puede pausarlo sin permiso, el tiempo facturable deja de ser evidencia.
- Multi-tenant por empresa y multi-sucursal por sucursal: el aislamiento de datos es una cuota, no un detalle.
- Los cambios de módulo se cerraron con cero migraciones nuevas, como restricción autoimpuesta y verificable.
El problema
El problema
Un taller automotriz real opera con papel. El vehículo entra sin evidencia fotográfica, la cotización no tiene trazabilidad, los tiempos no se miden, los técnicos no tienen rendimiento medible y las refacciones viven en la memoria de alguien.
El sistema convierte ese paso por el taller en una máquina de estados auditada de nueve etapas:
borrador → activo → en_diagnostico → en_cotizacion → en_solicitud_refacciones
→ en_reparacion → en_control_calidad → listo_para_entrega → entregado
Cada transición escribe su fila. El estado actual nunca se guarda: se deriva. Y cuando alguien pregunta por qué un vehículo tardó seis días en un paso, la respuesta está en la base de datos, no en la memoria del asesor.
Lo que hace difícil este dominio
No es CRUD con estados. Son tres tensiones reales:
El reloj. El tiempo es dinero, y quien lo controla normalmente es quien cobra por él. Si el técnico puede pausar su propio cronómetro sin consecuencias, el tiempo facturable deja de ser evidencia y pasa a ser un acuerdo de confianza. La solución es control de cuatro ojos: la pausa se solicita y otro rol la autoriza o la rechaza, con las dos acciones auditadas.
El dueño del servicio. Un jefe de asesores necesita reasignar trabajo —un asesor se enferma, otro está saturado, un cliente es de alguien más— y al mismo tiempo necesita evaluar carga real por persona. Las dos cosas exigen historia. Por eso reasignar cierra la asignación anterior en lugar de borrarla: el servicio cambia de dueño sin perder de quién fue.
La cuota. El sistema es multi-empresa y cada empresa paga por almacenamiento. Contar la cuota correctamente es un COUNT filtrado… si cada tabla de archivos lleva la empresa como columna. Si no la lleva, es un salto por cuatro relaciones en cada subida. La columna denormalizada es el precio de que la ruta más caliente del sistema sea barata, y está documentada como decisión con su costo aceptado, no como un accidente de esquema.
Documentar el estado real, no el deseado
Hay un detalle que me parece el más honesto de todo el proyecto.
El repositorio tiene un archivo que se declara la fuente de verdad sobre lo implementado, y advierte en su encabezado que los README de cada módulo pueden estar marcados como completos aunque el código no exista. En un cambio posterior, el registro corrigió en la dirección incómoda: un módulo reportado como incompleto en realidad estaba terminado, y el avance se recalculó hacia arriba.
Eso es raro de ver. Lo normal es que el estado documentado se infle; aquí está instrumentado para que no.
Sobre el equipo
Este sistema no lo escribí línea por línea yo solo. Definí el modelo de datos, las reglas del dominio y el orden de los módulos; mi equipo implementó los módulos sobre ese diseño, y el trabajo pasó por revisión con Pull Requests contra ramas de trabajo.
Los números —44,725 líneas, 72 modelos, 585 tests, 31 de 33 módulos cerrados— describen la magnitud de lo que se entregó, no una suma de commits. Lo mío fue el diseño y el núcleo; lo del equipo, la ejecución bajo ese diseño.
Y una cosa más: el proyecto tiene una sola interfaz en todo el dominio, y es la del proveedor externo de catálogo de vehículos. Eso no es casual. La abstracción se aplicó exactamente donde hace falta para poder testear sin depender de un tercero, y en ningún otro lado por reflejo. Un dominio con quince capas y una interfaz es alguien que entendió para qué sirven.
Decisiones
- 01
El estado del servicio es un log append-only, no una columna que se mueve
Cada transición del vehículo escribe una fila con su marca de entrada y su marca de salida; una etapa activa es la que tiene la salida nula. El estado actual es una consulta, no un dato almacenado. El historial completo —quién movió qué y cuándo— queda disponible sin triggers, sin tabla de auditoría paralela y sin oportunidad de desincronización entre el estado y su historia.
DescartadoUna columna `current_stage` que se sobrescribe. Es más barato de leer y significa que el día que alguien pregunte "por qué este carro tardó seis días", la respuesta ya no existe.
- 02
La reasignación cierra la asignación anterior, nunca la borra
Reasignar un servicio entre asesores no elimina la fila previa: la cierra con su marca de fin. El servicio se puede mover de dueño sin destruir la historia de quién lo tuvo, que es exactamente lo que un jefe de asesores necesita para evaluar carga de trabajo.
- 03
La cuota de multimedia se cuenta sobre una columna denormalizada, y está escrito por qué
Cada tabla de archivos lleva `company_id` aunque sea derivable por relación. Contar la cuota de una empresa exigiría saltar a través de servicios, cotizaciones, reparaciones y control de calidad hasta cada una de las cinco tablas de media. Denormalizar convierte una verificación que ocurre en cada subida en un conteo indexado por tabla. El costo aceptado es mantener la columna consistente.
DescartadoEl join transitivo, correcto en teoría y caro en la ruta más caliente del sistema.
- 04
Pausar el reloj requiere la firma de alguien más
El tiempo de un técnico se pausa con una solicitud que otro rol debe autorizar o rechazar, y las dos acciones quedan auditadas con sus tests propios. Es control de cuatro ojos aplicado al reloj, porque quien controla su propio cronómetro no produce evidencia.
- 05
Los bonos son un dominio con sus propias acciones, no un cálculo adentro de otra cosa
Semanas de bono, registros de bono y autorizaciones de bono son entidades separadas, con acciones dedicadas y su propia batería de tests —incluido el cálculo del bono del jefe, que es dinero que se paga a una persona para evaluar a otras. Las reglas de dinero no se esconden en un método de controlador.
- 06
El perfil es el que se autentica, y el `User` queda detrás
Hay seis perfiles distintos —administración, supervisión, jefatura de asesores, jefatura de taller, asesor y técnico— y el middleware sustituye al `User` por el perfil activo, así que el controlador recibe el perfil y alcanza al usuario real por relación. El efecto es que las políticas de acceso hablan el idioma del rol. La trampa —creer que se tiene un `User` cuando se tiene un asesor— está escrita en la documentación del proyecto precisamente porque muerde.
- 07
El estado del proyecto se documenta contra la realidad, no contra el optimismo
El repositorio tiene un archivo de estado que se declara a sí mismo como la fuente de verdad sobre la implementación, con la advertencia explícita de que los README de cada módulo pueden estar marcados como completos aunque el código no exista. El registro de cambios corrige en la dirección incómoda: un módulo marcado como incompleto estaba terminado, y el avance se recalculó hacia arriba sin adornarlo.
DescartadoConfiar en el README de cada módulo. El README lo escribe quien quiere cerrar el ticket.
Resultado
- 44,725 líneas de PHP en 360 archivos, 72 modelos y 140 migraciones.
- 75 archivos de test con 585 métodos, sobre Pest con plugin de arquitectura.
- 31 de 33 módulos cerrados, con un cambio de referencia entregado en 3 PRs, 54 archivos y 39 tests sin regresiones.
- Un único cambio de módulo completo sin abrir una sola migración nueva, por restricción propia.
- 15 capas de aplicación con una sola interfaz en todo el dominio, y esa interfaz es la del proveedor externo.