ZapSign: webhook lia campo errado, contratos expiravam há semanas e a migration nunca tinha aplicado
Três commits para fechar um bug em cascata no webhook do ZapSign: campo errado, invariante furado e migration MySQL que nunca aplicou.
A noite começou com uma reclamação interna: convidado assinou o contrato, mas o sistema não marcou como ASSINADO. O cron passava, via que ainda estava ENVIADO, e expirava. Fazia semanas que isso rodava em silêncio.
Webhook lendo o campo errado
Mandei o Claude Code puxar o log de produção e cruzar com a documentação oficial da ZapSign. O diagnóstico apareceu rápido.
O handler estava lendo:
$event['document']['token']
Mas a ZapSign manda o token do documento no nível raiz do payload:
$event['token']
Todo evento doc_signed caía em “Payload sem document.token”. O contrato nunca avançava de status. O cron então expirava corretamente do ponto de vista do sistema — incorretamente do ponto de vista do contrato real, que já havia sido assinado.
Primeiro commit (fc2da6bb):
- Lê o token da raiz com fallback pro campo aninhado
- Extrai lógica pós-assinatura para
zapsign_aplicar_assinatura()— fonte única compartilhada entre webhook e cron pra evitar drift futuro - Adiciona cron semanal de reconciliação (
cron_zapsign_reconciliar.php) como rede de segurança pra webhooks perdidos
”Tem certeza que cobriste tudo?”
Depois do primeiro commit, cobrei: “essas correções estão de acordo com a documentação oficial do ZapSign? Tem certeza? Todas as possibilidades foram cobertas?”
A pesquisa cruzada com a doc oficial e com mais logs de produção revelou um buraco mais sério. Existia o caminho onde o contrato virava ASSINADO sem o PDF baixado. O invariante implícito “status ASSINADO ⇒ possuímos o PDF assinado” estava furado.
Segundo commit (3babd694):
zapsign_aplicar_assinaturaagora baixa o PDF antes de marcarASSINADO. Se o download falha, o status viraASSINADO_SEM_ARQUIVO. Nunca mais “Assinado” falso sem arquivo.- Webhook ganhou guard
status === 'signed'— porquedoc_signeddispara por signatário; assinatura parcial chega comopending. doc_refusedviraCANCELADO.- Ramos mortos removidos:
signer.signed,document.signed,doc.signednão existem na API ZapSign e estavam acumulando código morto. - Migration MySQL idempotente nova pras colunas
download_tentativas,ultimo_erro,proxima_tentativa.
Nesse ponto apareceu um bug em cima de bug: a migration antiga (2026_03_06) usava ADD COLUMN IF NOT EXISTS em sintaxe MariaDB. Em MySQL puro isso falha com #1064 e nunca tinha aplicado. Resultado: retry e downgrade quebravam em produção com #1054 (coluna inexistente). A migration nova resolve sem depender de sintaxe proprietária.
Por último: o enum status não listava EXPIRADO, mas o código já gravava esse valor. Adicionado no mesmo commit.
O edge case que faltou na reconciliação
Depois do fix de integridade, o Claude Code levantou: a query de reconciliação cobria ENVIADO, EXPIRADO e ASSINADO sem pdf — mas não ASSINADO_SEM_ARQUIVO, que acabava de ser criado como novo status.
Terceiro commit (0752a179): incluiu ASSINADO_SEM_ARQUIVO na varredura. Curto, mas necessário. Sem isso os registros recém-criados nesse status ficavam presos fora da varredura de recuperação indefinidamente.
Diagnóstico operacional: link público de assinatura
No início da noite, antes do bug do ZapSign aparecer, o ponto de partida foi verificar se havia tutorial ou FAQ público cobrindo o fluxo de assinatura pro usuário final. Ficou claro que o link de assinatura pública está redirecionando o convidado pra login — mas ele precisa preencher e assinar sem ter conta.
Não virou código. Virou diagnóstico mapeado pro próximo ciclo.
Auditoria do prompt do Grok para títulos de produções
Manhã e tarde foram numa frente paralela: o admin de títulos que está nascendo no kmaroteApp, que vai usar Grok via API pra gerar título com CTR/SEO em cima dos dados da produção. O prompt estava comprido; fui revisar campo por campo.
Comecei pedindo ao Claude Code uma lista markdown com todas as perguntas, respostas e dados de participantes que entram no prompt — pra validar com o próprio Grok antes de codar qualquer coisa. A primeira análise marcou vários campos como “desnecessários” ou “faltando”. Devolvi:
“Boa parte desses já está fora do prompt — me mostra como ele realmente sai.”
Refizemos o documento antes de mandar pro Grok.
O Grok devolveu pontos sobre formato de entrega (System + User + JSON Schema), atributos de aparência e palavras proibidas. Uma sutileza importante emergiu: palavras proibidas no nosso caso não são ações ou contextos — são palavras específicas que plataformas não aceitam. A abordagem tem que forçar sinônimos, não remover contexto.
Decisões tomadas:
- Manter
biotipoe atributos de aparência como whitelist explícita - As 3 primeiras palavras do título não podem parecer sem sentido pro humano que lê — CTR/SEO importam, mas só se o título for crível
foto_perfil,rg_numero,estado_nascimento,estado_cisaem do prompt (não entram no texto do título)- Algumas opções entram como
opcional para testar, não como default - Grupos aprovados → plano 3.0 como v2.1.1 antes de codar
O admin de títulos ainda está nascendo. Esta sessão fechou o desenho do prompt e da validação. Codar vem no próximo ciclo.
Estatísticas do dia:
Atividade no PC:
- Tempo ativo: 8h31min
- AFK: 30h48min
- Janela total monitorada: 39h19min
Por categoria (do que ficou ativo):
- Coding: 2h41min
- Uncategorized: 2h34min
- AI Chat: 1h16min
- Larissa Project: 57min
- Communication: 38min
- Browsing: 13min
- Reading: 8min
Top apps: Chrome (4h39min) · Antigravity IDE (2h41min) · WhatsApp (38min)
Top sites navegados: <PRIVATE-HOST> · chatgpt.com · github.com
Trabalho com IA:
- Conversas claude.ai: 0
- Sessões Claude Code: 2 (kmaroteApp — títulos/Grok manhã e tarde; ZapSign noite)
Código produzido:
- Commits: 4 (3 kmaroteApp · 1 elquercarlos)
Devlog do dia:
- 1 draft consolidado (2026-06-11-2330.md)