Correção do função de editar lançamento para quer consiga listar todas

This commit is contained in:
2026-07-21 16:23:25 -03:00
parent 2b0a3b52ef
commit faa5226e95
4 changed files with 390 additions and 19 deletions

View File

@@ -33,6 +33,9 @@ class Admin::EdicaoLancamentosController < ApplicationController
# #
# Com um único lançamento já vem o card montado, para não custar um segundo # Com um único lançamento já vem o card montado, para não custar um segundo
# round-trip no caso comum. # round-trip no caso comum.
#
# Aceita ?de=&ate= para varrer um período (a API só filtra por UM dia, então
# varremos dia a dia). Sem período, usa as datas que o espelho conhece.
def buscar def buscar
authorize :edicao_lancamento authorize :edicao_lancamento
@@ -40,15 +43,21 @@ class Admin::EdicaoLancamentosController < ApplicationController
return render_erro('Informe o número da NF.') if nf.blank? return render_erro('Informe o número da NF.') if nf.blank?
entregas = Entrega.por_nf(nf).to_a entregas = Entrega.por_nf(nf).to_a
return render_erro("NF #{nf} não encontrada na base de rastreio.") if entregas.empty? datas = datas_de_busca(entregas)
return render_erro(sem_datas_msg(nf, entregas)) if datas.empty?
ocorrencias = ocorrencias_da_nf(nf, entregas) falhas = []
ocorrencias = ocorrencias_da_nf(nf, entregas, datas, falhas)
unica = ocorrencias.first if ocorrencias.one? && ocorrencias.first[:id].present? unica = ocorrencias.first if ocorrencias.one? && ocorrencias.first[:id].present?
render json: { render json: {
ok: true, ok: true,
ocorrencias: ocorrencias, ocorrencias: ocorrencias,
visita: (unica && payload_visita(unica[:id], entrega_de(entregas, unica[:tracking_id]))), visita: (unica && payload_seguro(unica, entregas, falhas)),
periodo: { de: datas.first, ate: datas.last, dias: datas.size },
# Dias que a API não respondeu. Vão para a tela: uma busca que devolve
# menos do que existe TEM que dizer que devolveu menos.
falhas: falhas,
motivos: motivos_seguros motivos: motivos_seguros
} }
rescue SimpliRoute::NotFound => e rescue SimpliRoute::NotFound => e
@@ -152,18 +161,53 @@ class Admin::EdicaoLancamentosController < ApplicationController
@client ||= SimpliRoute::Client.new @client ||= SimpliRoute::Client.new
end end
# Máximo de dias varridos numa busca por período. A API só filtra por UM dia,
# então cada dia é uma chamada — com `search` cada uma custa ~0,6 s e elas vão
# em lotes paralelos, o que torna dois meses viável; o teto existe para a tela
# não virar uma varredura sem fim.
MAX_DIAS_VARREDURA = 62
# Quais dias consultar na API:
# • com ?de=/?ate= — o período pedido (o SimpliRoute duplica plano para
# outra data, e aí o espelho não tem como saber qual é);
# • sem período — as datas que o espelho conhece (barato: 1 ou 2 chamadas).
def datas_de_busca(entregas)
de = data_param(:de)
ate = data_param(:ate)
if de || ate
inicio, fim = [de || ate, ate || de].minmax
(inicio..fim).first(MAX_DIAS_VARREDURA)
else
entregas.filter_map { |e| e.planned_date&.to_date }.uniq.sort
end
end
def data_param(chave)
valor = params[chave].to_s.strip
return nil if valor.blank?
Date.parse(valor)
rescue Date::Error
nil
end
# Sem datas não há o que consultar — e o motivo muda a saída para o ADM.
def sem_datas_msg(nf, entregas)
if entregas.empty?
"NF #{nf} não encontrada na base de rastreio. Informe um período para procurar direto no SimpliRoute."
else
"NF #{nf} está na base de rastreio mas sem data planejada. Informe um período para procurar na API."
end
end
# Todos os lançamentos da NF, cruzando as DUAS fontes: # Todos os lançamentos da NF, cruzando as DUAS fontes:
# • espelho de rastreio (Entrega) — traz motorista/veículo; # • espelho de rastreio (Entrega) — traz motorista/veículo;
# • API do SimpliRoute, nas datas desses lançamentos — pega a visita # • API do SimpliRoute, nas datas varridas — pega a visita duplicada que a
# duplicada que a sincronização ainda não trouxe para o espelho. # sincronização ainda não trouxe para o espelho.
# def ocorrencias_da_nf(nf, entregas, datas, falhas)
# A API não tem busca global por NF (só por data), então as datas vêm do
# espelho: 1 chamada por data distinta (plano duplicado costuma cair no mesmo
# dia, e aí é 1 chamada só).
def ocorrencias_da_nf(nf, entregas)
datas = entregas.filter_map { |e| e.planned_date&.to_date }.uniq
trackings = entregas.map { |e| e.tracking_id.to_s } trackings = entregas.map { |e| e.tracking_id.to_s }
do_dia = datas.flat_map { |data| client.visitas_da_data(data) } do_dia = visitas_dos_dias(datas, nf, falhas)
visitas = do_dia.select { |v| v['reference'].to_s == nf || trackings.include?(v['tracking_id'].to_s) } visitas = do_dia.select { |v| v['reference'].to_s == nf || trackings.include?(v['tracking_id'].to_s) }
.uniq { |v| v['id'] } .uniq { |v| v['id'] }
@@ -176,13 +220,48 @@ class Admin::EdicaoLancamentosController < ApplicationController
# ele existe em vez de sumir silenciosamente — que é o bug que estamos # ele existe em vez de sumir silenciosamente — que é o bug que estamos
# corrigindo. # corrigindo.
vistos = visitas.map { |v| v['tracking_id'].to_s } vistos = visitas.map { |v| v['tracking_id'].to_s }
orfas = entregas.reject { |e| vistos.include?(e.tracking_id.to_s) }.map { |e| ocorrencia_do_espelho(e) } orfas = entregas.reject { |e| vistos.include?(e.tracking_id.to_s) }
.map { |e| ocorrencia_do_espelho(e, datas.include?(e.planned_date&.to_date)) }
# Mais recente primeiro (a data vem como texto ISO da API e como timestamp # Mais recente primeiro (a data vem como texto ISO da API e como timestamp
# do espelho — os 10 primeiros caracteres normalizam as duas). # do espelho — os 10 primeiros caracteres normalizam as duas).
(achadas + orfas).sort_by { |o| [o[:data].to_s[0, 10], o[:id].to_i] }.reverse (achadas + orfas).sort_by { |o| [o[:data].to_s[0, 10], o[:id].to_i] }.reverse
end end
# Dias consultados de uma vez. Cada chamada abre a própria conexão HTTP (ver
# SimpliRoute::Client#requisicao) e não toca o banco, então dá para
# paralelizar em lotes; o lote pequeno evita martelar a API do SimpliRoute.
MAX_PARALELO_VARREDURA = 6
# Passa a NF como `search` — sem isso cada dia baixaria ~3,8 MB (o dia inteiro)
# e um mês seria inviável. Ver a nota em SimpliRoute::Client#visitas_da_data.
def visitas_dos_dias(datas, nf, falhas)
mutex = Mutex.new
datas.each_slice(MAX_PARALELO_VARREDURA).flat_map do |lote|
lote.map { |data| Thread.new { visitas_do_dia(data, nf, falhas, mutex) } }.flat_map(&:value)
end
end
# Um dia que a API não responde NÃO pode derrubar a busca inteira: registra a
# falha e segue com os outros dias. (Foi assim que uma NF com visitas em duas
# datas passou a não devolver nada em vez de devolver o que deu certo.)
def visitas_do_dia(data, nf, falhas, mutex)
client.visitas_da_data(data, busca: nf)
rescue SimpliRoute::Error => e
mutex.synchronize { falhas << { data: data.strftime('%d/%m/%Y'), erro: e.message } }
[]
end
# Abrir o card do lançamento único é conveniência: se falhar, a lista ainda
# tem que aparecer.
def payload_seguro(ocorrencia, entregas, falhas)
payload_visita(ocorrencia[:id], entrega_de(entregas, ocorrencia[:tracking_id]))
rescue SimpliRoute::Error => e
falhas << { data: ocorrencia[:data].to_s[0, 10], erro: "não foi possível abrir o lançamento: #{e.message}" }
nil
end
def entrega_de(entregas, tracking_id) def entrega_de(entregas, tracking_id)
return nil if tracking_id.blank? return nil if tracking_id.blank?
@@ -233,7 +312,7 @@ class Admin::EdicaoLancamentosController < ApplicationController
} }
end end
def ocorrencia_do_espelho(entrega) def ocorrencia_do_espelho(entrega, data_varrida)
{ {
id: nil, id: nil,
tracking_id: entrega.tracking_id, tracking_id: entrega.tracking_id,
@@ -246,7 +325,11 @@ class Admin::EdicaoLancamentosController < ApplicationController
veiculo: entrega.vehicle, veiculo: entrega.vehicle,
editavel: false, editavel: false,
no_painel: true, no_painel: true,
aviso: 'Só no painel — não está entre as visitas desta data no SimpliRoute, então não dá para editar' aviso: (if data_varrida
'Só no painel — não está entre as visitas desta data no SimpliRoute, então não dá para editar'
else
'Fora do período consultado — inclua a data deste lançamento no período para poder editá-lo'
end)
} }
end end

