ADR 0009 ya había decidido PayU LatAm como proveedor (única opción que
no exige cuenta Bancolombia/Nequi) y confirmado que no existe ningún
plugin real de Medusa para su API -- los paquetes que aparecen
buscando "PayU" son para PayU Polonia/CEE, API completamente distinta
(clientId/posId/secondKey vs. apiLogin/apiKey/merchantId/accountId).
Este commit implementa esa decisión: módulo de pago custom
(AbstractPaymentProvider) para PSE (transferencia bancaria), primer
alcance del método de pago -- Nequi/Daviplata quedan fuera por ahora.
Backend (commerce-backend):
- src/modules/payu-pse/: initiatePayment arma y postea SUBMIT_TRANSACTION
contra la API real de PayU LatAm; authorizePayment devuelve
"pending_authorization" (PSE es async, la confirmación real llega por
webhook, no en el momento de completar el carrito); getWebhookActionAndData
valida la firma MD5 del webhook antes de confiar en cualquier
confirmación de pago -- falla cerrado si no matchea. refundPayment/
retrievePayment tiran error explícito en vez de simular éxito: esos
endpoints de PayU no quedaron verificados en vivo y hay dinero real de
por medio.
- No hace falta un subscriber propio para completar la orden -- el
subscriber nativo de Medusa (payment-webhook + process-payment-workflow)
ya hace ese trabajo con los métodos de arriba bien implementados
(confirmado leyendo el código fuente instalado de @medusajs/medusa
2.18.0, no la documentación).
- Correlación webhook↔sesión: Medusa inyecta su propio session_id en
`data` antes de initiatePayment; se usa ese mismo valor como
referenceCode de PayU, así el reference_sale que vuelve en el webhook
ya es directamente el session_id de Medusa.
- src/lib/payu.ts: cliente HTTP + firmas (orden y webhook, fórmulas
distintas, confirmadas contra el SDK PHP oficial de PayU).
- src/api/store/payments/pse-banks/route.ts: lista de bancos PSE,
cacheada en memoria.
- medusa-config.ts, Dockerfile, configmap/secrets.template: 7 env vars
nuevas (PAYU_API_LOGIN/API_KEY/MERCHANT_ID/ACCOUNT_ID/ENV/RESPONSE_URL/
NOTIFY_URL), dummies de build agregados, secrets con placeholder
"CAMBIAR".
Frontend (ecommerce):
- Nuevo step "payment" en components/checkout/checkout-client.tsx
(reemplaza al step "review" que solo ofrecía WhatsApp): selector de
banco, tipo de persona (N/J), tipo/número de documento, botón "Pagar
con PSE" que crea la sesión de pago, completa el carrito y redirige
al banco (navegación completa, no iframe -- así lo exige PayU).
WhatsApp queda como respaldo manual si falla la iniciación del pago.
- lib/headless/providers/medusa-payment.ts + 3 rutas proxy nuevas bajo
app/api/cart/, mismo patrón que el resto del carrito
(medusaCartFetch, ahora exportado desde medusa-cart.ts).
- app/checkout/pse/return/: página de vuelta del banco -- nunca afirma
éxito, la confirmación real es el webhook async.
Arranca contra las credenciales de sandbox genéricas y públicas de
PayU (developers.payulatam.com) mientras el usuario termina su propio
registro de comercio -- swap de las 4 credenciales via secret cuando
estén listas, sin cambios de código.
Verificado: build real de Medusa con las env vars dummy (no solo tsc),
tipos de AbstractPaymentProvider/WebhookActionResult/PaymentActions
confirmados contra @medusajs/utils y @medusajs/types instalados
(node_modules limpiado después), typecheck + lint + build completo del
frontend, páginas nuevas renderizando sin error. Pendiente: prueba
end-to-end contra el sandbox real de PayU (necesita el backend
desplegado) y habilitar el provider en la región Colombia desde el
Admin.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
Motor de busqueda self-hosted, sin que Medusa lo conozca todavia
(eso es Fase 2). Mismo patron que redis/postgres/minio en este mismo
namespace: StatefulSet + PVC via volumeClaimTemplates en local-path,
nunca Deployment ni emptyDir -- el indice de Meilisearch necesita
sobrevivir a un reinicio del pod para no reindexar todo desde cero.
Imagen getmeili/meilisearch:v1.53.1 (ultimo release estable no-RC,
verificado contra la API de GitHub, no memoria de entrenamiento).
Recursos exactos del ADR (request 100m/256Mi, limit 500m/768Mi).
Sin securityContext explicito -- verificado en vivo que la imagen no
trae un usuario no-root (Alpine minimo, /etc/passwd sin usuario
dedicado), misma convencion que redis.yaml/minio.yaml en este repo,
que tampoco fuerzan runAsNonRoot.
MEILISEARCH_HOST (no secreto) va al ConfigMap commerce-config.
MEILI_MASTER_KEY/MEILISEARCH_API_KEY (mismo valor, dos nombres de env
var -- uno para el servidor, otro para el futuro plugin de Medusa en
Fase 2) documentados en secrets.template.yaml; el valor real ya se
aplico al Secret commerce-secrets en el cluster antes de este merge
(patch puntual, no via create-commerce-secrets.sh completo -- ese
script regenera TODAS las credenciales en cada corrida y hubiera
rotado postgres/redis/minio ya en uso).
Antes el precio actualizado en Medusa tardaba hasta ~60s (o más, con
varias réplicas del frontend sin cache handler compartido) en verse en
el front, por el revalidate:60 del fetch a /store/products.
Ahora:
- Nueva ruta app/api/revalidate en el storefront que llama a
revalidateTag("medusa-products") al recibir un POST autenticado con
el header x-revalidate-secret.
- Nuevo subscriber en el backend Medusa (product.updated,
product-variant.updated/created/deleted) que llama a esa ruta usando
STOREFRONT_URL (ya existe en commerce-config) y un REVALIDATE_SECRET
compartido entre ambos servicios.
- secrets.template.yaml documenta la nueva clave REVALIDATE_SECRET en
commerce-secrets y commerce-storefront (mismo valor en ambos).
Requiere reaplicar commerce-secrets y commerce-storefront con el script
actualizado en el repo scripts (fix/revalidate-secret-scripts) para que
el REVALIDATE_SECRET exista en el cluster.