BE-06 Webhooks Mercado Pago idempotentes
BE-06 — Webhooks Mercado Pago idempotentes
Sección titulada «BE-06 — Webhooks Mercado Pago idempotentes»- Depende de: BE-05, DS-03, ADR-05, ADR-08.
- Módulo:
checkout(adaptador de entrada),shared(webhook_events). - Estado: DONE (2026-09-21). El cierre de la compra (orden PAID) es BE-07. El despacho del outbox es BE-12.
Objetivo
Sección titulada «Objetivo»Procesar notificaciones Webhooks de Mercado Pago de forma segura e idempotente.
Alcance
Sección titulada «Alcance»- Adaptador REST
mercadoPagoWebhook(POST /webhooks/mercadopago) sin JWT. - Validación de origen: header
x-signature(ts,v1) con HMAC-SHA256 y manifiestoid:[data.id];request-id:[x-request-id];ts:[ts];(documentación oficial de Webhooks / Checkout Pro). - Tras validar, consultar el pago en la API de Mercado Pago con
data.id; no se confía solo en el cuerpo. - Registro en
webhook_eventsporevent_idúnico (id de la notificación). ProviderMERCADO_PAGO. approved→ pago SUCCEEDED +checkout.PaymentSucceeded;rejected/cancelled→checkout.PaymentFailed, vía Outbox.- Liberar reserva y confirmar stock al emitir esos eventos (puerto de inventory ya existente, síncrono en la misma transacción).
- Yape no es un webhook distinto: llega como pago Checkout Pro con su
payment_method_id. - Secreto
MERCADO_PAGO_WEBHOOK_SECRET(no se versiona; dueño de no versionar YAML local: BE-11).
Criterios de aceptación (DoD de esta tarea)
Sección titulada «Criterios de aceptación (DoD de esta tarea)»- Evento repetido (mismo
event_id) no duplica pago ni eventos. - Firma inválida = 400 ProblemDetail.
- El cambio de estado del pago y el evento Outbox se escriben en la misma transacción.
Fuera del DoD
Sección titulada «Fuera del DoD»- Pasar la orden a PAID y exponer
listMyOrders: BE-07 (consumecheckout.PaymentSucceeded/checkout.PaymentFailed). - Relay que marca
published_aty entrega el outbox a handlers: BE-12 (BACKLOG). - E2E Playwright del camino feliz: FE-12.
Evidencia para DONE
Sección titulada «Evidencia para DONE»Integración Testcontainers: replay del mismo evento (firma válida e inválida), más 400 si falta el id de notificación.
- test: backend/src/test/kotlin/com/qhatu/ecommerce/checkout/MercadoPagoWebhookIntegrationTest.kt
- test: backend/src/test/kotlin/com/qhatu/ecommerce/checkout/ProcessMercadoPagoWebhookUseCaseTest.kt
- test: backend/src/test/kotlin/com/qhatu/ecommerce/checkout/mercadopago/MercadoPagoSignatureValidatorTest.kt
- file: backend/src/main/kotlin/com/qhatu/ecommerce/checkout/application/ProcessMercadoPagoWebhookUseCase.kt
- file: backend/src/main/kotlin/com/qhatu/ecommerce/checkout/infrastructure/mercadopago/MercadoPagoWebhookController.kt
- Contrato:
POST /webhooks/mercadopagoencontract/openapi.yaml(200, 400, 503).
Runtime local (2026-09-21): bootRun --spring.profiles.active=local en :8081, Postgres :5433, ngrok http 8081. HMAC inválido → 400; sin id → 400 incomplete-webhook-notification; pago 404 en MP → 503 sin persistir event_id (MP reintenta). Replay con id presente no duplica outbox.
Contrato que consume BE-07
Sección titulada «Contrato que consume BE-07»Outbox aggregateType=checkout (filas con published_at NULL hasta que exista BE-12):
eventType |
Payload JSON | Efecto ya hecho en checkout/inventory |
|---|---|---|
checkout.PaymentSucceeded |
eventId, paymentId, orderId, amountCents, occurredAt |
pago SUCCEEDED; reserva CONFIRMED |
checkout.PaymentFailed |
eventId, paymentId, orderId, reason, occurredAt |
pago FAILED; reserva liberada |
La correlación del webhook usa external_reference (UUID de orden), data.id (payment id) o preference_id. Tras un match, payments.provider_reference pasa al payment id si no hay colisión.
Notas de auditoría (2026-09-21)
Sección titulada «Notas de auditoría (2026-09-21)»Docs oficiales: Webhooks HMAC (data.id query; alfanumérico en minúsculas) y ACK HTTP 200/201 (si no, MP reintenta). SDK PHP 2026-06 firma con la capitalización original: el validador acepta ambos manifiestos.
- CA-2: HMAC inválido → 400
invalid-webhook-signature. - CA-1: replay con
idpresente no duplicawebhook_eventsni outbox. - Notificación sin
id→ 400incomplete-webhook-notification(ya no se persisteevent_id=0). GET /v1/payments/{id}404 → 503webhook-resource-unavailablesin persistir el evento.- Tests Testcontainers/unitarios: OK sin token MP real en el entorno de Gradle.
Siguiente (mañana)
Sección titulada «Siguiente (mañana)»Empezar BE-07. Pedir get_context(mode=task, taskId=BE-07). No reabrir firma HMAC ni webhook_events. El camino vivo webhook → orden PAID también necesita BE-12 (hoy no hay relay).