View File

@@ -41,8 +41,27 @@ module SimpliRoute
end end
# Todas as visitas de uma data (Array de hashes). # Todas as visitas de uma data (Array de hashes).
def visitas_da_data(data) #
Array(get("/v1/routes/visits/?planned_date=#{data.to_date.iso8601}")) # `busca` vira `&search=` — parâmetro NÃO documentado, mas verificado contra
# a API real em 21/07/2026: filtra de verdade e derruba a resposta de um dia
# de ~3,8 MB / ~9 s (1879 visitas) para ~1 KB / ~0,6 s. Sem ele, varrer um
# mês é inviável.
#
# ⚠️ NÃO confiar só nele: `reference`, `reference_id`, `q` e `title` são
# ignorados em SILÊNCIO pela API (devolvem 200 com o dia inteiro). Se o
# `search` mudar de nome, cai no mesmo comportamento — por isso quem chama
# continua filtrando por `reference`, e a perda é só de desempenho.
#
# ⚠️ SEM `planned_date` a API responde um conjunto padrão (~2469 visitas,
# medido) que NÃO cobre o histórico: a NF 82891 voltava só com a visita de
# 21/07, escondendo a de 17/07. Todo filtro de intervalo testado
# (planned_date_from/to, since/until, __gte/__lte, date_from/to) é ignorado.
# Por isso a data continua obrigatória e a varredura é dia a dia.
def visitas_da_data(data, busca: nil)
caminho = "/v1/routes/visits/?planned_date=#{data.to_date.iso8601}"
caminho += "&search=#{URI.encode_www_form_component(busca.to_s)}" if busca.present?
Array(get(caminho))
end end
# Resolve o `id` numérico (usado na URL de escrita) a partir de uma Entrega # Resolve o `id` numérico (usado na URL de escrita) a partir de uma Entrega

