199 lines
8.8 KiB
Ruby
Executable File
199 lines
8.8 KiB
Ruby
Executable File
#!/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
|