Skip to content

feat(storefront): Show recommended products on cart and checkout - #812

Open
vitorrgg wants to merge 2 commits into
mainfrom
feat/storefront-cart-recommendations
Open

feat(storefront): Show recommended products on cart and checkout#812
vitorrgg wants to merge 2 commits into
mainfrom
feat/storefront-cart-recommendations

Conversation

@vitorrgg

Copy link
Copy Markdown
Member

Problema

Hoje /app/#/cart e /app/#/checkout não mostram recomendação nenhuma — mas não é porque falta código. O SPA legado (@ecomplus/storefront-app) já renderiza <recommended-items> nas duas telas: está fixo no TheCart.html e presente no EcCheckout.html com canRecommendItems default true. Conferi no bundle publicado (app-cart.js e app-checkout.js do beta.228) e a vitrine está lá.

O que quebra é o segundo passo da busca. O componente pega os IDs no grafo de co-compra e depois procura os produtos via @ecomplus/search-engine, que monta {terms: {_id: [...]}} no search/_els. Esse filtro retorna zero hits no índice v3:

# 1 hit
curl -s -X POST -H "X-Store-ID: 1024" -H "Content-Type: application/json" \
  -d '{"size":3,"query":{"bool":{"filter":[{"terms":{"sku":["PA606"]}}]}}}' \
  "https://ecomplus.io/v2/search/_els/items.json"

Trocando sku por _id, com o ID do mesmo produto, vem total: 0. Query ids funciona, term/terms em _id não. Como o componente faz v-if="items.length", a seção inteira some sem erro no console. Isso derruba junto o BuyTogether e os favoritos da conta.

O grafo em si está vivo e populado — apx-graphs.e-com.plus/products/{id}/recommended.json devolve co-compra normalmente para a loja 1024.

Solução

Recomendação passa a vir do core, sem depender do SPA legado:

  • use-cart-recommendations.ts — itens do carrinho → grafo (recommended, com fallback related) → search/v1?_id=, que funciona. Ordena pela relevância do grafo, filtra indisponível/sem estoque e o que já está no carrinho, e cacheia as respostas do grafo em memória. Em /app/ o carrinho é do SPA legado, então o composable detecta window.ecomCart e assina o evento change — leitura apenas, sem escrever na mesma chave de localStorage. Fora do /app/ usa o shoppingCart reativo normal.
  • CartRecommendations.vue — a vitrine, usando o ProductCard do próprio tema (~/components/ProductCard.vue), então sai com a identidade de cada loja. Passa list-name para os eventos view_item_list / select_item.
  • cart-recommendations.ts — monta o app Vue logo abaixo do #storefront-app, passando pelo ~/pages/_vue do tema para herdar $t, $money, ALink, AImg e o que mais o tema registrar.
  • vbeta-app.ts — desliga a vitrine morta do SPA (canRecommendItems: false + CSS escondendo .recommended-items, para não duplicar se o search-engine for consertado um dia) e faz o import dinâmico da montagem apenas quando a rota bate, então o Vue não entra no bundle em rotas que não usam.

Rollout: zero arquivo por loja

A montagem vive no vbeta-app, que o core já injeta na página /app/. Nenhum tema e nenhuma loja precisa mudar arquivo — só subir a versão do @cloudcommerce/storefront. Isso vale para os 16 temas store-* e as 10 lojas v3.

Quem quiser sair: window.propsCartRecommendations = false, e a vitrine legada fica intacta como está hoje. Quem quiser ajustar atribui props no mesmo global, por exemplo { onRoutes: ['cart', 'confirmation'], limit: 8 }.

Como foi testado

Unitário (6 testes, npx vitest run em packages/storefront): carrinho vazio, busca por _id no search/v1, fallback recommendedrelated, exclusão do que já está no carrinho e de indisponíveis, seleção da origem por quantidade, e o gate de rota. O primeiro é a guarda de regressão que importa: a busca tem que sair como _id= no search/v1, nunca como terms._id no _els.

