- "Detalles del producto" (marca, edad, categoría, colección) pasa de
acordeón colapsado a una mini tabla siempre visible -- mismos datos
reales de siempre, solo que ya no hace falta un click para verlos.
- Buy-box: nueva franja de confianza real con enlaces a /envios y
/cambios (5 días hábiles, envío de vuelta gratis) -- mismas
políticas ya publicadas en el footer, no texto inventado.
- "También te puede interesar" ahora muestra hasta 2 sugerencias en
vez de 1, mismo cálculo real por categoría/colección que ya
alimentaba el carrusel de abajo.
Investigado de paso (sin fix): la discrepancia de stock reportada
("Quedan 1" vs available:false) no es un bug de ProductPurchase -- la
página de producto usa /store/products de Medusa (en vivo, correcto),
mientras que /store/search (Meilisearch) devuelve inventario
desactualizado para el mismo producto. Eso vive en commerce-backend,
fuera de alcance de este cambio de frontend.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
- max-w de la página de producto pasa de 1280px a 1600px -- aprovecha
pantallas grandes en vez de dejar margenes vacíos enormes.
- Nueva 3ra columna desde xl: buy-box propio (tarjeta con borde,
sticky) para precio/variantes/carrito, separado del texto
descriptivo. Por debajo de xl cae al flujo normal de la 2da columna
sin duplicar el componente (mismo <ProductPurchase>, un solo
id="product-buy-button" en el DOM).
- Dentro del buy-box: un producto sugerido real de la misma
categoría/colección (mismo cálculo que ya alimenta el carrusel de
abajo, no un dato inventado).
- MobileAddToCart: la barra fija ahora también requiere haber
scrolleado más de 80px además de que el botón real no esté en
pantalla -- antes aparecía inmediatamente sobre la vista inicial del
producto (imagen + variantes) apenas cargaba la página.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
- El panel de zoom lateral ahora calcula tamaño/posición contra el
viewport real (window.innerWidth/innerHeight) en cada mousemove, en
vez de un breakpoint fijo (lg:block) con ancho fijo -- a resoluciones
altas con poco espacio a la derecha de la imagen terminaba
saliéndose de la pantalla. Si no entra un panel de al menos
PANEL_MIN_SIZE, se cae automáticamente al overlay in-place (antes
atado a "por debajo de lg", ahora a "no hay espacio real").
- La barra fija de "Añadir al carrito" en mobile quedaba fija desde el
primer render, tapando permanentemente parte de la imagen del
producto. Ahora el botón real (ProductPurchase) también se muestra
en mobile, y la barra sticky solo aparece cuando ese botón sale del
viewport (IntersectionObserver) -- mismo patrón que el "Buy Now"
sticky de Amazon.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
Sin cambio de comportamiento -- solo hooks estables para la suite de
Playwright en .browser-scratch/checks.mjs (el selector genérico
div[aria-hidden] dejó de alcanzar para distinguir la lupa, el overlay
in-place y el panel lateral, que ahora coexisten en el DOM).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
El zoom in-place anterior (overlay de fondo dentro de la misma caja)
seguía sintiéndose confuso: el usuario esperaba el patrón clásico de
e-commerce (Amazon) -- un recuadro semitransparente ("lupa") que marca
qué región de la foto se está mirando, y un panel aparte, más grande,
que muestra esa región ampliada. Sin scroll ni superposición dentro de
la misma caja.
- Lupa: recuadro que sigue al cursor sobre la imagen principal,
clampeado para no salirse de los bordes (visible en lg+).
- bgX/bgY ahora se calculan por fracción de recorrido de la lupa
(posición del borde de la lupa entre 0 y el ancho disponible), no por
posición cruda del cursor -- mapeo correcto incluso cuando la lupa
queda pegada a un borde.
- Panel: a la derecha de la imagen (position absolute, se superpone al
contenido de al lado igual que en Amazon), mismo mecanismo de
background-image con la foto completa que ya evitaba el problema de
object-cover recortando partes de la imagen.
- Por debajo de lg (sin espacio para un panel aparte) se conserva el
overlay in-place como fallback.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
El hover-zoom escalaba (transform:scale) la misma <Image> ya recortada
por object-cover -- para fotos verticales (ej. figura de acción de pie)
eso dejaba partes del producto (la cabeza) fuera del recorte desde el
propio render, así que ningún nivel de zoom las mostraba. Se reemplaza
por un overlay con background-image usando la foto completa
(background-size en un solo valor preserva el aspect ratio real) y
background-position siguiendo el cursor, que sí permite recorrer toda
la imagen.
De paso: las miniaturas ahora cambian la imagen principal también con
hover (onMouseEnter), no solo con click.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
- ENVIOCLICK_ORIGIN_DANE_CODE en configmap.yaml estaba cargado con el
código DANE de Bogotá (11001000) por error; corregido a Santiago de
Cali, Valle del Cauca (76001000). Toda cotización real de envío hecha
antes de esta fecha se calculó desde el origen equivocado.
- Corregido también el fallback stale en medusa-config.ts
(originDaneCode) que seguía apuntando a Bogotá.
- Actualizado el texto de /envios y /contacto (misma dirección física,
Cra 100 #42-65, solo cambia la ciudad).
- Footer: Instagram y Facebook reales enlazados (verificados en vivo
contra los perfiles antes de linkear), y "Preguntas frecuentes" /
"Cambios" ya apuntan a contenido real en vez de /catalog.
- Nuevas páginas /cambios (5 días hábiles, envío de devolución a cargo
de ARI Shopping) y /preguntas-frecuentes.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
El footer "Ayuda" no tenía contenido real -- los 4 links apuntaban a
/catalog. Se agregan dos páginas nuevas con información real:
- /envios: transportadoras reales de EnvíoClick (Coordinadora, Envía,
TCC, Rappi, Pibox -- confirmadas en una cotización real, ver ADR
0010 §2) y cómo se calcula el costo.
- /contacto: WhatsApp real (+57 301 613 8155, mismo número ya usado en
el fallback de checkout) y correo [email protected] (reenviado
por Cloudflare Email Routing -- configuración manual de DNS pendiente
del lado del usuario, fuera de alcance de este cambio).
"Preguntas frecuentes" y "Cambios" quedan sin contenido real definido
todavía, apuntando a /catalog como antes.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
Checkout con carrito vacío, fallback WhatsApp, filtros de catálogo,
búsqueda sin resultados (bug encontrado y corregido en otro commit),
slug de producto inválido (hallazgo pendiente de investigar: responde
200 en vez de 404), zoom interactivo, wishlist, otros viewports,
página 404 genérica, accesibilidad básica del buscador, y registro/
login con email -- todo verificado con Chromium real vía el harness de
.browser-scratch/.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
Encontrado probando en vivo con navegador real (Docker + Playwright):
una búsqueda de 2+ caracteres sin coincidencias no mostraba nada -- ni
"Buscando…" ni ningún mensaje. El usuario quedaba sin feedback de que
su búsqueda de verdad corrió. Se agrega un estado vacío con el botón
de buscar en todo el catálogo (esa búsqueda completa puede usar
criterios más amplios que el autocompletado).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
Cubre el resto de la superficie del storefront (checkout edge cases,
filtros de catalogo, variantes de producto, cuenta/wishlist,
accesibilidad, otros viewports) agrupado por riesgo, no solo lo ya
probado.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
Cierra la mayor parte del checklist de pruebas reales que había quedado
pendiente: pago PSE hasta la redirección al banco, envío calculado
visible, banner y buscador mobile -- todo verificado con Chromium real
(Docker + Playwright en la NAS, ver .browser-scratch/README.md). Deja
documentados los 2 bugs reales que esa prueba encontró (ya corregidos
en otro commit) y un checklist ampliado para la próxima sesión.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
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
Trivy bloqueó el build del frontend en el pipeline de CI: Next.js
15.5.10 tiene un RCE no autenticado en la API de optimización de
imágenes vía archivos AVIF (CVE-2026-75604 / GHSA-2xp9-vwfh-vxw4),
corregido en 15.5.24. Bump mínimo dentro del mismo minor, sin otros
cambios de dependencias.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
- hero-carousel.tsx: slides derivados de productos featured/nuevos reales
(imagen, nombre, precio, link, badge de % OFF) en vez de un array
hardcodeado; fallback estático solo si la tienda no tiene productos
activos.
- navbar.tsx: en mobile/tablet el buscador pasa a una segunda fila a
todo el ancho debajo de logo+iconos (antes competía por espacio en una
sola fila y quedaba recortado); desktop sin cambios.
- docs: ADR 0010 documentando el pivote de pago PayU→Rapyd(descartado)→
ePayco, la integración de EnvíoClick y checklist de pruebas reales
pendientes desde el storefront; puntero agregado desde ADR 0009.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
Placeholders reemplazados por los datos reales que dio el usuario --
dirección de la bodega (Cra 100 #42-65), email y teléfono de contacto.
Estos valores aparecen como remitente en las guías reales de EnvíoClick.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
ADR 0009 §2 ya había anticipado esto: la shipping option actual es un
placeholder de negocio ($15.000 COP fijo), no un cálculo real por
peso/volumen -- y el modelo de direcciones del checkout (IDs DIVIPOLA
en vez de texto libre en lib/data/colombia.ts) ya se había construido
pensando en esta integración. Este commit la implementa.
Investigación en vivo (no solo documentación -- misma disciplina que
ya costó varios rehechos con las pasarelas de pago): la SPA de docs
públicas (apidoc.envioclickpro.com.co) no renderiza sin sesión, pero
está generada con apiDoc, así que se bajó y leyó directo su
api_data.json/api_project.json (spec completa real), y se probó
/quotation en vivo con la API key real del usuario -- 200 OK con
tarifas reales de PIBOX/COORDINADORA/ENVIA/RAPPI/TCC. Esto corrigió
varios datos mal asumidos de un plugin de WordPress desactualizado
usado como referencia inicial (API v1): la URL base real es
api.envioclickpro.com.co/api/v2 (no api.envioclickpro.com a secas), y
el header de auth real es "Authorization" a secas, no
"AuthorizationKey".
Hallazgo crítico evitado leyendo el código fuente de
@medusajs/utils/dist/common/define-config.js: defineConfig() ya
registra por defecto un módulo de fulfillment con el provider
manual_manual, aunque medusa-config.ts no lo mencione explícitamente
-- pero transformModules() indexa por serviceName y el último que
llega pisa al anterior (no mezcla providers). Agregar un bloque propio
de fulfillment sin incluir también manual_manual ahí habría roto en
silencio el envío manual que ya usa la shipping option existente.
Verificado cargando la config compilada real: ambos providers quedan
registrados juntos.
- src/modules/envioclick-fulfillment/: AbstractFulfillmentProviderService.
calculatePrice() cotiza en vivo durante el checkout -- Medusa advierte
explícitamente que un error acá bloquea el checkout completo, así que
NUNCA se deja propagar una excepción: cualquier falla (red, dirección
sin id DIVIPOLA, EnvíoClick caído) cae al precio fijo de $15.000 que
ya se usaba, para no romper la compra. createFulfillment() vuelve a
cotizar en el momento de generar la guía real en vez de reusar el
idRate mostrado en checkout (más correcto: CalculatedShippingOptionPrice
no tiene forma de persistir ese id entre llamadas, confirmado leyendo
el tipo real, y un idRate viejo podría estar vencido si el fulfillment
pasa horas/días después). cancelFulfillment() sí cancela de verdad
(cancellation/batch/order existe y está confirmado, a diferencia del
refund de ePayco que quedó sin implementar).
- Solo 1 paquete por cotización/guía (límite real de la API, confirmado:
"min 1, max 1") -- el carrito se agrega a un solo paquete. Como hoy
NINGÚN producto tiene peso/dimensiones cargados (confirmado en vivo:
18 productos activos, 0 con weight), se usa un paquete de fallback
conservador completo en vez de mezclar datos reales con inventados a
medias -- la cotización no va a ser precisa hasta que se carguen esos
datos (tarea de datos, no de código, fuera de alcance de este commit).
- Frontend: cambio chico pero necesario en checkout-client.tsx/
medusa-cart.ts -- el checkout mandaba el nombre de la ciudad a Medusa,
nunca el id DIVIPOLA (aunque el form sí lo tenía disponible). Hay
municipios con el mismo nombre en distintos departamentos de Colombia,
así que el nombre solo no alcanza para identificar la ciudad sin
ambigüedad -- se guarda el id real en la metadata de la dirección
(Medusa sí tiene ese campo), que el fulfillment provider lee para
derivar el código DANE real que pide EnvíoClick (5 dígitos + "000" de
relleno, confirmado contra su propio ejemplo: "11001000" = Bogotá).
Pendiente de verificar con una llamada real (no se quiso generar una
guía de prueba en la cuenta real del usuario sin avisar): /shipment_sandbox
y cancellation/batch/order, verificados contra la spec oficial real
pero no probados en vivo todavía.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
El commit anterior agregó epayco-sdk-node a package.json pero el
lockfile nunca se actualizó (se generó localmente con
--package-lock=false para evitar un problema no relacionado del
registry en este entorno) -- npm ci en el Dockerfile exige que el
lockfile esté sincronizado con package.json y rompió el build en CI.
Regenerado con npm install normal; confirmado que npm ci (el comando
real del Dockerfile) ahora sí instala limpio.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR