SOP — Pipeline aduanal: módulo Proceso y Kanban
Tipo: procedimiento operativo estándar. Estado: demo-ready. Rutas:
/proceso(mapa de proceso) ·/kanban(tablero pipeline aduanal). El servidor RE-VALIDA el RBAC enavanzarEtapa; la UI es defensa de segundo orden, nunca la fuente de autorización final. La terminología de "contenedor" puede variar por vertical (verlib/tenant/labels.ts); este SOP usa "contenedor" como término genérico.
0. Propósito y alcance
Este SOP describe cómo un operador avanza la etapa de un contenedor en el pipeline aduanal
de Patio Abierto, ya sea desde la vista de mapa de proceso (/proceso) o desde el tablero
Kanban (/kanban).
Alcance:
- Transición de etapas mediante la máquina de estados (
lib/patio/state-machine.ts). - Tres vistas de lectura: Línea / Flujo / Tablero (
/proceso). - Avance por botón y por drag-and-drop de contenedor (
/kanban). - Asignación de personal a contenedor, modo drag (
/kanban, ILUSTRATIVO en demo). - Gate RBAC evento → permiso (
lib/auth/roles.ts,permisoDeEvento).
Fuera de alcance: registro inicial vía garita (/garita), bloqueo de salida,
administración de usuarios.
1. Roles y permisos que aplican
La siguiente tabla define quién DEBE poder ejecutar cada evento de avance, derivada de
PERMISO_DE_EVENTO en lib/auth/roles.ts. El gate es por evento, no por ruta UI; el
mismo permiso se aplica tanto al botón del Kanban como al drag-and-drop.
| Evento | Estado origen → destino | Permiso requerido | Roles habilitados |
|---|---|---|---|
BUQUE_ETA | (nuevo) → EN_BUQUE | ninguno | todos (evento de entrada) |
DESCARGA | EN_BUQUE → DESCARGADO | descarga:registrar | admin, supervisor, operador_patio |
INGRESO_PATIO | DESCARGADO → EN_PATIO | ingreso:registrar | admin, supervisor, operador_patio |
MODULACION | EN_PATIO → MODULADO | modulacion:operar | admin, supervisor, operador_patio |
GARITA_SALIDA | MODULADO → LIBERADO | garita:operar | admin, supervisor, operador_garita |
SALIDA | LIBERADO → SALIDO | salida:registrar | admin, supervisor, operador_garita |
cliente_readonlySOLO tienepatio:ver; NO DEBE ver botones de avance activos ni arrastrar.SALIDOes estado terminal: NO DEBE admitir ningún evento posterior.
2. Precondiciones
- El usuario DEBE estar autenticado (Supabase IdP,
lib/auth/server.ts). - El usuario DEBE tener el permiso correspondiente al evento que desea disparar (ver §1).
- El contenedor DEBE estar en el estado exacto que precede al evento (
ESTADO_REQUERIDOenlib/patio/state-machine.ts); de lo contrario,evaluarTransiciondevuelveok: false. - No DEBE haber un avance en vuelo (
avanzandoIso !== null) para el mismo contenedor; evita doble-optimista en cola. - Para el drag de contenedor: la variable de entorno
NEXT_PUBLIC_DNDDEBE ser"true". Si está ausente o es distinta, el tablero se renderiza sin DnD y solo el botón está disponible.
3. Procedimiento
3.1 Consultar el pipeline desde el mapa de proceso (/proceso)
- Navegar a
/proceso. El servidor calcula conteos en vivo desdegetInventario()ygetEventosPatio()(nunca snapshot de build; la página está marcadaforce-dynamic). - El
ProcessMapLayoutpresenta el selector segmentado de tres vistas en la barra superior:- Línea (
LineaView): representación secuencial horizontal de etapas. - Flujo (
FlujoView): grafo React Flow con nodos por etapa (vista por defecto). - Tablero (
TableroView): tabla con conteos, bloqueos y dwell máximo por etapa.
- Línea (
- Seleccionar la vista deseada con clic o con
ArrowLeft/ArrowRight; la selección se persiste enlocalStorage(claveproceso-vista) y sincroniza entre pestañas. - Revisar los indicadores de cada nodo/fila:
count(contenedores en etapa),bloqueos(consalida_bloqueada = true) ydwellMaxDias(dwell máximo en días desde eventos).- Si
dwellMaxDiasesnull, NO hay dwell calculable aún (sin eventos registrados).
- Si
- La vista en
/procesoes solo lectura; ningún avance de etapa se ejecuta desde aquí. Para avanzar, ir a/kanban(§3.2).
3.2 Avanzar la etapa de un contenedor desde el Kanban (/kanban)
3.2.1 Usando el botón de avance (siempre disponible)
- Navegar a
/kanban. El tablero agrupa los contenedores en 6 columnas segúnetapaDe(estatus)(orden dePIPELINE_ADUANAL). La columnaSALIDOmuestra las 40 salidas más recientes (MAX_SALIDO = 40). - Localizar el contenedor a avanzar. La tarjeta DEBE mostrar el botón de acción cuyo
label proviene de
LABEL_EVENTO[siguienteEvento(estatus)]:EN_BUQUE→ botón "Registrar descarga"DESCARGADO→ botón "Ingresar al patio"EN_PATIO→ botón "Registrar modulación"MODULADO→ botón "Registrar salida garita"LIBERADO→ botón "Confirmar salida"SALIDO→ sin botón (terminal).
- Verificar que el botón esté activo: si el rol no tiene el permiso del evento
(
permisoDeEvento(siguienteEvento(estatus))), el botón DEBE renderizarse deshabilitado. - Hacer clic en el botón. La UI aplica una actualización optimista (
moverOptimista) antes de la confirmación del servidor. - El servidor ejecuta
avanzarEtapa, que RE-VALIDA el RBAC y llama aaplicarTransicion.- Si la transición es válida: estado actualizado, tarjeta se mueve a la columna siguiente.
- Si la transición falla (
TransicionInvalidaError): el update optimista se revierte; la tarjeta regresa a su columna original. Ver §5.
3.2.2 Usando drag-and-drop de contenedor → columna (requiere NEXT_PUBLIC_DND=true)
- Solo contenedores para los que
puedeArrastrar(estatus, can, avanzandoIso)seatrueson arrastrables. La función verifica: (a) existesiguienteEvento, (b) el rol tiene el permiso, (c) no hay otro avance en vuelo. - Arrastrar el contenedor. Las columnas resaltan; la única zona de drop válida es
etapaDestinoDe(estatus)— la columna inmediatamente siguiente en el pipeline. - Soltar sobre la columna destino válida.
debeAvanzar(estatus, overId)confirma queoverId === etapaDestinoDe(estatus). Drop fuera, en la misma columna, o en una no-adyacente NO DEBE disparar ningún avance (sin update optimista, sin flash). - Si
debeAvanzardevuelvetrue, el flujo es idéntico a §3.2.1 paso 4–5 (optimista + validación en servidor).
3.3 Asignar personal a un contenedor (ILUSTRATIVO — solo si NEXT_PUBLIC_PERSONAL_MOCK=true)
[ILUSTRATIVO — DEMO] El panel de personal es datos mock (
lib/patio/personal-mock.ts). No tiene efecto de negocio ni persistencia real. No bloquea ni habilita ningún avance.
- Con el flag activo, la sidebar de personal PUEDE mostrarse en
/kanbany/proceso. - Arrastrar un chip de personal (
idcon prefijopers:) sobre una tarjeta de contenedor (zona de drop con prefijocont:). El mismoDndContextdistingue ambos dominios por prefijo para evitar solapamiento con el drag de etapa. asignacionDeDrop(activeId, overId)devuelve{ personalId, iso }si y solo si se arrastró un personal sobre un contenedor; en cualquier otro caso devuelvenull.rolSugeridoParaEtapa(rol, etapa)PUEDE resaltar visualmente si el rol del operador coincide con la cuadrilla sugerida para esa etapa; NUNCA bloquea la asignación.
4. Reglas e invariantes
Máquina de estados
- La cadena lineal de estados DEBE mantenerse:
EN_BUQUE → DESCARGADO → EN_PATIO → MODULADO → LIBERADO → SALIDO. siguienteEvento(estatus)DEBE devolvernullenSALIDO(terminal). NO DEBE existir ruta de UI que dispare eventos sobre un contenedor en estado terminal.evaluarTransicionNO DEBE lanzar excepción; devuelve{ ok: false, motivo }si la transición es inválida.aplicarTransicionlanzaTransicionInvalidaErroren ese caso.MAPA_EVENTO_ESTADOyESTADO_REQUERIDOson el contrato canónico (idéntico acustoms-api-v1.md). Modificarlos requiere alineación con la API del agente A.
RBAC
- El gate de permiso DEBE ser la función
permisoDeEvento(evento)delib/auth/roles.ts. NO DEBE existir lógica de autorización paralela en componentes UI. - La UI puede ocultar/deshabilitar controles como defensa de UX; el servidor DEBE ser la autoridad final. La validación UI NO reemplaza a la del servidor.
- Agregar un nuevo
TipoEventoPatioal enum DEBE obligatoriamente actualizarPERMISO_DE_EVENTO(el tipoRecord<TipoEventoPatio, Permiso | null>lo hace exhaustivo;tscrompe si falta alguno).
Kanban
- El drag de etapa y el drag de personal NO DEBE solaparse: los IDs con prefijo
pers:son de personal; los IDs sin prefijo son de contenedor (etapa); las zonas dropcont:son para asignación de personal. NEXT_PUBLIC_DNDcontrola el componente (KanbanBoardDndvsKanbanBoard); ambos comparten el mismo gate RBAC — NO DEBE existir doble-estándar entre botón y drag.- La columna
SALIDODEBE truncarse a las 40 entradas más recientes en UI. Si el total supera 40, se DEBE reportarsalidoTotalal usuario.
Vistas de proceso
- La vista por defecto DEBE ser
"flujo"(snapshot SSR). SilocalStoragecontiene una vista válida ("linea","flujo"o"tablero"), DEBE respetarse en cliente. - Las métricas de
dwellMaxDiasybloqueosDEBEN derivarse exclusivamente degetInventario()/getEventosPatio(). NO DEBE mostrarse ningún número inventado.
5. Manejo de errores y escalamiento
| Situación | Acción inmediata | Escalamiento |
|---|---|---|
| Botón deshabilitado inesperadamente | Verificar rol actual (selector de demo) y estado del contenedor (estatus). | Si el rol es correcto y el estado precede al evento, reportar a supervisor/admin. |
| Update optimista revertido (flash de tarjeta) | La tarjeta vuelve a su columna. Revisar logs del servidor para el motivo de TransicionInvalidaError. | Si es error de estado inconsistente (estado en DB no coincide con UI), escalar a admin. |
| Drag sin efecto (no avanza) | Verificar que NEXT_PUBLIC_DND=true y que se soltó EXACTAMENTE sobre la columna destino (adyacente). | Si el flag está activo y el drop falla repetidamente, reportar issue con overId capturado en consola. |
| Error de red / acción de servidor no responde | El update optimista DEBE revertirse. La tarjeta regresa a su posición. Reintentar manualmente. | Si el error persiste >2 min, escalar al equipo de infra (ver deploy.sop.md §4). |
Contenedor en estado no reconocido (estatus no en enum) | El agrupamiento en Kanban lo descarta silenciosamente (guard validos.has(u.estatus)). | Reportar como dato inconsistente; no intentar avanzar. |
Métricas nulas en /proceso | Normal si no hay eventos registrados. Mostrar — en UI; NO inventar valor. | Sin escalamiento; informar al usuario que faltan eventos en inventario. |
6. Demo vs producción
| Aspecto | En demo (actual) | En producción |
|---|---|---|
| Inventario | getInventario() sobre repo in-memory (lib/patio) — datos mock. | getInventario() sobre Prisma/Postgres (Supabase). Requiere DATABASE_URL. |
| Avance de etapa | Server action guarda en memoria de sesión; no persiste entre recargas. | Server action persiste en DB; transaccional ($transaction Prisma). |
| Personal / cuadrilla | [ILUSTRATIVO] personal-mock.ts; sin efecto de negocio. Flag: NEXT_PUBLIC_PERSONAL_MOCK. | A definir (ADR-0002: datos de personal como entidad real). Requiere GO CEO. |
| DnD | Desactivado por defecto (NEXT_PUBLIC_DND no seteado). | Activar en Vercel → Settings → Environment Variables. |
| Autenticación | Selector de rol en demo (sin Supabase Auth real). | Supabase IdP activo (D-017). El usuarioActual() devuelve el usuario real. |
| Bloqueo de salida | Campo salida_bloqueada en datos mock. | Gate real con bloqueo:gestionar + integración SAT/aduana (pendiente). |
Toda métrica numérica mostrada en demo (conteos, dwell, bloqueos) es ILUSTRATIVA y proviene de datos mock. No representa operación real. Ver gate D-011.
Referencias
lib/patio/state-machine.ts— máquina de estados,siguienteEvento,evaluarTransicion,LABEL_EVENTO.lib/auth/roles.ts—PERMISO_DE_EVENTO,permisoDeEvento,PERMISOS_POR_ROL.lib/customs/aduana.ts—EtapaAduanal,PIPELINE_ADUANAL,etapaDe.components/kanban/kanban-dnd-logic.ts—puedeArrastrar,debeAvanzar,etapaDestinoDe.components/kanban/kanban-personal-logic.ts—asignacionDeDrop,rolSugeridoParaEtapa.components/process-map/ProcessMapLayout.tsx— selector de vistas,localStoragekeyproceso-vista.app/proceso/page.tsx— Server Component del mapa de proceso.app/kanban/page.tsx— Server Component del tablero Kanban; flagsNEXT_PUBLIC_DND/NEXT_PUBLIC_PERSONAL_MOCK.docs/sops/deploy.sop.md— gates de deploy y escalamiento de infra.- Gate D-011 (nada inventado), D-015 (merge gate), D-016 (GO visual CEO), D-017 (Supabase Auth).