Skip to content

fix: use webhook variation data as fallback for importation queue - #174

Open
luizabrancaglion wants to merge 2 commits into
ecomplus:masterfrom
luizabrancaglion:fix/tiny-variation-queue-fallback
Open

fix: use webhook variation data as fallback for importation queue#174
luizabrancaglion wants to merge 2 commits into
ecomplus:masterfrom
luizabrancaglion:fix/tiny-variation-queue-fallback

Conversation

@luizabrancaglion

Copy link
Copy Markdown

Problema

Após 413b1aa, o guard de variação e o POST estão corretos — mas variações novas adicionadas no Tiny nunca chegavam a ser vinculadas ao produto pai no E-Com Plus.

Causa raiz: O bloco de enfileiramento (linha 268) verificava produto.variacoes, que vem do /produto.obter.php. Esse endpoint nunca retorna o array variacoes para produtos pai — a condição era sempre false, nada era enfileirado, o scheduledSync não processava e a variação nunca era vinculada.

O payload do webhook já carrega a lista completa de variações em tinyProduct.variacoes (via spread de dados), no formato não-encapsulado [{codigo, grade, ...}].

Correção

Resolve _variacoesQueue preferindo o resultado do /produto.obter.php (encapsulado, quando presente) e usando tinyProduct.variacoes normalizado como fallback. O forEach e toda a lógica downstream permanecem iguais.

Fluxo completo após este PR

  1. Webhook Tiny dispara com produto pai + variações novas
  2. PATCH do produto pai (nome/SKU intactos) ✓
  3. SKUs das variações são enfileirados em __importation.skus com ;:parentId ← este fix
  4. scheduledSync processa cada SKU → guard de 413b1aa detecta product.sku !== produto.codigo → POST /products/:id/variations.json → variação vinculada ✓

Como testar

  1. No Tiny, abrir um produto pai existente no E-Com Plus e adicionar uma variação nova
  2. Salvar — webhook dispara
  3. Aguardar até 4 min (próximo ciclo do scheduledSync)
  4. Conferir no E-Com Plus que a variação aparece vinculada ao produto pai
  5. Nos logs: #<storeId> POST /products/.../variations.json <sku-variacao>

Nota: Variações adicionadas antes deste deploy não são recuperadas automaticamente. Para reimportar, basta reabrir e salvar o produto pai no Tiny.

🤖 Generated with Claude Code

/produto.obter.php never returns variacoes for parent products, so the
queuing condition was always false and new variations added in Tiny were
never enqueued for scheduledSync to process.

tinyProduct.variacoes (from the webhook dados spread) already carries
the full variation list in unwrapped format. Use it as fallback,
normalizing to the wrapped format the forEach expects.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vitorrgg

Copy link
Copy Markdown
Member

Revisei o diff rastreando o caminho completo — routes/tiny/webhook.js → PubSub (lib/pubsub/webhook-tiny.js) → import-product.js → fila __importation.skus → trigger applications em routes/ecom/webhook.js → guard de 413b1aa — com foco em se a variação nova realmente chega vinculada ao pai e em efeitos colaterais de carga e concorrência. Aviso honesto: parte disso depende do contrato do Tiny (/produto.obter.php e corpo dos webhooks), que não consegui executar — marco em itálico o que depende de payload real. As referências de linha são do arquivo com o PR aplicado.

Resumo: o desenho do fix está certo e o caminho antigo fica intacto (quando produto.variacoes existe, o comportamento é idêntico). Mas o enfileiramento — que agora é o coração do fix — roda numa promise flutuante que ninguém aguarda, e pode ser perdido silenciosamente em produção; além disso o fallback re-enfileira todas as variações a cada webhook do pai, e não é defensivo contra os formatos que o próprio repo admite existir. Proposta de patch fechada no final, resolvendo os três sem mudar a estratégia.

