#!/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 ') 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 puts 'Anote o resultado no cabeçalho deste arquivo antes de commitar.'