View File

@@ -40,8 +40,40 @@
Buscar Buscar
</button> </button>
</div> </div>
<%# Período — a API do SimpliRoute só filtra visita por UM dia, então achar
uma NF exige saber em quais dias procurar. Sem período, procuramos nas
datas que o painel já conhece; quando o plano foi duplicado para outra
data, o painel não sabe dela e é preciso ampliar aqui. %>
<div class="flex flex-wrap items-end gap-2 mt-3">
<div>
<label class="block text-xs text-gray-500 mb-1" for="campo-de">De</label>
<input id="campo-de" type="date"
class="bg-[#1a1a1a] border border-white/10 rounded-xl px-3 py-2 text-white text-sm
focus:outline-none focus:border-orange-500 min-h-[42px]">
</div>
<div>
<label class="block text-xs text-gray-500 mb-1" for="campo-ate">Até</label>
<input id="campo-ate" type="date"
class="bg-[#1a1a1a] border border-white/10 rounded-xl px-3 py-2 text-white text-sm
focus:outline-none focus:border-orange-500 min-h-[42px]">
</div>
<button id="btn-30dias" type="button"
class="px-4 py-2 bg-[#1a1a1a] hover:bg-[#252525] border border-white/10 text-gray-200
text-sm rounded-xl transition-colors min-h-[42px] whitespace-nowrap">
Últimos 30 dias
</button>
<button id="btn-limpar-periodo" type="button"
class="px-3 py-2 text-gray-500 hover:text-gray-300 text-sm min-h-[42px]">Limpar</button>
</div>
<p class="mt-2 text-xs text-gray-500">
Sem período, procuramos só nas datas que o painel conhece. Se o plano foi duplicado para outra
data, amplie o período — a API do SimpliRoute não busca NF por intervalo, então varremos dia a
dia (máx. 62 dias).
</p>
<p id="busca-erro" class="hidden mt-2 text-sm text-red-400"></p> <p id="busca-erro" class="hidden mt-2 text-sm text-red-400"></p>
<p id="busca-loading" class="hidden mt-2 text-sm text-gray-400">Consultando SimpliRoute…</p> <p id="busca-loading" class="hidden mt-2 text-sm text-gray-400">Consultando SimpliRoute…</p>
<p id="busca-periodo" class="hidden mt-2 text-xs text-gray-500"></p>
<div id="busca-falhas" class="hidden mt-2 p-3 rounded-xl bg-red-900/20 border border-red-500/40 text-xs text-red-200 space-y-1"></div>
</div> </div>
<%# Lançamentos da NF — só aparece quando há mais de um. Plano duplicado no <%# Lançamentos da NF — só aparece quando há mais de um. Plano duplicado no
@@ -199,9 +231,16 @@
const loadEl = document.getElementById('busca-loading'); const loadEl = document.getElementById('busca-loading');
const cardEl = document.getElementById('card'); const cardEl = document.getElementById('card');
const campoDe = document.getElementById('campo-de');
const campoAte = document.getElementById('campo-ate');
const periodoEl = document.getElementById('busca-periodo');
const falhasEl = document.getElementById('busca-falhas');
async function buscar() { async function buscar() {
const nf = campoNf.value.trim(); const nf = campoNf.value.trim();
erroEl.classList.add('hidden'); erroEl.classList.add('hidden');
periodoEl.classList.add('hidden');
falhasEl.classList.add('hidden');
if (!nf) { mostrarErroBusca('Informe o número da NF.'); return; } if (!nf) { mostrarErroBusca('Informe o número da NF.'); return; }
loadEl.classList.remove('hidden'); loadEl.classList.remove('hidden');
@@ -209,14 +248,18 @@
ocorrencias = []; ocorrencias = [];
selecionado = null; selecionado = null;
try { try {
const resp = await fetch(URLS.buscar + '?nf=' + encodeURIComponent(nf), { headers: { 'Accept': 'application/json' } }); let url = URLS.buscar + '?nf=' + encodeURIComponent(nf);
if (campoDe.value) url += '&de=' + encodeURIComponent(campoDe.value);
if (campoAte.value) url += '&ate=' + encodeURIComponent(campoAte.value);
const resp = await fetch(url, { headers: { 'Accept': 'application/json' } });
const data = await resp.json(); const data = await resp.json();
if (!data.ok) { renderOcorrencias(); mostrarErroBusca(data.erro || 'Não foi possível localizar a entrega.'); return; } if (!data.ok) { renderOcorrencias(); mostrarErroBusca(data.erro || 'Não foi possível localizar a entrega.'); return; }
ocorrencias = data.ocorrencias || []; ocorrencias = data.ocorrencias || [];
renderPeriodo(data.periodo, data.falhas);
// Só veio o card pronto quando a NF tem um lançamento único; com mais de // Só veio o card pronto quando a NF tem um lançamento único; com mais de
// um, o ADM escolhe na lista antes de abrir qualquer coisa. // um, o ADM escolhe na lista antes de abrir qualquer coisa.
if (data.visita) { abrirCard(data.visita); } else { visita = null; renderOcorrencias(); } if (data.visita) { abrirCard(data.visita); } else { visita = null; renderOcorrencias(); }
if (!data.visita && !ocorrencias.length) mostrarErroBusca('Nenhum lançamento localizado para esta NF.'); if (!data.visita && !ocorrencias.length) mostrarErroBusca('Nenhum lançamento localizado para esta NF no período consultado.');
} catch (e) { } catch (e) {
renderOcorrencias(); renderOcorrencias();
mostrarErroBusca('Erro de conexão ao buscar a NF.'); mostrarErroBusca('Erro de conexão ao buscar a NF.');
@@ -226,9 +269,37 @@
} }
function mostrarErroBusca(msg) { erroEl.textContent = msg; erroEl.classList.remove('hidden'); } function mostrarErroBusca(msg) { erroEl.textContent = msg; erroEl.classList.remove('hidden'); }
// Dizer o que foi consultado e o que falhou. Uma busca que devolve menos do
// que existe precisa admitir isso — senão parece que a NF não existe.
function renderPeriodo(periodo, falhas) {
if (periodo && periodo.dias) {
periodoEl.textContent = 'Consultado: ' + fmtData(periodo.de) +
(periodo.dias > 1 ? ' a ' + fmtData(periodo.ate) + ' (' + periodo.dias + ' dias)' : '') + '.';
periodoEl.classList.remove('hidden');
}
if (falhas && falhas.length) {
falhasEl.innerHTML = '<p class="font-semibold">A API do SimpliRoute falhou em ' + falhas.length +
' dia(s) — pode haver lançamento faltando na lista:</p>' +
falhas.map(f => '<p>' + esc(f.data) + ' — ' + esc(f.erro) + '</p>').join('');
falhasEl.classList.remove('hidden');
}
}
document.getElementById('btn-buscar').addEventListener('click', buscar); document.getElementById('btn-buscar').addEventListener('click', buscar);
campoNf.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); buscar(); } }); campoNf.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); buscar(); } });
document.getElementById('btn-30dias').addEventListener('click', () => {
const hoje = new Date();
const trinta = new Date(hoje.getTime() - 30 * 86400000);
const iso = d => d.toISOString().slice(0, 10);
campoDe.value = iso(trinta);
campoAte.value = iso(hoje);
if (campoNf.value.trim()) buscar();
});
document.getElementById('btn-limpar-periodo').addEventListener('click', () => {
campoDe.value = ''; campoAte.value = '';
});
// ── Lista de lançamentos da NF ────────────────────────────── // ── Lista de lançamentos da NF ──────────────────────────────
const ocEl = document.getElementById('ocorrencias'); const ocEl = document.getElementById('ocorrencias');
const ocLista = document.getElementById('oc-lista'); const ocLista = document.getElementById('oc-lista');

