ADR-045: La anulación de un radicado la decide otra persona que la solicitó¶
Estado: Aceptado Fecha: 2026-10-22 Autores: Giampiero (mantenedor principal); lote D, bloque D-1 Relacionados: ADR-039 (misma separación de funciones para desclasificar), ADR-028 (el estado del radicado), ADR-010 (auditoría inmutable), ADR-003 (SQL crudo)
Contexto¶
Anular un radicado es retirarlo del trámite sin borrarlo: el número es inmutable (Ley 594/2000, Acuerdo AGN 060/2001) y el acto debe ser trazable. La solicitud existía, pero la decisión podía tomarla la misma persona que la pidió si tenía ambos permisos (PERM_SOL_ANULAR y PERM_PANU_CODI), y nada impedía que un UPDATE a mano dejara un radicado en anulado sin solicitud aprobada. Tampoco había salida para quien pedía por error: la solicitud quedaba solicitada hasta que otra persona la rechazara.
Decisión¶
- La separación de funciones vive en el
WHERE, no en una comprobación previa:AnulacionRepository.resolveraprueba/rechaza cona.estado = 'solicitada' AND a.solicitado_por <> $3y el clearance del radicado (EXISTS ... nivel_seguridad <= $5) en la misma sentencia. Un 0-filas se explica después condiagnosticar(404 neutro frente al clearance; 403separacion_de_funciones; 409anulacion_ya_decidida). - Dos fronteras en la base (migración tenant
052): CHECKsra_separacion_funciones(decidido_por <> solicitado_por),ra_decision_forma,ra_retirada_por_solicitante,ra_rechazo_motivado(observación ≥10); triggerradicado_anulacion_guard(los estados terminales son inmutables; borrar solo en cascada,pg_trigger_depth() >= 2) y triggerradicados_anulacion_guardsobreradicados.status: pasar aanuladoexige una filaaprobadacondecidido_at = now()(la MISMA transacción) y otrodecidido_por; salir deanuladoes irreversible.solicitado_porpasa aNOT NULL. - Estado nuevo
retiradayPOST /documents/{id}/anulacion/retirar(PERM_SOL_ANULAR): solo el solicitante, con observación opcional. Es el "me equivoqué" que antes no existía. - Una identidad es obligatoria: sin
X-User-Idel servicio responde 403forbidden; ningún actorNonedecide. - Asientos
document.anulacion_aprobada|rechazada|retiradaconsolicitado_porydecidido_por, atómicos con la escritura; el evento al outbox sale solo al aprobar. - Cola: cada fila lleva
propia(la solicitó quien mira) para que la UI no ofrezca decidir lo propio; el recuento usa la misma condición que las filas. - UI:
/anulacionesofrece "Retirar mi solicitud" en filas propias;/admin/gruposavisa si el único grupo que decide es el único que solicita.
Alternativas descartadas¶
- Solo validar en el servicio: un
UPDATEdirecto o un camino nuevo la esquivaría; el CHECK y el trigger son la última frontera. - Caducar las solicitudes: se descartó en este lote; la salida es retirar o rechazar (residuo declarado).
Residuos declarados¶
- Un radicado
anuladoimportado (INT-05) esquiva el trigger de frontera: llega ya anulado, sin solicitud. - Dos personas con ambos permisos pueden pedirse mutuamente: la separación es por persona, no por grupo.
Consecuencias¶
- Una anulación siempre tiene dos personas distintas y un asiento por acto; revertirla exige un acto nuevo, no un
UPDATE. - Los tests que anulaban a mano deben pasar por
AnulacionServiceo aprobar en la misma transacción (HINT anulacion_requiere_aprobacion).