# app/services/romaneios/plano_do_dia.rb # # Busca o plano de UMA data e devolve as paradas na forma canônica do # Romaneios::Normalizador — a mesma que Romaneios::PlanoPlanilha entrega. # # ⚠️ NÃO está verificado se a visita de /v1/routes/visits/ carrega veículo/ordem, e # isso não é testável na máquina de desenvolvimento (sem token, sem .env). Por isso # aqui existe uma ESCADA DE QUEDA explícita, e não um caminho feliz único: # # a) rotas da API trazem vehicle + lista ordenada de visitas -> origem :api # b) as visitas trazem alguma chave de veículo/ordem -> origem :api # c) nada disso (ou sem token) -> ESPELHO local # d) espelho vazio na data -> NotFound, com a # mensagem que manda o operador usar o .xlsx do plano # # A rung (c) não é consolo: o romaneio é impresso ANTES do dia começar, e ela cobre # "plano publicado e já sincronizado, mas a API fora do ar" — a falha mais provável. # Rode bin/sondar_plano_do_dia no servidor para fechar (a)/(b). module Romaneios class PlanoDoDia # Chaves candidatas nos hashes da API. Só as usamos se vierem PREENCHIDAS — # a API devolve chave existente com valor nil o tempo todo. CHAVES_VEICULO = %w[vehicle vehicle_name vehicle_id].freeze CHAVES_MOTORISTA = %w[driver driver_name].freeze CHAVES_ORDEM = %w[order sequence position route_order].freeze attr_reader :origem def initialize(data:, client: nil) @data = data.to_date @client = client @origem = nil end def linhas @linhas ||= carregar end private def carregar linhas = da_api return linhas if linhas.any? linhas = do_espelho return linhas if linhas.any? raise SimpliRoute::NotFound, "O plano de #{@data.strftime('%d/%m/%Y')} não está disponível no SimpliRoute " \ 'nem no espelho de rastreio. Envie a planilha do plano (.xlsx).' end # ── (a) e (b) ──────────────────────────────────────────────────────────── def da_api return [] unless SimpliRoute.configurado? cliente = @client || SimpliRoute::Client.new visitas = cliente.visitas_da_data(@data) return [] if visitas.empty? linhas = por_rotas(cliente, visitas) linhas = por_visitas(visitas) if linhas.empty? @origem = 'api' if linhas.any? linhas rescue SimpliRoute::Error # Token ausente/expirado ou API fora: cai para o espelho, que é a mesma # informação já sincronizada. [] end # (a) A rota traz o veículo e a lista de visitas na ordem do roteiro. def por_rotas(cliente, visitas) rotas = cliente.rotas_da_data(@data) return [] if rotas.empty? por_id = visitas.index_by { |v| v['id'] } linhas = [] rotas.each do |rota| veiculo = primeiro_valor(rota, CHAVES_VEICULO) next if veiculo.empty? ids = Array(rota['visits']).map { |v| v.is_a?(Hash) ? v['id'] : v } ids = visitas.select { |v| v['route'].to_s == rota['id'].to_s }.map { |v| v['id'] } if ids.empty? ids.each_with_index do |id, i| visita = por_id[id] next if visita.nil? linhas << monta(visita, veiculo, primeiro_valor(rota, CHAVES_MOTORISTA), i + 1) end end linhas end # (b) A visita traz o veículo direto; a ordem sai de `order`/`sequence` ou, # na falta, do horário estimado de chegada. def por_visitas(visitas) com_veiculo = visitas.reject { |v| primeiro_valor(v, CHAVES_VEICULO).empty? } return [] if com_veiculo.empty? com_veiculo.group_by { |v| primeiro_valor(v, CHAVES_VEICULO) }.flat_map do |veiculo, lista| ordenadas(lista).each_with_index.map do |visita, i| monta(visita, veiculo, primeiro_valor(visita, CHAVES_MOTORISTA), i + 1) end end end def ordenadas(lista) lista.sort_by.with_index do |v, i| chave = CHAVES_ORDEM.filter_map { |k| v[k] }.first [chave.nil? ? Float::INFINITY : chave.to_i, v['estimated_time_arrival'].to_s, i] end end def monta(visita, veiculo, motorista, ordem) Normalizador.linha( veiculo: veiculo, ordem: ordem, motorista: motorista, titulo: visita['title'], endereco: visita['address'], anotacoes: visita['notes'], nota_fiscal: visita['reference'], tracking_id: visita['tracking_id'] ) end def primeiro_valor(hash, chaves) chaves.filter_map { |k| hash[k] }.map(&:to_s).find { |v| !v.strip.empty? }.to_s.strip end # ── (c) Espelho local ──────────────────────────────────────────────────── # `db_reem_simplerout_2026` já tem driver, vehicle, title, address, notes e eta. # Só leitura, como sempre. def do_espelho registros = Entrega.da_conta_gade .no_periodo(@data, @data) .where.not(vehicle: [nil, '']) .order(:vehicle, :eta) .to_a return [] if registros.empty? @origem = 'espelho' registros.group_by(&:vehicle).flat_map do |veiculo, lista| lista.each_with_index.map do |e, i| Normalizador.linha( veiculo: veiculo, ordem: i + 1, motorista: e.driver, titulo: e.title, endereco: e.address, anotacoes: e.notes, nota_fiscal: e.reference_id, tracking_id: e.tracking_id ) end end end end end