219 lines
9.3 KiB
Ruby
Executable File
219 lines
9.3 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
# frozen_string_literal: true
|
|
#
|
|
# Descobre COMO a API do SimpliRoute expõe o par veículo/ordem de um plano — que é
|
|
# o que o romaneio ("CONTROLE DE ENTREGA") precisa para saber quantas folhas
|
|
# imprimir e em que sequência.
|
|
#
|
|
# ── POR QUE ESTE SCRIPT EXISTE ──────────────────────────────────────────────
|
|
# Romaneios::PlanoDoDia tem uma escada de queda:
|
|
# (a) /v1/routes/routes/ traz vehicle + lista ordenada de visitas
|
|
# (b) a própria visita traz alguma chave de veículo/ordem
|
|
# (c) nada disso -> espelho local (db_reem_simplerout_2026), por vehicle + eta
|
|
# (d) espelho vazio -> manda o operador subir o .xlsx do plano
|
|
# As rungs (a) e (b) NUNCA foram verificadas: não há SIMPLIROUTE_TOKEN na máquina
|
|
# de desenvolvimento. Este script fecha isso.
|
|
#
|
|
# ── RESULTADO DA RODADA DE __/__/____ ───────────────────────────────────────
|
|
# (preencher depois de rodar, como está em bin/sondar_busca_nf)
|
|
# [ ] /v1/routes/routes/ existe? status: ___ rotas: ___
|
|
# [ ] a rota traz vehicle? ___
|
|
# [ ] a rota embute a lista de visitas? ___
|
|
# [ ] a visita traz vehicle/order? ___
|
|
# [ ] veículos pela API x pelo espelho: ___ x ___
|
|
# => rung usada: ___
|
|
#
|
|
# SÓ LEITURA: o script faz apenas GET. Não altera nada.
|
|
#
|
|
# Rode NO SERVIDOR (é lá que vive o SIMPLIROUTE_TOKEN):
|
|
#
|
|
# SIMPLIROUTE_TOKEN=xxx bin/sondar_plano_do_dia --data 2026-08-31
|
|
#
|
|
# --data precisa ser um dia com plano PUBLICADO (senão a resposta vem vazia e não
|
|
# prova nada).
|
|
#
|
|
# ⚠️ Por que olhar os VALORES e não só as chaves: a API é Django REST e devolve
|
|
# chave existente com valor nil o tempo todo. "A visita tem a chave `vehicle`" NÃO
|
|
# significa que ela venha preenchida — a prova é o valor.
|
|
|
|
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
|
|
|
|
CHAVES_INTERESSE = /vehicle|route|driver|order|sequence|position/i.freeze
|
|
|
|
def sair(msg)
|
|
warn msg
|
|
exit 1
|
|
end
|
|
|
|
sair('Defina SIMPLIROUTE_TOKEN no ambiente.') if TOKEN.empty?
|
|
|
|
data = nil
|
|
ARGV.each_with_index { |a, i| data = ARGV[i + 1] if a == '--data' }
|
|
sair('Uso: bin/sondar_plano_do_dia --data <YYYY-MM-DD>') if data.to_s.empty?
|
|
Date.parse(data) rescue sair("Data inválida: #{data}")
|
|
|
|
def get(caminho)
|
|
uri = URI.join(BASE, caminho)
|
|
req = Net::HTTP::Get.new(uri)
|
|
req['Authorization'] = "Token #{TOKEN}"
|
|
req['Accept'] = 'application/json'
|
|
|
|
inicio = Time.now
|
|
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https',
|
|
open_timeout: 10, read_timeout: 120) do |http|
|
|
http.request(req)
|
|
end
|
|
[res.code.to_i, res.body.to_s, Time.now - inicio]
|
|
rescue StandardError => e
|
|
[0, e.message, Time.now - inicio]
|
|
end
|
|
|
|
def json(corpo)
|
|
JSON.parse(corpo)
|
|
rescue JSON::ParserError
|
|
nil
|
|
end
|
|
|
|
def amostra(hash)
|
|
hash.select { |k, _| k.to_s.match?(CHAVES_INTERESSE) }
|
|
.map { |k, v| " #{k} = #{v.inspect.slice(0, 90)}" }
|
|
end
|
|
|
|
puts "== SONDAGEM DO PLANO DE #{data} =="
|
|
puts
|
|
|
|
# ── 1. Visitas do dia ───────────────────────────────────────────────────────
|
|
codigo, corpo, seg = get("/v1/routes/visits/?planned_date=#{data}")
|
|
puts "[1] GET /v1/routes/visits/?planned_date=#{data}"
|
|
puts " HTTP #{codigo} · #{corpo.bytesize} bytes · #{seg.round(2)}s"
|
|
|
|
visitas = json(corpo)
|
|
sair(' Resposta não é JSON — abortando.') unless visitas.is_a?(Array)
|
|
puts " visitas: #{visitas.size}"
|
|
sair(' Nenhuma visita nessa data. Escolha um dia com plano publicado.') if visitas.empty?
|
|
|
|
primeira = visitas.first
|
|
puts " chaves: #{primeira.keys.sort.join(', ')}"
|
|
puts ' chaves de interesse (com VALOR — chave nil não serve):'
|
|
linhas = amostra(primeira)
|
|
puts(linhas.empty? ? ' (nenhuma)' : linhas)
|
|
|
|
preenchidas = primeira.select { |k, v| k.to_s.match?(CHAVES_INTERESSE) && !v.nil? && v.to_s.strip != '' }
|
|
puts " => rung (b) viável? #{preenchidas.keys.any? { |k| k.to_s.match?(/vehicle/i) } ? 'SIM' : 'NÃO'}"
|
|
puts
|
|
|
|
# ── 2. Rotas do dia ─────────────────────────────────────────────────────────
|
|
codigo, corpo, seg = get("/v1/routes/routes/?planned_date=#{data}")
|
|
puts "[2] GET /v1/routes/routes/?planned_date=#{data}"
|
|
puts " HTTP #{codigo} · #{corpo.bytesize} bytes · #{seg.round(2)}s"
|
|
|
|
rotas = json(corpo)
|
|
if rotas.is_a?(Array) && rotas.any?
|
|
rota = rotas.first
|
|
puts " rotas: #{rotas.size}"
|
|
puts " chaves: #{rota.keys.sort.join(', ')}"
|
|
puts ' chaves de interesse:'
|
|
puts(amostra(rota).then { |l| l.empty? ? ' (nenhuma)' : l })
|
|
visits = rota['visits']
|
|
puts " embute 'visits'? #{visits.nil? ? 'NÃO' : "SIM (#{Array(visits).size} itens, #{Array(visits).first.class})"}"
|
|
tem_veiculo = %w[vehicle vehicle_name vehicle_id].any? { |k| rota[k].to_s.strip != '' }
|
|
puts " => rung (a) viável? #{tem_veiculo && !visits.nil? ? 'SIM' : 'NÃO'}"
|
|
else
|
|
puts " sem rotas utilizáveis (corpo: #{corpo.slice(0, 200)})"
|
|
puts ' => rung (a) NÃO viável'
|
|
end
|
|
puts
|
|
|
|
# ── 3. Cruzamento com o espelho ─────────────────────────────────────────────
|
|
# Quantos veículos a API enxerga? O número tem que bater com o do espelho local;
|
|
# se não bater, alguma rota está ficando de fora e o romaneio sairia incompleto.
|
|
veiculos_api = visitas.filter_map { |v| %w[vehicle vehicle_name].filter_map { |k| v[k] }.first }
|
|
.map(&:to_s).reject(&:empty?).uniq
|
|
if rotas.is_a?(Array)
|
|
veiculos_api |= rotas.filter_map { |r| %w[vehicle vehicle_name].filter_map { |k| r[k] }.first }
|
|
.map(&:to_s).reject(&:empty?)
|
|
end
|
|
puts "[3] veículos distintos vistos pela API: #{veiculos_api.size}"
|
|
puts " #{veiculos_api.sort.first(10).join(', ')}#{'…' if veiculos_api.size > 10}"
|
|
puts
|
|
puts ' Compare com o espelho, no console do app:'
|
|
puts " Entrega.da_conta_gade.no_periodo(Date.parse('#{data}'), Date.parse('#{data}'))" \
|
|
'.distinct.count(:vehicle)'
|
|
puts
|
|
|
|
# ── 4. O plano tem NOME? ────────────────────────────────────────────────────
|
|
# POR QUE ISTO IMPORTA: a operação NÃO trabalha por data — ela identifica o
|
|
# plano pelo NOME ("EMAD 09.2026" e afins), que é inclusive o texto impresso no
|
|
# canto do romaneio. Hoje a tela pede a data porque é o único filtro que
|
|
# sabemos que a API aceita (/v1/routes/visits/?planned_date=). Para trocar por
|
|
# uma BUSCA POR NOME é preciso primeiro descobrir se existe endpoint de planos
|
|
# e qual campo carrega o nome — é o que esta seção responde.
|
|
#
|
|
# As URLs abaixo são CANDIDATAS: a única rota /v1/plans/ que o app já usa é
|
|
# /v1/plans/visits/<id>/detail/, então sabemos que o namespace existe, mas não
|
|
# se ele lista planos. 404/403 aqui é resposta válida — significa "não dá por
|
|
# esse caminho", e a busca por nome teria que ser feita filtrando as visitas.
|
|
puts
|
|
puts '[4] procurando um endpoint de PLANOS (para buscar por nome em vez de data)'
|
|
|
|
CHAVES_NOME = /name|title|label|descri|plan/i.freeze
|
|
|
|
candidatas = [
|
|
'/v1/plans/',
|
|
"/v1/plans/?planned_date=#{data}",
|
|
'/v1/routes/plans/',
|
|
"/v1/routes/plans/?planned_date=#{data}"
|
|
]
|
|
|
|
achou_algum = false
|
|
candidatas.each do |caminho|
|
|
codigo, corpo, seg = get(caminho)
|
|
puts " GET #{caminho}"
|
|
puts " HTTP #{codigo} · #{corpo.bytesize} bytes · #{seg.round(2)}s"
|
|
|
|
dados = json(corpo)
|
|
# A API é Django REST: ora devolve lista, ora {"results": [...]}.
|
|
itens = dados.is_a?(Array) ? dados : (dados.is_a?(Hash) ? Array(dados['results']) : [])
|
|
if itens.empty?
|
|
puts " sem itens (#{corpo.slice(0, 120)})"
|
|
next
|
|
end
|
|
|
|
achou_algum = true
|
|
item = itens.first
|
|
next unless item.is_a?(Hash)
|
|
|
|
puts " itens: #{itens.size}"
|
|
puts " chaves: #{item.keys.sort.join(', ')}"
|
|
nomes = item.select { |k, v| k.to_s.match?(CHAVES_NOME) && !v.nil? && v.to_s.strip != '' }
|
|
if nomes.empty?
|
|
puts ' => nenhum campo de nome PREENCHIDO neste item'
|
|
else
|
|
puts ' => campos de nome preenchidos:'
|
|
nomes.each { |k, v| puts " #{k} = #{v.inspect.slice(0, 90)}" }
|
|
puts " => primeiros nomes: #{itens.filter_map { |i| i.is_a?(Hash) ? nomes.keys.filter_map { |k| i[k] }.first : nil }.first(5).inspect}"
|
|
end
|
|
end
|
|
|
|
# Plano B: se não há endpoint de planos, o nome pode vir na própria visita.
|
|
unless achou_algum
|
|
puts ' nenhum endpoint de planos respondeu com itens.'
|
|
puts ' procurando um campo de nome na VISITA (plano B — filtrar visitas pelo nome):'
|
|
nomes_visita = primeira.select { |k, v| k.to_s.match?(CHAVES_NOME) && !v.nil? && v.to_s.strip != '' }
|
|
if nomes_visita.empty?
|
|
puts ' (nenhum) => busca por nome NÃO é possível pela API; a data continua sendo'
|
|
puts ' o único filtro, e o nome fica só como rótulo do PDF.'
|
|
else
|
|
nomes_visita.each { |k, v| puts " #{k} = #{v.inspect.slice(0, 90)}" }
|
|
end
|
|
end
|
|
|
|
puts
|
|
puts 'Anote o resultado no cabeçalho deste arquivo antes de commitar.'
|