Navegador (Puppeteer, loja demo 1011, contra uma worktree limpa do ecomplus/store no fc2445e — sem nenhum arquivo de tema modificado, o que prova o rollout de custo zero):

rota chamadas ao grafo cards container legado desligado CSS injetado
#/cart 3 3 sim sim sim
#/checkout 2 3 sim sim sim
#/order/... 0 0 não sim sim
#/cart com opt-out 1 0 não não mexe não injeta

Uma única chamada search/v1?_id=...&limit=24 por render. Como a loja demo não tem histórico de compras, o grafo dela volta vazio — interceptei só a resposta do grafo e deixei a busca e a renderização reais.

Ressalvas

  • Na rota #/cart ainda saem 3 chamadas ao grafo: 2 nossas e 1 do SPA legado. O canRecommendItems só existe no EcCheckout; no carrinho o <recommended-items> está fixo no TheCart.html, então dá para esconder por CSS mas não para impedir o fetch. É uma requisição cacheada, e some de vez quando essa parte do storefront-app sair do ar.
  • O vitest.config.ts entrou aqui porque o teste não roda sem ele. Ele nasceu em outra frente (lista de presentes); se for entrar por outro PR, é só remover deste.
  • packages/storefront não tem script test, então estes testes ainda não rodam no pnpm test. Fora do escopo deste PR por mexer no package.json.
  • O onRoutes padrão inclui checkout. No primeiro passo (só o campo de e-mail) a vitrine sobe bastante na página; com o formulário cheio ela cai abaixo da dobra. Se preferir jogar seguro na conversão, ['cart', 'confirmation'] resolve.

🤖 Generated with Claude Code

The shelf the legacy checkout SPA already renders never shows anything:
it looks products up by `{terms: {_id}}` on `search/_els`, which returns
zero hits on the current index, so the section silently hides itself.

Recommendations now come from a core composable that reads the cart
(bridging `window.ecomCart` on `/app/` pages), fetches the co-purchase
graph with a `related` fallback, and looks the products up by `_id` on
`search/v1`, which does work.

Mounting happens from `vbeta-app`, lazily and only on the cart, checkout
and confirmation routes, so no theme or store file has to change. Stores
opt out with `window.propsCartRecommendations = false`, or tweak it by
assigning props to that same global.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vitorrgg

Copy link
Copy Markdown
Member Author

Revisão adversarial

Escopo

O PR deveria: vitrine de recomendados (grafo → search/v1?_id=) nas rotas cart/checkout/confirmation do /app/, desligando a vitrine morta do SPA legado, com rollout sem tocar arquivo de loja. Desvio assumido no próprio PR: vitest.config.ts veio de outra frente.

Comparei com o padrão estável de use-product-shelf.ts (mesmo fluxo _id= + SEARCH_ENGINE_DEFAULTS.fields + checkInStock) — o composable novo está consistente. Ponto estrutural: nenhum arquivo de packages/storefront/src/lib importava de ~/ até agora; CartRecommendations.vue e cart-recommendations.ts invertem a direção core→tema pela primeira vez.


Required

R1. Inversão core→tema: build de tema sem ~/components/ProductCard.vue quebra no upgradeCartRecommendations.vue:25
O import é estático; mesmo com o import() dinâmico no vbeta-app, o Vite resolve o chunk em build. Cenário de falha: tema store-* que renomeou/moveu o card (ou cujo ProductCard tem prop obrigatória extra) → build da loja falha num upgrade de rotina do @cloudcommerce/storefront — o oposto da promessa de "zero arquivo por loja". Verifiquei base ecomplus/store e tiasonia (ok, props compatíveis); os outros ~15 temas não. Antes do release: varrer os 16 temas por src/components/ProductCard.vue e props requeridas. Alternativa defensiva: defineAsyncComponent com onError degradando para não renderizar.
Teste sugerido: build smoke em CI contra o tema base + 1 tema customizado.

