c245f3b67a1dc59c75bbd6deb7efdf2b4a599d7c
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ff59618a2a |
fix: filtrar producto sin stock del banner y banco placeholder de PSE
Encontrado probando en vivo con navegador real (Docker + Playwright contra shop.cruzcloud.net): - app/page.tsx: un producto featured pero sin stock (available: false) salía primero en el hero banner con botón "Comprar ahora" hacia una página donde en realidad no se puede comprar. - pse-banks/route.ts: el sandbox real de ePayco (bank.getBanks()) incluye un primer elemento placeholder (bankCode "0", "A continuación seleccione su banco...") mezclado con los bancos reales, mostrado como opción seleccionable. Se filtra explícitamente. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR |
||
|
|
12c4afa09c |
fix(payments): reemplazar PayU LatAm por ePayco como pasarela PSE
PayU LatAm (integrado en el commit anterior) resultó inviable: Rapyd compró ese negocio completo en LatAm/África en 2023, y el registro real del usuario quedó en la plataforma de Rapyd (board.rapyd.net, credenciales access_key/secret_key incompatibles con el módulo ya construido, API completamente distinta). Se evaluó reconstruir contra Rapyd, pero el usuario comparó costos para su volumen real (~8 ventas/mes) y encontró que ePayco cobra menos en PSE. Se investigó ePayco en vivo: es 100% de Davivienda desde 2024, acepta liquidación a cualquier banco colombiano (no exige Bancolombia/Nequi como Wompi) y da tarifa preferencial a cuentas Davivienda -- el usuario ya tiene una. Mejor encaje de negocio y más barato que PayU/Rapyd. Confirmado como elección definitiva. - src/modules/epayco-pse/ reemplaza a src/modules/payu-pse/: mismo contrato AbstractPaymentProvider (initiatePayment/authorizePayment "pending_authorization"/getWebhookActionAndData con firma SHA256 verificada), pero envuelve el SDK oficial `epayco-sdk-node` en vez de llamar la API cruda -- leyendo el código fuente del SDK (no la documentación pública, que resultó desactualizada en más de un campo) se confirmó que el protocolo real de ePayco para PSE es bastante más particular de lo que sugieren los ejemplos: cada campo del payload se cifra con AES usando la private key del comercio, más un login previo que da un bearer token que también viaja en la misma request. Reimplementar esto a mano tenía mucho más riesgo de error que delegar en el SDK mantenido por el propio proveedor. - Corregido en el camino: el método real para listar bancos es `bank.getBanks()`, no `pseBank()` (así aparecía en un resumen de búsqueda, pero no existe en el SDK real -- confirmado leyendo node_modules/epayco-sdk-node/lib/resources/bank.js directamente). El campo real para la URL de confirmación es `method_confirmation` (con guión bajo en medio), no `metodoconfirmacion` como mostraba la documentación pública -- confirmado contra tests/bank.js del propio paquete. - retrievePayment sí se implementó esta vez (endpoint de validación de ePayco documentado); refundPayment sigue sin implementar -- ese endpoint no quedó verificado en vivo, tira error explícito en vez de fingir éxito. - Frontend: el step de pago recupera el selector de banco + tipo de persona + documento (a diferencia del diseño que se había planeado para Rapyd, donde esos datos se recolectaban en la página del propio Rapyd/SafetyPay) -- ePayco sí necesita esos campos de nuestro lado, como PayU. - Dos puntos quedan sin verificar contra un sandbox real (anotados en el código con qué hacer si fallan): el nombre exacto del campo con la URL del banco en la respuesta de bank.create() (initiatePayment tira un error con el payload completo si no matchea ningún candidato probado), y si p_cust_id_cliente/p_key del webhook son iguales a las API keys o valores aparte del dashboard de ePayco. Verificado: build real de Medusa con las 7 env vars EPAYCO_* dummy, smoke test del wrapper del SDK cargando y la verificación de firma dando el resultado esperado contra un caso conocido, typecheck+lint+ build completo del frontend. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR |
||
|
|
2ceea23f40 |
feat(payments): integrar PayU LatAm (PSE) como pasarela de pagos
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 |
||
|
|
2056e6c766 |
Fase 2 fix: /store/search resuelve currency_code, no solo region_id
Confirmado en vivo: pasar solo region_id a QueryContext no alcanza -- "Method calculatePrices requires currency_code in the pricing context" persistia. A diferencia de /store/products nativo (resuelto por el middleware setPricingContext), esta ruta propia no pasa por esa cadena, asi que resuelve la moneda real de la region con un query.graph adicional antes de pedir calculated_price -- no asume COP fijo por si el catalogo suma otra region/moneda despues. |
||
|
|
f8f540af6a |
Fase 2 fix: /store/search acepta region_id para calculated_price
Mismo error encontrado en el gate de verificacion, ahora en la ruta propia en vez del job de sync: "Method calculatePrices requires currency_code in the pricing context" -- query.graph necesita region_id/currency_code en el contexto para resolver variants.calculated_price. El storefront ya manda region_id contra /store/products (medusa.ts, regionId()); esta ruta ahora acepta el mismo query param y arma el contexto de pricing con QueryContext(), igual que la ruta oficial /store/meilisearch/products del plugin. |
||
|
|
6be8dd409c |
Fase 2 ADR 0006 Etapa 2: plugin de Meilisearch + ruta de facetas
Instala @rokmohar/medusa-plugin-meilisearch@^1.4.1 (linea v1, la unica compatible con nuestro Medusa 2.18.0 exacto -- confirmado contra peerDependencies reales de npm: @medusajs/* ^2.15.2, satisfecho por 2.18.0). Conectado via un array `plugins:` nuevo en medusa-config.ts (primer uso en este repo, no `modules:`), env vars leidos con `?? ""` en vez de required() -- si Meilisearch esta caido o mal configurado, el plugin se degrada solo en vez de tumbar el arranque de Medusa entero. No se escribe un subscriber propio: se verifico contra el codigo fuente real del paquete (descargado el tarball exacto de npm, inspeccionado en /app/node_modules dentro de una imagen Docker de verdad) que trae sus propios subscribers de product/category/ collection/variant/etc, descubiertos por Medusa en runtime desde dentro del propio paquete -- confirmado que sobreviven a `npm prune --omit=dev` y llegan intactos a la imagen final. Tambien trae un job de indexado inicial de una sola corrida al boot (numberOfExecutions: 1, no un cron recurrente -- no viola la restriccion del ADR de "nunca cron"). Nueva ruta Store API propia (src/api/store/search/route.ts): las rutas /store/meilisearch/* que trae el plugin no devuelven facetDistribution (verificado leyendo su codigo fuente), asi que esta ruta pide facets explicitamente via additionalOptions.facets, y despues re-hidrata los productos via query.graph (mismos campos que ya usa el storefront) en vez de confiar en precios/inventario indexados -- mismo patron "safe path" que ya usa la ruta oficial del plugin para /store/meilisearch/products. Transformer custom en medusa-config.ts que espeja mapProduct() del storefront (category/collection/brand/ageRange desde metadata, disponibilidad desde la primera variante) para que el documento indexado sea exactamente lo que el storefront ya muestra. Validado en vivo: build completo de Docker (target builder) corre limpio -- npm ci, medusa build (typecheck + compile backend/frontend), npm prune -- cero errores. package-lock.json regenerado con un contenedor node:22.18.0-bookworm-slim real (mismo base image que el Dockerfile), no a mano. Dockerfile: dummy MEILISEARCH_HOST/MEILISEARCH_API_KEY en el stage de build (insurance barata, no son required() asi que no eran estrictamente necesarios para que el build pasara, y de hecho paso sin ellos en la prueba -- se dejan de todas formas por consistencia con el resto de dummies de esa capa). |