fix(analytics): Ensure Awin conversions carry customer-facing order number and net amount - #811
Conversation
Awin commission must be calculated over the order subtotal only, but we were sending the full order total (with freight) on both the server-side conversion API call and the fallback pixel. - Deduct params.shipping and params.tax from params.value on sendToAwin (orders amount and commission group amount) - Same deduction for the fallback conversion pixel amount - Resulting value equals subtotal minus discounts, per affiliate network convention Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ayment redirects Customers returning from external payment pages land on the confirmation route without the order number, so the purchase event reached Awin with the internal order ID as reference. Now the SSR analytics handler reads the order from the API to resolve the customer-facing number (and exact amounts) whenever the client event misses it, the client fallback pixel is skipped without the number to avoid double-counting with a mismatched reference, and the Asaas success URL carries the order number. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…kout The order number is assigned asynchronously by the API and could still be missing 400ms after creation, making checkout respond without it — so the confirmation route, purchase events and Awin conversions fell back to the internal order ID. Retries the order read up to 3 more times (backoff) before responding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
leomp12
left a comment
There was a problem hiding this comment.
Revisei os quatro arquivos e o entorno. Antes dos problemas, o que está certo e não precisa ser revisitado:
A aritmética do valor líquido bate nos dois caminhos. fixMoneyValue (vbeta-app.ts:54) é literalmente Math.round(v*100)/100, idêntico ao de send-to-awin.ts:87, e os dois clampam em zero. O desconto não entra em dobro: amount.total já vem com amount.discount subtraído, que inclui o extra_discount.value (packages/api/types/orders.d.ts:204-233), e o voucher é só rótulo. O for com await não derruba o lote — o fetchOrderFallback engole os erros e devolve null. A rota de dois segmentos existe: baixei o bundle pinado em vbeta-app.ts:300 e o @ecomplus/storefront-app@2.0.0-beta.228 declara confirmation/:id?/:number?/:json?, então a URL do Asaas está na ordem certa. E a troca do forEach por laço indexado alinha o arquivo com os três irmãos send-to-*.
O que trava são duas coisas, e a primeira é uma interação entre dois commits desta mesma PR.
🏗️ Arquitetural — é a quarta PR no mesmo defeito, e o mecanismo que faz recorrer continua lá
| quando | PR | o que prometeu |
|---|---|---|
| 21/07 | #784 | adapta os dados da conversão e adiciona o pixel de fallback |
| 01/08 | #789 | persiste o awc em cookie |
| 06/08 | #803 | "Send customer-facing order number to Awin, not internal id" |
| 06/08 | 4ab02f5 |
manda o orderReference como string |
| 14/08 | #811 | de novo: garantir que o Awin receba o número do pedido |
O #803 fez o óbvio — leu number do corpo que a rota de confirmação já tinha e passou adiante com params.order_number || params.transaction_id. Não segurou porque o corpo frequentemente não tem number: o checkout lê o pedido 400ms depois do POST e a API atribui o número de forma assíncrona. O || caiu no _id, em silêncio, e só se soube quando a Awin reclamou do pedido nº 3044575.
O padrão é o ||. Cada rodada acrescentou uma camada de fallback em vez de tratar a origem, e o fallback degrada sem erro, sem log, sem sinal — o dado errado sai com cara de certo. E o #811 mantém o mesmo || em send-to-awin.ts:100: se o retry do checkout e o fetchOrderFallback falharem os dois, volta a sair o _id calado, e a quinta rodada é o mesmo ticket. Um logger.warn nesse ponto — e no handle-order-transaction.ts, quando o laço se esgota sem number — é o que transforma isso em algo que a gente descobre antes do parceiro.
Vale registrar por que agora: esta é a primeira das quatro que sai do analytics e entra no checkout compartilhado e num app de pagamento. A integração hoje mora em cinco arquivos e quatro pacotes.
E uma correção de crédito: o retry do handle-order-transaction.ts é justificado, mas não por causa da Awin. O order.number vira transactionBody.order_number em new-order.ts:58 e alimenta todos os gateways — asaas-create-transaction.ts:83,96 (Pedido ${orderNumber}, que é o que o cliente lê no boleto), woovi-create-transaction.ts:70, galaxpay-create-transaction.ts:93-94, pagarme-create-transaction.ts:68, pagaleve-create-transaction.ts:67. Nenhum tem guarda: sem número, o boleto sai "Pedido undefined". Isso é um defeito de plataforma que já degradava silenciosamente para todo mundo, e a PR o corrige de passagem sem dizer. Quem olhar depois não vai saber por que existem 2,8s ali — vale o commit e o corpo da PR dizerem isso.
🔴 Bloqueante — os dois commits se cancelam no retorno do Asaas, e a conversão sai com valor errado carimbada com a referência certa
send-to-awin.ts:73 só busca o pedido if (!orderNumber). O commit do Asaas põe o número na URL (asaas-create-transaction.ts:98), então params.order_number vem preenchido e o fetchOrderFallback nunca roda justamente na rota que ele existiria para salvar.
E nessa rota não há corpo do pedido. O successUrl é /confirmation/${orderId}/${orderNumber} — sem o terceiro segmento :json?. Em vbeta-app.ts:206-212, sem params.json o emitPurchase é chamado só com id e número, então orderJson é undefined, o order é undefined, e vbeta-app.ts:99-105 cai no window.storefrontApp, que não carrega o pedido. Com amount indefinido:
vbeta-app.ts:108→params.value = fixMoneyValue(shoppingCart.subtotal || 0)vbeta-app.ts:117-124→params.shippingeparams.taxnem chegam a ser setados
E shoppingCart.subtotal (shopping-cart.ts:185-192) é quantity * price dos itens: sem frete, sem imposto e sem o cupom.
Pedido de R$ 300 — R$ 250 de itens, R$ 30 de cupom, R$ 80 de frete — tem comissionável de R$ 220. A Awin recebe R$ 250. E se o cliente voltar do Asaas em outro navegador ou dispositivo, com o carrinho vazio, subtotal = 0 e a conversão vai como R$ 0,00.
O que torna isso pior que o estado atual: antes o valor errado ia com ref = _id, uma referência que a Awin não reconcilia com nada. Agora ele vai carimbado com o orderReference real — e como a Awin desduplica por referência, a conversão correta não entra mais depois. A PR troca "dado inútil" por "dado errado com aparência de autoritativo".
O caminho mais curto é o inverso do que está: deixar o fetchOrderFallback rodar sempre que faltar amount, não só quando faltar orderNumber.
🔴 Bloqueante — o retry joga fora uma leitura que já tinha dado certo, e o cliente vê erro num pedido que existe
handle-order-transaction.ts:19-33:
const readFullOrder = async () => {
attempts += 1;
try {
const { data: order } = await api.get(`orders/${orderId}`, { headers: { 'x-primary-db': 'true' } });
if (!order.number && attempts <= 3) {
setTimeout(readFullOrder, 400 * attempts);
return;
}
resolve({ order });
} catch (err: any) {
logger.error(err);
resolve({ order: null, err });
}
};Na tentativa 1 o pedido volta íntegro — só sem number. Se a tentativa 2, 3 ou 4 pegar falha transitória (timeout, 502, 429 depois dos três retries internos do próprio client), o catch resolve { order: null, err } e o new-order.ts:265 responde CKT701, "There was a problem saving your order".
Só que o pedido existe na API. Nenhuma transação foi criada, o cliente vê erro e refaz o checkout: pedido duplicado e estoque preso no primeiro. Antes desta PR havia uma leitura e uma janela de falha; agora são quatro, e três delas sobre um dado que já está na mão.
Guardar o último order lido com sucesso e resolvê-lo no catch fecha isso — o pior caso volta a ser "pedido sem número", que é o que o laço já aceita na saída normal.
🟠 Estruturais
A leitura da Store API entrou no caminho da resposta do /_analytics, que tem orçamento de 10s. serve-storefront.ts:194 faz await sendAnalyticsEvents(...) antes do res.sendStatus(201), e o ssrFunctionOptions.timeoutSeconds é 10 por padrão (packages/firebase/src/config.ts:183). O api.get do fetchOrderFallback herda timeout = 20000 e maxRetries = 3, com backoff de 5s em 429/5xx (packages/api/src/api.ts:174-175,238-246) — ou seja, a chamada sozinha pode passar do orçamento da function inteira. E ela está no mesmo Promise.allSettled do GA4, Meta e TikTok: Orders API lenta derruba o lote todo, não só a Awin. Os três irmãos send-to-ga4, send-to-meta e send-to-tiktok são transform puro mais um POST de saída; este virou o único com I/O de entrada. Passar timeout curto e maxRetries: 0 nessa chamada resolve.
Pular o pixel sem order_number remove o único canal de algumas lojas. O S2S exige AWIN_ADVERTISER_ID e AWIN_API_KEY no runtime do SSR (send-to-awin.ts:13,59 — sem os dois, awinAxios é null e a função retorna). O pixel exige só o AWIN_ADVERTISER_ID, injetado em build (BaseHead.astro:97). São duas configurações independentes: numa loja que tem só o advertiser ID, o pixel era o único canal, e agora a conversão sem número não é registrada em lugar nenhum. Antes ia com referência errada, mas ia. Vale condicionar o skip a ter o S2S de fato ativo.
O || silencioso, nos dois lugares. send-to-awin.ts:100 e o resolve({ order }) sem number no handle-order-transaction.ts. Um logger.warn em cada um é o que faz a próxima recorrência aparecer no log em vez de num e-mail do parceiro.
🟢 Minors
send-to-awin.ts:65— oeslint-disable-next-line no-continueé diretiva não usada:no-continuejá está'off'empackages/eslint/base.eslintrc.cjs:67, e o repo usacontinue;puro em ~10 lugares (apps/emails/src/event-to-emails.ts:61). Não há scriptlintna raiz nem passo de lint notest-apps.yml, então só o CodeFactor pega — e ele reporta exatamente 1 issue nesta PR. Ono-await-in-loopda linha 74, esse está correto.send-to-awin.ts:43-45— ocatchloga só{ status }e descarta mensagem e stack. Em falha de rede oApiErrornão setastatusCode(packages/api/src/api.ts:30-38), então o log vira{ status: undefined }e não sobra nada para diagnosticar. A convenção do repo é levar o erro junto —apps/woovi/src/woovi-create-transaction.ts:164-172, e o próprio orquestrador emsend-analytics-events.ts:128-139. Sub-item:err.response?.statusé axios-ismo; este client é fetch-based e expõe.statusCode.asaas-create-transaction.ts:98— sem guarda, a URL vira literalmente/confirmation/<id>/undefinedquando o retry se esgota. A rota degrada bem (Number('undefined') → NaN), mas aí o pixel é pulado e a referência cai pro_id.send-to-awin.ts:38— não mandax-primary-db: 'true', ao contrário da leitura equivalente emhandle-order-transaction.ts:21. Risco baixo porque essa leitura acontece minutos depois, mas é a mesma premissa de "número recém-gravado".packages/ssrnão tem script de teste nem um único arquivo de teste, o que explica três reincidências sem nada segurando. Jápackages/modules, onde entra o retry, tem vitest etests/1-modules.test.ts— o laço é testável no harness que já existe./_analyticsé não autenticado e otransaction_idvem do chamador, então o servidor passou a resolver número e total reais de um pedido por ID. Nada volta para o chamador, mas forjar conversão deixou de exigir saber o valor do pedido.- O corpo da PR abre com "the three issues reported by Awin's affiliate team" e descreve duas. Falta a terceira, ou o texto ficou incompleto.
Pra entrar antes do merge
- Fazer o
fetchOrderFallbackrodar quando faltaramount, não só quando faltarorderNumber— hoje o commit do Asaas desliga a rede de segurança na única rota que precisa dela. - Guardar o último
orderlido com sucesso e resolvê-lo nocatchdo retry, para o cliente não receber CKT701 num pedido que existe. - Limitar
timeout/maxRetriesna leitura do/_analytics, que hoje pode sozinha exceder os 10s da function e derrubar GA4, Meta e TikTok junto. logger.warnnos dois fallbacks silenciosos, e condicionar o skip do pixel a ter o S2S ativo.- Tirar o
eslint-disablenão usado (provável issue do CodeFactor) e levar o erro junto nologger.warndocatch.
Uma nota de escopo, mais para o corpo da PR do que para o código: o retry no checkout é a melhor coisa daqui e está vendida como detalhe de Awin. Ela conserta "Pedido undefined" no boleto de cinco gateways. Vale dizer isso em algum lugar, senão a latência vira mistério para quem mexer depois.
… never fail existing orders Addresses PR review: the order fetch fallback now also covers external payment redirects where amounts were wrong, and checkout no longer responds with error for an order that was actually created. - SSR analytics reads the order from the API whenever the purchase event misses the number OR the exact amounts: on returns from external payment pages (Asaas success URL) the event carried the real order reference but cart-subtotal values, and Awin dedups by reference - That order read runs with short timeout and no retries to stay within the SSR function time budget without starving GA4/Meta/TikTok events - Checkout's order read retry keeps the last successful read and resolves it on transient errors instead of responding CKT701 (and risking a duplicated order) when the order exists - The client fallback pixel, with S2S active, only fires when the page has both the customer-facing number and the real amounts; stores without the S2S API key keep the pixel as their only channel with legacy behavior - Fallbacks to the internal order ID now log warnings instead of degrading silently - Asaas success URL omits the number segment when unset instead of sending a literal "undefined" Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Valeu pela revisão @leomp12, pontos ajustados em cea8b1b: Bloqueante 1 (Asaas return sem amount): o fetch do pedido no Bloqueante 2 (CKT701 em pedido existente): o retry guarda o último Estruturais:
Minors: Sobre teste pro |
leomp12
left a comment
There was a problem hiding this comment.
Segunda rodada, em cima do cea8b1bb1. Os cinco pedidos do review anterior estão fechados — confirmei um por um:
| # | Pedido | Onde ficou |
|---|---|---|
| 1 | fetchOrderFallback rodar quando faltar amount, não só orderNumber |
send-to-awin.ts:81 — if (!orderNumber || params.shipping === undefined) |
| 2 | Guardar o último order lido e resolvê-lo no catch (CKT701 em pedido que existe) |
handle-order-transaction.ts:17,37-41 — lastOrderRead |
| 3 | Limitar timeout/maxRetries na leitura do /_analytics |
send-to-awin.ts:43-44 |
| 4 | logger.warn nos dois fallbacks + skip do pixel condicionado ao S2S ativo |
send-to-awin.ts:94-98, handle-order-transaction.ts:31-33, BaseHead.astro:98 + vbeta-app.ts:177-178 |
| 5 | Tirar o eslint-disable não usado; levar o erro junto no catch |
eslint limpo; logger.warn(..., { err, status: err.statusCode }) |
Minors também: guarda no successUrl do Asaas, x-primary-db na leitura da Awin, e a nota de escopo do retry no corpo da PR.
Verificação mecânica: tsc --noEmit limpo em packages/ssr, packages/modules e packages/apps/asaas; eslint limpo nos 4 arquivos; workflow test verde.
Três premissas novas que eram o risco natural das heurísticas, todas conferidas e corretas:
params.shipping === undefinedé sinal seguro. O checkout sempre inicializaamount = { subtotal, discount, freight: 0, total }(checkout.ts:49-54), então pedido da plataforma sempre temfreightdefinido — a heurística não dispara à toa nem pula o pixel em pedido normal.AWIN_S2S_ENABLEDsai preenchido de verdade. Conferi no HTML de produção da tia sônia quewindow.AWIN_ADVERTISER_ID = '128977', ou seja var não-PUBLIC_resolve emimport.meta.envno build do Astro. E numa página em cache antiga oundefinedcai em!undefined→ pixel dispara = comportamento legado. Default seguro nos dois lados.- O guard
/^[0-9a-f]{24}$/antes do fetch (send-to-awin.ts:36) é o instinto certo —/_analyticsé não autenticado e otransaction_idvem do chamador.
Não-regressão em GA4, Meta e TikTok
Como a PR passou a fazer I/O dentro do lote de analytics, fui verificar o que poderia respingar nos outros três:
| O que poderia quebrar | Verificação | Resultado |
|---|---|---|
Awin mutar o params compartilhado |
send-to-awin.ts |
tudo é cópia local (orderNumber, grossValue, shipping, tax, voucher); o objeto entregue aos irmãos é idêntico ao de antes |
Mudança na forma do params |
diff do vbeta-app.ts |
só o bloco do pixel; params.shipping/tax/order_number são pré-existentes |
| Irmãos lendo os campos tocados | send-to-ga4/meta/tiktok.ts |
nenhum dos três lê shipping, tax ou order_number |
emitGtagEvent cair dentro do condicional |
vbeta-app.ts:168 |
continua antes e fora do if (canEmitAwinPixel) |
| Dedup de purchase quebrar | vbeta-app.ts:190 |
localStorage.setItem('gtag.orderIdSent', orderId) continua fora do condicional — se tivesse entrado junto, purchase re-emitiria para todos os providers |
| Falha da Awin derrubar o lote | try/catch próprio no fetchOrderFallback + Promise.allSettled no orquestrador |
sendToAwin não rejeita para fora |
| Custo imposto a tráfego não-Awin | send-to-awin.ts:65 |
if (!awinAxios || !awc) return é antes do primeiro await — sessão sem awc sai com custo zero; a leitura extra só existe em tráfego atribuído à Awin |
| Dependência nova no bundle SSR | packages/ssr/package.json:36, cron-ssr-save-views.ts:3 |
@cloudcommerce/api já era dep declarada e já era importado |
| Outros gateways | diff | só o Asaas mudou; a rota confirmation/:id?/:number?/:json? aceita com e sem o segmento |
Está limpo. O único acoplamento que sobra é o orçamento de 10s da SSR function: a perna da Awin agora começa até 3s mais tarde e o awinAxios (send-to-awin.ts:14) não tem timeout. Nenhum dos três irmãos define timeout também, então é padrão pré-existente do arquivo — mas a Awin passou a ser a única que entra atrasada, e um timeout no axios.create fecha isso sem tocar em ninguém.
Registrando sem pedir: no caminho lento da confirmação, GA4/Meta/TikTok recebem value = shoppingCart.subtotal. É anterior a esta PR e não piora com ela — a única mudança que chega neles é order_number presente com mais frequência, param aditivo que o GA4 ignora. Fica anotado, não é escopo daqui.
🟠 O retry multiplica por 4 uma leitura bloqueante do checkout
Primeiro o crédito, porque o corpo da PR agora diz certo: este retry não é pela Awin. order.number vira transactionBody.order_number (new-order.ts:58) e alimenta Pedido ${orderNumber} na descrição da cobrança de Asaas, Woovi, GalaxPay, Pagar.me e Pagaleve — nenhum com guarda. Sem ele o boleto sai "Pedido undefined". É conserto de plataforma, e não dá pra evitar a releitura porque o POST orders devolve só { _id }.
O problema não é ele existir, é o teto. A linha do tempo:
t=0 POST orders → _id
t=400ms leitura #1 (attempts=1) → tem number? resolve : agenda +400ms
t=800ms leitura #2 (attempts=2) → tem number? resolve : agenda +800ms
t=1600ms leitura #3 (attempts=3) → tem number? resolve : agenda +1200ms
t=2800ms leitura #4 (attempts=4) → resolve de qualquer jeito (+ warn)
E isso mora em new-order.ts:39, antes de qualquer transação ser criada: o cliente segura o POST /checkout os 2,8s inteiros com nenhuma cobrança na mão. Não é background job, é o spinner do checkout.
Cada leitura usa os defaults do client (packages/api/src/api.ts:174-175): timeout: 20000, maxRetries: 3. Confirmei que timeout e erro de rede não disparam retry interno (api.ts:205-215; só 429/5xx entram no laço em :238-246) e que o catch resolve na hora com lastOrderRead, então a cadeia para na primeira falha — o pior caso teórico é bem menor do que parece. O que sobra, e é o que importa: as 4 leituras são pagas em série quando todas respondem 200 devagar. API em 300ms → +1,2s. API em 3s sob carga → +12s. O custo se multiplica por 4 exatamente no momento em que a Store API já está ruim, que é quando o checkout menos pode pagar.
O caminho que não devolve o CKT701 que a PR acabou de consertar: a leitura #1 e as releituras têm papéis diferentes. A #1 decide se o checkout continua — encurtar o timeout dela faz uma API lenta-mas-funcionando devolver erro num pedido que existe. As #2–4 são best-effort só pelo número, e se falharem já existe lastOrderRead.
const { data: order } = await api.get(`orders/${orderId}`, {
headers: { 'x-primary-db': 'true' },
// Rereads are best-effort for the order number, the first read is the
// one that decides whether the checkout can go on
...(attempts > 1 ? { timeout: 1500, maxRetries: 0 } : null),
});Com isso o teto que a PR acrescenta fica em ~2,8s de espera + ~4,5s de leituras, e o comportamento de CKT701 fica idêntico ao do main. O maxRetries: 0 nas releituras também é coerente: o laço externo já é a política de retry, o interno é redundância escondida.
E o número de tentativas em si é discutível — attempts <= 2 (3 leituras, 1,6s) provavelmente pega quase tudo. Para decidir isso com dado em vez de palpite, o attempts precisa aparecer no log do resolve, não só no esgotamento: hoje o logger.warn só dispara quando as 4 falham, então não dá pra saber se o custo é cauda ou regra. Uma semana de log responde.
Não consegui verificar se o @ecomplus/storefront-app tem timeout no fetch do POST /checkout — é bundle externo a este repo. Se tiver e for menor que o novo pior caso, o cliente vê erro de rede com o pedido criado e sem transação, que é o cenário de pedido duplicado com estoque preso. Vale alguém que conheça o bundle confirmar, porque isso muda a urgência do cap.
🟠 Pixel e S2S divergem no channel
O pixel manda ch=${trackingIds.awin_channel || 'aw'} cru (vbeta-app.ts:42); o servidor manda validChannels.has(channel) ? channel : 'aw' (send-to-awin.ts:105). Como o template oficial da Awin grava o AwinChannelCookie em minúsculas e a whitelist tem 'Other'/'Organic' capitalizados (:25), para organic e other o pixel diz organic e o S2S diz aw, no mesmo orderReference — e quem vencer a dedup decide se a comissão vai para a Awin num last click que não foi deles.
É a terceira PR seguida que edita este arquivo passando ao lado dessa linha (levantei no #803 como adjacente), e é a mesma classe de defeito que esta PR está consertando: precisão do que a Awin recebe para calcular comissão. O caminho é repassar o valor do cookie em minúsculas e sanitizado em vez de manter whitelist — a doc da Awin não enumera valores aceitos, o campo é livre (≤20 alfanuméricos) com aw só como fallback obrigatório.
🟢 Minors
fieldsem GET por id não tem precedente no repo. Todos os outrosfields: [...]são em listagem, e oResponseBody(packages/api/types.d.ts:394-399) só aplicaListFieldsa list endpoints — o TS entregaOrdersinteiro de qualquer jeito. Se a Store API ignorar o param no single-resource, otimeout: 3000foi dimensionado para 3 campos mas puxa o documento completo. Um curl confirma.awinAxiossemtimeout(send-to-awin.ts:14), conforme a seção de não-regressão acima.- CodeFactor vermelho com a página vazia. O status no
cea8b1bb1diz "1 issue found", a página da PR lista "No issues found" e o eslint local está limpo. Provavelmente stale, mas trava o gate — vale um re-run.
Pra entrar antes do merge
- Capar as releituras (
timeout+maxRetries: 0a partir da segunda), mantendo a primeira como está. É o que impede o checkout inteiro de multiplicar por 4 quando a Store API está lenta. - Logar
attemptsno resolve, não só no esgotamento — sem isso não dá pra calibrar o número de tentativas depois. - Passar o
channelem minúsculas em vez da whitelist capitalizada. Uma linha, no arquivo que a PR já edita, e fecha um furo de atribuição de comissão que está aberto desde a #784.
O resto é opcional. A não-regressão nos outros providers de analytics está verificada e limpa, e os dois bloqueantes da rodada anterior estão de fato resolvidos.
…sion
Awin's `channel` is a free-form field, not an enumerated list: the
capitalized whitelist turned the real lowercase cookie values ('other',
'organic') into 'aw' on the server while the fallback pixel sent them raw.
Same `orderReference` reaching Awin with two different channels, and
whichever wins their dedup decides if the commission is theirs on a last
click that wasn't.
Replaces the whitelist with sanitization (lowercase alphanumeric, capped
at 20 chars, 'aw' as the mandated fallback) and normalizes the value where
the cookie is read, so pixel and server-side conversion can't diverge.
The `typeof` guard matters: `awin_channel` comes from the request body.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019suoRjQ2zxAJNARnwoopsh
… is slow The retry that waits for the order number to be set can issue up to four reads in series, and each one used the API client defaults (timeout 20s, 3 internal retries). When the Store API is slow the cost multiplies by four exactly when the checkout can least afford it, with the customer holding the spinner and no charge created yet. Rereads now use `timeout: 1500` and `maxRetries: 0` from the second attempt on: they are best-effort just for the number and already resolve with the last successful read on failure. The first read keeps the client defaults untouched, since it is the one deciding whether the checkout can go on, so CKT701 behavior is identical to main. Also logs `attempts` on every resolved order instead of only on exhaustion, so the number of retries can be calibrated with data, and demotes a reread failure that was fully recovered from error to warn. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019suoRjQ2zxAJNARnwoopsh
leomp12
left a comment
There was a problem hiding this comment.
Terceira rodada, em cima do cd3ca7b40. Os três pedidos do review anterior estão fechados, e eu mesmo empurrei os commits — então vale explicitar o que foi conferido antes de aprovar.
| # | Pedido | Onde ficou |
|---|---|---|
| 1 | Capar as releituras (timeout + maxRetries: 0 a partir da 2ª) |
handle-order-transaction.ts:23-26 |
| 2 | Logar attempts no resolve, não só no esgotamento |
handle-order-transaction.ts:35-48, com payload estruturado { orderId, attempts } nos dois ramos |
| 3 | channel em minúsculas em vez da whitelist capitalizada |
send-to-awin.ts:27-30 (parseChannel) + set-tracking-ids.ts:79-85 |
Duas coisas entraram além do pedido literal, ambas por premissa que não fechava:
- Guard
typeofnoparseChannel.awin_channelvem dereq.body(serve-storefront.ts:194) e o defaultchannel = 'aw'do parâmetro só cobreundefined—nullou objeto passariam, e um.toLowerCase()cru lançariaTypeErrorengolido peloPromise.allSettleddesend-analytics-events.ts:117, descartando a conversão inteira em silêncio. - Normalização também na fonte (
set-tracking-ids.ts, 7º arquivo). Só o servidor não dá paridade: com cookie capitalizado o pixel mandariaOrganice o S2Sorganic.trackingIds.awin_channelé o ponto único que alimenta os dois canais (só 2 consumidores no repo:vbeta-app.ts:42esend-analytics-events.ts:88), então normalizar ali fecha os dois de uma vez.
Não-regressão
Tabela antes→depois do canal: aw/ppcgeneric/ppcbrand/display/social/direct inalterados; Other/Organic passam a chegar lowercase; other/organic deixam de virar aw (é o fix); undefined/null/''/objeto continuam em aw. Nenhum caso piora.
No checkout: enumerados os cinco caminhos do Promise — exatamente um resolve em cada, sem caminho órfão. A 1ª leitura fica byte-idêntica ao main (spread de null é no-op), então CKT701 dispara exatamente nos mesmos casos de antes: falha do POST ou falha da 1ª leitura. As releituras nunca produzem order: null por causa do guard lastOrderRead. O pior caso das releituras cai de ~288s (4 leituras × timeout 20s + retries de 5s — que estourava o deadline da function) para ~7,4s.
Mudanças observáveis intencionais: ERROR→WARNING no caminho recuperado do catch (deixa de abrir incidente no Error Reporting numa situação benigna) e +1 log INFO por checkout, que é o propósito do pedido 2.
tsc --noEmit limpo em modules, ssr e apps/asaas; eslint 0 problemas nos 7 arquivos com --max-warnings 0 --no-ignore; workflow test verde.
CodeFactor
Não é stale e não é required check (required_status_checks.contexts: [] na proteção do main), então não trava o merge. Identifiquei a causa: é Complex Method em sendToAwin. Medido com a regra complexity do eslint, a função vai de 6 (47 linhas) no merge-base para 23 (78 linhas) — cruza qualquer threshold default. Nem o config do repo nem o eslint:recommended habilitam complexity, que é porque não reproduzia localmente. Fica como follow-up: extrair a montagem do awinOrder derruba o número sem tocar em comportamento.
Segue para merge.
Fixes the issues reported by Awin's affiliate team on Tia Sônia's integration testing:
Wrong order reference (internal _id instead of customer-facing number)
Awin's test order (nº 3044575, 2026-08-13) was received with the internal order ID. Traced via Firebase logs (DEBUG_SERVER_ANALYTICS): the purchase event reached the server without order_number. Two gaps allowed this:
Amount sent with freight included
Awin expects the commissionable amount without freight and taxes. The S2S payload and the fallback pixel now send value − shipping − tax (falling back to the order's amount fields when fetched server-side).
Scope note — the checkout order read retry fixes more than Awin:
order.numberbecomestransactionBody.order_numberand feeds every payment gateway. Without it, Asaas/Woovi/GalaxPay/Pagar.me/Pagaleve rendered "Pedido undefined" on customer-facing charge descriptions (e.g. boleto). The retry (up to ~2.8s worst case) closes that silent degradation for all of them, not just Awin.Review follow-up (cea8b1b):
timeout: 3000+maxRetries: 0+x-primary-dbto stay within the SSR function's 10s budget without starving GA4/Meta/TikTok;logger.warninstead of degrading silently;undefined); unusedeslint-disable no-continueremoved; fetch error logged with the error object.