ADR-029: los datos sensibles se enmascaran al pasar a la DLQ
Aceptado el 15/11/2026 por Andrés (opción A). Lo implementa el carril `pagos-y-adaptadores` en payment-service; order-service aplica el mismo marcador al outbox (`entrada-valida`, R8).
Contexto
docs/to-be/06-eventos.md fija que el mensaje de reintento y de DLQ (dead letter queue: cola donde quedan los mensajes que agotaron sus reintentos) es el original, sin cambios en su payload ni en sus cabeceras CloudEvents. Esa regla sirve: el mensaje apartado muestra el contrato y los datos que viajaron, y se puede reprocesar tal cual.
La revisión independiente (docs/revision-final.md, hallazgo R19) encontró que payment-service copia completo un PaymentRequested a su DLQ, con payment.token (el token de la pasarela que envía el canal). La DLQ guarda 30 días. La regla 5 de CLAUDE.md prohíbe datos de pago en eventos. Quitar el token chocaba con 06 y con el contrato, que declaraba en la DLQ el mismo PaymentRequested, cuyo esquema exige token con mode: CAPTURE.
Hechos que pesan:
- El tópico de reintento necesita el token: es lo que se vuelve a cobrar, con la misma llave de idempotencia. Guarda 7 días.
- Un
PaymentRequestedque llega a la DLQ ya no sirve para cobrar: su orden vence a los 15 minutos, y payment-service rechaza un comando vencido (PAYMENT_EXPIRED) o, si ya hubo intento, lo resuelve buscando el cobro por la orden (R4). Reprocesarlo nunca cobra.
Opciones
| Opción | A favor | En contra |
|---|---|---|
A. Enmascarar los datos sensibles al pasar a la DLQ: valor fijo --data_redacted-- y cabecera andes_payload_redacted con los campos enmascarados; el reintento conserva el original |
Cumple la regla 5 en la cola de 30 días; el resto del mensaje sigue siendo el original y se lee contra el contrato; la cabecera dice qué falta | Excepción a la regla de 06; el mensaje enmascarado no sirve para repetir la operación |
| B. No cambiar nada y anotar el sacrificio | Sin trabajo; la DLQ se puede reprocesar tal cual | 30 días de tokens en un tópico; contradice la regla 5 |
Decisión
Opción A, con una regla general para todos los servicios:
- El mensaje de reintento y de DLQ es el original, sin cambios, para poder entender el contrato y los datos que viajaron. La única excepción son los datos sensibles: siempre se enmascaran al pasar a la DLQ y nunca aparecen en logs.
- Enmascarar es reemplazar el valor por el marcador fijo
--data_redacted--, uno solo para toda la capa, sin nombre de proveedor ni de campo (en payment-service, la constanteSensitiveFields.MASK), y listar los campos en la cabeceraandes_payload_redacted, separados por coma, con la ruta del campo en el mensaje (por ejemplopayment.tokenpara eltokendelPaymentCommand). Si el payload no es JSON, no se pueden ubicar los campos: se enmascara entero y la cabecera vale*. - Cada servicio tiene una sola lista de campos sensibles por tipo de mensaje, como fuente única. En payment-service es
consumer/SensitiveFields.java. - El tópico de reintento conserva el original, porque el reintento lo necesita para repetir la operación.
- El contrato declara el mensaje enmascarado en el canal DLQ (
PaymentRequestedMasked, contokenigual aMaskedValue).
Qué se sacrifica
- Un mensaje de la DLQ con un campo enmascarado no sirve para repetir la operación. En el pago no importa: la orden vence a los 15 minutos y el comando ya no cobraría.
- El tópico de reintento guarda el token hasta 7 días.
- Una excepción a la regla de 06, escrita en 06 y en
08-seguridad.md.