CL 2026-09-11 — BK-412: credito de cliente invisivel no checkout
Sintoma
Pedido 73917 (cliente 51198): PIX de R$ 44,00 pago as 07:44, cliente clicou em "virar credito"
3 minutos depois e nao conseguiu recomprar a mesma sessao (pedido 73918, R$ 44,00).
Hipotese inicial do ADMIN: o credito teria virado R$ 43,56 por causa da tarifa. Nao era isso.
O Mercado Pago cobrou R$ 0,44 (liquido R$ 43,56), mas o ledger gravou R$ 44,00 corretamente
(tb_cliente_credito_ledger.id = 17). A regra "credito = valor pago; estorno com 20% sobre o
saldo red" ja estava implementada (taxa_red_20, rnt_cliente_carteira_simular_estorno_admin).
Causa
rnt_cliente_produtor_creditos_get() le o saldo unificado de tb_produtores.creditos. Cliente
que so compra ingresso nao tem linha ali: o creditos_set() depois do credito afeta 0 linhas e
o saldo ficava so no ledger. Checkout, carteira e simulacao de estorno mostravam R$ 0,00.
Afetados na data: 51198 (R$ 44), 21061 (R$ 40, desde 03/05), 48841 (R$ 25, desde 10/04).
Correcao
config/functions_cliente_carteira.php: sem linha emtb_produtores, o saldo e o total do
ledger. Quem tem linha de produtor segue igual.
admin/modulos/pedidos_2/action.php("Virou credito" do admin): creditavapedidos.valor,
que e o valor unitario. Agora credita valor x qtd, igual ao painel do cliente. Achado
pelo gpt-6-astra; o pedido 73705 pagou R$ 50 e recebeu R$ 25.
Validacao
- Sintaxe PHP ok (
php83 -lephp -l) nos dois arquivos. - Simulacao com o arquivo real: 51198 disponivel R$ 44,00, uso maximo R$ 44,00, estorno
R$ 8,80 de taxa / R$ 35,20 liquido; 50877 (tem linha de produtor) inalterado.
- Checkout do pedido 73918 renderizado como o cliente 51198: "Creditos disponiveis: R$ 44,00"
e formulario pay_wallet_credit com R$ 44,00.
- Nao testado: o consumo real do credito (nao houve compra de teste).
Pontas abertas
Ver BK-412 no docs/BACKLOG.md.
2a rodada (11/09/2026 13:35) — compra so com credito nao avisava ninguem
Com o saldo visivel, o cliente 51198 pagou o pedido 73918 so com credito as 13:23 e o
verifica_wa_compra_aprovada acusou "WhatsApp de compra aprovada NAO saiu". Nao era o BK-359:
o pagamento so com credito (action.php, efetuacompra_credito_site) nao passa pelo webhook
do MP e aprovava o pedido sem mandar nada. Nunca tinha sido exercitado, porque ate hoje o
saldo aparecia zerado.
action.php: depois de aprovar, manda o mesmo WhatsApp de compra aprovada do webhook
(rnt_msg_compra_aprovada_cliente), o aviso de venda aos 2 numeros do dono e o push admin.
- o UPDATE ganhou
AND status_transacao < 3e o aviso so sai comaffected_rows = 1
(o cliente disparou 3 POSTs no mesmo segundo).
- de proposito NAO dispara o webhook interno de vendas: a receita ja entrou no pedido de origem.
- validado:
php83 -lephp -l; bloco executado comenviar_whatsappem stub para o 73918;
re-seal do monitor do MP feito (--status OK).
- a mensagem do 73918 em si nao foi reenviada automaticamente.
3a rodada (11/09/2026 14:00) — confirmacao de compra barrada pelo anti-spam
Segunda leitura do gpt-6-astra, com evidencia mascarada extraida do banco (sem credencial
entregue ao modelo): o _whatsapp_pode_enviar_pro_cliente compara so os ultimos 100 bytes, e o
final da confirmacao era o link da peca + assinatura fixa. Resultado: a 2a compra do mesmo
cliente para a mesma peca em 24h (outra sessao, ingressos para amigos) ficava sem confirmacao e
o gestor recebia alerta de "loop". Foi o que barrou o reenvio manual do 73918 as 13:39.
includes/whatsapp_helper.php(rnt_msg_compra_aprovada_cliente): linha "Numero do pedido"
logo antes do rodape, dentro dos 100 bytes. Vale para as 3 rotas (webhook MP,
notifications.php, compra so com credito).
- validado:
php83 -l/php -l; 3 pedidos geram 3 sufixos distintos com o id dentro dos 100
bytes; titulo inalterado, entao o verifica_wa_compra_aprovada continua casando; re-seal do MP.
- correcao do relato anterior: o alerta do 73918 NAO era falso — a confirmacao daquele pedido
nunca saiu (rota de credito sem envio); o texto que o cliente tinha era o do 73917.
- achado: pela conversa (msg 3698) o cliente disse que clicou em "cancelar" por engano 12s
depois de pagar; a conversao em credito foi acidental.
4a rodada (11/09/2026 14:20) — confirmacao antes de cancelar pedido pago
No painel do cliente, pedido pago -> "Solicitar Cancelamento" abre a escolha (saldo 100% ou
estorno com 20%), e um toque na opcao cancelava na hora. O cliente do 73917 cancelou por
engano 12s depois de pagar.
painel/js/painel.js: a opcao agora abre a confirmacao ("Tem certeza que deseja cancelar o
pedido #N?", com o efeito de cada escolha); so o "Sim, cancelar pedido" chama o action.php.
"Nao, manter meu pedido" fecha sem fazer nada. Pedido nao pago ja tinha o "Tem certeza?".
painel/modulos/pedidos/index.php:painel.js?ver=1.0.14para furar o cache do navegador.- validado:
node --check,php83 -l/php -l; fluxo simulado em jsdom com o jquery.js do
painel: escolha -> confirmacao sem AJAX -> "Sim" envia {act: cancelar, id, cancel_choice}
(credito e estorno).
5a rodada (11/09/2026 15:00) — credito de ingresso sumia quando o cliente virava produtor
O saldo unico da pessoa mora em tb_produtores.creditos; o credito de ingresso de quem ainda
nao tinha cadastro ficava so no ledger. Quando o cadastro nascia (6 INSERTs diferentes: compra
de creditos, cadastro de produtor, perfil, ativacao premium...) nascia com 0.00 e o credito
sumia do checkout e da divulgacao. Pedido do ADMIN: o credito serve para as duas coisas.
- nova coluna
tb_produtores.carteira_sincronizada_em(SQL/2026-09-11_BK-412_carteira_sincronizada.sql);
os 19 cadastros existentes foram auditados (nenhum credito anterior ao cadastro) e marcados.
config/functions_produtor_conta.php:rnt_produtor_conta_sql_sincronizar_carteira()soma o
ledger ao saldo UMA vez por cadastro (WHERE ... IS NULL); rnt_produtor_conta_buscar()
sincroniza antes de ler; sem cadastro, rnt_produtor_saldo_creditos() devolve o ledger.
- trava por cliente (
GET_LOCK, reentrante no 5.7, vale entre conexoes) no lancamento e na
sincronizacao — achado do gpt-6-astra: cadastro criado entre o INSERT do lote e a atualizacao
do saldo dobrava o credito (R$ 40 + R$ 20 = R$ 80). Sem a trava nada e gravado: a leitura nao
sincroniza (fica para a proxima) e o lancamento devolve erro (espera ate 15s; quem chama ja
trata falha). Lancar sem trava era o que ainda permitia dobrar (2a leitura do astra).
- se o incremento do saldo falhar, o lote recem-gravado e desfeito e a funcao devolve erro;
falha na sincronizacao se corrige sozinha na proxima leitura.
config/functions_cliente_carteira.php:registrar_creditosincroniza antes do INSERT e troca
o "le e grava" por soma relativa so em cadastro ja sincronizado.
- telas que liam a coluna crua passam pelo helper:
painel/modulos/eventos/bootstrap.php,
painel/modulos/creditos/index.php, admin/modulos/clientes/detalhe.php,
admin/modulos/produtores/detalhe.php.
- validado:
php83 -l/php -lnos 6; 10 cenarios com dados reais dentro de transacao desfeita
(checkout e divulgacao antes/depois do cadastro, credito novo sem dobrar, gasto em divulgacao
refletido no checkout, compra de creditos + ledger = 100); corrida real com 3 processos e
cliente descartavel (apagado): resultado R$ 60, nao R$ 80 (rodado 2x); trava ocupada por outra
conexao: lancamento recusado em 15s sem gravar lote.
- debitos sob a mesma trava e relativos (3a leitura do astra: ingresso e divulgacao liam o mesmo
saldo e gravavam o valor absoluto; 100 - 40 - 50 podia terminar em 50 ou 60). Novo
rnt_produtor_saldo_debitar() nas duas ativacoes de premium (painel e admin); o consumo do
checkout trava do inicio ao fim e debita creditos - X dentro da transacao (rollback se falhar).
- o lote espelho do consumo (
ajuste_admin_credito, quando o ledger tem menos que o saldo)
passa sem_saldo: so iguala o ledger, sem somar de novo em tb_produtores.
- fluxo completo com cliente descartavel (dados gravados e apagados): ingresso paga divulgacao,
divulgacao recusa sem saldo, credito comprado paga ingresso, espelho nao infla, e disputa
simultanea divulgacao x ingresso com R$ 25: so uma passa, saldo final R$ 5.
- AGENTS.md dizia MySQL 5.6; e 5.7.40 (a reentrancia do GET_LOCK depende disso).
- debito do checkout so em cadastro JA sincronizado (4a leitura do astra): cadastro nascido
durante o consumo, com R$ 100 comprados sobre R$ 100 de ledger e ingresso de R$ 30, terminava
em R$ 140; agora R$ 170 (testado com cadastro criado de fato no meio do consumo).
rnt_checkout_wallet_finalize_order(): webhook MP e rotas de cartao ignoram o retorno do
consumo; quando falha em pedido ja aprovado (pagamento misto com saldo gasto no meio), grava
carteira_consumo_falha_alertada no pedido e avisa o gestor UMA vez (WhatsApp nos 2 numeros +
push). Testado com envio em stub: 2 chamadas, 1 alerta.
- consumo: "ja consumido?" (flag no pedido e lote de debito) agora vem ANTES de "tem saldo?" —
o checkout de cartao chama o consumo varias vezes depois de pago, e o alerta novo virava alarme
falso na 2a chamada (5a leitura do astra). Testado: repetir o consumo com saldo abaixo do valor
devolve "reaproveitado" e nao desconta de novo.
admin/modulos/produtores: saldo com centavos (step=0.01, semintval()), e o saldo so e
alterado se o admin mudar o valor — aplicando a DIFERENCA de forma relativa, para nao apagar
credito/debito feito com a tela aberta.
- incidente de teste:
pedidose MyISAM, o ROLLBACK nao desfez o pedido temporario 73919
(status 3, cliente 21061). Apagado 18s depois; nenhum cron, log ou WhatsApp chegou a ve-lo
(conferencia de WhatsApp so julga pedido com mais de 10 min). O id 73919 ficou consumido.
- limite conhecido: o gasto em divulgacao desconta so de
tb_produtorese nao aparece como linha
no extrato da carteira do cliente (o saldo bate; o historico fica em tb_historico_creditos).
6a rodada (11/09/2026 19:40) — reserva no pagamento misto, extrato e dados
Fechamento das pontas abertas, a pedido do ADMIN.
- Reserva (pagamento misto): o credito so saia do saldo quando o pagamento aprovava; entre
gerar o PIX e pagar, o mesmo saldo podia ser gasto em divulgacao e o pedido saia aprovado sem
o desconto. Agora o saldo de pedido pendente (status 1/2, com carteira_uso_valor, criado ha
menos de RNT_CARTEIRA_RESERVA_MIN = 15 min) e descontado na LEITURA do saldo
(rnt_cliente_carteira_reservado() / rnt_produtor_carteira_reservado()). Nao move dinheiro,
entao nao ha o que devolver se o cliente abandonar o PIX — a reserva cai sozinha no prazo.
valor_disponivel() e clamp_uso() recebem o pedido a ignorar (senao o checkout se
auto-bloqueava); o consumo na aprovacao usa o saldo bruto, porque quem pagou tem prioridade.
- Extrato da carteira passa a incluir
tb_historico_creditos: gasto em divulgacao e compra
de creditos (so o ledger aparecia; o saldo batia e o historico ficava pela metade).
- Dados: os 2 creditos antigos da conta de teste (ledger 10 e 12, pedidos 73719 e 73697)
foram corrigidos para o valor pago (R$ 2 e R$ 4); saldo 699 -> 702. check_carteira.php limpo.
- validado:
php83 -l/php -lnos 4 arquivos; 8 cenarios de reserva com cliente e pedido
descartaveis (reserva bloqueia divulgacao, nao bloqueia o proprio checkout, aprovacao consome,
reserva expira em 15 min e libera); 10 cenarios da 5a rodada sem regressao; re-seal do MP.
- lembrete mensal (BK-413) conferido: cron ativo, 3 clientes elegiveis em 11/10.
Ajuste da reserva (6a leitura do gpt-6-astra)
- a reserva contava 15 min fixos da CRIACAO do pedido, mas o pedido nasce quando o cliente
escolhe a data e o PIX so e gerado depois: pedido criado ha 14 min perdia a reserva com o PIX
valido. Agora o carimbo e carteira_uso_reservado_em, gravado com o relogio do BANCO na hora
de gerar o pagamento, e a janela e a validade do PIX + 5 min
(rnt_cliente_carteira_reserva_minutos(); hoje 15). Pedido antigo, sem carimbo, cai no
created — comportamento anterior.
rnt_checkout_wallet_prepare()agora confere o disponivel E grava a reserva sob a trava do
cliente: dois checkouts simultaneos liam o mesmo saldo livre e os dois reservavam.
- validado: 2 processos pedindo R$ 60 cada com R$ 100 de saldo -> 60 + 40, soma 100, livre 0
(pedidos criados ha 2h, PIX agora); reserva com 20 min de carimbo libera o saldo nas duas
telas; 10 cenarios da 5a rodada sem regressao; re-seal do MP.
Falha na reserva agora interrompe o pagamento (7a leitura do gpt-6-astra)
Sem a trava, ou se a reserva nao gravar, o prepare seguia cobrando so a diferenca e deixava o
credito livre para outro uso. Agora devolve erro (trava|gravacao) com credito zerado, e os 3
caminhos de pagamento (so credito, PIX, cartao) param com aviso ao cliente: "Nao foi possivel
reservar o seu saldo agora...". Validado: com a trava ocupada por outra conexao, erro=trava,
credito 0 e nada gravado no pedido; no fluxo normal, R$ 60 de credito + R$ 20 no gateway.
Reserva cobre o pedido aprovado e o pedido antigo (8a leitura do gpt-6-astra)
- o webhook grava status 3/4 e so DEPOIS consome o credito; nessa fresta a reserva sumia (a busca
so via status 1/2) e o saldo podia ser gasto em divulgacao. Agora pedido aprovado que ainda nao
teve o credito descontado tambem segura a reserva, com janela de 24h (nao depende mais da
validade do PIX; cobre retentativa de webhook e conserto manual) e expira sozinha.
- o filtro
created >= NOW() - 1 DAYignorava pedido antigo com pagamento gerado agora; saiu.
A vigencia e decidida linha a linha pelo carimbo da reserva.
- validado: pedido aprovado de 3 dias atras com R$ 60 nao descontados sobre R$ 100 de saldo ->
livre 40 nas duas telas, divulgacao de R$ 60 recusada, de R$ 40 aceita, e a reserva some quando
o desconto e feito; 10 cenarios da 5a rodada sem regressao.
- atencao para quem for testar:
metadadosmontado na mao pelo shell nao desserializa e a reserva
simplesmente nao conta — gravar sempre pelo PHP (rnt_cliente_carteira_atualizar_meta_pedido).
Pagamento em aberto e vigencia por estado (9a leitura do gpt-6-astra)
- gerar um PIX de R$ 40 usando R$ 60 de credito e, sem pagar, gerar outro com R$ 20 mudava a
reserva do pedido: pagar o primeiro faria o webhook descontar so R$ 20. Agora, com pagamento
valido em aberto (transaction_id + reserva vigente, nada consumido), mudar a composicao e
recusado (erro=pagamento_aberto) com mensagem ao cliente; gerar de novo com o MESMO valor
(recarregar a tela) continua funcionando.
- vigencia por estado: pedido pago sem desconto NAO expira (o credito e devido; o gestor ja e
avisado), cartao em analise (status 2) tem 24h, e so o aguardando pagamento usa a janela do PIX.
- validado: 3 cenarios de pagamento em aberto (60 -> muda para 20 recusado -> 60 de novo aceito);
10 cenarios da 5a rodada sem regressao; re-seal do MP.
Protecao durante a geracao do pagamento (10a leitura do gpt-6-astra)
- a protecao exigia
transaction_id, que so e gravado depois da resposta do gateway; nessa espera
outra requisicao ainda trocava a composicao. Agora a reserva protege desde que e gravada, o
pedido e RELIDO dentro da trava, e reserva sem pagamento gerado ha mais de 2 min e liberada
(tentativa que morreu no meio nao trava o cliente). Idade medida pelo relogio do BANCO — o
php.ini desta VPS usa PRC e time() daria horas de diferenca.
- a consulta de reservas perdeu o
LIMIT 200(podia esconder reserva antiga ainda valida) e passou
a filtrar por metadados LIKE '%carteira_uso_valor%', que e mais barato e nao corta nada.
- validado: 4 cenarios (gera 60 sem transaction_id -> tentar 20 recusado -> 60 de novo aceito ->
reserva abandonada ha 5 min libera a troca); 10 cenarios da 5a rodada; re-seal do MP.
Limpeza da reserva e janelas por meio de pagamento (11a leitura do gpt-6-astra)
- escolher pagar SEM credito depois de um PIX misto abandonado nao apagava a reserva antiga (a
gravacao so acontecia com valor > 0): o pedido ficava com R$ X e o webhook ainda descontaria.
Agora a reserva e apagada nesse caso.
- cartao "in_process" vira status 1 aqui (nao 2), entao a janela de 24h passa a valer tambem por
METODO de pagamento (CARD/CREDIT/CARTAO), nao so pelo status.
- pedido recusado/cancelado (status 7) entra na conta da reserva enquanto a janela do PIX nao
passa: um PIX gerado antes ainda pode chegar ao webhook e aprovar o pedido.
- validado: 5 cenarios (reserva apagada ao trocar para pagamento integral; cartao em analise ha
30 min ainda segura; recusado ha 30 min libera; recusado agora continua segurando);
10 cenarios da 5a rodada; re-seal do MP.
Mesma regra nos dois lados (12a leitura do gpt-6-astra)
Consequencia da rodada anterior: a janela por METODO entrou na leitura do saldo, mas nao no ponto
que protege a composicao — cartao pendente ha 30 min reservava o saldo e ainda deixava trocar o
valor do credito. Agora o prepare rele tambem o metodo_pagamento e usa a mesma
regra. Alem disso, falha ao APAGAR a reserva (troca para pagamento integral) interrompe a cobranca
com erro=gravacao, em vez de seguir com o credito antigo sujeito a desconto.
- validado: cartao em analise ha 30 min -> saldo reservado (livre 40 de 100) E troca de
composicao recusada (pagamento_aberto); 10 cenarios da 5a rodada; re-seal do MP.
- limpeza: os dois retornos antecipados do prepare nao soltam mais a trava na mao (o
finallyja
fazia isso; soltar duas vezes so confundia a leitura).
Normalizar o modo antes de comparar (13a leitura do gpt-6-astra)
A protecao comparava o valor pedido com a reserva ANTES de normalizar o wallet_mode. Um POST com
modo invalido (ex.: pix_only) e valor 60 passava pela protecao e, logo depois, a normalizacao
zerava o credito e o bloco de limpeza APAGAVA a reserva de um PIX ainda pagavel — o pedido podia
ser aprovado sem descontar os R$ 60. Agora a normalizacao vem primeiro.
- validado: com PIX de R$ 60 em aberto, POST
modo=pix_only+ 60 e recusado (pagamento_aberto),
a reserva fica intacta e o saldo segue reservado (livre 40 de 100); 10 cenarios sem regressao.
Modo amarrado a rota e composicao sempre carimbada (14a leitura do gpt-6-astra)
- a rota do PIX aceitava
wallet_card: gravava R$ 60 de credito e mesmo assim cobrava o valor
cheio. Agora cada rota so aceita o seu modo (credit_only, wallet_pix, wallet_card), via
4o parametro do prepare.
- gerar PIX INTEGRAL e depois acrescentar credito passava, porque sem reserva anterior nao havia
carimbo para comparar — e pagar o PIX antigo cobraria o cheio E descontaria o credito. Agora a
composicao e carimbada sempre, inclusive com credito zero (zero nao reserva dinheiro, so
registra o combinado).
- validado: rota PIX com modo de cartao -> credito 0; PIX integral -> tentar acrescentar R$ 60 e
recusado (pagamento_aberto), saldo e composicao intactos; regerar o mesmo PIX integral segue
funcionando; 10 cenarios da 5a rodada; re-seal do MP.
Tentativa de cartao pendente manda na janela (15a leitura do gpt-6-astra)
Trocar cartao pendente por PIX trocava o metodo_pagamento do pedido e encurtava a janela da
reserva de 24h para 15 min; passados 16 min dava para mudar a composicao enquanto o cartao antigo
ainda podia aprovar. Agora o pedido carrega carteira_uso_cartao_pendente e essa marca manda na
janela (24h) ate o pedido ser pago ou a reserva expirar.
- validado: cartao com R$ 60 -> troca para PIX com o mesmo valor (aceita) -> 16 min depois tentar
R$ 20 e recusado (pagamento_aberto), composicao e saldo preservados; 10 cenarios sem regressao.
Compra REAL de ponta a ponta (conta de teste da familia do ADMIN)
Primeira prova fora do teste de codigo: sessao montada para o cliente 50877, tudo por HTTP no site
em producao.
- carteira mostrou R$ 702,00;
act=efetuacompracriou o pedido 73935 (Lisa, Lesa & Louca, 15/11,
1 ingresso, R$ 22);
checkout_pix.phprenderizou "Creditos disponiveis: R$ 702,00" e a opcao SALDO RNT;act=efetuacompra_credito_siteaprovou: status 3,CARTEIRA_CLIENTE, saldo 702 -> 680, lote de
debito no ledger amarrado ao pedido;
- WhatsApp de nova venda chegou nos 2 numeros do dono; push "Pedido pago #73935" saiu no cron das
20:45;
- cancelamento pelo painel do cliente (
cancel_choice=credito) devolveu 100%: saldo 702 de novo,
status 11 e o ingresso voltou para a sessao (30 disponiveis).
Dois achados do teste real:
- o WhatsApp de compra aprovada ao CLIENTE falhou (HTTP 500 no envio): o celular cadastrado na
conta de teste e invalido — (55) 2 1979-9799 vira 5555219799799. E dado, nao codigo; o proprio
bot avisou o gestor da falha de envio.
- push disparado de dentro de requisicao web NUNCA sai:
enviar_push_admin()chama o sender por
exec, e exec/shell_exec/system/popen estao em disable_functions no PHP do site. Quem manda o
push de venda e o cron notifica_pedido_push.php (a cada 5 min) — foi ele que entregou. As
chamadas de push dentro de webhook/action sao inofensivas, mas sao codigo morto.
Ajustes da 16a leitura do gpt-6-astra
- as rotas de PIX e cartao regravavam a composicao FORA da trava, podendo apagar a marca de cartao
pendente de outra tentativa simultanea; a regravacao saiu (o prepare ja grava sob a trava).
- cartao recusado (status 7/6) nao segura mais o credito por 24h: so a janela do PIX vale, porque a
tentativa de cartao ja acabou.