R2. isFetching fica travado em true para sempreuse-cart-recommendations.ts (fetchRecommendations)
Cenário: exec 1 (carrinho com itens) seta isFetching = true e aguarda o grafo; usuário esvazia o carrinho → exec 2 entra no early-return de !_sourceIds.length, que não reseta isFetching; exec 1 retoma, cai no guard execId !== execCount e retorna sem resetar. Mesmo vale para o guard após o loop de graphs. O componente atual não usa isFetching, mas ele é exportado — o primeiro tema que ligar um skeleton nele fica com loading eterno.
Fix: resetar no early-return e nos returns por guard (ou finally condicionado a execId === execCount).
Teste sugerido: carrinho com itens → esvaziar antes do grafo resolver → expect(isFetching.value).toBe(false).

R3. A guarda de regressão que motivou o PR não roda em CI
packages/storefront não tem script test e o turbo run test filtra por script existente — o teste que trava o bug do terms._id nunca executa automaticamente. Um teste que não roda em CI é documentação, não guarda. "test": "vitest run" resolve.

R4. jsdom não é dependência declarada de ninguémcart-recommendations.test.ts:1
O pragma @vitest-environment jsdom funciona localmente porque o jsdom chega transitivo (1 entrada em .pnpm). Com pnpm isolado, um install limpo em CI pode não resolver → "Cannot find package 'jsdom'". Declarar em devDependencies junto com o R3.

Optional

O1. Opt-out inconsistente: o CSS injetado derrota canRecommendItems: true explícito do temavbeta-app.ts
O spread { canRecommendItems: false, ...propsEcCheckout } respeita a escolha do tema, mas #storefront-app .recommended-items{display:none} a esconde de qualquer forma. Se o search-engine legado for consertado, tema com canRecommendItems: true terá fetch + render invisível. Ou o CSS respeita o mesmo opt-out, ou documenta-se que propsCartRecommendations = false é o único caminho.

O2. Rota confirmation no default provavelmente nunca renderizacart-recommendations.ts
Após o pedido, o legado esvazia o carrinho → changesourceIds = [] → vitrine some. A tabela de testes cobre #/order/..., não uma confirmation real pós-compra. Ou remove do default, ou (futuro) usa os itens do pedido como fonte.

O3. Polling de 10s por window.ecomCart em toda página que montar o composableuse-cart-recommendations.ts (watchLegacyCart)
Roda incondicionalmente no client; se um tema usar CartRecommendations fora do /app/, são 50 tentativas × 200ms atrás de um global que nunca existe, e o listener ecomCart.on('change') nunca é removido nem no dispose do escopo. Condicionar à presença de #storefront-app ou a onRoutes.

O4. Fetch do grafo sem timeout/AbortController
apx-graphs lento não trava a página, mas segura isFetching e pode renderizar a vitrine muito depois, empurrando layout no checkout (CLS). AbortSignal.timeout(5000) resolve.

O5. Lacunas na suíte: sem teste para (a) cache do grafo (2 chamadas ao mesmo produto → 1 fetch), (b) erro do grafo (res.ok = false → fallback gracioso), (c) minIds atingido → related não é chamado, (d) o bug R2.

Nit

  • N1. Comentários em português em CartRecommendations.vue, cart-recommendations.ts e docblocks do teste — o restante de packages/storefront/src/lib comenta em inglês.
  • N2. X-Store-ID: "undefined" literal se ECOM_STORE_ID faltar (contextos de CMS preview).
  • N3. vitest.config.ts declara environment: 'node' mas o único teste exige o pragma jsdom; o alias não cobre @@i18n/~ — o primeiro teste de componente quebra de forma confusa.
  • N4. typeof props === 'object' aceita array — propsCartRecommendations = [] viraria spread de índices.

Eixos sem achados

