teste #10
@@ -33,6 +33,9 @@ class Admin::EdicaoLancamentosController < ApplicationController
|
||||
#
|
||||
# Com um único lançamento já vem o card montado, para não custar um segundo
|
||||
# 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
|
||||
authorize :edicao_lancamento
|
||||
|
||||
@@ -40,15 +43,21 @@ class Admin::EdicaoLancamentosController < ApplicationController
|
||||
return render_erro('Informe o número da NF.') if nf.blank?
|
||||
|
||||
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?
|
||||
|
||||
render json: {
|
||||
ok: true,
|
||||
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
|
||||
}
|
||||
rescue SimpliRoute::NotFound => e
|
||||
@@ -152,18 +161,53 @@ class Admin::EdicaoLancamentosController < ApplicationController
|
||||
@client ||= SimpliRoute::Client.new
|
||||
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:
|
||||
# • espelho de rastreio (Entrega) — traz motorista/veículo;
|
||||
# • API do SimpliRoute, nas datas desses lançamentos — pega a visita
|
||||
# duplicada que a sincronização ainda não trouxe para o espelho.
|
||||
#
|
||||
# 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
|
||||
# • API do SimpliRoute, nas datas varridas — pega a visita duplicada que a
|
||||
# sincronização ainda não trouxe para o espelho.
|
||||
def ocorrencias_da_nf(nf, entregas, datas, falhas)
|
||||
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) }
|
||||
.uniq { |v| v['id'] }
|
||||
@@ -176,13 +220,48 @@ class Admin::EdicaoLancamentosController < ApplicationController
|
||||
# ele existe em vez de sumir silenciosamente — que é o bug que estamos
|
||||
# corrigindo.
|
||||
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
|
||||
# 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
|
||||
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)
|
||||
return nil if tracking_id.blank?
|
||||
|
||||
@@ -233,7 +312,7 @@ class Admin::EdicaoLancamentosController < ApplicationController
|
||||
}
|
||||
end
|
||||
|
||||
def ocorrencia_do_espelho(entrega)
|
||||
def ocorrencia_do_espelho(entrega, data_varrida)
|
||||
{
|
||||
id: nil,
|
||||
tracking_id: entrega.tracking_id,
|
||||
@@ -246,7 +325,11 @@ class Admin::EdicaoLancamentosController < ApplicationController
|
||||
veiculo: entrega.vehicle,
|
||||
editavel: false,
|
||||
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
|
||||
|
||||
|
||||
@@ -41,8 +41,27 @@ module SimpliRoute
|
||||
end
|
||||
|
||||
# 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
|
||||
|
||||
# Resolve o `id` numérico (usado na URL de escrita) a partir de uma Entrega
|
||||
|
||||
@@ -40,8 +40,40 @@
|
||||
Buscar
|
||||
</button>
|
||||
</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-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>
|
||||
|
||||
<%# 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 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() {
|
||||
const nf = campoNf.value.trim();
|
||||
erroEl.classList.add('hidden');
|
||||
periodoEl.classList.add('hidden');
|
||||
falhasEl.classList.add('hidden');
|
||||
if (!nf) { mostrarErroBusca('Informe o número da NF.'); return; }
|
||||
|
||||
loadEl.classList.remove('hidden');
|
||||
@@ -209,14 +248,18 @@
|
||||
ocorrencias = [];
|
||||
selecionado = null;
|
||||
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();
|
||||
if (!data.ok) { renderOcorrencias(); mostrarErroBusca(data.erro || 'Não foi possível localizar a entrega.'); return; }
|
||||
ocorrencias = data.ocorrencias || [];
|
||||
renderPeriodo(data.periodo, data.falhas);
|
||||
// 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.
|
||||
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) {
|
||||
renderOcorrencias();
|
||||
mostrarErroBusca('Erro de conexão ao buscar a NF.');
|
||||
@@ -226,9 +269,37 @@
|
||||
}
|
||||
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);
|
||||
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 ──────────────────────────────
|
||||
const ocEl = document.getElementById('ocorrencias');
|
||||
const ocLista = document.getElementById('oc-lista');
|
||||
|
||||
198
bin/sondar_busca_nf
Executable file
198
bin/sondar_busca_nf
Executable 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
|
||||
Reference in New Issue
Block a user