# 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. # # ⚠️ A ORDEM AQUI É O CONSERTO DE UM BUG REAL, não estilo. Na API o campo # `vehicle` da rota é o **ID numérico** do veículo (630011), e o nome que a # operação usa — o mesmo que a planilha do plano traz na coluna "Veículo" — # é GADE_038. Com `vehicle` na frente, o ID ganhava sempre e o romaneio saía # impresso "CONTROLE DE ENTREGA — 630011", com o motorista sem saber que # carro pegar. Nome primeiro; ID só como último recurso, porque veículo # vazio faz a linha ser descartada e o romaneio sair sem paradas. CHAVES_VEICULO = %w[vehicle_name vehicle_label vehicle vehicle_id].freeze # Dentro do objeto do veículo (quando a API aninha em vez de mandar o id). CHAVES_NOME_VEIC = %w[name label display_name plate].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, cliente) 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 = nome_do_veiculo(rota, cliente) 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, cliente) com_veiculo = visitas.reject { |v| nome_do_veiculo(v, cliente).empty? } return [] if com_veiculo.empty? com_veiculo.group_by { |v| nome_do_veiculo(v, cliente) }.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 # O NOME do veículo como a operação o conhece (GADE_038), nunca o id. # # Três formas já vistas/possíveis no mesmo campo, por isso as três camadas: # { "vehicle_name" => "GADE_038" } -> nome direto # { "vehicle" => { "name" => "GADE_038" } } -> aninhado # { "vehicle" => 630011 } -> só o id, precisa resolver # # Quando sobra só o id, procuramos o nome na lista de veículos da conta. Se # essa busca não responder, devolvemos o id mesmo: um romaneio com o número # do carro é ruim, um romaneio VAZIO (que é o que acontece devolvendo "") é # pior — a linha sem veículo é descartada. def nome_do_veiculo(hash, cliente) aninhado = hash['vehicle'] return primeiro_valor(aninhado, CHAVES_NOME_VEIC) if aninhado.is_a?(Hash) valor = primeiro_valor(hash, CHAVES_VEICULO) return valor unless valor.match?(/\A\d+\z/) nomes_de_veiculo(cliente)[valor].presence || valor end # id (string) => nome. Uma chamada por importação, memoizada: são ~70 veículos # e a alternativa seria uma consulta por rota. def nomes_de_veiculo(cliente) @nomes_de_veiculo ||= cliente.veiculos.each_with_object({}) do |v, memo| next unless v.is_a?(Hash) id = v['id'].to_s.strip nome = primeiro_valor(v, CHAVES_NOME_VEIC) memo[id] = nome unless id.empty? || nome.empty? end 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