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>
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/— 37 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