Conversation
Porta o app Bling ERP (app_id 102418) do repositório app-bling-erp-v2 para
o monorepo, usando a API v3 do Bling: exportação de pedidos e produtos,
importação de estoque, pedidos e categorias, e renovação automática dos
tokens OAuth.
Funções: `blingerp-onStoreEvent` (eventos da loja), `blingerp-callback`
(callbacks de estoque/pedidos do Bling), `blingerp-authCallback` (fluxo de
autorização OAuth) e `blingerp-cronRefreshToken`.
A fila do app v1 em Firestore (`queue/{storeId}/events` + `running_events`)
foi substituída pelo PubSub de eventos do monorepo, e o `appSdk` multi-loja
pelo `@cloudcommerce/api`. Tokens ficam em `blingTokens/{storeId}` e o cache
de situações de venda em `blingStatuses/{storeId}`.
Correções sobre o comportamento do app v1:
- atualização de produto com variações falhava com 400 no Bling, porque as
variações eram enviadas sem ID (a listagem `/produtos?codigo=` devolve o
produto resumido);
- preço por variação era perdido na exportação (o Bling aplica o preço do
produto pai), agora corrigido com PUT por variação divergente;
- callback de estoque de variação sem SKU no Bling era descartado, agora usa
o ID do Bling como referência;
- configuração "Importar produto" não tinha efeito, pois o callback forçava
`canCreateNew: false`;
- status "Devolvido" era enviado como fulfillment inválido;
- limite diário da API gravava a flag invertida, liberando novas chamadas;
- grades de variação importadas viram `size`/`age_group`/`gender` como no
sentido inverso, em vez de slug do rótulo;
- sem situação correspondente no Bling, o pedido não falha mais: registra
aviso e segue exportado.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Erros em exportações disparadas por evento da loja (não pela fila manual) só apareciam no log do Cloud Functions, ficando invisíveis para o lojista no painel. Sucessos de importação continuam fora do log para não inundá-lo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🤖 Revisão adversarial —
|
Pedido devolvido era enviado ao Bling como "aprovado", então o estoque não retornava e a nota fiscal não era cancelada; agora vai como "cancelado", no mesmo padrão do tiny-erp. O callback público aceitava requisições sem token quando nenhum estava configurado, permitindo importação forjada de pedidos e produtos na loja; agora exige token e rejeita quando não há nenhum configurado. Inclui testes de regressão para os dois casos. O round-trip que regride "entregue" para "nf emitida" em contas Bling padrão fica marcado como todo, pois o conserto pertence ao import. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Um refresh concorrente (cron e callback ao mesmo tempo) fazia o perdedor da corrida receber invalid_grant e gravar isBloqued, desativando a integração da loja até re-autorização manual, mesmo tendo havido um refresh bem-sucedido ao lado. O Bling rotaciona o refresh_token a cada uso, então essa corrida é esperada. Agora, ao falhar, o doc é relido e o token renovado por outro processo é reusado; só bloqueia em invalid_grant genuíno sem refresh concorrente. A contagem de erros transitórios passa a usar FieldValue.increment, evitando a corrida de read-then-set. A decisão fica isolada num módulo puro, com testes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contas Bling padrão não têm situações de envio/entrega, então "enviado" e "entregue" colapsam em "Atendido", que volta como nf emitida. O pedido regredia de "entregue" para "nf emitida" a cada importação. Agora o import ignora a transição para trás dentro da esteira de fulfillment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A exportação comparava a quantidade da loja com saldoVirtualTotal (virtual, somado de todos os depósitos), mas ajusta o saldo físico e a importação lê o saldo do depósito configurado. Em lojas com reserva ou múltiplos depósitos isso movimentava o estoque a cada exportação ou deixava de corrigir o físico. Agora compara contra o saldo físico do depósito usado, a mesma base do import. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adiciona um job que roda `pnpm install --frozen-lockfile` em PRs que mexem em qualquer package.json ou no lock. Hoje o CI instala com --no-frozen-lockfile e mascara um lock desatualizado; este check falha em vez de regenerar em silêncio. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…urado O fail-closed anterior rejeitava toda loja que nunca configurou callback_token (campo opcional, não auto-gerado). Como o corpo do callback só traz identificadores e os handlers re-buscam o dado no Bling autenticado, o risco de um callback forjado é apenas disparar importação dos próprios dados da loja (sem injeção). Não justifica derrubar lojas em produção, então volta ao comportamento anterior: exige token apenas quando a loja configurou um. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A exportação comparava o estoque sempre pelo saldo físico, mas a importação usa o saldo virtual quando a loja tem reserva de estoque (has_stock_reserve). Lojas com reserva divergiam em toda exportação, sobrescrevendo o físico do Bling com o virtual. Extrai parseStockFromDeposits para um helper único usado pelos dois lados, garantindo a mesma base (virtual/físico e soma por depósito). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…estore A releitura do doc de tokens no tratamento de erro do refresh não tinha catch: uma falha do Firestore substituiria o erro original e pularia a gravação de estado. Envolve em catch devolvendo undefined, preservando a decisão. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🔄 Status atualizado — fixes aplicados + re-review adversarial do deltaSeguindo a revisão adversarial acima, os 6 Criticals foram tratados e, depois, rodei um segundo review adversarial focado só nos commits de fix (para pegar regressão que o próprio fix pudesse introduzir — o autor tem ponto cego sobre o próprio código). Esse re-review pegou 2 regressões nos meus fixes e corrigiu um exagero do review original — tudo já remediado. Estado verificado abaixo. Placar final dos Criticals
O que o re-review encontrou (e como foi resolvido)
Pendências (não-Critical)
VerificaçãoTodos os fixes verificados com build real ( Fixes e re-review gerados com Claude Code (Opus). O re-review do delta rodou 3 agentes adversariais sobre os commits de fix — pegou C6 e C2 antes do merge. |
leomp12
left a comment
There was a problem hiding this comment.
Revisei o port inteiro contra o app publicado (app-bling-erp-v2) e contra o tiny-erp como integração de referência. Antes dos problemas, o que está certo e não precisa ser revisitado:
As dez correções sobre o v1 são reais — conferi as que dava para conferir por código, e a de variação sem variacoes no PUT e a do outher_config (com fallback pro nome errado, migração bem feita em get-customer-bling.ts:13-14) são achados de quem foi atrás do comportamento, não de quem só transcreveu. Os testes são ganho de padrão: o tiny não tem nenhum, e o teste unitário de função pura offline não existia em app nenhum do monorepo — os irmãos só têm e2e atrás de credencial. A config compartilhada está limpa: nenhum appId duplicado entre os 31 apps, o nome exportado não colide, e o merge com main resolve automático. E as duas rodadas adversariais que você rodou pegaram coisa de verdade — os achados abaixo são o que sobrou depois delas, quase tudo em caminho sem cobertura de teste.
Também confirmei que after-bling-queue, order-to-bling, get-customer-bling, get-products-bling, payment-method e product-to-bling são ports fiéis, e que o canCreateNew tri-state, os nomes de config e o gate de preço/quantidade reproduzem exatamente o webhook.js:92-121 do app publicado. Nada disso é para mexer.
O que trava são quatro coisas, e as duas primeiras se compõem.
🏗️ Arquitetural — o lockfile.yml não pertence a esta PR
O d2645980f adiciona .github/workflows/lockfile.yml, que não tem relação nenhuma com o Bling. Três motivos para sair:
- Ele falha na própria PR que o introduz —
ERR_PNPM_OUTDATED_LOCKFILE ... not up to date with <ROOT>/packages/apps/bling-erp/package.json. Sobe vermelho por desenho. - O filtro de bot não funciona em PR. O
iftestagithub.event.head_commit.author.name, que é campo de evento push; empull_requesté nulo, ocontainsdá falso e o job roda assim mesmo. - Ele briga com o ciclo do renovate que este repo já aceita. Todo PR do renovate merga com os 20 importers de loja fora do lock, e o
chore: Fix package versions and submodules post-releaserestaura depois — o histórico do lock mostra isso em35e33f14e(#798) ed47f33902(#786). O workflow transforma um processo aceito em falha permanente de CI.
Some-se que ele usa actions/checkout@v7 sem submodules:, então validaria o lock contra uma árvore onde os 20 importers não existem em disco. A ideia é boa e eu quero ela — mas em PR própria, com o filtro corrigido e a decisão sobre submódulo explícita.
🔴 Bloqueante — o estoque anda para baixo até zerar, em loja com qualquer reserva
parse-stock-from-deposits.ts:1-6 documenta a invariante:
Usado pelos DOIS lados (importação e exportação) para que a base de comparação nunca divirja
Só que o call site da importação a pula justamente na configuração padrão. import-product-from-bling.ts:35-53:
if (typeof blingItem.estoqueAtual !== 'number'
&& typeof blingItem.estoque?.saldoVirtualTotal === 'number') {
blingItem.estoqueAtual = Math.max(0, blingItem.estoque.saldoVirtualTotal); // vira número aqui
}
if (Array.isArray(blingItem.depositos)
&& (blingDeposit || typeof blingItem.estoqueAtual !== 'number')) { // ← falso com bling_deposit vazio
blingItem.estoqueAtual = parseStockFromDeposits(...);
}Com bling_deposit vazio e has_stock_reserve desligado — o padrão — a importação grava o saldo virtual e a exportação compara contra a soma do físico (export-product-to-bling.ts:194-196, que sempre chama parseStockFromDeposits). Havendo qualquer pedido reservando estoque, virtual < físico e as duas bases nunca batem.
A catraca, com físico 20 e 5 reservados:
| passo | Bling físico | reservado | virtual | loja |
|---|---|---|---|---|
| import | 20 | 5 | 15 | 15 |
export (operacao: 'B') |
15 | 5 | 10 | 15 |
| import | 15 | 5 | 10 | 10 |
| export | 10 | 5 | 5 | 10 |
E assim até zerar. O motor que roda o ciclo é o bloqueante seguinte.
Colateral do mesmo ponto: com bling_deposit vazio a comparação soma todos os depósitos, mas a escrita vai só para depositos[0].id (:180). Em conta multi-depósito a soma nunca converge e o depósito 0 é sobrescrito com o total da loja.
O C6 (tests/parse-stock-from-deposits.test.mjs:26) afirma exatamente essa invariante e passa — porque testa o helper, não o call site. É o que faz o CI ficar verde em cima disso.
🔴 Bloqueante — o app não marca as próprias escritas, e o evento volta para ele
A plataforma tem supressão de auto-evento, e é central: EVENT_SKIP_FLAG = '_skip' (packages/firebase/src/const.ts:1), e o poller consulta a API com 'flag!': EVENT_SKIP_FLAG (check-store-events.ts:134), então evento marcado nunca chega a app nenhum. O updateAppData usa (update-app-data.ts:55), inclusive publicando direto no tópico antes, para a fila andar sem depender do poller.
O import de estoque não usa:
// import-product-from-bling.ts:75-79
endpoint += '/quantity';
// @ts-ignore
return api.put(endpoint, quantity); // ← sem X-Event-FlagE o app assina products-quantitySet (config.ts:154), que o tiny não assina. Então todo callback de estoque do Bling grava na loja, o evento volta, e event-to-bling.ts:79 reexporta pro Bling. Custo por callback recebido, ainda que a catraca acima não estivesse lá: +2 leituras de Firestore, +4 requisições ao Bling, +1 PATCH e +4 s de throttle. Dobra o custo de toda sincronização de estoque.
Vale notar que nenhum app de packages/apps/ usa o flag em escrita de recurso — o tiny tem a mesma omissão, mas não assina evento de estoque, então nunca dispara. O Bling é o primeiro a materializar.
Marcar a escrita fecha os dois bloqueantes de uma vez: sem o eco, a divergência de base do anterior deixa de ser realimentada. Ainda assim eu alinharia as bases, porque a divergência sozinha já produz um POST /estoques desnecessário por exportação.
🔴 Bloqueante — o callback lê o próprio segredo do documento que o chamador escolhe
bling-callback.ts:34-49:
const applicationId = req.query._id; // ← escolhido pelo chamador
const appEndpoint = applicationId && typeof applicationId === 'string'
? `applications/${applicationId}`
: `applications/app_id:${appId}`;
const application = (await api.get(appEndpoint)).data;
const appData = { ...application.data, ...application.hidden_data };
const callbackToken = process.env.BLINGERP_CALLBACK_TOKEN || appData.callback_token;
if (callbackToken) {
if (req.query.token !== callbackToken) { res.sendStatus(401); return; }
}Sem BLINGERP_CALLBACK_TOKEN no ambiente, um ?_id=<outro app instalado na loja> carrega um doc sem callback_token, o if não entra e a requisição segue sem autenticação. A partir daí:
:76— no erro,afterQueue(queueEntry, appData, application, err)recebe oapplicationque o atacante escolheu.after-bling-queue.ts:76— o callback usaisNotQueued: trueeaction: 'importation', então a condição reduz aisErrorpuro. E o erro é garantido, porque o app escolhido não tem credencial Bling.- Resultado: escrita não autenticada no
hidden_datade um documento de aplicação arbitrário da loja — até 200 entradas,notesde até 5000 caracteres cada, e ologs.unshiftempurra para fora os logs reais do app vítima. O laço de:81-96itera sobrepedidos/estoquesdo corpo da requisição, então o atacante controla quantas escritas por request.
O BLINGERP_CALLBACK_TOKEN neutraliza tudo — mas a action.yml não tem input para ele. Tem tinyerp-token → TINYERP_TOKEN (:57-58,307,365) e nenhum equivalente Bling, então no caminho oficial de deploy ele só entra via custom-dotenv genérico. O estado padrão do deploy é o vulnerável, e o README.md:28-33 manda definir a variável sem que exista o caminho para isso.
Duas correções, e as duas são pequenas: resolver sempre por applications/app_id:${appId} (o fallback que já está lá), e adicionar o input na Action.
🔴 Bloqueante — erro sem .response descarta o item da fila do lojista
create-access.ts:50 lança Error puro para o limite diário do Bling. O contrato de retry de after-bling-queue.ts:38 só entra se payload.response existir; sem isso cai no else (notes = payload.stack) e segue direto para o splice incondicional de :92-108, que remove o id da fila.
O irmão resolve no produtor, não no consumidor — post-tiny-erp.ts:44-56:
if (tinyErrorCode <= 2) response.status = 401;
else if (tinyErrorCode === 6) response.status = 503;
else if (tinyErrorCode === 20) response.status = 404;
const error: any = new Error(...);
error.response = response; // ← é isto que faz o gate do consumidor funcionarO port copiou o consumidor literalmente e não portou o contrato do produtor que o sustenta. Consequência: quando o limite diário estoura — evento esperado, não excepcional, e que se auto-limpa em 12h — a exportação manual do lojista perde itens em silêncio. E vale para todo estado terminal novo que bling-auth/ vier a lançar.
A correção certa é o client.ts normalizar os próprios erros como o post-tiny-erp faz, não somar mais um else if no after-bling-queue.
🟠 Estruturais
Variação criada nasce com estoque 0 e não se corrige. export-product-to-bling.ts:207-218 lê newVariations de responseData?.variations?.saved || responseData?.variacoes; variations.saved é shape que não existe na v3 (herdado quebrado do legado) e o POST /produtos responde só com o id. Com newVariations vazio, isUpdateStockVariation nunca dispara — e com export_quantity desligado o produto fica zerado para sempre.
O rastreio real é descartado. bling-callback.ts:83 desestrutura só { numero } do corpo do callback. O legado guardava transporte/codigosRastreamento na entrada da fila e fundia no pedido relido, com comentário explícito de que GET /pedidos/vendas/{id} nunca devolve urlRastreamento. Sem isso, todo rastreio importado recebe o link genérico do Melhor Rastreio, e volume que o Bling só expõe com urlRastreamento não gera rastreio nenhum (order-from-bling.ts:25 sai cedo). É regressão funcional contra o app publicado.
O smoke script mata a integração da loja. scripts/bling-smoke.mjs:31-50 se anuncia como somente leitura, mas a primeira coisa que faz é grant_type=refresh_token — e o próprio PR documenta que o Bling rotaciona o refresh token a cada uso. O README.md:56-62 manda rodar com o token de "uma loja já autorizada". Feito isso, o próximo refresh recebe invalid_grant, decideRefreshFailure não vê updatedAt novo e vai para isBloqued: true — integração morta até re-autorização manual.
A política de bloqueio está em dois lugares que discordam. check-enable-api.ts:17 usa janela de 24h; create-access.ts:36 usa 12h. E só o createAccess tem o ramo que limpa a flag — o gate roda antes (event-to-bling.ts:92, bling-callback.ts:55) e retorna false, então na trilha de evento e de callback nada destrava; só o cron. Entre 12h e 24h a integração fica escura enquanto a política já liberou. Vale notar que isso é comportamento novo: no legado o ramo gravava isRateLimit: false, então a flag nunca foi persistida em produção.
Sem camada de reconciliação. O único cron é o refresh de token, e a recuperação é o setTimeout(reject) dentro da janela de eventMaxAgeMs = 60000 — e só para item isQueued, porque o gate conflaciona "erro transitório" com "veio da fila manual". Evento automático não tem retry algum. O tiny pareia o mesmo handler com cronSendOrders varrendo pedidos pendentes a cada 3h. Composto com os dois itens acima, o caminho de volta à consistência é o lojista reenfileirar na mão.
O dedupe de retry sumiu sem substituto. O legado mantinha integration_retries/{...} com janela de 5 min. Restou o redelivery do PubSub, e como o splice só acontece no fim do afterQueue, a janela de duplo processamento é o handler inteiro. POST /estoques é idempotente por usar operacao: 'B'; POST /pedidos/vendas não é.
O throttle tem o escopo invertido. client.ts:33 guarda lastRequest em campo de instância e createBlingClient() é chamado dentro de cada handler. No legado isso era correto (um processo, N contas, limite por conta); numa instância por loja o escopo certo virou nível de módulo. Como está, checkTime curto-circuita na primeira request de toda invocação e não espaça chamadas concorrentes — as Promise.all de :187-228 calculam o mesmo atraso e disparam juntas contra um limite de 3 req/s.
Chaves de Firestore por storeId num projeto de uma loja. blingTokens/{storeId} e blingStatuses/{storeId} são os únicos documentos do monorepo particionados assim, e o storeId vem de ECOM_STORE_ID, constante de deploy. A convenção aqui chaveia pelo que varia — paypalTokens/${PAYPAL_CLIENT_ID}, pixSetup/${clientId}:${clientSecret}. Não é cosmético: o PayPal ainda deleta o doc no 401, porque a chave carrega a identidade da credencial. Aqui a chave é constante, então trocar client_id/client_secret nas configurações não invalida nada — o refresh token velho vai com credencial nova, dá invalid_grant e vira isBloqued. Rotação de credencial fica indistinguível de autorização revogada.
Terceira cópia do upload para a Storage API. try-image-upload.ts:16 tem ecomAccessToken module-scoped que nunca revalida; expirado em instância quente, todo upload baixa a imagem inteira, falha 401 e cai no fallback que hotlinka a URL do Bling para sempre, sem sinal de degradação. As outras duas cópias estão em tiny-erp/.../product-from-tiny.ts:26-64 e cli/src/ext/import-feed.ts:50. E os uploads são sequenciais (product-from-bling.ts:280-283) numa função sem timeoutSeconds explícito, ou seja 60s — produto com muitas imagens estoura e o import inteiro é descartado.
Trabalho pago antes de saber se é necessário. import-product-from-bling.ts:186-223 busca preço multiloja e importa categoria antes de descobrir que o caminho é isStockOnly — que é o de todo callback de estoque em loja sem update_product. export-order-to-bling.ts:74-77 busca /formas-pagamentos antes de saber se o pedido será criado, desperdiçando 1-2 chamadas em cada mudança de status de pedido já exportado. E export-product-to-bling.ts:100-104 refaz um GET /produtos/{id} idêntico em toda exportação de produto simples, porque a guarda não distingue resposta de listagem de resposta de detalhe. Tudo isso contra a mesma cota diária que o app tem uma máquina inteira para sobreviver.
Todo erro automático vira escrita de até ~1 MB. after-bling-queue.ts:76 inverteu a condição do tiny para logar também falha de evento automático — decisão deliberada e documentada, mas quem chega ali são os erros persistentes (os 429/5xx retornam antes). Em modo de falha estável, cada callback gera um PATCH de até 1 MB, em série, numa função maxInstances: 1.
🟢 Minors
client.ts:61—Promise<any>onde o TS infeririaPromise<AxiosResponse>; é a única superfície pública do contrato Bling e ~30 call sites fazem.data.datasem checagem. Uma linha tipa todos.bling-callback.ts:63—handler: anynorunQueueEntry, que é o segundo entrypoint de fan-out para os mesmos handlers; chama com 5 argumentos, masimport-order-from-bling.ts:22-26declara 3. Um tipoIntegrationHandlercobriria os dois.order-from-bling.ts:10— retornaRecord<string, any>enquanto o parser irmãoproduct-from-bling.ts:114retornaProductSet;OrderSetestá exportado no mesmo@cloudcommerce/types.export-product-to-bling.ts:10—getBlingStockBalances(bling: any)enquanto 6 helpers do pacote importam o tipo do client.order-from-bling.ts:91-93—else if (invoiceIndex && ...): índice 0 é falsy, então o caso normal nunca recebe o back-fill; e o branch não setashipping_lines, então nem persistiria. Morto nas duas pontas.order-from-bling.ts:96-108—/notafiscal/{numero}/{serie}é endpoint v2 sobbaseURLv3: 404 sempre, engolido pelo.catch(() => null). Dívida herdada, mas promete uma feature que não funciona.scripts/tests.sh:10-13—exit 1semlib/, enquanto todotests.shirmão sai 0. Comturbo.json:19-21declarandotestsemdependsOn,pnpm test:appsnum checkout limpo derruba o fan-out. Raiz:"test": { "dependsOn": ["build"] }.tests/*.test.mjs— importam de../lib/**, a saída de build, em vez do fonte; acopla a suíte aobuild-lib.sh.tests/decide-refresh-failure.test.mjs:52— 2 erros de eslint e 3 warnings demax-len. Nada no repo linta.mjs, então é o primeiro a normalizar o resto.- Comentários misturam português e inglês dentro do mesmo pacote (PT em 7 arquivos, EN em quantidade parecida); nos outros 30 apps não há comentário em PT.
- Prefixos
[STOCK]/[PRICE_MULTILOJA]/[CATEGORY_IMPORT]em log são um terceiro dialeto — o repo usa>/>>e structured logging no 2º argumento, que vira label filtrável no Cloud Logging. describedos testes prefixado porC1/C4/C5/C6, que referenciam um documento fora do repo, e em PT enquanto os outros dois arquivos da mesma suíte estão em inglês.guard-fulfillment-transition.tsexportashouldAdvanceFulfillment— é o único helper cujo nome de arquivo não casa com o símbolo.payment-method.ts:16,47—getPaymentBlingexportado named e default.order-to-bling.ts:6-13— feriados hardcoded cobrindo só 2026-2027; em jan/2028 odataPrevistadegrada sem log e sem teste que falhe.bling-auth-callback.ts:40gravaexpiredAtcomexpires_in - 3600ecreate-access.ts:73comexpires_in - 300— dois escritores do mesmo campo discordando em 55 minutos.- Crontab
'36,51 * * * *'para token de ~6h: ~46 execuções no-op/dia, cada uma com umgetAppDatae uma leitura de Firestore. Veio verbatim de um fan-out multi-tenant onde os dois minutos ímpares faziam sentido. get-products-bling.ts:5-13— montacodigo[]=com todos os itens do pedido semlimite/paginanem chunking; a v3 pagina em 100.- Falta
CHANGELOG.md— é o único dos 31 apps sem, e o__skeletonjá traz um.
Pra entrar antes do merge
- Marcar o
api.put(.../quantity)comX-Event-Flag: _skipe alinhar a base de estoque entre importação e exportação. As duas juntas fecham a catraca; a primeira sozinha para a realimentação, mas a divergência continua gerandoPOST /estoquesdesnecessário. - Resolver o callback sempre por
applications/app_id:${appId}e adicionar o input deBLINGERP_CALLBACK_TOKENnaaction.yml. - Normalizar os erros no
client.tspara a forma que oafter-bling-queuesabe classificar, como opost-tiny-erpfaz. - Tirar o
lockfile.ymlpara PR própria. - Corrigir o
newVariationsdas variações criadas, e o aviso do smoke script no README — ou fazer o script parar de dar refresh.
Os 🟠 restantes dão para tratar em follow-up, mas queria tua leitura sobre quais viram issue antes do merge; nenhum deles tem issue aberta hoje, nem os seis follow-ups de packages/modules que você listou no corpo.
Uma ressalva de método: não rodei a suíte localmente (exige pnpm build antes, e o CI já a cobre verde), e o caminho de imagem, o OAuth e os callbacks não têm cobertura nenhuma — a validação deles foi leitura e rastreamento de fluxo, mais comparação com o app publicado. Os quatro bloqueantes eu conferi na fonte um por um antes de escrever.
Marca as escritas de importação na Store API com `X-Event-Flag: _skip` (mesmo flag do polling de eventos), fechando o loop importação -> evento -> exportação de volta ao Bling, que dobrava o custo de toda sincronização de estoque e reexportava status de pedido recém-importado. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…servados A importação passa a ler a quantidade sempre de `depositos[]` via `parseStockFromDeposits`, a mesma base que a exportação compara, em vez do `saldoVirtualTotal` quando não há depósito configurado: bases divergentes faziam o estoque descer a cada ciclo até zerar. Também deixa de exportar estoque em conta multi-depósito sem `bling_deposit` configurado (a soma nunca converge e sobrescreveria o primeiro depósito com o total da loja), e relê o produto criado no Bling para inicializar o estoque das variações — o `POST /produtos` da API v3 responde só com o id, então `newVariations` ficava sempre vazio. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ário O documento da aplicação é sempre resolvido pelo `app_id` fixo do Bling: aceitar `?_id=` da query permitia ao chamador escolher o doc de outro app instalado (possivelmente sem `callback_token`), pular a autenticação e fazer o log de erro da fila gravar no `hidden_data` do app escolhido. Também adiciona o input `blingerp-callback-token` na GitHub Action de deploy, que era o caminho que faltava para definir a variável `BLINGERP_CALLBACK_TOKEN` recomendada no README. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… Bling estoura Os erros de estado da autenticação são normalizados na forma que o `after-bling-queue` sabe classificar, como o app Tiny faz no `post-tiny-erp`: limite diário vira status 429 (mantém o item na fila para retry via redelivery em vez de removê-lo em silêncio) e token inválido/não autorizado vira `isConfigError` com mensagem clara. Alinha também a janela de rate limit do `checkEnableApi` (24h) com a do `createAccess` (12h), que é quem limpa a flag — a janela maior deixava a integração parada esperando o cron mesmo com a política já liberada. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s concorrentes O controle de intervalo entre requisições vai para o nível de módulo: como uma instância do client é criada dentro de cada handler, o campo de instância nunca espaçava nada, e chamadas concorrentes (`Promise.all`) calculavam o mesmo atraso e disparavam juntas contra o limite de 3 req/s. Cada chamada agora reserva o próximo slot de 1s. Tipa também o retorno do client como `AxiosResponse` em vez de `any`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
O script passa a aceitar `BLING_ACCESS_TOKEN` direto (do doc do Firestore, válido ~6h) como caminho preferido, sem refresh: o `grant_type=refresh_token` rotaciona o token, e rodado com o refresh token de uma loja ativa bloqueava a integração no próximo refresh (`invalid_grant` -> `isBloqued`). O fluxo com refresh continua aceito, com aviso explícito no script e no README para gravar o novo token de volta. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
O workflow não tem relação com o Bling, falhava na própria PR que o introduz e o filtro de bot não funciona em `pull_request`. Vai para PR própria com o filtro corrigido e a decisão sobre submódulos explícita. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… Bling
O corpo do callback é a única fonte de `transporte`/`codigosRastreamento`
com `urlRastreamento` — o `GET /pedidos/vendas/{id}` nunca o devolve — e
estava sendo descartado: todo rastreio importado recebia o link genérico
do Melhor Rastreio, e volume só com URL não gerava rastreio nenhum. Os
dados do callback agora seguem na entrada da fila e são fundidos no
pedido relido da API, como o app publicado fazia.
Também corrige o back-fill da chave de acesso da nota em índice 0 (o
`else if (invoiceIndex && ...)` nunca rodava para o caso normal e não
persistia), remove o endpoint `/notafiscal` da API v2 que sempre
respondia 404 sob a baseURL v3, e tipa os handlers de integração dos
dois entrypoints com `IntegrationHandler`.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Os docs `blingTokens` e `blingStatuses` passam a ser chaveados pelo `client_id` (a identidade da credencial, como `paypalTokens`), não pelo `storeId`, constante no projeto: trocar `client_id`/`client_secret` nas configurações deixava o refresh token antigo ser usado com a credencial nova, resultando em `invalid_grant` e integração bloqueada — rotação de credencial ficava indistinguível de autorização revogada. Unifica também o desconto do `expiredAt` entre os dois escritores (autorização gravava -3600s e refresh -300s) numa constante única. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
O token da Storage API era module-scoped e nunca revalidado: expirado em instância quente, todo upload baixava a imagem inteira, falhava com 401 e caía para sempre no fallback que hotlinka a URL do Bling. Agora o token tem TTL de 30min e o upload é retentado uma vez após 401. A função de eventos ganha `timeoutSeconds: 300` (o padrão de 60s estoura em produto com muitas imagens, baixadas e reenviadas sequencialmente) — com o repasse de `timeoutSeconds` adicionado ao `createPubSubFunction` do pacote firebase, sem mudança de padrão para os demais apps. O cron de refresh do token cai de 2x para 1x por hora, suficiente para a janela de renovação de 70min de um token de ~6h. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…am o resultado
- Importação só de estoque pula preço multiloja e categoria, que não
seriam usados no `PUT .../quantity`;
- Mudança de status de pedido já exportado não busca mais
`/formas-pagamentos` (fica para quando o pedido vai ser criado);
- Exportação de produto simples não repete o `GET /produtos/{id}`
quando a resposta de detalhe já foi carregada;
- Falha persistente repetida no callback não regrava o mesmo log
(cada ocorrência virava um `PATCH` de até ~1MB no `hidden_data`);
- Busca de produtos por SKU pagina em lotes com `limite` explícito
(a listagem da API v3 corta em 100).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- `getPaymentBling` com um export só (era named e default); - Aviso quando a tabela de feriados hardcoded (2026-2027) expirar, em vez de degradar o `dataPrevista` em silêncio; - `describe` dos testes sem os prefixos C1/C4/C5/C6 (referência a documento fora do repo) e em inglês como o resto da suíte; - Lint limpo nos `.mjs` de teste; - Helper renomeado para `should-advance-fulfillment` casando com o símbolo exportado; - `turbo.json` com `test` dependendo de `build`, então `pnpm test:apps` funciona em checkout limpo (o `tests.sh` sai com erro sem `lib/`); - `CHANGELOG.md` presente como nos demais apps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e saldo
O `GET /produtos/{id}` da API v3 não traz estoque nas variações e a consulta a
`/estoques/saldos` é tolerada com `catch`, então uma falha transitória (429, 5xx
ou o limite diário) deixava as variações sem `estoqueAtual`. O parser assumia 0
nesse caso e a importação gravava quantidade zero no produto e em todas as
variações, tirando a loja do ar até o próximo callback bem-sucedido.
Agora a quantidade só entra no body quando o Bling realmente devolveu um saldo;
sem saldo o produto mantém o estoque atual. Produto novo continua nascendo com
quantidade zero.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Quando a renovação do token falhava (`invalid_grant`, por exemplo), o erro do `POST /oauth/token` chegava ao registro de falhas com o corpo da requisição junto, e o `refresh_token` do Bling acabava salvo em texto puro no `hidden_data` da aplicação — visível no painel do lojista e para qualquer token com leitura de aplicações. O corpo das requisições de autenticação deixa de ser registrado e todo o texto do log passa por uma redação de `refresh_token`/`access_token`/`client_secret`, preservando os dados que ajudam a diagnosticar a falha. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…o Bling Dois problemas na busca do pedido já exportado: - com "número aleatório" ligado, a busca era montada a partir do metafield `bling:numero`, que nunca era gravado: a primeira exportação de todo pedido consultava `/pedidos/vendas?numero=undefined` e a API do Bling rejeitava a requisição, derrubando a exportação. Sem número a procurar a busca agora é pulada, e o número usado passa a ser guardado no pedido; - quando o número da loja já existia no Bling (numeração compartilhada com outro canal), a exportação gerava um número alternativo que o parser descartava, reenviando o número duplicado e tendo o pedido recusado a cada mudança de status. O número resolvido pela exportação agora é respeitado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`pnpm test:apps` executa a tarefa `test:apps` do turbo, que nenhum pacote de app declara — os testes do Bling nunca chegavam a rodar no CI. O pacote passa a declarar o script, e a dependência de `build` sai da tarefa `test` (que cobre os demais pacotes) para `test:apps`, que é quem precisa do `lib` compilado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
leomp12
left a comment
There was a problem hiding this comment.
Revisei os 12 commits que vieram depois da review anterior. Os quatro bloqueantes fecharam, e o arquitetural também — conferi cada um no código, não na mensagem de commit:
- A catraca de estoque acabou:
export-product-to-bling.ts:209compara pela mesmaparseStockFromDepositsque a importação grava, e o colateral do multi-depósito virou guarda explícita em:190. skipEventHeadersestá nos cinco pontos de escrita na Store API.- O callback resolve sempre por
applications/app_id:${appId}, com comentário explicando o vetor (bling-callback.ts:35-38). newRateLimitError()carregaerr.response = { status: 429 }, que é exatamente o contrato que oafter-bling-queue.ts:36sabe classificar — e osplicefica depois doreturn, então o item permanece na fila.
E quase todos os 🟠 também: newVariations ganhou releitura de /produtos/{id} quando o POST não devolve as variações; os tokens passaram a ser chaveados por client_id, seguindo o paypalTokens/${PAYPAL_CLIENT_ID}; a janela de bloqueio virou constante única de 12h; o throttle subiu para escopo de módulo; o upload de imagem ganhou TTL e as funções ganharam timeoutSeconds; e o log de erro ganhou dedupe. Os minors saíram quase todos — Promise<AxiosResponse>, IntegrationHandler, o rename do should-advance-fulfillment, o CHANGELOG, o EXPIRES_IN_GAP_SEC unificado, o crontab.
Também confirmei que o pnpm-lock.yaml sem entrada do bling-erp não bloqueia nada: nenhum pipeline usa --frozen-lockfile (o único pnpm install de CI é test-apps.yml:70, explicitamente --no-frozen-lockfile; o action.yml usa npm ci sobre o lock do repo da loja; e o release.mjs:46 publica com pnpm publish -r seguido de pnpm fix-install, que é o que regenera o lock). Tirar o lockfile.yml não deixou buraco.
O que sobrou são dois bloqueantes — e o primeiro é culpa da minha recomendação anterior, que nomeou o problema certo e o mecanismo errado.
🔴 Bloqueante — o X-Event-Flag: _skip não é por app: ele apaga o evento para a loja inteira
Eu pedi para marcar as escritas do app com o flag. O flag existe, funciona, e o commit 3bd3c33 aplicou certo. Só que ele não tem escopo por app, e eu não verifiquei isso antes de recomendar.
check-store-events.ts:133-137 monta um filtro único:
const baseApiEventsFilter = {
'flag!': EVENT_SKIP_FLAG,
...
};Esse filtro roda uma consulta por nome de evento, e o resultado é distribuído a todos os assinantes no fan-out de :262-266 (activeApps.map(...) com appConfig?.events.includes(listenedEventName)). Evento marcado não volta da consulta, logo não chega a app nenhum.
Quem assina orders-anyStatusSet, por config.ts: emails (1243), melhorEnvio (1236), loyaltyPoints (124890), tinyErp (105922), evendas (109851), affiliateProgram (119753) e webhooksApp (123113). Sete apps além do Bling.
O caminho concreto: o lojista marca o pedido como "Atendido"/"Enviado" no Bling → o callback importa → import-order-from-bling.ts:104 faz api.post('orders/{id}/fulfillments', ..., { headers: skipEventHeaders }) → o orders-anyStatusSet é suprimido → o cliente nunca recebe o e-mail de "pedido enviado". Junto vão o Melhor Envio, os pontos de fidelidade, a comissão do afiliado e os webhooks. Tudo em silêncio, sem erro em lugar nenhum.
O mesmo vale para products-quantitySet e products-priceSet (import-product-from-bling.ts:80,108,111), que o pagarMeV5 assina.
O eco que eu queria matar era blingErp → blingErp. A supressão pega todos.
A direção certa é filtrar o auto-evento no consumidor, não na origem: packages/api/types.d.ts:382 mostra que o evento carrega authentication_id, e o app conhece o próprio (getEnv().apiAuth.authenticationId). Ignorar em event-to-bling.ts os eventos de autoria do próprio app resolve o eco sem esconder nada dos outros sete. Isso também torna o skip-event-headers.ts desnecessário para escrita de recurso — o flag continua legítimo para applications, que é o uso do updateAppData.
🔴 Bloqueante — o rastreio vem do corpo do callback e é escrito sem confirmação
O callback aceita requisição sem token quando nenhum está configurado, e isso foi decidido conscientemente: o corpo só traz identificadores e os handlers rebuscam o dado autenticado no Bling. Fui conferir se a premissa se sustenta em todos os caminhos. No estoque, sim — import-product-from-bling.ts usa do corpo só a referência e busca quantidade em /produtos e /estoques/saldos. No pedido, não:
// import-order-from-bling.ts:46-56
const callbackOrder = queueEntry._callbackOrder;
if (callbackOrder) {
if (callbackOrder.transporte?.volumes?.length) {
blingOrder.transporte = { ...blingOrder.transporte, volumes: callbackOrder.transporte.volumes };
}
if (callbackOrder.codigosRastreamento && !blingOrder.codigosRastreamento) {
blingOrder.codigosRastreamento = callbackOrder.codigosRastreamento;
}
}Esses dois campos vêm direto do corpo e não são confirmados. order-from-bling.ts:29-38 monta { code, link: volume.urlRastreamento || <fallback> } e grava em shipping_lines[].tracking_codes via api.patch. Um POST anônimo com o numero de um pedido real e um urlRastreamento arbitrário escreve o link de rastreio que o cliente vai clicar — na página do pedido e no e-mail. Número de pedido é sequencial.
Trava parcial: isGeneratedFallback (:15-21) só deixa sobrescrever quando não há rastreio ou quando o existente é o fallback gerado. Não dá para trocar um código real já vindo do Bling, mas qualquer pedido ainda não despachado está aberto.
E isso é estrutural, não descuido: entrou no c8caabd, o commit que corrigiu o meu 🟠 do rastreio descartado, e o comentário explica — GET /pedidos/vendas/{id} nunca devolve urlRastreamento, então o corpo é a única fonte. Corrigir aquele ponto exigiu confiar no corpo.
Some-se o custo de carga: a função HTTPS não tem maxInstances, cada callback de estoque custa até 4-7 chamadas ao Bling e o client.ts:21 serializa a 1 req/s. Um chamador anônimo queima a cota diária, e aí create-access.ts:16 levanta o limite e check-enable-api.ts:12 apaga a integração por 12h.
Agora que o input blingerp-callback-token existe na action.yml, exigir o token deixou de custar caro: deploy novo nasce coberto. Se preferir manter o fail-open, o mínimo é parar de escrever campo vindo do corpo sem confirmação.
🟠 Estruturais
timeoutSeconds: 300 sem subir o eventMaxAgeMs torna o retry inerte. pubsub.ts:26 mantém eventMaxAgeMs = 60000 e :45-49 descarta o evento cuja idade passa disso. O bling-erp.ts:21 passa timeoutSeconds: 300 e deixa o default — então, no exato cenário que motivou o timeout (importação com muitas imagens), a primeira tentativa falha, o failurePolicy reentrega, e a reentrega chega com idade acima de 60s e morre em "Dropping event". Pior: com maxInstances: 1 segurando a instância por até 5 minutos, todo evento publicado nesse intervalo envelhece e é descartado — um produto pesado apaga ~5 minutos de eventos de preço, estoque e pedido da loja. Como este é o único uso do knob no repo, o próximo app copia o par errado.
A guarda de múltiplos depósitos é larga demais. export-product-to-bling.ts:190 testa stockBalances.depositos?.length > 1, não "2+ depósitos com saldo". Numa conta com "Geral" + qualquer segundo depósito (devoluções, avaria) e sem bling_deposit configurado, o return acontece antes de todo o bloco de estoque: o produto é criado no Bling e fica com estoque 0 para sempre, porque toda exportação seguinte para no mesmo ponto. O único sinal é um logger.warn, que não entra em logs do hidden_data — o lojista não vê nada no painel. A premissa do comentário só vale quando o outro depósito carrega saldo.
bling_deposit configurado mas ausente no produto cai no caso pior. parse-stock-from-deposits.ts:16 faz const deposits = depositFind ? [depositFind] : blingItem.depositos; — id que não bate faz a função somar todos os depósitos, dos dois lados, em silêncio. A guarda de :190 só dispara com !blingDeposit, então essa combinação passa: a comparação usa a soma e a escrita vai para o id configurado. É exatamente a divergência que o fix se propôs a eliminar, e um id digitado errado é indistinguível de um correto.
O callback ficou com timeoutSeconds: 120 enquanto o throttle virou 1 req/s. bling-callback.ts:82-131 processa pedidos e estoques em laços sequenciais com await; cada importProduct faz 4-7 chamadas ao Bling e cada importOrder faz 3. A ~1s por chamada, isso dá ~4-7s por SKU. A partir de ~20 itens num mesmo callback a função estoura os 120s, devolve 500 e perde o resto do lote — as entradas são isNotQueued: true, então não há retry nem permanência em fila. O onStoreEvent ganhou 300s no mesmo delta; o callback não.
A troca de chave storeId → clientId não tem migração. tokens-doc.ts:30-33 é a escolha certa e eu pedi por ela, mas loja já autorizada antes do deploy passa a não ter doc: check-enable-api.ts:16-18 retorna false → bling-callback.ts:57 devolve 403 e event-to-bling.ts:98 retorna null, ambos só com logger.warn, nada em logs. A integração para sem sinal até reautorizar. Se a 1011 foi autorizada durante a PR, vale nota de release.
Seguem sem resposta, e tudo bem tratar em follow-up (os dois já eram follow-up aceitável na review anterior): não há camada de reconciliação — o único cron continua sendo o refresh de token, e evento automático não tem retry; e o dedupe de retry não voltou, então a janela de duplo processamento do redelivery do PubSub continua sendo o handler inteiro, com POST /pedidos/vendas não idempotente dentro dela.
🟢 Minors
try-image-upload.ts:16-32,67-75— o fix do token quente entrou só aqui; o gêmeotiny-erp/src/integration/parsers/product-from-tiny.ts:26-45tem o mesmo bug latente em produção, sem correção. São ~15 linhas para portar, ou uma issue antes que ninguém lembre. Não vale extrair as duas cópias agora: não há pacote comum de apps e@cloudcommerce/firebaseteria que ganharaxioseimage-sizepara 2 apps.export-product-to-bling.ts:161e:226— doisGET /produtos/{blingProductId}idênticos na mesma execução (produto novo com variações de preço divergente entra nos dois blocos, que não são exclusivos), num PR cujoeab62a6é justamente sobre poupar a cota diária.after-bling-queue.ts:77-86— o dedupe compara só comlogs[0], então dois recursos falhando alternadamente nunca casam e o flood volta; e quando deduplica, otimestampda primeira ocorrência nunca é atualizado.skip-event-headers.ts:9— se o flag continuar sendo usado paraapplications, vale importarEVENT_SKIP_FLAGem vez do literal. Falta só uma linha noexportsdepackages/firebase/package.json("./lib/const"); os dois lados do contrato na plataforma já usam a constante (update-app-data.ts:55,check-store-events.ts:134).client.ts:12-15— o comentário afirma que "um campo de instância nunca espaçaria nada", e isso é falso: ocheckTimeantigo espaçava as chamadas sequenciais da mesma instância. O que ele não cobria era concorrência. O trade-off real que o escopo de módulo introduziu — todo tráfego Bling do processo serializado a 1 req/s, entre handlers independentes — fica escondido.check-enable-api.ts:12—RATE_LIMIT_WINDOW_MSficou no consumidor e é importado pelocreate-access, que é quem implementa a política. OEXPIRES_IN_GAP_SECdo mesmo delta foi para otokens-doc, módulo neutro. Duas convenções para o mesmo problema no mesmo commit.check-enable-api.ts:5-11— o bloco de doc docheckEnableApificou colado na constante inserida entre ele e a função.order-to-bling.ts:16—hasWarnedHolidaysé estado mutável de módulo dentro do arquivo que oscripts/tests.sh:3documenta como função pura, e a suíte importa esse parser direto: a ordem dos testes passa a importar.import-product-from-bling.ts:29,66—isStockOnlyvirou parâmetro morto: ele implica!update_product, então o primeiro disjunto do guard nunca muda o resultado, e oisQuantityOnlynovo (:291) já escreve a forma reduzida. É a terceira cópia do mesmo predicado.integration-handler.ts:14—export defaultde um tipo, sem consumidor (os dois importadores usam import nomeado). O precedente do repo épubsub.ts:96,export type { PubSubHandler, ApiEventHandler }no módulo dono.export-product-to-bling.ts:190— ostockBalances &&é guarda morta: ter passado peloif (!estoqueId) returncom!blingDepositjá prova quedepositos[0].idé truthy.export-product-to-bling.ts:80,93,106—isDetailLoadedé derivável deblingProducts, que segue em escopo e inalterado.turbo.json— a arestatest → buildresolve o caso dobling-erp, que é o único app cujos testes importamlib/em vez de rodar contra o emulador. A tasktest:appscontinua semdependsOn, então as duas definições divergem. Encosta na issue #237, que segue aberta.- O corpo da PR ainda diz 37 testes; são 58.
Pra entrar antes do merge
- Trocar a supressão de auto-evento: filtrar por
authentication_idnoevent-to-bling.tsem vez de marcar a escrita na origem. Do jeito atual, importar status do Bling apaga o e-mail de "pedido enviado" e mais seis integrações. - Parar de escrever o rastreio vindo do corpo do callback sem confirmação — ou exigir o token, agora que a
action.ymltem o input. - Subir o
eventMaxAgeMsjunto com otimeoutSeconds, e alinhar o timeout do callback com o throttle novo. - Estreitar a guarda de depósitos para "2+ com saldo", e tratar o
bling_depositque não bate como erro visível em vez de somar tudo.
Os demais 🟠 dão para tratar em follow-up; nenhum deles tem issue aberta hoje.
Uma ressalva de método, como da outra vez: não rodei a suíte (o worktree está sem node_modules e o CI a cobre verde). Os dois bloqueantes eu tracei na fonte um por um — o do flag inclusive contra a minha própria recomendação anterior, que estava incompleta.
…es do Bling O eco importação -> exportação passa a ser cortado no consumidor, ignorando em `event-to-bling.ts` os eventos com o `authentication_id` do próprio app. O `X-Event-Flag: _skip` nas escritas de recurso saiu: o flag não tem escopo por app — `check-store-events.ts` filtra `flag!=_skip` numa consulta única antes do fan-out —, então marcar um status importado do Bling apagava o `orders-anyStatusSet` para os outros sete assinantes (e-mail de "pedido enviado", Melhor Envio, fidelidade, afiliados, webhooks) e o `products-*Set` para o Pagar.me v5. Apontado na review de 19/08. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…alidado
O `GET /pedidos/vendas/{id}` nunca devolve `urlRastreamento`, então
`transporte`/`codigosRastreamento` vêm do corpo do callback — mas sem token
configurado o callback é anônimo, e um POST forjado com o número (sequencial)
de um pedido ainda não despachado gravava o link de rastreio que o cliente
clica na página do pedido e no e-mail. O `_callbackOrder` agora só entra na
fila quando o token foi validado; requisição sem token segue aceita apenas
para disparar importação por identificadores, com todo dado rebuscado
autenticado no Bling. Apontado na review de 19/08.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s longas O `timeoutSeconds: 300` do `onStoreEvent` mantinha o `eventMaxAgeMs` padrão de 60s: a reentrega do `failurePolicy` após um timeout chegava sempre velha demais e morria em "Dropping event", e com `maxInstances: 1` uma importação de produto pesado envelhecia e apagava os eventos publicados no intervalo. A idade máxima sobe para 10min junto com o timeout. O callback HTTPS sobe de 120s para 300s: com o client Bling serializado a ~1 req/s e 3-7 chamadas por item, um lote de ~20 itens estourava o timeout e perdia o resto sem retry (entradas `isNotQueued`). Apontado na review de 19/08. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…pósito mal configurado A guarda de múltiplos depósitos travava com `depositos.length > 1` mesmo quando o segundo depósito (devolução/avaria) estava vazio, deixando o produto criado no Bling com estoque 0 para sempre e só um `logger.warn` que o lojista nunca vê. Agora ela só dispara com 2+ depósitos COM saldo e vira `isConfigError`, visível nos `logs` do painel; sem depósito configurado a escrita vai para o (único) depósito com saldo, convergindo com a base comparada. E `bling_deposit` configurado mas ausente na resposta do Bling deixou de somar todos os depósitos em silêncio — um id digitado errado era indistinguível de um correto — e passa a falhar com erro de configuração visível, nos dois lados (import e export). Apontado na review de 19/08. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
🔄 Review de 19/08 — os 4 pontos "pra entrar antes do merge" aplicadosOs dois 🔴 e os dois itens estruturais promovidos a pré-merge entraram em 4 commits, um por ponto: 🔴 1 · Supressão de auto-evento trocada pelo filtro no consumidor —
|
leomp12
left a comment
There was a problem hiding this comment.
Terceira rodada, sobre os 4 commits que responderam aos pontos pré-merge de 19/08. Três fecharam e estão verificados no código:
- Rastreio do corpo só com token validado (
1c9926c18) —_callbackOrder: isTokenValidated ? … : undefined, e oisTokenValidatedé confiável porque a comparação com 401 vem antes: passar dali significa "token exigido e conferido". De lambuja o degradê é auto-curável —isGeneratedFallbackdeixa o link real sobrescrever o fallback depois que o token for configurado. eventMaxAgeMsjunto com otimeoutSeconds(27b8dda1a) — a opção existe de verdade nopubsub.ts:16,26,45, e o comentário no código explica o par para o próximo app não copiar o knob pela metade. Callback de 120s para 300s.- Guarda de depósitos +
bling_depositinválido visível (7c92a2745) — conferi a convergência nos três casos (1 depósito com saldo / nenhum com saldo / 2+ com saldo), e othrownovo doparseStockFromDepositsé pego pelorunQueueEntry(bling-callback.ts:66-83) →afterQueue→isConfigError→notesnoslogsdo painel, sem derrubar o resto do lote. Bem feito.
O quarto trocou o mecanismo errado por outro que quebra mais.
🔴 Bloqueante — o filtro por authentication_id não separa o Bling dos outros apps, separa o deploy inteiro da loja
event-to-bling.ts:43-48 ignora eventos cujo authentication_id é o do próprio app:
if (
apiEvent.authentication_id
&& apiEvent.authentication_id === getEnv().apiAuth.authenticationId
) {
logger.info(`>> ${key} - Skipped self-caused event`);
return null;
}Só que getEnv().apiAuth.authenticationId é o ECOM_AUTHENTICATION_ID (packages/config/src/env.ts:32,38), e packages/api/src/api.ts:90 mostra que toda requisição do @cloudcommerce/api no deploy autentica com ela. É uma credencial por loja, escrita uma vez no functions/.env pela action.yml e compartilhada por todos os codebases — esse é o padrão da plataforma para todos os apps, não um detalhe do Bling. Nenhum app do monorepo consegue se identificar por authentication_id, porque todos escrevem com a mesma.
Então o authentication_id de um evento causado pelo Bling é idêntico ao de um evento causado pelo checkout, pelos webhooks de pagamento, pelo tiny-erp, pelo loyalty-points. A prova de que esses eventos chegam ao app é o próprio eco que o filtro quer matar: ele existe justamente porque escrita com ECOM_AUTHENTICATION_ID volta como evento.
O caminho concreto, que é o principal da integração:
- Cliente paga → o app de pagamento faz
api.post('orders/{id}/payments_history')—asaas-events.ts:73,pagarme-webhook.ts:108,woovi-events.ts:102,galaxpay/webhook.ts:316,409,mp-webhook.ts:81,paghiper/handle-webhook.ts:129,paypal-events.ts:135,braspag-functions.ts:108,pagaleve-webhook.ts:100,yapay-events.ts:98,loyalty-create-transaction.ts:35; financial_statusmuda →orders-anyStatusSet(check-store-events.ts:51-56);event-to-bling.tsretornanull.
Nenhum pedido é exportado automaticamente para o Bling.
O que sobrevive é exatamente o que um teste manual exercita: status mudado à mão pelo lojista no painel (autenticação de usuário, diferente) e a fila manual — updateAppData publica direto no tópico com authentication_id: null (update-app-data.ts:34), então o && do guard nem entra. O caminho automático morre em silêncio, com um logger.info de "Skipped self-caused event".
E se o campo vier ausente — ele é opcional no tipo (packages/api/types.d.ts:382) — o guard vira no-op e o eco volta inteiro. Nos dois casos o campo não resolve, porque não carrega a informação "qual app escreveu". Vale notar que o bling-erp é hoje o único leitor de authentication_id em packages/ — não há precedente, e não convém criar um, porque qualquer app que copiar isso quebra da mesma forma.
O que muda o tamanho do problema: o eco custa cota, não corrompe dado
Fui medir quanto o eco ainda dói depois que a catraca de estoque fechou na rodada anterior:
export-product-to-bling.ts:222— só posta/estoquesquandoblingBalance !== productQuantity;export-order-to-bling.ts:248— só faz o PATCH de situação quandoString(situacao?.id) !== String(newStatusBling.id);- as duas exportações só escrevem
metafieldsde volta na loja (:144,:204), emetafieldsnão casa com nenhummodified_fieldsdos eventos assinados (financial_status/fulfillment_status/status,price,quantity).
Ou seja: o eco termina em um salto — import → evento → export encontra igual → não escreve nada. Depois que as bases de estoque foram alinhadas, ele deixou de ser bug de dado e virou custo: ~4-7 chamadas ao Bling e alguns segundos de throttle por importação.
Trocar isso por "nenhum pedido é exportado automaticamente" é um mau negócio. Os caminhos, em ordem de custo:
- Tirar o filtro por autoria. O eco já é limitado, idempotente e termina sozinho. Isso destrava a exportação automática hoje, sem nada em troca além da cota.
- Se a cota incomodar, marcador por recurso com TTL curto —
{resourceId, timestamp}no Firestore, ignorar evento do mesmo id por ~60s. É o que o app v1 fazia comintegration_retries, é preciso, e não depende de autoria.
Uma ressalva de método: é a segunda recomendação minha nesse mesmo ponto, e a primeira (o X-Event-Flag: _skip) errou o escopo. Desta vez conferi as duas pontas antes de escrever — a credencial única em api.ts:90 e as guardas de igualdade nos dois exports.
🟢 Minors novos
hasDepositBalancenão considerahas_stock_reserve, enquantoparseStockFromDepositsescolhe a base por ele. Depósito comsaldo: 0esaldoVirtual > 0conta como "com saldo" na guarda mas soma 0 na base — uma linha para alinhar, num helper cujo comentário promete que "a base de comparação nunca divirja".estoqueId = blingDeposit || depositsWithBalance[0]?.id || blingDeposits[0]?.id— sembling_deposite com o saldo num depósito secundário (devolução, avaria), a escrita passa a ir para ele em vez do "Geral". Converge, mas é semanticamente estranho e invisível para o lojista.- Callback em 300s continua sem
maxInstances: se o Bling desistir e reenviar antes disso, o mesmo lote roda concorrente. Antes a janela era 120s. - CodeFactor marca 14 issues no head, mas a página não lista nenhuma e o
eslintlocal acusa só ummax-len(create-access.ts:131). Provavelmente ruído, mas trava o gate visual. - A suíte verde (71 testes) não cobre nada disso — são unitários de função pura, e o filtro de evento não tem teste nenhum. Mesmo padrão do C6 da primeira rodada, em que o CI ficou verde em cima do bloqueante.
Pra entrar antes do merge
Um item: trocar o filtro por autoria. Os outros três pontos da rodada anterior estão fechados e verificados.
E os follow-ups continuam sem issue aberta — migração/nota de release da chave storeId → clientId, dedupe de log além de logs[0], camada de reconciliação + dedupe de redelivery do PubSub, e o port do fix do token quente para o tiny-erp. Você disse que abriria antes do merge; enquanto não abre, some.
Ressalva de método, como nas outras rodadas: não rodei a suíte (este checkout está sem node_modules do pacote e o CI a cobre verde). O bloqueante eu tracei na fonte, dos dois lados.
…tidade
Com "exportar estoque" ligado, qualquer mudança de quantidade na loja
(`products-quantitySet`) reexportava o documento inteiro do produto com
`PUT /produtos/{id}`, sobrescrevendo no Bling edições feitas por lá — nome,
descrição, preço, dimensões, NCM — a cada movimentação de estoque. O mesmo
caminho é o eco de um callback de estoque do próprio Bling (importação ->
evento -> exportação), que assim desfazia no Bling o que o lojista acabou de
editar, além de custar 2-3 chamadas extras da cota por movimentação.
Evento de quantidade agora só compara e lança estoque: o corpo do produto
continua sendo montado (o lançamento por variação depende dele para casar
variação da loja com o id do Bling), mas nada além de `/estoques` é escrito.
Produto ainda não exportado não é criado a partir de evento de quantidade.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rigem O filtro por `authentication_id` descartava muito mais que o eco do próprio app: todos os apps do deploy autenticam na Store API com a mesma credencial (`ECOM_AUTHENTICATION_ID`), então o pedido pago — gravado em `payments_history` pelo app de pagamento — chegava como evento "de autoria própria" e era ignorado. Nenhum pedido era exportado automaticamente ao Bling; só mudanças manuais no painel e a fila manual (publicada com `authentication_id: null`) passavam, que é exatamente o que um teste manual exercita. O filtro sai e o eco importação -> evento -> exportação passa a ser aceito: ele termina em um salto porque a exportação compara antes de escrever — evento de quantidade não reexporta o produto (commit anterior) e estoque/situação iguais não geram requisição. A decisão evento -> fila é extraída para `parse-event-config` (função pura, como `decide-refresh-failure`) com a matriz de testes que faltou nas duas tentativas anteriores de cortar o eco: todo evento assinado é processado, nunca filtrado por autoria. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Terceira rodada respondida em dois commits. Antes de mexer, confirmei a mecânica do bloqueante na fonte: 82b95d0 — evento de quantidade não reexporta o documento do produto. Antes de tirar o filtro, fui medir a premissa "o eco termina sem escrever" e ela não valia para produto: com c11dcea — o filtro por A ordem dos commits mantém cada estado intermediário são: o corte do 77 testes passando, build e eslint limpos. Ressalva: o desvio do Os minors novos da rodada ( 🤖 Generated with Claude Code |
…rno do Bling
O pedido estornado pelo gateway era exportado ao Bling como "Cancelado" — a
situação é o alvo de cinco status financeiros da loja (`status-to-bling`) —, o
Bling disparava o callback da própria mudança e a importação regravava o pedido
como `voided`, porque a volta devolve só esse (`status-from-bling`). O histórico
guardava o `refunded`, mas o status corrente degradava e o app de e-mails
mandava "seu pedido foi cancelado" logo depois do "pagamento estornado".
`shouldApplyFinancialStatus` é a contrapartida do `should-advance-fulfillment`,
que já cortava o mesmo colapso do lado do fulfillment ("Atendido" voltando como
`invoice_issued`). O corte é só no degradê de `refunded`/`partially_refunded`
para `voided`: cancelamento humano no Bling sobre pedido pago, em disputa ou não
autorizado continua propagando.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013SJEqUap3HEdf5E8gG2iMD
…o de estoque O `PATCH` de metafields saía sempre que o produto tivesse qualquer metafield — ou seja, em toda exportação depois da primeira, mesmo sem nada a persistir. Com o eco de callback aceito, isso virou uma escrita na Store API por movimentação de estoque, por SKU. Além do custo, o `PATCH` substitui o array inteiro a partir do documento que veio no evento, então uma gravação de outro app entre o snapshot e a escrita era descartada. Passa a gravar só quando `bling:id` ou `bling:codigo` mudaram de fato. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013SJEqUap3HEdf5E8gG2iMD
leomp12
left a comment
There was a problem hiding this comment.
Quarta rodada, sobre 82b95d0d6 e c11dcea47. O bloqueante fechou, e fechou certo — conferi no código, não na mensagem:
- O filtro por autoria saiu (
c11dcea47) e a extração paraparse-event-config.tsé refactor fiel: o diff é movimentação pura, sem delta de comportamento além doisStockOnlyEventpassar a vir do helper. A matriz de testes fixa o contrato certo, e o bloco que registra as duas tentativas erradas está onde a próxima pessoa vai olhar antes de tentar a terceira. - A premissa "o eco termina sem escrever" você mediu e ela não valia — o
82b95d0d6é o achado da rodada.products-quantitySetchegava emexport-product-to-bling.ts:129-131com o documento inteiro, então toda movimentação de estoque sobrescrevia no Bling o que o lojista editou por lá. Bug independente do eco, e a ordem dos commits mantém todo estado intermediário são. - O eco termina mesmo em um salto: as únicas escritas do app na Store API são
metafields(export-order-to-bling.ts:204,export-product-to-bling.ts:160), echeck-store-events.ts:60-62só casa produto porprice/quantity. A fila manual não duplica:updateAppDatacarimbaX-Event-Flag: _skipnoPATCHe publica no tópico direto. - 77 testes verdes no CI (
32767293495),eslintlimpo nos arquivos tocados.
Uma peça de contexto que vale registrar no comentário do parse-event-config: o v1 filtrava por authentication_id também (webhook.js:99, trigger.authentication_id !== auth.myId) — e lá funcionava, porque no Store API v1 cada app tem autenticação própria (auth-callback.js:21). A 2ª tentativa não foi chute, foi port fiel de um mecanismo que deixa de valer em silêncio sob a credencial única do monorepo. Dito assim, o comentário fica à prova da 3ª tentativa.
Correção minha: eu disse que os follow-ups seguiam sem issue. Estão abertos desde 21/08 — #814, #815, #816, #817.
Veredito: nada bloqueia
Achei quatro coisas reais. Todas as quatro existem no app v1 em produção — fui atrás de cada uma no app-bling-erp-v2 e cito abaixo. Pelo critério de "defeito que já roda em produção não trava a migração", nenhuma é pré-merge. Registro porque três têm fix de uma linha e um deles a PR já escreveu para o gêmeo.
🟠 O round-trip de situação degrada o status financeiro do pedido — corrigido em 881404f24
O eco foi medido no caminho loja → evento → exportação. Existe um segundo laço, que não passa por evento nenhum: loja → exportação → Bling → callback → importação → loja. Esse escreve.
status-to-bling.ts:51-56 colapsa cinco status financeiros em um (voided|refunded|in_dispute|unauthorized|partially_refunded → ['cancelado']), e status-from-bling.ts:55-57 devolve só um ('cancelado' → voided). Com o callback de pedido que o README.md:26-33 manda cadastrar:
- Gateway notifica estorno → app de pagamento faz
POST orders/{id}/payments_historycomrefunded; orders-anyStatusSet→ agora chega no app;export-order-to-bling.ts:228-249resolve "Cancelado" ≠ atual →PATCH /pedidos/vendas/{id}/situacoes/{id};- Bling dispara o callback →
bling-callback.ts:86-111→importOrder; import-order-from-bling.ts:94-105:refunded ≠ voided→POST payments_history { status: 'voided' }.
O payments_history preserva o registro de refunded; o que degrada é o current e, principalmente, a notificação: emails/src/util/get-mail-templ.ts:96-118 mapeia voided → "Seu pedido foi cancelado" e refunded → "Pagamento estornado", e o dedupe de event-to-emails.ts:64-67 só pula quando o último status notificado é o mesmo. Então o cliente estornado recebe, em seguida, um "seu pedido foi cancelado". Termina em um salto (voided → ['cancelado'] → já cancelado → sem PATCH), sem oscilação, inclusive com parse_status customizado.
O elo que eu achei mais atacável — "o Bling dispara callback quando a mudança veio da própria API?" — sobrevive por evidência da própria branch: o 7738fee86 descreve o pedido regredindo de "entregue" para "nf emitida" a cada importação, o que só acontece se a sequência export-PATCH → callback → import rodou ao vivo em loja real; e o c51443af9 já registrava o round-trip como TODO.
Portado. O v1 tem a cadeia inteira: mesmo colapso (parsers/order-to-bling/status.js:46-51), mesma volta (parsers/order-to-ecomplus/status.js:49-50), mesma escrita sem guarda (import-order.js:58-76), mesmo compare-antes-do-PATCH (export-order.js:242-255) — e a entrada passava pelo filtro dele, porque o estorno chega com o authentication_id do app de pagamento, não do Bling. O tiny-erp deste monorepo roda a mesma cadeia em produção, sem filtro de autoria nenhum: status-to-tiny.ts:12-16, status-from-tiny.ts:52-54, import-order-from-tiny.ts:64-77.
Mas o gêmeo desse bug você já corrigiu nesta PR. should-advance-fulfillment.ts:5-11 existe exatamente para essa classe, e o comentário descreve o mesmo mecanismo. A guarda só foi aplicada a um dos dois subrecursos — import-order-from-bling.ts:96 testa subresource === 'fulfillments'. payments_history ficou sem nada. Como o gêmeo já estava escrito e o corte não tem colateral, apliquei direto: 881404f24 cria should-apply-financial-status.ts no mesmo formato do irmão (função pura + 7 testes) e o liga em import-order-from-bling.ts. Corta só o degradê refunded/partially_refunded → voided.
Retrato uma alternativa que eu ia recomendar e que é pior: checar identidade com parseStatusToBling(order, appData).includes(situacao). Funciona e é mais geral, mas tem três colaterais que o enum não tem — mata returned → voided (status-to-bling.ts:76-79 também mapeia para cancelado), mata o cancelamento humano de pedido em disputa, e fica ambígua com parse_status em que uma situação é alvo de um estado e sinal de outro. Além de exigir financial_status,fulfillment_status no fields= de :67-70, que hoje não vêm. O enum é cirúrgico e cobre também a variante não-eco (lojista cancela no Bling depois do estorno).
Deixar in_dispute e unauthorized de fora do enum é proposital: nesses o cancelamento humano no Bling ainda deve propagar.
🟠 canCreateNew: false não segura a criação quando a busca volta vazia
export-product-to-bling.ts:86-89 é a única guarda de criação, e vive dentro do if (Array.isArray(blingProducts) && blingProducts.length). Quando a busca não acha nada — data: [] na listagem, ou o 404 que findBlingProducts converte em null (:73) — ela não dispara e o fluxo cai em :110:
if (canCreateNew || appData.export_quantity || !blingStore) {Basta !blingStore (loja sem multiloja) ou export_quantity ligado — acontece com multiloja configurada também. :129-131 sem originalBlingProduct → POST /produtos: cria o produto no Bling com new_products desligado. E a listagem vazia não é hipótese: o próprio v1 documenta que o Bling devolve lista vazia até para SKU existente (lib/events/handle-events.js:55-59).
Portado, linha a linha: guarda aninhada em lib/integration/export-product.js:72-80, mesmo fall-through em :82, mesmo POST em :94. A PR já fechou metade sozinha — o 82b95d0d6 cobriu products-quantitySet, que no v1 também criava.
O fix é generalizar a guarda que você acabou de escrever em :103-106 e apagar a de :86-89:
if (!canCreateNew && !originalBlingProduct) {
logger.info(`${productId} not on Bling and cannot create new`);
return null;
}O lugar é esse mesmo, depois do if/else de :81-94 — antes dele originalBlingProduct ainda não foi atribuído e a guarda mataria todo export de produto existente. Tri-state confere: undefined só existe para pedido (parse-event-config.ts:53-75 sempre devolve boolean em produto), então !canCreateNew é seguro.
Trade-off a citar no commit: produto com bling:id apagado no Bling deixa de ser recriado por evento de preço/estoque — hoje, e no v1, é recriado.
🟠 Estoque de variação sem codigo no Bling — a raiz é do v1, o silêncio é novo
Com isStockOnlyEvent, response é null → newVariations = [] (:249), e a releitura de :250-260 exige !originalBlingProduct, que a guarda :103-106 garante existir → nunca roda. O casamento variação → id do Bling passa a depender só de variationFind.id, que product-to-bling.ts:147-160 só preenche quando o codigo bate exato. Variação criada na interface do Bling sem SKU tem codigo vazio, e a importação grava o id do Bling como SKU da loja (product-from-bling.ts:262, dito com todas as letras no bling-callback.ts:117-120). Na volta, "12345" não casa com '' → sem id → :270 retorna e nenhum /estoques é lançado.
Portado: no v1 nunca funcionou — parser casa igual, só por codigo (parsers/product-to-bling.js:122-124, id em :136-138); sem id ia o PUT assim mesmo e tomava 400 (o bug nº 1 do corpo desta PR), e o POST /estoques ia com id: NaN, engolido pelo .catch(logger.error) (export-product.js:167-176).
O que muda é a visibilidade: antes do 82b95d0d6 a falha era um 400 que subia para os logs do painel (a correção nº 5 da própria PR); agora é no-op sem log. Mínimo: um logger.warn no skip de :270.
O fix é espelhar o que o import-product-from-bling.ts:58 já faz, em duas passadas (primeiro codigo, depois String(id)), não num OR de uma passada, para não abrir chance de casar variação errada:
const blingVariationOriginal = originalBlingProduct?.variacoes?.find(
({ codigo: codigoFind }) => codigoFind === codigo,
) || originalBlingProduct?.variacoes?.find(({ id }) => String(id) === codigo);Colateral positivo: em export completo a variação passa a ir com id, o que conserta também o 400 do PUT nesse caso. E :156 passa a gravar o SKU-id como codigo da variação no Bling — cicatriza o vínculo, mas é escrita nova, vale citar no commit.
🟠 PATCH de metafields incondicional — corrigido em ead0d3fb3
export-product-to-bling.ts:159-161 grava metafields sempre que o produto tiver algum metafield (depois do primeiro export, sempre), substituindo o array inteiro a partir do doc do evento (:32-33) — metafield que outro app escreveu entre o snapshot e o PATCH se perde.
Portado e pior no v1: mesmo PATCH incondicional de array inteiro (export-product.js:129-137), e lá empurrava bling:codigo duplicado a cada export, sem checar existência (:106-117). O que muda é a frequência — com o eco aceito de propósito, cada callback de estoque carrega um PATCH de carona.
Também sem colateral, então aplicado: flag isMetafieldsChanged nos pontos de mutação, PATCH só quando bling:id ou bling:codigo mudaram de fato. Em regime estável nada muda. A corrida do replace continua nas vezes em que patcha; eliminar exigiria reler e mesclar, que o v1 nunca teve.
🟢 Minors
- Os três da rodada anterior seguem abertos, como você disse —
hasDepositBalancesemhas_stock_reserve(parse-stock-from-deposits.ts:8-13),estoqueIdcaindo em depósito secundário (:222), e o callback em 300s commaxInstances: 100herdado dohttpsFunctionOptions(config.ts:188). - Documento do produto agora só vai ao Bling em evento de preço e na fila manual — consequência certa do
82b95d0d6, mas vale uma linha no README: quem editar nome/descrição na loja não vê no Bling até mexer no preço. - Corpo da PR ainda diz "71 testes"; são 77.
- CodeFactor segue vermelho com 14 issues e a página segue sem listar nenhuma;
eslintlocal limpo nos arquivos tocados.
O que empurrei e o que deixei para você
Empurrei na branch os dois que não têm trade-off nenhum — 881404f24 e ead0d3fb3. Suíte em 84 verdes (os 77 + 7 do guard novo), build e eslint limpos. Desfaz sem cerimônia se discordar do recorte.
Os outros dois eu não apliquei porque cada um muda comportamento e a chamada é sua:
canCreateNew: fechar a guarda faz produto combling:idapagado no Bling deixar de ser recriado por evento de preço/estoque. Hoje é recriado, no v1 também. É o comportamento certo pelo rótulo da config, mas é mudança de produção.- Casamento da variação por id: além de destravar o estoque, passa a gravar o SKU-id como
codigoda variação no Bling, onde hoje está vazio. Cicatriza o vínculo, mas é escrita nova em dado do lojista.
Se topar os dois, aplico do mesmo jeito. O resto cabe na #817.
Ressalva de método: não rodei a suíte localmente (o build depende do lib do @cloudcommerce/firebase, que este checkout não tem); li os 77 verdes no log do CI. As quatro cadeias eu tracei na fonte, e a classificação "existe no v1" está ancorada em arquivo:linha do app-bling-erp-v2 em cada uma.
leomp12
left a comment
There was a problem hiding this comment.
Aprovado. As três rodadas de bloqueantes fecharam e estão verificadas no código, e os quatro achados que sobraram na 4ª rodada existem todos no app-bling-erp-v2 em produção — port fiel, não regressão da migração, então não travam.
Dos quatro, os dois sem trade-off entraram na própria branch (881404f24 guard financeiro, ead0d3fb3 flag de metafields). Os outros dois viraram follow-up opcional na #817, com o trade-off de cada um nomeado.
Estado: test verde (84 testes), eslint limpo, sem conflito com a main. O CodeFactor segue vermelho com 14 issues que a página não lista e o eslint local não reproduz — não é check obrigatório e não bloqueia.
Porta o app Bling ERP (
app_id102418) do repositório app-bling-erp-v2 para o monorepo, usando a API v3 do Bling.Funções
blingerp-onStoreEventapplications-dataSetblingerp-callbackblingerp-authCallbackcodedo fluxo OAuth e grava os tokensblingerp-cronRefreshTokenaccess_tokenantes de expirarMudanças de arquitetura em relação ao app v1
queue/{storeId}/events+running_events+handle-queue) deu lugar ao PubSub de eventos do monorepo (maxInstances: 1) com a fila emdatado app;appSdkmulti-loja substituído por@cloudcommerce/apicom as credenciais da própria loja;blingTokens/{storeId}e cache das situações de venda emblingStatuses/{storeId}, no projeto Firebase da loja;products/skus:{sku}no lugar do ElasticSearch.Correções sobre o comportamento do v1
Encontradas ao portar e ao validar contra a API real:
/produtos?codigo=devolve o produto resumido, semvariacoes, então oPUTia sem os IDs e o Bling rejeitava como se fossem novas variações;PUT /produtos/{idVariacao}apenas para as divergentes;canCreateNew: false;fulfillment_statusinválido;other_configera lido comoouther_config(typo), então o tipo de contato nunca era aplicado;quantity), causando lançamento redundante a cada exportação;Grades de variação importadas passam a mapear para
size/age_group/gender(antes sóCorera normalizada), mantendo o round-trip estável com a exportação.Testes
packages/apps/bling-erp/tests/— 71 testes comnode --test, offline, sem credenciais: pedido e produto nos dois sentidos, mapeamento de status (incluindoparse_statuscustomizado), endereço/CEP, parcelamento, prazo de entrega com dias úteis e feriados, variações sem SKU e normalização de grades.scripts/bling-smoke.mjsfaz uma varredura read-only na API do Bling validando credenciais e todos os endpoints usados.Validação em produção
Loja de teste (1011) + conta Bling de teste, com as funções deployadas em um projeto Firebase real:
blingerp-authCallback→ tokens no Firestore;blingerp-onStoreEvent→ pedido criado no Bling com contato, item vinculado, frete, etiqueta e parcela;delivered→ "Atendido") e round-trip de status;blingerp-cronRefreshTokenexecutando e validando o token.Notas
admin_settings) continua no repositórioapp-bling-erp-v2; este pacote traz apenas o runtime;ignore_triggersnas configurações do app para o app central parar de processá-la;pnpm-lock.yamlnão foi atualizado neste PR — o CI instala com--no-frozen-lockfile.🤖 Generated with Claude Code