Correção do relatório que é gerado para completo
This commit is contained in:
149
app/jobs/sincronizar_series_aparelho_job.rb
Normal file
149
app/jobs/sincronizar_series_aparelho_job.rb
Normal file
@@ -0,0 +1,149 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user