fix(storefront): Show kit contents and pick variations on product page - #799
fix(storefront): Show kit contents and pick variations on product page#799vitorrgg wants to merge 4 commits into
Conversation
Kit products (`kit_composition`) were rendered like plain products: the composition was never listed and kit items with variations went to cart without a selected SKU. - `use-product-details`: expose `isKit`, `kitComposition` and `selectKitVariation`, load kit items on mount and require a variation per composition slot before buying; shipping is now calculated with the kit items instead of the (weightless) kit product - `use-product-card`: fetch `variations` for kit items, honor a fixed `kit_composition[].variation_id`, cap kit quantity by the least available item (was taking the max) and set `kit_product` before adding to cart, otherwise a matching standalone item on cart was merged into the kit - new `KitComposition.vue` shared component listing each item with picture, link and quantity, with a `variations` slot for theme SKU selectors Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Splits the kit branch into `matchKitItem`, `sumToKitComposition` and `parseKitCartItems`, dropping `loadToCart` back under the complexity threshold flagged by CodeFactor. No behavior change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
leomp12
left a comment
There was a problem hiding this comment.
Boa PR — a direção está certa e três das quatro correções eu consegui confirmar contra dado real, não só lendo o código:
- A inversão
max→minno estoque do kit é a correção mais valiosa daqui. Simulei os dois algoritmos contra a API da tia sônia (loja 1024, 19 kits): o "Kit Amor que nutre" anunciava 289 unidades e o mínimo real dos componentes é 15; o "Kit com 5 Granolas" anunciava 726 contra 96 reais. Estávamos vendendo kit que não existe. kit_productantes doaddCartItemestá certo e o motivo é exatamente o descrito:add-cart-item.ts:29só entra no laço de merge quando!newItem.kit_product, então gravar depois realmente fundia o componente com o item avulso que já estava no carrinho.shippedItemspela composição encaixa direitinho na peça que já existe —use-shipping-calculator.ts:169-184já sabe buscar peso e dimensão por_ide aplicar a variação porvariation_id, então o cálculo passa a usar o componente real sem precisar de nada novo do outro lado.- A extração do commit 2 deixou
loadToCartlegível de verdade. E o básico está conferido:AImg/ALink/Skeletonsão globais registrados empages/_vue.ts:3-6,i19selectVariationei19outOfStockexistem empackages/i18n, evariation_iddentro dekit_product.compositionestá previsto no schema (packages/api/types/carts.d.ts:207).
O que me preocupa está quase todo concentrado nos quatro campos novos do kitItemFields.
🔴 Bloqueante — o gate de visible derruba um kit que está à venda hoje na tia sônia
use-product-card.ts:64 adiciona visible ao kitItemFields e use-product-card.ts:108 passa a recusar o item:
if (!kitItem?.available || kitItem.visible === false) return null;Fui atrás do dado na loja 1024:
KIT PIC5533 "Kit Cookies Chips + Cartela de Adesivos" /kit-cookies-chips
visible: true available: true R$ 26,90 estoque para 40 kits
└─ componente RV0078 "Cartela de Adesivos – Cookies Chips"
visible: FALSE available: true quantity: 219
É o padrão clássico: a cartela de adesivos é um brinde de R$2,90 que só existe dentro do kit, então está escondida do catálogo de propósito. Com essa PR o matchKitItem devolve null, o parseKitCartItems aborta, e o cliente que clicar em comprar recebe só o isFailedToCart genérico — sem nenhuma pista do que houve. Antes funcionava.
A tia sônia agrava o caso porque o ProductDetails.vue dela usa useProductCard direto (ProductDetails.vue:245), não o useProductDetails — ou seja, ela nem ganha a lista de composição pra dar contexto ao erro; só o botão que falha calado.
O ponto de fundo é que oculto do catálogo ≠ não vendável dentro do kit. Se a ideia era barrar componente desligado, available já cobre isso e já era checado antes da PR. Eu tiraria visible do gate. Se houver motivo pra mantê-lo, que seja pelo menos visible === false && available === false, e nunca sozinho.
🟠 min_quantity no kitItemFields só tem um consumidor, e é o errado
Os dois lugares que precisam de min_quantity no código novo o sobrescrevem — use-product-card.ts:153 e use-product-details.ts:102 passam a quantidade exigida pelo kit. Sobra um único consumidor real: o parseProduct, em parse-product.ts:24:
quantity: minQuantity > 0 ? Math.max(minQuantity, quantity) : quantity,Antes o campo não vinha no fields, então isso era inerte. Agora, um componente com min_quantity: 6 numa composição que pede 1 unidade gera item de carrinho com quantity: 6, enquanto kit_product.composition[].quantity e pack_quantity seguem em 1 — o carrinho passa a exibir quantidade e subtotal inflados. No checkout o fix-items.ts:122 já derrubava esse item de qualquer jeito, então não é dinheiro perdido; é o cliente vendo um carrinho que não corresponde ao que vai ser cobrado.
Nenhum dos 30 componentes de kit da tia sônia tem min_quantity > 1 hoje, então não materializa lá — mas é latente pra qualquer loja. Eu simplesmente tiraria o campo do kitItemFields.
🟠 isSkuSelected responde "sim" enquanto os itens do kit ainda estão carregando
use-product-details.ts:116-118 decide pelo kitComposition, mas durante o carregamento todo slot tem product: null → variations: [] → isSelected: true. Resultado: isSkuSelected fica true, o checkVariation deixa passar, o loadToCart espera o load e só então descobre que falta SKU — e devolve o mesmo isFailedToCart genérico, sem o alerta de campo pendente que existe justamente pra isso.
O isLoadingKitItems já está exposto pra resolver isso; falta consumi-lo no gate — isSkuSelected retornar false (ou o addToCart aguardar) enquanto o load não terminou.
🟠 O seletor de variação some quando a variação escolhida zera
KitComposition.vue:62-72 encadeia o aviso de esgotado e o seletor no mesmo v-if/v-else-if, e isInStock (use-product-details.ts:97-104) já leva a variação escolhida em conta. O caminho fica: cliente escolhe o tamanho P → P está sem estoque → o bloco vira "esgotado" e o <select> desaparece → não há como voltar e trocar por M. O kit inteiro fica travado numa escolha ruim.
Aviso e seletor precisam conviver, não se substituir.
🟠 O agrupamento de produto repetido monta um carrinho que o checkout rejeita
O corpo da PR lista "agrupa produtos repetidos na composição" como resolvido, mas o fix-items.ts não aceita o formato. O use-product-card.ts:158-163 funde por _id + variation_id; do outro lado, fix-items.ts:186-195 calcula packQuantity como a soma das quantidades da composição e fix-items.ts:214 valida kitTotalQuantity % (minPacks * packQuantity) === 0.
Para uma composição [A×1, A×1, B×1]: o cliente manda A=2 e B=1 (total 3); o servidor calcula packQuantity = 3 e minPacks = 2, cai em 3 % 6 ≠ 0 e remove todos os itens do kit (fix-items.ts:230-239).
Não é regressão — o código antigo também não passava nessa validação. Mas vale alinhar: o caso que de fato funciona é o mesmo produto com variações diferentes (aí as chaves não fundem e o servidor aceita), e a forma suportada de pedir duas unidades é uma entrada só com quantity: 2. Ou ajusta o texto da PR, ou trata de verdade — o que exigiria mexer no fix-items.ts junto.
🟠 Pré-existente, mas está exatamente na função que a PR reescreve: comprar N kits mostra o preço de 1
use-product-card.ts:152,156 acumula packQuantity += (comp.quantity || 1) * quantityToAdd, ou seja, já multiplicado pelo número de packs. Aí o shopping-cart.ts:156 faz final_price = kit_product.price / pack_quantity e divide pelo total inflado.
Comprando 2 kits de R$100 com composição [A×1, B×1]: pack_quantity = 4, final_price = 25, e o carrinho exibe R$100 em vez de R$200. O fix-items.ts:217,223 recalcula pack_quantity sem multiplicar pelos packs, então o pedido sai certo — a divergência é só no carrinho, mas é o cliente vendo um valor e pagando outro.
A aritmética é idêntica à do código antigo, então não é regressão desta PR. Só que a tia sônia tem 18 kits vivos com seletor de quantidade, e a correção aqui é não multiplicar quantityToAdd no packQuantity — uma linha, dentro de uma função que você já está reescrevendo. Se preferir deixar pra outra PR, tudo bem, mas vale abrir a issue.
🟢 Minors
use-product-card.ts:108usa!kitItem?.availableeuse-product-details.ts:98usaavailable !== false— mesma regra escrita de dois jeitos, e a UI pode marcar como disponível o que o carrinho recusa.use-product-card.ts:144— a mesma referência do arraycompositionvai em todos os itens do kit e oadd-cart-item.ts:48só faz cópia rasa; mutar um mexe em todos.use-product-card.ts:165—Object.keys(...).map((key) => cartItemsByKey[key])éObject.values(cartItemsByKey).map(...).use-product-card.ts:90-96—getKitItemStocksó considera a variação quando a composição fixa uma; pra componente com variação livre usa o estoque agregado do pai, então oproduct.quantitydo kit fica otimista.use-product-card.ts:283— o memoloadingKitItemsnunca invalida no sucesso: enquanto orefetchStockatualiza o produto-kit, os componentes ficam com o estoque da primeira carga pelo resto da sessão.use-product-details.ts:143— oas Array<Record<string, any>>joga fora o tipo à toa;use-shipping-calculator.ts:51já aceita(ShippedItem | CartOrProductItem)[].use-product-details.ts:162— oif (kitShippedItems.length)mantém o produto-kit noshippedItemsquando o load falha, e o frete volta a ser calculado com o item sem peso, que é justamente o bug que a PR resolve.KitComposition.vue:71— o slot expõe{ item, selectVariation }mas nãohasSelectionAlert, então o tema não consegue reproduzir o destaque de campo pendente que o<select>de fallback tem.KitComposition.vue:50—target="_blank"semrel="noopener".
Pra entrar antes do merge
- Tirar
visibledo gate domatchKitItem— é o único item que quebra loja em produção. - Tirar
min_quantitydokitItemFields— não tem consumidor útil e distorce oparseProduct. - Gate de loading no
isSkuSelected, e o seletor de variação convivendo com o aviso de esgotado noKitComposition. - A PR do tema. Procurei em
ecomplus/storee ela ainda não existe (só a #61 do renovate e a #26 do A/B). Sem ela, esta PR entrega só a validação mais rígida e nenhuma UI de seleção — e como todoProductCard.vuechamaloadToCart(1)semkitVariationIds, kit com componente com variação fica sem caminho de compra em qualquer tema. As duas precisam subir juntas.
⚠️ Nota de deploy — tia sônia
A correção de estoque está certa, mas muda número em produção no dia que subir. Rodei os dois algoritmos sobre o catálogo da loja 1024: 16 dos 18 kits visíveis passam a anunciar bem menos estoque. Amostra (snapshot de hoje — são números vivos):
Kit Granola Premium com Chocolate 726 → 90 Kit Amor que nutre 289 → 15
Kit Clássico da Torcida 726 → 202 Kit Sabor que nutre 289 → 42
Kit com 5 Granolas - Linha Sabores 726 → 96 Kit Granola Zero Low Carb 432 → 118
Kit Granola Sabores / Mais Sabor 726 → 74 Kit Cookies Chips 219 → 40
Boa notícia: nenhum kit sai do ar. Os dois com componente zerado ("Kit mais Coquinho e Castanha" e "Kit Sabores do Brasil") já apareciam como 0 antes — o break em !kitItem.quantity do código antigo já os zerava. A mudança é só de teto de estoque, para menos, que é o ponto.
Ainda assim vale avisar a loja: quem olha o painel vai ver o estoque dos kits despencar de um dia pro outro.
A barra do céu (loja 3967) não tem nenhum produto com kit_composition — varri os 5.040 produtos. Está fora de risco, apesar do kit_composition no fields do SelectedProductsSection.astro:38.
…it prices on cart Address review points from PR #799: - Keep hidden (`visible: false`) kit components buyable, they're commonly gifts/addons kept out of the catalog on purpose; - Stop fetching `min_quantity` for kit items so `parseProduct` can't inflate cart item quantity over the composition required quantity; - `isSkuSelected` no longer answers true while kit items are still loading, and `addToCart` awaits the kit items load to properly alert pending variation selection instead of failing silently; - Variation selector is kept together with the out of stock warning so the customer can switch to another variation when one runs out; - `kit_product.pack_quantity` is no longer multiplied by the number of packs to add, fixing cart displayed price when buying 2+ kit units; - Each kit cart item gets its own `composition` array copy to prevent shared reference mutations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ep kit stocks fresh Remaining review points from PR #799: - Kit stock ceiling for items with free (unfixed) variation now uses the best variation stock instead of the parent aggregated quantity, which sums all variations and oversells the kit; - Kit items load memo is cleared on settle so later calls refetch fresh stocks instead of keeping the first load for the whole session; - Availability rule aligned between cart parsing and details UI (`available === false`), so the UI can't offer what the cart refuses; - Kit `shippedItems` no longer falls back to the weightless kit product when the composition items aren't loaded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
leomp12
left a comment
There was a problem hiding this comment.
Voltei nos quatro commits. Os nove pontos da review anterior estão fechados — conferi cada um contra o código, não contra a mensagem de commit — e sobraram duas regressões, ambas pequenas e dentro deste repo.
Os nove pontos anteriores: fechados ✅
| Ponto | Correção |
|---|---|
🔴 gate de visible |
fora de kitItemFields e de matchKitItem |
🟠 min_quantity no fields |
removido |
🟠 isSkuSelected durante o load |
use-product-details.ts:118 + addToCart aguarda o load |
| 🟠 seletor sumindo ao esgotar | v-else-if → v-if |
| 🟠 agrupamento de repetido | corpo da PR qualifica o caso suportado |
| 🟠 preço de N kits | use-product-card.ts:170, packQuantity += quantityPerPack |
🟢 available inconsistente |
alinhado em available === false dos dois lados |
🟢 composition compartilhada |
composition.map((item) => ({ ...item })) |
🟢 Object.keys().map |
Object.values() |
O que validei além de ler:
- O bloqueante, contra dado real. Na tia sônia (1024) o componente
RV0078da cartela de adesivos seguevisible: false, available: true, quantity: 219. Comvisiblefora do gate, o kitPIC5533volta a ser vendável e o teto saimin(30, 29, 219) = 29kits. Correto. min_quantity: sem o campo nos fields,parse-product.ts:17calculaminQuantity = 0e a linha 29 vira identidade. O inflar de quantidade não tem mais como acontecer.pack_quantity: refiz a aritmética contra o servidor nos dois sentidos. Kit de R$100 com[A×1, B×1]comprando 2 → cliente mandapack_quantity: 2e exibe 2×50 + 2×50 = R$200;fix-items.tscalculapackQuantity = 2,minPacks = 2,4 % (2*2) = 0, passa, e cobra os mesmos R$200. Testei também[A×2, B×1]com 1 e 2 packs. Fecha. A divergência carrinho×pedido acabou, e o valor novo é justamente o que ofix-itemsjá esperava.- Composição repetida: variações distintas passam (
3 % (1*3) = 0), mesmo produto+mesma variação continua caindo em3n % 6n ≠ 0. O texto da PR está correto. removeCartItemnão dependia da referência compartilhada dacomposition— usa a própria cópia como checklist. A cópia profunda é segura ali.
E os pontos que levantei e não se sustentaram, para você não perder tempo: hasSelectionAlert está declarado em Props e é exposto no render context do <script setup>, então o v-bind compila; o reduce do getKitItemStock está guardado por variations?.length, nunca roda em array vazio, e a seed 0 é conservadora; limpar o memo no .finally() não abre corrida nem apaga kitItems num refetch que falhe; e nenhum consumidor de addToCart usa o retorno ou depende de execução síncrona.
O que regride — os dois que eu resolveria aqui
1. Subtotal do frete na página de kit
main mandava [{...kitProduct, quantity}] para a calculadora: subtotal = preço do kit, correto. A PR troca pelos componentes (use-product-details.ts:167), e o subtotal é derivado da própria lista em use-shipping-calculator.ts:117-121:
const baseAmountSubtotal = computed(() => {
return shippedItems.value.reduce((subtotal, item) => {
return subtotal + getPrice(item) * item.quantity;
}, 0);
});Como price está em kitItemFields, cada componente entra com o preço avulso. Isso vira body.subtotal (use-shipping-calculator.ts:221), e o app consome direto: custom-shipping-calculate.ts:116 faz amount = params.subtotal || 0, usado em :197 na regra de frete grátis (amount >= rule.min_amount) e em :225 na taxa percentual.
O peso melhora — era o objetivo, e está certo. O valor piora, e sistematicamente na mesma direção, porque kit quase sempre custa menos que a soma das partes: kit de R$100 com componentes de R$80 + R$70 anuncia frete grátis num piso de R$120, e no checkout o subtotal real é R$100 e o frete aparece.
Some setando final_price: kitProduct.price / packQuantity nos itens enviados ao cálculo, ou passando baseParams.subtotal com o preço do kit.
2. shippedItems fica vazio para sempre se o loadKitItems falhar
O b0f41f4b tirou a guarda if (kitShippedItems.length) antes do splice (use-product-details.ts:165-167). Se a API falhar, kitItems fica null → todos os product da composição são null → a lista fica [] e não volta mais.
Aí use-shipping-calculator.ts:219 não seta body.items nem body.subtotal, e o POST sai só com o CEP. Pelo menos o app da mandae rejeita explicitamente (mandae-calculate.ts:170-175, CALCULATE_EMPTY_CART), o que cai em scheduleRetry() (:300, timeout de 10s) → novo erro → novo retry, indefinidamente. A página de produto não passa canAutoSubmit, então só começa se o cliente digitar o CEP — mas antes o fallback [kitProduct] ao menos gerava request válida.
A guarda volta em uma linha, ou não substitui enquanto kitItems.value for null.
Parte 2 (temas): o que precisa entrar junto com o bump
Não é bloqueio desta PR — o fluxo é cloud-commerce primeiro, lojas depois com os pacotes publicados. Mas vale registrar o escopo, porque nenhum caller passa kitVariationIds hoje:
store/.../ProductCard.vue:76→@click.stop.prevent="loadToCart(1)", sem os IDs. Idem nos cinco temas deecomplus-stores/e noCartSuggestedProductCard.vuedo barradoce.barradoce:278,tiasonia:271eefacini:142usamuseProductCarddireto e chamamloadToCart(quantity, { variationId })— a página de produto dessas três também precisa de ajuste.- O tema padrão,
mundoquadrieforjaestelarusamuseProductDetailsmas não renderizam oKitComposition, entãoisSkuSelectedficafalsee os dois caminhos de compra param (ProductDetails.vue:47levapreventDefault,:57devolvenull).
Só afeta kits cujos componentes têm variação — os de componente simples, como o PIC5533, seguem funcionando em todas as superfícies. Mas o kit de demonstração da própria PR (DEMO-NICHO-kit-camisaria-masculina, três componentes has_variations: true) é exatamente o caso que para.
Duas coisas que ajudam nessa segunda parte:
- O bump não é um passo humano.
store.renovate.json:41-52temautomerge: truepara@cloudcommerce/*minor/patch, e os seis repos herdam o mesmo preset. Como isto sai comofix(storefront), a versão entra sozinha nas lojas — vale sincronizar as PRs dos temas com a publicação, não depois dela. - O
ProductCardnão tem como saber que aquele kit exige seleção de SKU.hasVariationséfalsepara o produto-kit, e é justamente o gate usado hoje (ProductCard.vue:69). Uma flag tipoisKitSkuRequirednouseProductCardresolve num lugar só e deixa a PR dos temas trivial — essa parte seria aqui.
Follow-ups (não são regressão, dá para testar junto da parte 2)
- A corrida que pode desfazer a correção
max→min.productStockFields(use-product-card.ts:33-38) incluiquantitye o watcher fazObject.assign(product, productStock); orefetchStocké debounced em 1200ms e oloadKitItemsresolve bem antes. A sequência gravaproduct.quantity = min(...)(:326) e ~1s depois ofreshStocksrestaura oquantitypróprio do kit.mainjá faziaproduct.quantity = maxKitQntno fim doloadKitItems, então é pré-existente — mas é a que eu olharia primeiro das quatro, porque põe em risco justamente o ganho principal da PR. Guardar o teto num ref próprio, em vez de mutarproduct.quantity, resolve os dois lados. isFailedToCartnunca é resetado (use-product-card.ts:375), e os temas fazem:disabled="isFailedToCart"/v-if="... && !isFailedToCart". Depois da primeira falha não há como disparar um novoloadToCartpara limpar a flag: só recarregando a página. Pré-existente, mas a PR multiplicou osreturn nulldo caminho de kit.- A composição nunca vai no HTML servido (
use-product-details.ts,onMounted). O bloco "Este kit contém" não indexa, os skeletons trocam por conteúdo com layout shift, e o teto de estoque só aperta depois da hidratação. Os IDs já estão no doc SSR ($storefront.apiContext.doc), então dá para buscar do lado do servidor — mas é decisão de escopo, não defeito. - Um kit dispara N eventos
add_to_cart, cada um com o preço avulso do componente (use-analytics.ts:299-304; nesse instante ofinal_pricerateado ainda não foi calculado). Kit de R$100 reporta R$150 em três eventos para GA4, Meta e TikTok. Pré-existente e a correção não é local a esta PR, mas é o buraco que sobra na consistência ponta a ponta. - Produto repetido com a mesma variação: o
checkInStocké por entrada e o item de carrinho é mesclado, então[{A,1},{A,1}]com estoque 1 passa duas vezes e montaquantity: 2. É exatamente a configuração que o corpo da PR já documenta como não suportada e que ofix-items.ts:214rejeita — fica como limitação conhecida.
Menor: o corpo da PR ainda diz que kitItemFields "passa a trazer variations, visible, base_price e min_quantity" — visible e min_quantity saíram nos commits novos.
Uma coisa que conferi e está certa, para constar: não há adição parcial. O parseKitCartItems valida a composição inteira e devolve null antes de qualquer addCartItem (use-product-card.ts:359-368), então nunca sobra meio kit no carrinho. E o split de addProductToCart em parseProduct + addCartItem é equivalente — addProductToCart é literalmente os dois em sequência (shopping-cart.ts:43-49) —, então a reordenação do kit_product não perdeu comportamento pelo caminho.
Problema
Produtos do tipo kit (com
kit_composition) eram renderizados na seção de detalhes como um produto comum: a composição nunca era listada e, quando os itens do kit tinham variações, eles iam para o carrinho sem SKU selecionado.Exemplo real (loja demo 1011,
DEMO-NICHO-kit-camisaria-masculina): três camisas, cada uma comhas_variations: truee 5 tamanhos — não havia como escolher o tamanho de cada peça.Junto com isso, a camada de dados tinha três defeitos:
loadKitItemspegava o maiorfloor(estoque / qnt)entre os itens em vez do menor, liberando mais kits do que os componentes permitem.kit_productera gravado depois doaddCartItem, então um componente já presente no carrinho era mesclado e passava a ser tratado como item de kit.Mudanças
use-product-card.tskitItemFieldspassa a trazervariations,visible,base_priceemin_quantityloadKitItemsdeduplica IDs, é idempotente e usa o mínimo entre os itens, respeitandokit_composition[].variation_idfixoloadToCartaceitakitVariationIds, valida SKU e estoque item a item, funde produto repetido na composição em um único item de carrinho (suportado quando as variações são distintas — o mesmo produto+variação duplicado na composição não passa na validação depack_quantitydo checkout) e montakit_product(comvariation_idemcomposition) antes de inserir no carrinhouse-product-details.tsisKit,kitComposition,selectKitVariationeisLoadingKitItemsonMountedisSkuSelectedpassa a exigir variação escolhida em cada slot do kit, reaproveitando o alerta já existenteshippedItemspassa a ser a composição real do kitKitComposition.vue(novo, em@@sf/components)variationspara o tema injetar seu próprio seletor de SKU, com<select>como fallbackTema
A renderização em si exige uma alteração correspondente em
ecomplus/store(functions/ssr/src/components/ProductDetails.vue): incluir o<KitComposition>passando oSkuSelectordo tema no slot, esconder o "Comprar agora" direto em kits (o link porproduct_idnão explode a composição) e usari19buyKitno CTA. Vai em PR separado.Verificação
tsc --noEmite ESLint limpos nos arquivos alterados/kit-camisaria-tres-basicasresponde 200 com o bloco da composição renderizado, e uma página não-kit segue idênticaspecificationsde cada componenteNão deu para exercitar o clique/hidratação (escolher tamanho → adicionar ao carrinho) sem navegador — vale um teste manual antes do merge.
🤖 Generated with Claude Code