198
bin/sondar_busca_nf Executable file
View File

@@ -0,0 +1,198 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# Descobre se a API do SimpliRoute aceita buscar visita por NF e/ou por período
# — a doc oficial só documenta `?planned_date=` de UM dia.
#
# ── RESULTADO DA RODADA DE 21/07/2026 (contra a API real) ───────────────────
# ✔ `&search=<NF>` FUNCIONA (não documentado): dia inteiro cai de ~3,8 MB/9 s
# para ~1 KB/0,6 s. É o que a tela usa hoje.
# ✘ `reference`, `reference_id`, `q`, `title`, `reference__*` — IGNORADOS em
# silêncio (200 + dia inteiro).
# ✘ Intervalo (`planned_date_from/to`, `since/until`, `__gte/__lte`,
# `date_from/to`) — TODOS ignorados: devolveram 2469, igual ao controle sem
# parâmetro nenhum.
# ✘ Sem `planned_date` a API devolve um conjunto padrão (~2469) que NÃO cobre
# o histórico — `?search=<NF>` sozinho escondeu a visita mais antiga da NF.
# ⇒ `planned_date` continua obrigatório; a busca por NF é dia a dia + search.
#
# Rode de novo se desconfiar que a API mudou.
#
# SÓ LEITURA: o script faz apenas GET/OPTIONS. Não altera nada.
#
# Rode NO SERVIDOR (é lá que vive o SIMPLIROUTE_TOKEN):
#
# SIMPLIROUTE_TOKEN=xxx bin/sondar_busca_nf --nf 82891 --data 2026-07-17
# SIMPLIROUTE_TOKEN=xxx bin/sondar_busca_nf --nf 82891 --data 2026-07-17 --sem-data
#
# --data deve ser um dia em que a NF EXISTE (é a referência de comparação).
# --sem-data testa a consulta sem `planned_date` nenhum; pode ser lenta ou vir
# gigante, por isso fica de fora por padrão.
#
# ⚠️ Por que comparar contagens: a API é Django REST. Parâmetro que o backend
# não registra é IGNORADO em silêncio — devolve 200 e a lista inteira. Então
# "voltou 200 com resultados" NÃO prova que filtrou. A prova é o filtro DIMINUIR
# o resultado do dia e sobrar só a NF pedida.
require 'net/http'
require 'json'
require 'uri'
require 'date'
BASE = ENV.fetch('SIMPLIROUTE_BASE_URL', 'https://api.simpliroute.com')
TOKEN = ENV['SIMPLIROUTE_TOKEN'].to_s
def sair(msg)
warn msg
exit 1
end
sair('Defina SIMPLIROUTE_TOKEN no ambiente.') if TOKEN.empty?
nf = nil
data = nil
sem_data = false
ARGV.each_with_index do |a, i|
nf = ARGV[i + 1] if a == '--nf'
data = ARGV[i + 1] if a == '--data'
sem_data = true if a == '--sem-data'
end
sair('Uso: bin/sondar_busca_nf --nf <numero> --data <YYYY-MM-DD> [--sem-data]') if nf.to_s.empty? || data.to_s.empty?
def requisicao(caminho, metodo: :get)
uri = URI.join(BASE, caminho)
req = metodo == :options ? Net::HTTP::Options.new(uri) : Net::HTTP::Get.new(uri)
req['Authorization'] = "Token #{TOKEN}"
req['Accept'] = 'application/json'
t0 = Time.now
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https',
open_timeout: 10, read_timeout: 120) { |h| h.request(req) }
corpo = begin
res.body.to_s.empty? ? nil : JSON.parse(res.body)
rescue JSON::ParserError
res.body.to_s[0, 200]
end
[res.code.to_i, corpo, res.body.to_s.bytesize, Time.now - t0]
rescue StandardError => e
[0, "ERRO: #{e.class}: #{e.message}", 0, Time.now - t0]
end
# A resposta pode ser lista crua OU paginada ({count, next, results}). No caso
# paginado o `count` já é o total — é a informação mais barata que existe aqui.
def itens(corpo)
return corpo if corpo.is_a?(Array)
return Array(corpo['results']) if corpo.is_a?(Hash) && corpo.key?('results')
[]
end
def total(corpo, lista)
corpo.is_a?(Hash) && corpo['count'] ? corpo['count'] : lista.size
end
def titulo(t)
puts "\n#{'─' * 70}\n#{t}\n#{'─' * 70}"
end
puts "Base: #{BASE} NF: #{nf} Data de referência: #{data}"
# ── 1. Linha de base: o dia inteiro, do jeito que a tela faz hoje ────────────
titulo("1. LINHA DE BASE · GET /v1/routes/visits/?planned_date=#{data}")
cod, corpo, bytes, seg = requisicao("/v1/routes/visits/?planned_date=#{data}")
sair("Falhou (HTTP #{cod}): #{corpo.inspect}") unless cod == 200
base_lista = itens(corpo)
base_total = total(corpo, base_lista)
base_ids = base_lista.map { |v| v['id'] }
da_nf = base_lista.select { |v| v['reference'].to_s == nf.to_s }
puts " HTTP 200 em #{seg.round(2)}s — #{bytes} bytes"
puts " visitas no dia .......... #{base_total}"
puts " paginado? ............... #{corpo.is_a?(Hash) ? "sim (count=#{corpo['count']}, next=#{corpo['next'].inspect})" : 'não (lista crua)'}"
puts " visitas com a NF #{nf} ... #{da_nf.size}"
da_nf.each { |v| puts " id=#{v['id']} tracking=#{v['tracking_id'].inspect} status=#{v['status']} data=#{v['planned_date']}" }
puts " ⚠️ tracking_id vem NULO na lista-por-data" if da_nf.any? && da_nf.all? { |v| v['tracking_id'].nil? }
sair("A NF #{nf} não aparece em #{data}. Passe uma data em que ela exista — sem isso não há como comparar.") if da_nf.empty?
# ── 2. Filtro por NF: o parâmetro DIMINUI o resultado do dia? ────────────────
# Se a contagem não mudar, o parâmetro foi ignorado (mesmo com HTTP 200).
titulo('2. FILTRO POR NF · <dia> + <param>=NF (diminuiu = filtro real)')
%w[reference reference_id search q title reference__exact reference__icontains].each do |param|
cod, corpo, _b, seg = requisicao("/v1/routes/visits/?planned_date=#{data}&#{param}=#{nf}")
lista = itens(corpo)
qtd = total(corpo, lista)
veredito =
if cod != 200 then "HTTP #{cod} — não aceito"
elsif qtd == base_total then 'IGNORADO (mesma contagem do dia inteiro)'
elsif qtd.zero? then 'aceito porém vazio — filtrou demais'
elsif lista.all? { |v| v['reference'].to_s == nf.to_s } then "★ FILTRA DE VERDADE — #{qtd} visita(s), todas da NF"
else "reduziu p/ #{qtd} mas veio NF de fora"
end
puts " #{param.ljust(22)} #{veredito} (#{seg.round(2)}s)"
end
# ── 3. Filtro por período ────────────────────────────────────────────────────
# Comparação: o intervalo <data>..<data+1> tem que devolver MAIS que só <data>.
dia2 = (Date.parse(data) + 1).to_s
titulo("3. FILTRO POR PERÍODO · #{data} a #{dia2} (> #{base_total} = intervalo respeitado)")
[
%w[planned_date_from planned_date_to],
%w[planned_date_after planned_date_before],
%w[planned_date__gte planned_date__lte],
%w[since until],
%w[start_date end_date],
%w[date_from date_to]
].each do |de, ate|
cod, corpo, _b, seg = requisicao("/v1/routes/visits/?#{de}=#{data}&#{ate}=#{dia2}")
lista = itens(corpo)
qtd = total(corpo, lista)
veredito =
if cod != 200 then "HTTP #{cod} — não aceito"
elsif qtd > base_total then "★ RESPEITA O INTERVALO — #{qtd} visitas (dia sozinho: #{base_total})"
elsif qtd == base_total then "mesma contagem do dia — inconclusivo/ignorado"
else "#{qtd} visitas — menos que o dia sozinho, estranho"
end
puts " #{"#{de}/#{ate}".ljust(38)} #{veredito} (#{seg.round(2)}s)"
end
# ── 4. OPTIONS: o DRF às vezes lista os filtros aceitos ──────────────────────
titulo('4. OPTIONS /v1/routes/visits/ (metadados do endpoint)')
cod, corpo, = requisicao('/v1/routes/visits/', metodo: :options)
if cod == 200 && corpo.is_a?(Hash)
puts " métodos ... #{corpo['renders'] ? corpo.slice('name', 'description', 'parses').inspect[0, 300] : corpo.keys.inspect}"
filtros = corpo['filters'] || corpo['filter_fields'] || corpo.dig('actions', 'GET')
puts " filtros ... #{filtros ? filtros.inspect[0, 500] : '(não declarados)'}"
else
puts " HTTP #{cod} — #{corpo.inspect[0, 200]}"
end
# ── 5. Sem planned_date nenhum — a pergunta direta ───────────────────────────
unless sem_data
puts "\n(Pulei o teste SEM planned_date. Rode com --sem-data para incluir — pode demorar ou vir gigante.)"
exit 0
end
titulo('5. SEM planned_date · a API devolve a NF inteira de uma vez?')
[
"/v1/routes/visits/?reference=#{nf}",
"/v1/routes/visits/?search=#{nf}",
'/v1/routes/visits/'
].each do |caminho|
cod, corpo, bytes, seg = requisicao(caminho)
lista = itens(corpo)
qtd = total(corpo, lista)
casam = lista.count { |v| v['reference'].to_s == nf.to_s }
puts " GET #{caminho}"
if cod == 200
puts " #{qtd} visita(s), #{casam} com a NF #{nf} — #{bytes} bytes em #{seg.round(2)}s"
datas = lista.select { |v| v['reference'].to_s == nf.to_s }.map { |v| v['planned_date'] }.uniq
puts " datas da NF: #{datas.inspect}" if datas.any?
puts ' ★ RESOLVE TUDO: a NF veio inteira, sem varrer dia a dia' if casam > 1 && casam == qtd
else
puts " HTTP #{cod} — #{corpo.inspect[0, 200]}"
end
end