ADR-029: Ubicación actual del radicado¶
Estado: Aceptado
Fecha: 2026-10-02
Autores: Giampiero (mantenedor principal); diseño de orfeo-architect
Relacionados: ADR-028 (el flujo mueve el estado del radicado), ADR-010 (auditoría inmutable), E05 (flujos), E01 (radicación), RF-SEG-08 (no-read-up)
Contexto¶
Al recorrer la aplicación con el navegador, el radicado 2026-DEMO-E-000117 aparecía en la
lista con la dependencia Oficina de Tecnología tras haber sido repartido a Talento Humano
y devuelto a Control Interno. La petición del usuario fue: «debería mostrar siempre en dónde
estaba ubicado el radicado».
La causa es que radicados.dest_dept/dest_dept_code guardan la dependencia de
radicación: a quién iba dirigido el documento, una instantánea legal que se fija al radicar. Solo
se corrige por PATCH mientras el radicado no esté cerrado (en closed → 409 dest_dept_immutable) y, al corregir
el nombre, el código se re-resuelve en el mismo UPDATE (NULL si el nombre no identifica UNA dependencia activa). Dónde está el radicado ahora solo se podía reconstruir recorriendo flow_steps,
que es de otro servicio. En Orfeo esto es radi_depe_actu / radi_usua_actu: dos columnas del
propio radicado que cada reparto reescribe.
Decisión¶
- El workflow-service escribe la ubicación, en el MISMO
UPDATEquestatus. Tres columnas nuevas enradicados:flujo_dept_code,flujo_dept(el nombre del paso, instantánea) yflujo_usuario(auth_users.iddel poseedor, nunca elsubde Keycloak). Las escribesincronizar_estado_radicado(ADR-028) —y por tanto también la reasignación en cascada—, dentro de la transacción del acto y antes del asiento: si la auditoría falla, no queda ni paso ni estado ni ubicación. document-service nunca las escribe: un solo escritor. - La ubicación que se lee son dos columnas GENERADAS (
STORED)ubicacion_dept_code/ubicacion_dept: la del flujo si el flujo ya movió el radicado; si no, la dependencia de radicación (destino y, a falta de él, origen). Al ser generadas no hay un segundo escritor que se desincronice, y un radicado sin pasos se lee igual que antes. Con índice (ix_radicados_ubicacion_dept_code). Se calculan por PARES, no con unCOALESCEcolumna a columna: conCOALESCE(flujo_dept_code, dest_dept_code, …)un nombre de flujo sin código único mostraría el código de otra dependencia (el de radicación). Es un refinamiento del diseño original, que proponía elCOALESCEsimple. - Una sola función pura (
workflow-service/app/services/estado_radicado.py), junto aderivar_estado_radicado:
- Con paso activo (
pending/in_progress): el de mayorstep_number. - Sin paso activo: el paso que se cerró más recientemente (
completed_atDESC, desempatestep_numberDESC), en cualquier estado. No por número de paso: tras un rollback el paso cancelado tiene el número más alto pero se cerró antes que el reactivado y completado después, y ordenar porstep_numberpondría el radicado en la dependencia que lo canceló. - Sin pasos:
None; la lectura cae a la dependencia de radicación. - Terminales congelan: un
closed/anuladocon ubicación ya escrita la conserva. Un cerrado queda en la dependencia donde se finalizó; un anulado, donde estaba al anularse (la anulación no reescribe nada). Un terminal anterior a este ADR (sin ubicación escrita) se deriva del historial, que es lo que hace el backfill. - El código de la dependencia solo se resuelve si el nombre es único.
flow_steps.to_deptguarda el nombre sin FK ydependencias.nombreno es único. El código se resuelve solo si el nombre casa con exactamente UNA dependencia activa; con cero o con varias,flujo_dept_codequedaNULLyflujo_deptconserva el nombre como instantánea. Elegir una sería inventar dónde está el radicado. Guardarto_dept_codeen el paso, que cierra el problema de raíz, queda como incremento aparte. - Migraciones.
document-service037_radicado_ubicacion.sqlcrea las columnas y el índice (dueño deradicados; idempotente).workflow-service016_radicado_ubicacion_desde_flujo.sqlrellena lo existente con el gemelo literal de la función Python (la prueba de integración compara cada fila con ella), deja la evidencia enradicado_ubicacion_recalculo—tabla de solo inserción, con trigger que rechazaUPDATE/DELETE— y un asiento-anclaworkflow.ubicacion_radicado.backfillque escribe el runner (patrón de 013; nunca SQL contraaudit_log). No tocaupdated_at. Guardas de orden: sinradicadosen el schema no hay nada que recalcular y se omite; conradicadossin las columnas (document-service aún no migró) lanzaRAISE, porque la migración se marca aplicada al terminar y unskipsilencioso dejaría la entidad sin backfill para siempre. Orden de despliegue: runner de document-service y después el de workflow-service. - Lecturas (document-service):
GET /api/v1/documentsyGET /api/v1/searchadmiten?ubicacion_dept_code=. El filtro va en la misma condición que filas yCOUNT(junto al clearance): el total nunca cuenta lo que la página no puede mostrar.- Las respuestas llevan
flujo_dept_code,flujo_dept,ubicacion_dept_code,ubicacion_dept;dest_deptsigue siendo la de radicación.flujo_usuariosolo sale en el detalle autenticado (GET /documents/{id}yby-tracking), nunca en listados ni búsquedas. - Reportes:
GET /reports/radicadosañadeby_ubicacion—lo que sigue en trámite, sin cerrados ni anulados— yby_dependenciapasa a documentarse como la de radicación; en el CSV la seccióndependenciase renombradependencia_radicaciony apareceubicacion_actual. - Export INT-01:
metadata_sistema.ubicacion_dept/ubicacion_dept_codecomo dato informativo; el import los ignora (la ubicación del importado la deriva el flujo de la entidad que recibe) y el poseedor no sale nunca en un paquete. - MCP:
listar_radicadosybuscar_radicadosganan el parámetroubicacion_dept_code, ydest_dept/dest_dept_codese describen como «de radicación» para que el modelo no filtre «lo que tiene Control Interno» por una columna que dice a quién iba dirigido. - Fuera, a propósito. Todo lo público (
schemas/public.py, verificación por código, OAI-PMH, CMIS, EAD): dónde está un radicado y quién lo tiene es trámite interno. Los eventos (workflow.radicado.estado_cambiadono cambia) y knowledge.workflow_rulessigue condest_dept(radicación): una regla de reparto automático decide a quién se reparte un radicado según a quién iba dirigido.
Consecuencias¶
- La lista, la búsqueda y el detalle dicen dónde está el radicado sin recorrer
flow_steps, y se puede filtrar «lo que está ahora en Control Interno» con índice. - Un radicado sin movimientos se lee exactamente como antes.
UPDATEdestatusy de la ubicación son uno: no hay ventana en la que el estado digadistributedy la ubicación no se haya movido.- Coste: tres columnas más escritas por el workflow-service en cada acto y dos generadas
STORED(reescritura de tabla al aplicar 037; lectura barata). - Verificado por mutación contra Postgres real (
RUN_INTEGRATION=1): quitar la escritura deflujo_*ensincronizar_estado_radicado, ordenar el «último» porstep_number, sacar el filtro delCOUNTy añadir la ubicación aPublicVerifyResponse— cada una tumba al menos una prueba.
Declarado fuera¶
- ~~El frontend~~ Hecho (2026-10-02):
/bandeja,/busqueda(columna y filtro),/reportes(panel «Por ubicación actual» y renombre del de radicación), panel principal y selector de radicados del expediente.by_dependenciaconserva su nombre en el JSON; la pantalla lo rotula «de radicación». flow_steps.to_dept_code(código en el paso): cerraría el caso de nombre no único de raíz; requiere migrar el paso y sus escritores.- La ubicación no viaja en el asiento de auditoría: es una proyección derivada del paso, que ya deja su propio asiento.