⚠️ Não dá para validar isso por código. O repo não tem nenhum *.test.js/*.spec.js e nem o package.json raiz nem o functions/package.json têm script test (só lint, serve, deploy). QA manual é o único gate. Checklist no final.


Premissas do PR — o que consegui confirmar

  1. "/produto.obter.php nunca retorna variacoes para produto pai"não confirmei; a doc pública do Tiny documenta um nó variacoes nesse retorno, então "nunca" merece um payload real. Dois pontos a favor mesmo assim: o bug é empiricamente real (se o obter devolvesse, o código antigo teria funcionado), e se devolver em algum caso o fallback simplesmente não é usado — o PR vira no-op inofensivo. A correção depende do "às vezes não", não do "nunca".
  2. "tinyProduct.variacoes vem desencapsulado" — corroborado por dois lugares do próprio repo: a rota trata as duas formas (routes/tiny/webhook.js:47-51, variacao.id ? variacao : variacao.variacao) e o parser consagra a convenção "flat quando tipo === 'produto', encapsulado caso contrário" (parsers/product-to-ecomplus.js:178-181). Ou seja: o formato não é garantido nem de um lado nem do outro — normalizar as duas fontes é o correto (ver Menor 1).
  3. "O bloco de enfileiramento é alcançado" — confirmado, com uma condição não dita na descrição: o webhook chega com isHiddenQueue = true (lib/pubsub/webhook-tiny.js:99-128), cai na branch de import-product.js:317-318, e só alcança o produto.obter.php da linha 203 — e portanto o bloco da 268 — se update_product estiver habilitado; sem ele a linha 156 curto-circuita para PUT de estoque/preço. Ver Menor 5.

Confirmei também que não há loop de auto-enfileiramento: a variação enfileirada (X;:parentId), ao ser processada, retorna no POST da linha 238 antes de chegar na 268; e o dedupe da 290-293 segura o crescimento da fila. ✅


🔴 Crítico — o enfileiramento roda numa promise flutuante, depois da function já ter terminado

A estrutura é pré-existente, mas o PR a transforma no mecanismo central do fix, justo no caminho (PubSub) onde a janela de risco é maior.

O job devolvido ao handleJob é promise (import-product.js:310), que resolve quando o PATCH do produto completa. O enfileiramento (import-product.js:273-303getAppData + updateAppData, duas viagens HTTP ao Store API) roda numa cadeia paralela que ninguém aguarda:

  1. PATCH resolve → handleJob cai no ramo isNotQueued e chama queueEntry.cb(null, true) (lib/integration/handle-job.js:189);
  2. o cb resolve a promise do handler PubSub (lib/pubsub/webhook-tiny.js:103-106) → a Cloud Function é dada como concluída;
  3. depois disso a instância pode ser congelada — as duas requisições do enfileiramento, iniciadas no mesmo tick, podem nunca completar.

Cenário concreto: loja de baixo tráfego, instância fria. Lojista salva o pai no Tiny → webhook processado, PATCH ok, log PATCH /products/... aparece normalmente → instância congelada antes do updateAppData da linha 296 → __importation.skus nunca é gravado → variação nunca vinculada, sem nenhum log de erro. O sintoma fica idêntico ao bug que o PR quer corrigir, só que intermitente. Em teste (instância quente, tráfego contínuo) tende a passar sempre — é exatamente o tipo de falha que passa no QA e aparece em produção.

Agravante no mesmo bloco: se o enfileiramento falhar de verdade (Store API 5xx), o .catch da linha 304-307 loga e relança numa cadeia que ninguém consome → unhandled rejection no runtime Node 16. O throw err da linha 306 não tem efeito útil nenhum.

🟠 Alto — todo webhook do pai re-enfileira TODAS as variações, não só as novas

O fallback usa a lista completa tinyProduct.variacoes (import-product.js:268-271) e o dedupe da linha 290 compara só com o que já está na fila — nunca com as variações já vinculadas no E-Com Plus.

Antes do PR (aceitando a premissa 1) esse bloco nunca rodava no fluxo do webhook. Depois dele, cada save do pai no Tiny (título, preço, descrição — qualquer coisa que dispare tipo=produto) com update_product ativo enfileira as N variações. Cada item custa um ciclo completo do consumidor (routes/ecom/webhook.js:158-171, delayMs de 6s): produtos.pesquisa.php + produto.obter.estoque.php/produto.obter.php no Tiny (import-product.js:322,332,203), um PUT/POST no Store API, e mais um PATCH de app data para remover o item da fila — que dispara o próximo trigger.

Cenário concreto: pai com 60 variações, lojista ajusta só o título → 60 entradas na fila → ~120 chamadas ao Tiny em sequência → bloqueio por volume (codigo_erro 6 → 503 em lib/tiny/constructor.js:35-36) → queueRetry re-enfileira com throttle de 7s (handle-job.js:5-45) → a fila leva dezenas de minutos para drenar, atrasando qualquer importação legítima atrás dela. Não corrompe nada (as variações existentes caem no caminho barato de variationId → PUT de estoque), mas é custo recorrente proporcional a N, a cada save.

Não consegui confirmar se webhooks tipo=estoque do pai também carregam variacoes no dadoslib/pubsub/webhook-tiny.js:82-93 aceita tipo=estoque e faz ...dados no produto, então se o Tiny mandar variacoes aí, a amplificação passa a valer a cada movimentação de estoque, não só a cada edição. Vale capturar um payload real antes do merge.

🟡 Menores

  1. Normalização assimétrica — o caminho primário continua frágil. O fallback é normalizado, mas produto.variacoes do obter é usado cru (import-product.js:269-270). O guard vinte linhas acima já admite que o obter pode vir flat (import-product.js:213-216, v.variacao || v). Se vier flat, forEach(({ variacao }) => { const { codigo } = variacao }) (linhas 281-282) faz destructuring de undefined → TypeError → o job inteiro rejeita e um PATCH que já foi aplicado é reportado como erro. Normalizar as duas fontes custa zero.
  2. Itens sem codigo poluem a fila. O repo admite item com sku em vez de codigo (routes/tiny/webhook.js:39-43 e lib/pubsub/webhook-tiny.js:92, ambos codigo || sku), mas o fallback só lê codigo (linha 282). Item flat só com skuskuAndId = "undefined;:parentId" → o consumidor processa o SKU literal "undefined"SKU undefined não encontrado no Tiny (linhas 339-342) → item removido e re-enfileirado no próximo webhook: ruído perpétuo, e a variação real nunca vinculada. Item null no array quebra antes, em v.variacao (linha 271).
  3. Read-modify-write sem lock. getAppData → muta skusupdateAppData que PATCHa o objeto inteiro (linhas 274-302). Dois webhooks de pais diferentes em paralelo: ambos leem skus = [], A grava [a1,a2], B grava [b1,b2] → os de A somem. Pré-existente em todo o app; o PR só torna o caminho quente. Recuperável re-salvando no Tiny — registro e não bloqueio.
  4. A descrição do PR erra o mecanismo de consumo. Não é o scheduledSync que processa __importation.skus: sync-from-tiny.js só toca ___importation (três underscores — linhas 66 e 110-114) e só com update_quantity ativo (linha 33). Quem drena __importation.skus é o trigger applications do Store API disparado pelo próprio updateAppData (routes/ecom/webhook.js:92-95 e 127-171, onde as variantes _/__/___ são geradas dinamicamente nas linhas 128-132). Implicação prática, não só semântica: não há retry periódico nessa cadeia — se um elo falhar, a fila fica parada até o próximo write de app data. (O bloco de "poke" de sync-from-tiny.js:120-144 não cobre isso: além de olhar importation sem underscore, ele só é alcançado quando promises fica vazio, o que exige payload.produtos não-vazio com todos os itens sem codigo — na prática nunca.) Isso se soma ao Crítico: os dois pontos frágeis do fix estão exatamente nos elos sem retry.
  5. Requisito não documentado: update_product. O fix só é alcançado com "Sobrescrever produtos" ativo (ecom.config.js, default false): sem ele a linha 156 curto-circuita antes do obter, e mesmo que algo entrasse na fila seria descartado em import-product.js:110. Coerente com o desenho do app, mas nem a descrição nem o roteiro de teste mencionam — vale constar na nota de release.
  6. Ponto bom: a normalização v.variacao ? v : { variacao: v } (linha 271) espelha exatamente a defensiva já existente na rota e a convenção do parser; o guard if (_variacoesQueue.length) (linha 272) mantém o caminho antigo byte a byte igual; e o retorno no POST da linha 238 evita loop de auto-enfileiramento. A lógica está certa — as ressalvas são de robustez e custo.

Patch sugerido — mesmo objetivo, sem os três problemas

Tudo dentro do mesmo bloco (import-product.js:268-310). Substituindo:

                  // normaliza AS DUAS fontes: obter.php (encapsulado) e webhook (flat)
                  const parseVariacoes = (list) => (Array.isArray(list) ? list : [])
                    .map((v) => (v && v.variacao ? v.variacao : v))
                    .filter((variacao) => variacao && (variacao.codigo || variacao.sku))

                  const variacoes = parseVariacoes(
                    (Array.isArray(produto.variacoes) && produto.variacoes.length)
                      ? produto.variacoes
                      : (tinyProduct && tinyProduct.variacoes)
                  )

                  if (!variacoes.length) {
                    return promise
                  }

                  // ATENÇÃO: `product` aqui está sombreado pelo callback do parseProduct (linha 247);
                  // as variações JÁ cadastradas estão em `payload.product` (linha 131)
                  const currentSkus = (payload.product && Array.isArray(payload.product.variations))
                    ? payload.product.variations.map(({ sku }) => sku)
                    : []

                  // encadeado no job: o enfileiramento termina ANTES da function resolver
                  return promise.then((apiRes) => {
                    if (!productId) {
                      productId = apiRes && apiRes.response &&
                        apiRes.response.data && apiRes.response.data._id
                    }
                    const pendingSkus = variacoes
                      .map(({ codigo, sku }) => String(codigo || sku))
                      .filter((codigo) => {
                        // nunca enfileirar o próprio pai nem o que já está vinculado
                        return codigo !== String(produto.codigo) && !currentSkus.includes(codigo)
                      })
                    if (!pendingSkus.length) {
                      return apiRes
                    }
                    return getAppData({ appSdk, storeId, auth })
                      .then((appData) => {
                        let skus = appData.__importation && appData.__importation.skus
                        if (!Array.isArray(skus)) {
                          skus = []
                        }
                        let isQueuedVariations = false
                        pendingSkus.forEach((codigo) => {
                          const skuAndId = productId ? `${codigo};:${productId}` : codigo
                          if (!skus.includes(codigo) && !skus.includes(skuAndId)) {
                            isQueuedVariations = true
                            skus.push(skuAndId)
                          }
                        })
                        if (!isQueuedVariations) {
                          return null
                        }
                        return updateAppData({ appSdk, storeId, auth }, {
                          __importation: {
                            ...appData.__importation,
                            skus
                          }
                        })
                      })
                      // falha de enfileiramento não pode derrubar um PATCH que já foi aplicado
                      .catch(logger.error)
                      .then(() => apiRes)
                  })

O que cada pedaço resolve:

  • return promise.then(...) + .then(() => apiRes) → mata o Crítico. O updateAppData completa antes do cb resolver a function, e o payload devolvido ao handleJob continua sendo o mesmo objeto de resposta (importante: handle-job.js:184 testa se o valor resolvido é thenable, e log() usa esse payload). De quebra, o throw err órfão da linha 306 deixa de existir.
  • currentSkus + filtro → mata o Alto. Só entra na fila o que ainda não está vinculado; o save de rotina do pai passa a custar zero. Um detalhe importante: product.variations não serve aqui — dentro do callback da linha 247 o nome product está sombreado e aponta para o payload já parseado, não para o produto da loja. Tem que ser payload.product (o do destructuring da linha 131), que continua acessível no closure.
  • parseVariacoes aplicado às duas fontes, com .filter → mata os Menores 1 e 2: obter.php vindo flat não quebra mais, item null é descartado, e codigo || sku evita a entrada "undefined;:parentId".
  • codigo !== String(produto.codigo) → evita enfileirar o próprio pai. Sem isso, se o variacoes do webhook incluir o SKU do pai, ele entra na fila, é processado, faz PATCH no pai, e o bloco re-enfileira tudo de novo — moinho que só para pelo dedupe.

Não mexi na estratégia do PR: a fonte de dados, a normalização e o formato sku;:parentId continuam os mesmos.

Opcional, fora deste PR: o Menor 4 (nenhum retry periódico para __importation) merece um scheduledSync que cutuque a fila __importation.skus quando ela estiver parada há mais de um ciclo — hoje, se a cadeia de triggers quebrar num elo, os SKUs ficam encalhados indefinidamente.


✅ QA manual — obrigatório antes do merge

Sem cobertura automatizada, nada aqui se valida sem teste manual. Todos com update_product ("Sobrescrever produtos") ativo, salvo indicação:

# Cenário Como reproduzir O que verificar
1 Caso-alvo: variação nova Pai já no E-Com Plus; adicionar variação nova no Tiny e salvar __importation.skus ganha sku;:parentId; depois log POST /products/.../variations.json <sku>; variação vinculada. Decisivo: é o que o PR se propõe
2 Instância fria, sem tráfego Loja de teste sem nenhum outro evento; disparar o cenário 1 uma única vez e não gerar mais tráfego; conferir __importation.skus na app data 1-2 min depois Se skus ficar vazio com o PATCH do pai logado, é o Crítico se manifestando. Decisivo: é o que separa "funciona no teste" de "funciona em produção"
3 Pai com muitas variações, edição trivial Pai com 30+ variações já vinculadas; mudar só o título no Tiny e salvar Quantas entradas caem na fila; tempo até drenar; se aparece 503/codigo_erro 6 do Tiny e retries em integration_retries. Decisivo para o Alto
4 Webhook duplicado Salvar o pai duas vezes em <5s no Tiny Fila sem SKUs duplicados; nenhuma variação duplicada no produto; se SKUs de um dos webhooks sumiram (Menor 3)
5 update_product desativado Cenário 1 com "Sobrescrever produtos" off Nada entra na fila e nada é vinculado — esperado, mas precisa ser confirmado e documentado (Menor 5)
6 Payload com sku em vez de codigo Capturar o JSON real do webhook (log storeId: ... => em routes/tiny/webhook.js:25) numa conta com mapeamento/multiloja Se os itens de variacoes trazem codigo; qualquer entrada undefined;:... na fila é o Menor 2
7 Webhook tipo=estoque no pai Movimentar estoque de uma variação no Tiny Se o dados traz variacoes e a fila é repovoada a cada movimentação (agravante do Alto)
8 Regressão do caminho antigo Importação manual: adicionar SKU de produto-com-variações na fila visível (importation.skus) Fluxo antigo intacto — variações importadas como antes do PR

Os cenários 1, 2 e 3 são os decisivos: o 1 diz se resolve, o 2 se resolve de forma confiável, o 3 quanto custa.


Onde eu posso estar errado, e vale conferir antes de tudo: (a) um payload real do /produto.obter.php de produto pai — se ele devolver variacoes, o fallback é no-op e a causa raiz é outra; (b) o corpo real dos webhooks tipo=produto e tipo=estoque (chaves codigo vs sku, presença de variacoes no de estoque) — o Menor 2 e o agravante do Alto dependem disso.

Com o patch acima (principalmente o encadeamento do enfileiramento) e os cenários 1-3 verdes, não vejo impedimento para o merge.

Review gerada com Claude Code (modelo Fable), conferida linha a linha contra o código.

@vitorrgg

vitorrgg commented Aug 3, 2026

Copy link
Copy Markdown
Member

Follow-up: fui atrás das duas premissas que deixei em aberto. Uma delas não se sustenta — e isso muda o veredito.

Também corrijo abaixo dois pontos que eu escrevi a mais no comentário anterior.


🔴 A premissa central do PR é falsa: produto.obter.php retorna variacoes no produto pai

Duas fontes independentes, e as duas contradizem o "esse endpoint nunca retorna o array variacoes para produtos pai".

1. A documentação oficial da API v2, na página do produto.obter.php, sobre retorno.produto.variacoes[].variacao:

"Estes campos só estarão preenchidos quando o campo tipoVariacao for P."

Ou seja, a doc afirma que o array vem preenchido exatamente no caso de produto pai.

2. A API real. Chamei o endpoint numa conta Tiny de teste, num produto pai com 5 variações:

// produto.obter.php  →  retorno.produto  (tipoVariacao: "P")
"variacoes": [
  { "variacao": { "id": "", "codigo": "2581", "preco": 24.2,
                  "preco_promocional": 0, "grade": { "Cor": "Azul-claro" } } },
  // … 5 itens
]
tipoVariacao variacoes no retorno Array.isArray()
P (pai) array com 5 itens, encapsulado {variacao:{…}} true
V (variação) string vazia "" false
N (simples) string vazia "" false

Consequência direta: a condição antiga Array.isArray(produto.variacoes) && produto.variacoes.length não era sempre false — ela é verdadeira justamente no cenário-alvo. O ternário do PR cai sempre no primeiro ramo e o fallback _tinyVars é inalcançável ali. O patch é no-op no caso que se propõe a corrigir.

O bug relatado é real, mas o mecanismo descrito não o explica — então o commit, do jeito que está, grava no histórico uma causa raiz que a doc contradiz. Quem debugar isso daqui a seis meses vai partir de uma premissa errada.

✅ A outra premissa se confirma, e a normalização está correta

Baixei o exemplo oficial de payload (api-docs/files/webhook-produto.json, "Exemplo de produto pai"): variacoes vem flat, cada item com codigo, id, preco, estoqueAtual, grade. A normalização v.variacao ? v : { variacao: v } é coerente com a convenção que o próprio repo já codifica em parsers/product-to-ecomplus.js:78 (isProduct = tipo === 'produto') e :179-181.

Um detalhe que valida a decisão de extrair só o codigo: o grade diverge entre as duas fontes — array de {chave, valor} no webhook, objeto {"Cor": "Azul"} no obter. O parser trata os dois (:193-219), mas qualquer tentativa futura de montar a variação direto do payload do webhook depende disso. O PR não cai nessa armadilha.

↩️ Duas correções ao meu comentário anterior

  1. O agravante do webhook de estoque não procede. A doc do tipo=estoque lista só tipoEstoque, saldo, idProduto, sku, skuMapeamento, skuMapeamentoPainão há variacoes. A amplificação da fila que levantei só pode ocorrer em webhook tipo=produto, isto é, em edição de cadastro, não em giro de estoque. O achado 🟠 continua de pé, com alcance menor do que eu descrevi.
  2. O "Menor 2" fica mais fraco. No payload de produto os itens de variacoes trazem codigo; o sku é chave do payload de estoque. O cenário "undefined;:parentId" é bem menos provável nesse caminho — mantenho o .filter como defesa barata, mas não como achado.

🔍 O que os logs de produção mostram

Numa janela de ~25 min de logs das functions, nenhuma ocorrência do formato sku;:productId e nenhuma fila __importation/skus sendo processada — só ___importation (três underscores, a do scheduledSync). Amostra pequena e sem nenhuma adição de variação no período, então não prova nada sozinha; mas é consistente com o mecanismo de enfileiramento não estar disparando na operação normal.

Onde isso deixa o PR

Sugiro segurar o merge e reinvestigar a causa raiz antes de decidir o que entra. O patch atual não é perigoso — é inócuo no caminho descrito.

A hipótese que sobe para primeira posição é justamente o 🔴 do comentário anterior: se a condição do bloco de enfileiramento era verdadeira o tempo todo, então a explicação mais provável para "nada era enfileirado" é o updateAppData rodando na promise flutuante, depois do cb já ter resolvido a function — perda silenciosa, intermitente, sem log. Isso bate com o sintoma relatado e é independente deste PR.

Ordem que eu seguiria:

  1. Reproduzir o cenário 1 do QA numa loja com update_product ativo, olhando o log em tempo real: o bloco de enfileiramento é alcançado? O updateAppData completa?
  2. Se confirmar a promise flutuante, aproveitar esta branch: novo commit com o encadeamento (item 3 do patch que propus), descrição reescrita, e a discussão fica aqui mesmo.
  3. Se apontar outra coisa, fechar este PR e abrir o correto.

O encadeamento do enfileiramento vale por si só de qualquer forma — com ou sem o fallback, aquele bloco não deveria estar rodando fora do job.

Verificação feita chamando a API v2 real numa conta de teste e conferindo contra a doc oficial (tiny.com.br/api-docs).

@vitorrgg

vitorrgg commented Aug 3, 2026

Copy link
Copy Markdown
Member

Causa raiz encontrada — e não é o que este PR corrige

Rastreei duas reproduções reais em produção no Cloud Logging (uma de hoje, outra de 23/07) e baixei o código que está de fato rodando. O resultado fecha a investigação.

1. O enfileiramento funciona — a fila é populada e consumida corretamente

Sequência real, produto pai com 3 variações novas adicionadas no Tiny (SKUs anonimizados, PAI e PAI-1/2/3):

16:52:48.51  onTinyEvents  > Tiny webhook: #loja PAI
16:52:48.64  onTinyEvents  {"sku":"PAI","hasProduct":true}
16:52:48.89  onTinyEvents  PATCH /products/<pai>.json PAI 5 0        ← PATCH do pai, correto
16:52:55.02  appv2         > Starting #loja __importation/skus/PAI-1;:<pai>   ← A FILA FOI POPULADA
16:53:01.22  appv2         {"sku":"PAI-1","productId":"<pai>","hasProduct":true}
16:53:09.39  appv2         PATCH /products/<pai>.json PAI-1 5 0      ← ❌ PATCH no PAI com dados da VARIAÇÃO
16:53:10.18  appv2         {"__importation":{"skus":["PAI-2;:<pai>","PAI-3;:<pai>"]}}
16:53:25.08  appv2         PATCH /products/<pai>.json PAI-2 5 0      ← ❌
16:53:37.03  appv2         PATCH /products/<pai>.json PAI-3 5 0      ← ❌

O bloco de enfileiramento rodou, gravou __importation.skus no formato sku;:parentId, o trigger consumiu os três, um a um. Nada disso está quebrado. A reprodução de 23/07 tem a sequência idêntica.

O produto pai terminou com o SKU e o nome da última variação processada, sem nenhuma variations — é o sintoma que a Luiza descreveu.

Consequência para este PR: a condição Array.isArray(produto.variacoes) && produto.variacoes.length era verdadeira o tempo todo, como a doc do Tiny já indicava. O fallback é inalcançável. Confirmado agora pelos três lados: documentação, API real e runtime.

2. Por que o guard do 413b1aa não impediu a sobrescrita: ele não está em produção

Baixei o source deployado (generateDownloadUrl da API do Cloud Functions) de todas as functions do projeto:

valor
updateTime de todas as functions (gen1 e gen2, appv2 inclusive) 2025-07-25T20:36Z
lib/integration/import-product.js deployado 314 linhas
ocorrências de variations.json 0
ocorrências de product.sku !== produto.codigo 0
ocorrências de variacaoData 0

O código em produção é o da release 4.2.0, de julho de 2025. Nem o f67f8ae (09/07) nem o f7842ae (17/07) chegaram lá — e este PR, se mergeado, também não chegaria.

Os workflows Deploy de 2026-07-10 e 2026-07-23 aparecem como success no Actions, com o step "Run deploy" verde, mas as functions não foram atualizadas. Os logs desses runs já expiraram, então a causa do falso-positivo ainda não está determinada — as hipóteses mais prováveis são o secret FIREBASE_PROJECT_ID apontando para outro projeto, ou o client.deploy() do scripts/firebase-deploy.js resolvendo sem efeito (o FIREBASE_TOKEN/firebase login:ci foi descontinuado pelo Google, e functions.config foi removido do firebase-tools recente).

O que fazer

  1. Consertar o deploy primeiro. É o bloqueador de tudo: qualquer correção de código é inócua enquanto o pipeline reportar sucesso sem publicar. Vale um step de verificação no workflow que compare a versão publicada com a do commit.
  2. Redeployar o f7842ae e reproduzir o cenário. É bem possível que o bug relatado simplesmente desapareça — o guard existe justamente para transformar esses três PATCH em POST /products/<pai>/variations.json.
  3. Só então avaliar o que ainda falta. Com o guard rodando, dá para ver se o variacaoData é montado (o produto.variacoes de uma variação vem como string vazia no obter, então tudo depende do produto.grade — vale conferir com payload real).
  4. Fechar este PR. A premissa não se sustenta e o patch é no-op. A branch pode ser reaproveitada para o encadeamento do enfileiramento (o 🔴 do primeiro comentário), que continua valendo por si só, mas é uma mudança de escopo — melhor abrir outro PR depois que o deploy estiver saudável.

Nada disso tira o mérito de ter caçado o sintoma certo, @luizabrancaglion — o caminho que você mapeou (webhook → fila → vinculação) é exatamente o certo. O que não dava para ver do código era que a produção estava rodando outra versão.

Diagnóstico feito com Cloud Logging (janelas exatas das duas reproduções) e com o source deployado baixado da API do Cloud Functions. Identificadores de loja e produto omitidos por serem dados de lojista.

@vitorrgg

vitorrgg commented Aug 3, 2026

Copy link
Copy Markdown
Member

Complemento: refiz o deploy hoje (re-run do workflow no mesmo commit) para confirmar ao vivo a hipótese do comentário anterior. Ficou verde de novo e não publicou nadaupdateTime das functions inalterado em 2025-07-25T20:36Z e o import-product.js deployado seguindo com 314 linhas e zero ocorrências do guard.

Com o log fresco deu para ver onde morre: o step dura 3 segundos, não emite nenhuma linha do firebase deploy e não chega nem no .then nem no .catch do scripts/firebase-deploy.js. Abri a #177 com o diagnóstico completo e a proposta de correção do pipeline — o assunto é maior que este PR e ficaria enterrado aqui.

Isso mantém a recomendação: este PR não muda nada em produção enquanto o deploy não for consertado, e a premissa dele já estava descartada de qualquer forma.

Three bugs in parseProduct when tinyProduct.anexos is an empty array:
1. TypeError at line 289: images[0] is undefined when anexos=[] but
   variation.picture_id=0 was always set - adds null guard
2. picture_id: 0 sent to API even when no images exist - now only
   set when tinyProduct.anexos is non-empty
3. variation.price set to NaN (null in JSON) when preco is undefined
   (Tiny v1.0.1 webhook variacoes) - added isNaN guard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

2 participants