Cerebro Studio · Backlog · Changelog
RioNoTeatro • /www/wwwroot/rionoteatro.com.br/docs/changelog/2026/CL-2026-09-11-BK-412-credito-cliente-invisivel.md • 2026-09-11T23:47:10.201Z

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 em tb_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): creditava pedidos.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 -l e php -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 < 3 e o aviso so sai com affected_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 -l e php -l; bloco executado com enviar_whatsapp em 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.14 para 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_credito sincroniza 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 -l nos 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, sem intval()), 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: pedidos e 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_produtores e 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 -l nos 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 DAY ignorava 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: metadados montado 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 finally ja

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.

  1. carteira mostrou R$ 702,00; act=efetuacompra criou o pedido 73935 (Lisa, Lesa & Louca, 15/11,

1 ingresso, R$ 22);

  1. checkout_pix.php renderizou "Creditos disponiveis: R$ 702,00" e a opcao SALDO RNT;
  2. act=efetuacompra_credito_site aprovou: status 3, CARTEIRA_CLIENTE, saldo 702 -> 680, lote de

debito no ledger amarrado ao pedido;

  1. WhatsApp de nova venda chegou nos 2 numeros do dono; push "Pedido pago #73935" saiu no cron das

20:45;

  1. 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.