From faa5226e9598d150c062f9c67c719acffc24297f1a455a3ae984a021e186df4a Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 21 Jul 2026 16:23:25 -0300 Subject: [PATCH] =?UTF-8?q?Corre=C3=A7=C3=A3o=20do=20fun=C3=A7=C3=A3o=20de?= =?UTF-8?q?=20editar=20lan=C3=A7amento=20para=20quer=20consiga=20listar=20?= =?UTF-8?q?todas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/edicao_lancamentos_controller.rb | 113 ++++++++-- app/services/simpli_route/client.rb | 23 +- .../admin/edicao_lancamentos/show.html.erb | 75 ++++++- bin/sondar_busca_nf | 198 ++++++++++++++++++ 4 files changed, 390 insertions(+), 19 deletions(-) create mode 100755 bin/sondar_busca_nf diff --git a/app/controllers/admin/edicao_lancamentos_controller.rb b/app/controllers/admin/edicao_lancamentos_controller.rb index 9877618..83e9c1b 100644 --- a/app/controllers/admin/edicao_lancamentos_controller.rb +++ b/app/controllers/admin/edicao_lancamentos_controller.rb @@ -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 diff --git a/app/services/simpli_route/client.rb b/app/services/simpli_route/client.rb index 49c87a7..5f59e78 100644 --- a/app/services/simpli_route/client.rb +++ b/app/services/simpli_route/client.rb @@ -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 diff --git a/app/views/admin/edicao_lancamentos/show.html.erb b/app/views/admin/edicao_lancamentos/show.html.erb index 41336fb..eb3b20a 100644 --- a/app/views/admin/edicao_lancamentos/show.html.erb +++ b/app/views/admin/edicao_lancamentos/show.html.erb @@ -40,8 +40,40 @@ Buscar + <%# 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. %> +
+
+ + +
+
+ + +
+ + +
+

+ 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). +

+ + <%# 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 = '

A API do SimpliRoute falhou em ' + falhas.length + + ' dia(s) — pode haver lançamento faltando na lista:

' + + falhas.map(f => '

' + esc(f.data) + ' — ' + esc(f.erro) + '

').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'); diff --git a/bin/sondar_busca_nf b/bin/sondar_busca_nf new file mode 100755 index 0000000..774435c --- /dev/null +++ b/bin/sondar_busca_nf @@ -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=` 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=` 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 --data [--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 · + =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 .. tem que devolver MAIS que só . +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