150 lines
5.7 KiB
Ruby
150 lines
5.7 KiB
Ruby
# app/jobs/sincronizar_series_aparelho_job.rb
|
|
#
|
|
# Busca na API do SimpliRoute o "Número de Série da Base (Aparelho)" das visitas
|
|
# e grava em series_aparelho, para a planilha Entregas do cliente sair com essa
|
|
# coluna preenchida.
|
|
#
|
|
# POR QUE EM BACKGROUND: o dado só existe na API (o espelho tem a coluna e nunca
|
|
# a preenche — 0 de ~6.000 linhas em fev/mar/mai/jul/2026). A API não filtra por
|
|
# intervalo: é uma chamada POR DATA, cada uma com o dia inteiro (~4 MB / ~9 s).
|
|
# Um mês são ~25 chamadas — inviável dentro do request do download, tranquilo
|
|
# num job noturno.
|
|
#
|
|
# Roda via Whenever → `rake simpli_route:series` (últimos DIAS_PADRAO dias).
|
|
# Para meses antigos, o backfill é manual: `rake simpli_route:series_operacao[tabela]`.
|
|
class SincronizarSeriesAparelhoJob < ApplicationJob
|
|
queue_as :default
|
|
|
|
# Mesmo limite da varredura do Admin::EdicaoLancamentosController — a API do
|
|
# SimpliRoute é lenta por dia, mas aguenta alguns dias em paralelo.
|
|
MAX_PARALELO = 6
|
|
|
|
# Janela padrão da rodada agendada. Cobre o mês corrente inteiro com folga,
|
|
# que é o que o cliente pede em planilha no dia a dia.
|
|
DIAS_PADRAO = 45
|
|
|
|
# Nomes conhecidos do campo em `extra_field_values`. O 1º que vier preenchido
|
|
# ganha; se nenhum casar, cai no fallback por /seri/ (ver #serie_de) — foi
|
|
# chutar nome de campo que deixou meia planilha vazia antes.
|
|
CAMPOS_SERIE = %w[
|
|
numero_de_serie_da_base_aparelho numero_de_serie_da_base numero_serie_base
|
|
num_serie_base serie_base numero_de_serie serie
|
|
].freeze
|
|
|
|
# Devolve um resumo { datas:, visitas:, series:, falhas: } para o rake logar.
|
|
def perform(de: nil, ate: nil, operacao: nil, dias: DIAS_PADRAO)
|
|
return resumo_vazio('SIMPLIROUTE_TOKEN não configurado') unless SimpliRoute.configurado?
|
|
|
|
datas = datas_alvo(de: de, ate: ate, operacao: operacao, dias: dias)
|
|
return resumo_vazio('nenhuma data com visitas no período') if datas.empty?
|
|
|
|
visitas = varrer(datas)
|
|
gravadas = gravar(visitas)
|
|
|
|
avisar_se_nada_casou(gravadas, visitas)
|
|
{ datas: datas.size, visitas: visitas.size, series: gravadas, falhas: @falhas }
|
|
end
|
|
|
|
private
|
|
|
|
# Datas que valem uma chamada à API. Sempre saem do espelho (planned_date das
|
|
# visitas que já conhecemos) — varrer dia sem visita é chamada jogada fora.
|
|
def datas_alvo(de:, ate:, operacao:, dias:)
|
|
escopo = Entrega.where.not(planned_date: nil)
|
|
escopo = escopo.da_operacoes([operacao]) if operacao.present?
|
|
|
|
inicio, fim = if de.present? && ate.present?
|
|
[de.to_date, ate.to_date]
|
|
elsif operacao.present?
|
|
[nil, nil] # operação inteira, sem recorte de data
|
|
else
|
|
[Date.current - dias.to_i, Date.current]
|
|
end
|
|
escopo = escopo.no_periodo(inicio, fim) if inicio && fim
|
|
|
|
escopo.distinct.pluck(Arel.sql('planned_date::date')).compact.sort
|
|
end
|
|
|
|
# Um dia que a API não responde não pode derrubar a rodada: registra e segue
|
|
# (mesma postura da varredura de NF do Admin::EdicaoLancamentosController).
|
|
def varrer(datas)
|
|
@falhas = []
|
|
mutex = Mutex.new
|
|
|
|
datas.each_slice(MAX_PARALELO).flat_map do |lote|
|
|
lote.map { |data| Thread.new { visitas_do_dia(data, mutex) } }.flat_map(&:value)
|
|
end
|
|
end
|
|
|
|
def visitas_do_dia(data, mutex)
|
|
client.visitas_da_data(data)
|
|
rescue SimpliRoute::Error => e
|
|
mutex.synchronize { @falhas << { data: data.to_s, erro: e.message } }
|
|
[]
|
|
end
|
|
|
|
# Grava só as visitas que TÊM série. upsert_all em vez de N inserts: uma
|
|
# operação de um mês passa de 1.000 visitas.
|
|
def gravar(visitas)
|
|
agora = Time.current
|
|
registros = visitas.filter_map do |visita|
|
|
serie = serie_de(visita)
|
|
next if serie.blank? || visita['tracking_id'].blank?
|
|
|
|
{
|
|
tracking_id: visita['tracking_id'],
|
|
reference_id: visita['reference'].to_s.presence,
|
|
numero_serie: serie,
|
|
planned_date: visita['planned_date'],
|
|
sincronizado_em: agora
|
|
}
|
|
end
|
|
# A mesma visita pode vir em duas datas varridas; upsert_all rejeita chave
|
|
# repetida no MESMO lote, então deduplica antes (fica a última).
|
|
registros = registros.index_by { |r| r[:tracking_id] }.values
|
|
return 0 if registros.empty?
|
|
|
|
SerieAparelho.upsert_all(registros, unique_by: :tracking_id, record_timestamps: true)
|
|
registros.size
|
|
end
|
|
|
|
# A série fica em `extra_field_values`, o mesmo hash das fotos do motorista.
|
|
# Ignora valor que seja URL: ali dentro convivem os campos foto_*.
|
|
def serie_de(visita)
|
|
extras = visita['extra_field_values']
|
|
return nil unless extras.is_a?(Hash)
|
|
|
|
chave = CAMPOS_SERIE.find { |c| texto_util?(extras[c]) } ||
|
|
extras.keys.find { |k| k.to_s.match?(/seri/i) && texto_util?(extras[k]) }
|
|
chave && extras[chave].to_s.strip
|
|
end
|
|
|
|
def texto_util?(valor)
|
|
v = valor.to_s.strip
|
|
v.present? && !v.match?(%r{\Ahttps?://}i)
|
|
end
|
|
|
|
# Varreu visitas e não achou UMA série? Quase certo que o campo mudou de nome
|
|
# na API. Loga as chaves que vieram para não virar falha silenciosa de novo.
|
|
def avisar_se_nada_casou(gravadas, visitas)
|
|
return if gravadas.positive? || visitas.empty?
|
|
|
|
chaves = visitas.filter_map { |v| v['extra_field_values'] }
|
|
.select { |e| e.is_a?(Hash) }.flat_map(&:keys).uniq.sort
|
|
Rails.logger.warn(
|
|
"[series_aparelho] #{visitas.size} visitas varridas e nenhuma série encontrada. " \
|
|
"Campos vistos em extra_field_values: #{chaves.inspect}. " \
|
|
'Se o campo da série estiver nessa lista, acrescente o nome em CAMPOS_SERIE.'
|
|
)
|
|
end
|
|
|
|
def client
|
|
@client ||= SimpliRoute::Client.new
|
|
end
|
|
|
|
def resumo_vazio(motivo)
|
|
Rails.logger.info("[series_aparelho] nada a fazer — #{motivo}.")
|
|
{ datas: 0, visitas: 0, series: 0, falhas: [], motivo: motivo }
|
|
end
|
|
end
|