Sistema de Expiracao de Pesquisas¶
Status: Implementado em
developestaging(branchfeat/survey-expiration+fix/sent-at-on-immediate-send)Data: 2026-06-02
Versao: 1.0
Contexto¶
Antes desta implementacao, execucoes (SurveyRun) ficavam eternamente com status pending ou in_progress quando o cliente nao respondia ao convite. Isso bloqueava o reenvio da mesma pesquisa para aquele contato indefinidamente, ja que ha uma regra em backend/surveys/views.py que impede disparo de pesquisa quando ha run ativa para o mesmo (survey, contact).
O sistema agora expira automaticamente runs antigas via tarefa Celery e tambem permite cancelamento manual pelo operador.
Modelo de Dados¶
Campos relevantes em SurveyRun¶
Indices criados¶
| Python | |
|---|---|
Permite que a task de expiracao filtre eficientemente runs antigas.
Regras de Expiracao¶
Tres regras independentes cobrem cenarios distintos:
Regra A: pending_no_send¶
Quando: status='pending' AND sent_at IS NULL
Janela: SURVEY_EXPIRY_PENDING_NO_SEND_HOURS (default 24h)
Referencia: created_at
Significado: Run foi criada mas o convite nunca chegou a ser enviado via WhatsApp (falha tecnica de envio). Cliente final nunca viu nenhuma mensagem.
Excecao: se
is_scheduled=Trueescheduled_atno futuro, a regra NAO se aplica (a run esta aguardando agendamento legitimo).
Regra B: pending_no_reply¶
Quando: status='pending' AND sent_at IS NOT NULL
Janela: SURVEY_EXPIRY_PENDING_NO_REPLY_HOURS (default 72h)
Referencia: sent_at
Significado: Template foi enviado para o cliente, mas ele nao respondeu SIM nem NAO. Apos 3 dias considera-se que ele optou por nao participar.
Regra C: in_progress_abandoned¶
Quando: status='in_progress'
Janela: SURVEY_EXPIRY_IN_PROGRESS_HOURS (default 24h)
Referencia: last_activity_at (ou started_at ou created_at como fallback)
Significado: Cliente comecou a responder mas parou no meio. Apos 24h sem nova resposta, considera-se abandono. Esta janela esta alinhada com a janela de 24h do WhatsApp Business API -- alem dela, o bot nao poderia mais enviar mensagens livres.
Configuracao¶
As janelas sao configuraveis via variaveis de ambiente:
| Bash | |
|---|---|
Lidas em backend/app/settings.py:
Implementacao¶
1. Metodos no Model (SurveyRun)¶
2. Task Celery (expire_stale_runs)¶
| Python | |
|---|---|
A task usa QuerySet.update() (bulk) ao inves de iterar com .save() para performance. Filtra usando os indices criados.
Retorna:
| Python | |
|---|---|
3. Registro de atividade do usuario¶
Em backend/whatsapp/services/survey_service.py:
| Python | |
|---|---|
Toda resposta do cliente (qualquer tipo: texto, botao, lista, midia) atualiza last_activity_at. Isso evita marcar como abandonada conversas em andamento legitimas.
4. Auto-heal no envio¶
Em backend/surveys/views.py:
Garante que mesmo se o operador disparar segundos antes da task rodar, o sistema reconhece runs ja expiradas.
5. Endpoint manual force-expire¶
Marca a run como expired imediatamente. Disponivel no botao "Cancelar Execucao" na tela de detalhes da run, visivel apenas quando status in ('pending', 'in_progress').
Fluxo Completo¶
Testes Realizados¶
| Teste | Resultado |
|---|---|
Task expire_stale_runs executada manualmente | 15 runs antigas expiradas (Regra A) |
Endpoint force-expire em Run pending | HTTP 200, status -> expired, completed_at preenchido |
| Bloqueio com payload estruturado | Retorna error_code, hours_remaining, reason_display |
Backfill de sent_at retroativo | 17 runs migraram de Regra A (24h) para Regra B (72h) |
touch_activity() em resposta de cliente | last_activity_at atualizado corretamente |
Operacao¶
Verificar estado das runs¶
| Bash | |
|---|---|
Executar expiracao manualmente¶
| Bash | |
|---|---|
Listar tarefas periodicas¶
| Bash | |
|---|---|
Migrations Aplicadas¶
0006_add_last_activity_at: adiciona campolast_activity_at+ index0007_create_expire_runs_task: registraPeriodicTask"Expirar pesquisas antigas" (1h)
Endpoints Modificados/Criados¶
| Metodo | URL | Acao |
|---|---|---|
| POST | /api/v1/surveys/{id}/start_survey/ | Modificado: payload estruturado quando bloqueia + auto-heal |
| POST | /api/v1/survey-runs/{id}/force-expire/ | Novo: cancela manualmente uma execucao |
Payload de bloqueio estruturado¶
O frontend trata esse payload diferenciado em frontend/src/components/runs/RunNewClient.tsx mostrando uma mensagem agrupada com tempo restante por contato.
Bug Relacionado Corrigido¶
sent_at nao era preenchido no envio imediato: O endpoint start_survey enviava o template mas nao atualizava sent_at na SurveyRun. Como consequencia, todas as runs caiam na Regra A (24h) em vez da Regra B (72h), reduzindo o prazo do cliente em 48h.
Correcao em backend/surveys/views.py: apos send_initial_survey_message() retornar True, faz survey_run.refresh_from_db() + seta sent_at = timezone.now().
Commits: 47b6a95 (merge em develop).
Backfill aplicado em producao para as 17 runs ja afetadas, copiando o timestamp do template_invitation mais antigo.