fix(storefront): Fix loyalty points not showing at checkout when store has discount preset - #805
Conversation
…e has discount preset When a store has discount_option or installments_option configured in modules settings, modulesInfo[list_payments] was already non-empty from the preset, causing fetchInfo to skip the API call entirely. As a result, loyalty_points_programs returned by the loyalty app was never merged into storefront.info, so PointsApplier never received the programs and the toggle was not rendered. Fix: always fetch from the API, emitting preset data immediately as an initial value for fast rendering while the API call completes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
leomp12
left a comment
There was a problem hiding this comment.
O diagnóstico está certo e é preciso. A condição antiga realmente confundia "tenho algum dado do preset" com "tenho o dado que preciso" — e como loyalty_points_programs só aparece em runtime (vem do app de fidelidade, packages/apps/loyalty-points/src/loyalty-list-payments.ts:57) e nunca do preset quando a loja não configurou o programa no CMS, qualquer loja com discount_option ou installments_option preenchidos ficava sem buscar. Chegar nisso partindo de "o toggle não renderiza" tem mérito. Deixar apply_discount fora da mudança também está certo — ele continua condicionado a UTM/cupom.
Uma correção de atribuição: o a3ab982da (que é meu) introduziu o modulesInfoEvents e o else que emite o preset, mas o skip do fetch é bem mais antigo — veio no aa587fd97 ("New optional global window.storefront.modulesInfoPreset"). O a3ab982da é onde o contrato de espera por evento nasceu e expôs o problema, não onde ele foi criado.
O que me trava é que "buscar sempre" liga um caminho destrutivo que até agora era inalcançável.
🔴 Bloqueante — o delete de modules-info.ts:122-124 passa a rodar em toda navegação, em toda loja
if (response.ok) {
Object.keys(modulesInfo[modName]).forEach((key) => {
delete modulesInfo[modName][key]; // apaga tudo
});
const modInfo = {};
const { result } = await response.json(); // ← await DEPOIS do deleteAntes desta PR esse bloco era código morto na prática: módulo com preset não era buscado (o delete nunca rodava nele) e módulo sem preset já estava vazio (no-op). Com o fetch incondicional ele entra no caminho quente. Quatro consequências:
1. Janela de estado vazio, mesmo quando a API responde tudo. O delete vem antes do await response.json(), e o scheduler do Vue faz flush em microtask — a UI renderiza o {} antes do merge. Como parsePhrase (:243-248) devolve string vazia quando o campo é falsy, usePitchBar.countValidSlides (composables/use-pitch-bar.ts:28-30) descarta o slide e useBanner.hasHeader (composables/use-banner.ts:71-73) esconde o header inteiro. As 4 lojas que tenho aqui têm free_shipping_from_value no content/settings.json (129, 150, 200, 200), então todas passam por isso.
2. Perda definitiva quando a API não repete o campo. O preset sai do content/settings.json, digitado à mão no CMS e independente da config dos apps. Os apps de frete só devolvem free_shipping_from_value se estiver configurado no próprio app (apps/correios/lib-mjs/calculate-shipping.mjs:29, apps/custom-shipping/src/custom-shipping-calculate.ts:79-86 — e este último tem early return em :14-17 quando não há shipping_rules). Divergiu, o número some da vitrine. discount_option tem o mesmo risco: o merge só assume o valor se algum payment_gateways[].discount casar value exato com apply_at !== 'freight' (:156-167); não casou, fica sem — e o preset já foi apagado.
3. Corrompe um objeto já entregue a terceiro. scripts/vbeta-app.ts:317-319 guarda a mesma referência reativa em window.storefront.info[modName]. O delete esvazia por baixo o objeto que o storefront-app já está lendo.
4. O vazio é persistido. sessionStorage.setItem (:187-190) grava o modulesInfo já apagado, então o page load seguinte parte do vazio por até 5 minutos.
Direção: computar o modInfo primeiro e só depois substituir — mover o bloco de delete para depois do await response.json(), limitando a remoção às chaves que o novo modInfo traz (ou trocar por um replace atômico). O objetivo original do delete era não deixar campo obsoleto para trás, e isso continua atingível sem passar por um estado vazio.
🟠 Estrutural — o cache de sessão deixa de evitar requisição
:66-79 restaura modulesInfo do sessionStorage com janela de 5 minutos. Com o fetch incondicional esse cache vira apenas valor inicial e o __timestamp fica vestigial. Na prática todo page view passa a disparar list_payments + calculate_shipping, e afetch é fetch puro, sem dedupe. Cada uma dessas chamadas faz fan-out server-side para todos os apps instalados (packages/modules/src/firebase/call-app-module.ts), vários deles com HTTP externo para transportadora ou gateway.
Não é motivo para reverter a direção, mas vale decidir explicitamente: ou o cache de 5 min volta a valer como short-circuit (buscando só o que falta), ou some e o custo é assumido. Do jeito que fica, o código sugere que há cache e não há.
🟢 Minors
- Test plan com os dois itens desmarcados — vale rodar o cenário 1 numa loja com
discount_optionno preset antes de mergear. - Com o push incondicional, o
forEachde:83-88só serve para o emit;modulesToFetchpodia já nascer com os dois módulos e o loop ficar só com a responsabilidade de emitir. - Confirma se o
waitStorefrontInfodostorefront-apptolera o emit dobrado (preset + API) que agora acontece em loja com preset — não consigo verificar daqui, o bundle vem de CDN.
Para mergear, só o bloqueante: mover o delete para depois do parse. O ponto do cache dá para tratar em PR separado, mas queria tua leitura antes de abrir issue.
…ng after page load Refreshing modules info cleared every field before parsing the API response, so values set on storefront settings disappeared whenever an app returned an error or the preview request brought no such field. - Merge over the preset instead of wiping: only fields absent from both the response and the preset are removed - Delete and assign are now adjacent, with no `await` in between, so the empty state is never rendered - Copy the preset objects on load, `modulesInfo` was aliasing (and therefore emptying) `window.$storefront.modulesInfoPreset` Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HtZi8sdmSDeQxrEeyzPJeS
|
Empurrei dois commits na branch em vez de devolver só o apontamento — o bloqueante dependia de uma decisão de semântica que era mais rápido escrever do que descrever. Segue o que mudou e por quê, pra você conferir se concorda.
O efeito líquido: primeira visita busca os dois, as seguintes dentro de 5 min não fazem requisição nenhuma, e o Um ponto residual que deixo registrado e não mexi: com o short-circuit, page view servido por cache volta a ter um único emit (no idle). Se algum listener registrar depois disso, ele perde o gatilho. Na prática o O test plan continua com você — em especial o cenário 1, e vale conferir também que |
|
Passei uma revisão adversarial em cima dos meus próprios commits, focada em performance e em loja sem programa de fidelidade — que é o caso majoritário e o que menos olhamos até aqui. Achou um bug meu, que já corrigi em O bug: cache negativo de 5 minutos quando um app falha. O gate era só Pior era a assimetria: um 5xx não marcava e tentava de novo na página seguinte, mas um 200 com todos os apps em erro congelava. Loja sem preset era a mais exposta, porque não tinha nada pra cair de volta — exatamente a loja sem fidelidade. Agora só marco o módulo como buscado quando nenhuma entrada do Uma consequência que fica registrada e eu não vou mexer: com "info persistida vence o preset", o HTML do SSR imprime o valor do CMS e a hidratação troca pelo valor real da API em toda navegação dentro do TTL. Numa loja com No eixo de performance não sobrou nada. Vale o registro de que o balanço ficou melhor que |
9543683 to
a20083d
Compare
|
Reescrevi a branch ( Por que Sobra TODO — content das lojasIndependente desta PR, vale preencher o programa de fidelidade no CMS das lojas que usam pontos.
"loyalty_points_program": { "id": null, "name": null, "ratio": null, "earn_percentage": null }Com id e ratio preenchidos, o preset passa a carregar o campo e o toggle aparece sem depender de resposta da API — inclusive antes desta PR. A tiasonia é o caso que bate exatamente com o perfil do bug (tem Isso não substitui o fix de código, e vice-versa: o content resolve a loja que for preenchida, o código resolve a loja que configurou fidelidade no app e não duplicou no CMS. Vale lembrar que essa duplicação manual é a mesma origem da divergência que comentei antes — CMS dizendo um número e app devolvendo outro. |
The request was skipped whenever module info wasn't empty, and the preset read from storefront settings also makes it not empty, so stores with a discount or installments configured never fetched `list_payments`, and fields that only exist on the response, such as `loyalty_points_programs`, never arrived. Skip based on where the info came from, but only for `list_payments`: it has three fields and the preset may fill just some of them. The storefront reads a single field from `calculate_shipping`, so a preset for it is already complete and its cache rules stay exactly as they were. A `__fetched` mark per module on the session payload carries the origin across page views. A response yielding no field is not marked, so it is requested again on the next page view instead of caching empty. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HtZi8sdmSDeQxrEeyzPJeS
a20083d to
1bd5eba
Compare
leomp12
left a comment
There was a problem hiding this comment.
Fiz o double-check de não-regressão no diff final (1bd5ebaa4) e está fechado. Resultado por módulo, comparando com main:
calculate_shipping — idêntico ao main em todos os caminhos. Sem preset e sem cache busca; com preset pula e emite; cache com dado pula; cache vazio busca. E o swap de hidratação que eu tinha registrado antes não se aplica mais aqui: loja com preset nunca busca esse módulo, então o cache nunca chega a conter um valor de API divergente do CMS. As regras de cache desse módulo ficaram intactas.
list_payments — exatamente uma célula muda. Loja com preset parcial saía de pula (o bug) para busca. Todo o resto preserva o comportamento anterior, incluindo o TTL de 5 min e o short-circuit por cache.
Também verifiquei: o delete seletivo continua removendo campo que veio só do cache e sumiu de preset+resposta, então não sobra dado obsoleto; apply_discount tem infoPreset indefinido, o que faz nextInfo === modInfo e reproduz o comportamento antigo; cache gravado no formato antigo (sem __fetched) degrada para "busca uma vez e regrava" sem quebrar; e __fetched/__timestamp saem do payload antes do Object.assign, sem colidir com nome de módulo.
Enxuguei também os comentários que eu tinha deixado — de 8 para 5 linhas, num arquivo que não tinha nenhum. Sobraram só os que protegem invariante que um refactor futuro quebraria sem perceber: a cópia do preset (que evita esvaziar window.$storefront.modulesInfoPreset) e a assimetria entre os dois módulos.
Aprovando com duas ressalvas explícitas: os commits que resolvem o bloqueante que eu mesmo levantei são meus, então quem valida o test plan é o @vitorrgg; e não rodei typecheck nem teste em nenhuma iteração — só ESLint, que passou em todas. modules-info.ts não tem cobertura e a validação foi por leitura e rastreamento de fluxo.
Fica o TODO do content das lojas registrado no comentário anterior, que é independente desta PR.
Summary
discount_optionconfigured in modules settings (e.g. PIX discount) hadmodulesInfo.list_paymentspre-populated from the presetfetchInfowas skipping the API call when any preset data existed, soloyalty_points_programsreturned by the loyalty app was never mergedwaitStorefrontInfo('list_payments', 'loyalty_points_programs')inPointsAppliernever resolved — loyalty points toggle was never renderedRoot cause commit: a3ab982
Fix: always fetch from the API; emit preset/cached data immediately as initial value while the API call completes.
Test plan
window.storefront?.info?.list_payments?.loyalty_points_programsis populated after checkout load