# app/services/simpli_route/client.rb # # Cliente HTTP da API do SimpliRoute (base de ESCRITA). A tabela de rastreio # local (model Entrega) é só leitura e sincroniza a partir daqui; para corrigir # um lançamento o ADM grava direto por este cliente. # # Usa Net::HTTP (stdlib) — o projeto não tem Faraday/HTTParty. # require 'net/http' require 'json' module SimpliRoute class Error < StandardError; end # Erro esperado/apresentável ao usuário (NF não achada, ambiguidade, etc.). class NotFound < Error; end class Client # Motivos de insucesso são fixos (todos type=failed). Cache curto porque # muda raríssimo, mas evita bater na API a cada abertura de tela. OBSERVATIONS_CACHE_KEY = 'simpliroute/observations'.freeze OBSERVATIONS_TTL = 1.hour def initialize(token: SimpliRoute.token, base_url: SimpliRoute.base_url) raise Error, 'SIMPLIROUTE_TOKEN não configurado no ambiente.' if token.blank? @token = token @base_uri = URI.parse(base_url) end # Lista de motivos [{ 'id' => uuid, 'type' => 'failed', 'label' => 'ÓBITO' }, ...] def observations Rails.cache.fetch(OBSERVATIONS_CACHE_KEY, expires_in: OBSERVATIONS_TTL) do Array(get('/v1/routes/observations/')) end end # Visita única (hash) pelo id numérico. def visita(id) get("/v1/routes/visits/#{id}/") 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}")) end # Resolve o `id` numérico (usado na URL de escrita) a partir de uma Entrega # do espelho local, que só tem tracking_id (SR...) + reference_id (NF) + # planned_date. Casa pelo tracking_id; se não achar, tenta pela NF. # Levanta NotFound (nada) ou Error (ambiguidade sem tracking_id). def resolver_id(entrega) data = entrega.planned_date&.to_date raise NotFound, 'Entrega sem planned_date — impossível localizar na API.' if data.blank? visitas = visitas_da_data(data) if entrega.tracking_id.present? achada = visitas.find { |v| v['tracking_id'].to_s == entrega.tracking_id.to_s } return achada['id'] if achada end nf = entrega.reference_id.to_s por_nf = visitas.select { |v| v['reference'].to_s == nf } raise NotFound, "Visita da NF #{nf} não encontrada na API na data #{data.strftime('%d/%m/%Y')}." if por_nf.empty? if por_nf.size > 1 raise Error, "Mais de uma visita para a NF #{nf} em #{data.strftime('%d/%m/%Y')} — não é possível resolver com segurança." end por_nf.first['id'] end # Atualiza campos da visita (PATCH — só o que vier em `attrs`). Retorna a # visita atualizada (hash). Preferimos PATCH ao checkout para não sobrescrever # assinatura/geo originais sem intenção. def atualizar_visita(id, attrs) patch("/v1/routes/visits/#{id}/", attrs) end # Histórico da visita (best-effort — a API pode devolver []). def historico(id) Array(get("/v1/routes/visits/#{id}/history/")) rescue Error [] end private def get(path) requisicao(Net::HTTP::Get.new(caminho(path))) end def patch(path, body) req = Net::HTTP::Patch.new(caminho(path)) req.body = body.to_json requisicao(req) end def caminho(path) URI.join(@base_uri.to_s, path) end def requisicao(req) req['Authorization'] = "Token #{@token}" req['Content-Type'] = 'application/json' req['Accept'] = 'application/json' res = Net::HTTP.start(@base_uri.host, @base_uri.port, use_ssl: @base_uri.scheme == 'https', open_timeout: 10, read_timeout: 30) do |http| http.request(req) end unless res.is_a?(Net::HTTPSuccess) raise Error, "SimpliRoute respondeu #{res.code}: #{res.body.to_s.truncate(300)}" end res.body.present? ? JSON.parse(res.body) : nil rescue JSON::ParserError => e raise Error, "Resposta inválida do SimpliRoute: #{e.message}" rescue Net::OpenTimeout, Net::ReadTimeout raise Error, 'Tempo esgotado ao falar com o SimpliRoute. Tente de novo.' end end end