Auth: só header público X-Store-ID + search/v1 público; sem token, sem vazamento. Concorrência: o race relevante é o R2; o cache por promise em graphsCache deduplica chamadas simultâneas e permite retry após falha. Contrato graphs: shape Neo4j assumido com results?.[0]?.data || [] — mudança de contrato degrada para vitrine vazia, sem crash.


Veredito: APROVAR COM RESSALVAS

O núcleo está bem construído — guard de execId, comparator de sourceIds e cache por promise mostram cuidado com os races óbvios, e o teste unitário trava a regressão certa (_id= no search/v1, nunca terms._id). Condiciono ao merge:

  1. R1 — verificação (ou blindagem) do ProductCard.vue nos 16 temas antes do release; único achado com potencial de quebrar build de loja em upgrade de rotina.
  2. R2 — fix do isFetching travado (3 linhas).
  3. R3 + R4 — script test + jsdom declarado, senão a suíte é decorativa.

Ficou sem verificar: os ~15 temas store-* além do base e tiasonia (submódulos não disponíveis); o comportamento real no browser (confiei na tabela Puppeteer do PR); a resposta real do apx-graphs e o claim do curl sobre terms._id; e o fluxo de confirmation com pedido real.

🤖 Generated with Claude Code

- Reset `isFetching` when the cart is emptied while requests are in flight,
  so theme skeletons bound to it can't spin forever
- Timeout Graphs API requests after 5s to prevent late layout shift on checkout,
  and only send `X-Store-ID` header when the store ID global is set
- Skip polling for legacy `window.ecomCart` outside the legacy app document
- Respect themes explicitly opting the legacy showcase back in
  (`canRecommendItems: true`) before hiding it with CSS
- Guard `window.propsCartRecommendations` against non-object values
- Wire tests to CI: `test` script on the package, `jsdom` declared, and new
  coverage for Graphs cache dedup/retry, API error fallback, `minIds`
  short-circuit and the `isFetching` race

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vitorrgg

Copy link
Copy Markdown
Member Author

Ajustes da revisão aplicados

Commit 361e68f resolve os achados da revisão acima:

Achado Fix
R2 isFetching travado Reset no early-return de carrinho vazio (use-cart-recommendations.ts) — validado por mutação: revertendo o fix, o teste novo falha
R3 teste fora do CI "test": "vitest run" no package.json do pacote — turbo run test agora inclui o storefront
R4 jsdom não declarado jsdom: ^25.0.1 em devDependencies + entrada mínima de 3 linhas no pnpm-lock.yaml (sem re-resolução do lock)
O1 CSS vs opt-in explícito CSS de esconder .recommended-items só é injetado se o canRecommendItems final for false — tema que opta pela vitrine legada explicitamente é respeitado (vbeta-app.ts)
O3 polling de 10s fora do /app/ watchLegacyCart sai cedo quando não há #storefront-app no documento
O4 fetch do grafo sem timeout AbortSignal.timeout(5000) com guard para browsers sem suporte
O5 lacunas de teste 4 testes novos: race do isFetching, dedup do cache + retry pós-falha, erro do Graphs degradando para [], minIds pulando related10/10 passando
N1–N4 Comentários em inglês, header X-Store-ID só quando o store ID está definido, alias @@i18n no vitest.config.ts, guard contra não-objeto/array em window.propsCartRecommendations

Pendente (fora do código deste PR)

  • R1 — varredura dos 16 temas store-* por src/components/ProductCard.vue com props compatíveis (product + listName?) antes do release; base ecomplus/store e tiasonia já verificados ok. Sem isso, um tema sem o arquivo quebra o build no upgrade de versão.
  • O2confirmation no default de onRoutes provavelmente nunca renderiza (carrinho esvazia pós-compra); decisão de produto manter ou trocar a fonte pelos itens do pedido.

🤖 Generated with Claude Code

@vitorrgg
vitorrgg requested a review from leomp12 August 21, 2026 14:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant