Implantação da criação de romaneios
This commit is contained in:
130
app/services/romaneios/casador_de_operacao.rb
Normal file
130
app/services/romaneios/casador_de_operacao.rb
Normal file
@@ -0,0 +1,130 @@
|
||||
# app/services/romaneios/casador_de_operacao.rb
|
||||
#
|
||||
# Casa o NOME DO PLANO do SimpliRoute com a tabela da operação do mês.
|
||||
#
|
||||
# ── POR QUE EXISTE ──────────────────────────────────────────────────────────
|
||||
# A operação não trabalha por data — ela identifica o plano pelo nome ("ENTREGAS
|
||||
# EMAD 09.2026", "UBS SUL E LESTE AGOSTO 2026"). E é a operação do mês que diz
|
||||
# quem é paciente NOVO: sem ela vinculada, `Romaneios::EnriquecimentoGade` nem
|
||||
# roda e a coluna APARELHO sai vazia para todos — um romaneio que parece certo e
|
||||
# só revela o erro no papel, com aparelho não entregue.
|
||||
#
|
||||
# A API pública do SimpliRoute NÃO tem endpoint que liste planos (a documentação
|
||||
# só expõe criação, /v1/plans/{data}/vehicles/ e /v1/plans/routes/{id}/visits/);
|
||||
# a lista que aparece na tela deles é da interface web. Então o nome chega aqui
|
||||
# DIGITADO ou COLADO pelo operador, e o casamento tem que aguentar variação.
|
||||
#
|
||||
# ── AS DIFERENÇAS SÃO SISTEMÁTICAS, NÃO ALEATÓRIAS ──────────────────────────
|
||||
# plano "UBS SUL E LESTE AGOSTO 2026" x tabela ubs_sul_leste_ago_2026
|
||||
# ↑ conectivo "E" ↑ mês abreviado
|
||||
# plano "ENTREGAS EMAD 09.2026" x tabela emad_set_2026
|
||||
# ↑ prefixo genérico ↑ mês NUMÉRICO
|
||||
# Por isso a comparação é por CONJUNTO DE PALAVRAS + (mês, ano), e não por
|
||||
# igualdade de texto — que erraria em todos os casos reais acima.
|
||||
module Romaneios
|
||||
class CasadorDeOperacao
|
||||
# Palavras que aparecem no nome do plano e nunca no nome da tabela. "ENTREGAS"
|
||||
# entra aqui porque é o prefixo genérico que o cliente usa nos planos; o
|
||||
# prefixo das tabelas (`gade_entregas_`) já é removido antes.
|
||||
RUIDO = %w[entregas entrega de da do das dos e o a as os plano planos].freeze
|
||||
|
||||
Resultado = Struct.new(:tabela, :confianca, :motivo, keyword_init: true)
|
||||
|
||||
def self.casar(nome, tabelas: nil)
|
||||
new(nome, tabelas: tabelas).casar
|
||||
end
|
||||
|
||||
def initialize(nome, tabelas: nil)
|
||||
@nome = nome.to_s
|
||||
@tabelas = tabelas
|
||||
end
|
||||
|
||||
# Devolve o nome da tabela, ou nil quando não dá para ter certeza.
|
||||
#
|
||||
# ⚠️ Empate devolve NIL de propósito. Vincular a operação errada é pior do que
|
||||
# não vincular: com a errada, APARELHO e TELEFONE saem preenchidos com dados
|
||||
# de OUTRO mês e ninguém desconfia; sem nenhuma, a tela avisa que está sem
|
||||
# operação e o operador escolhe.
|
||||
def casar
|
||||
resultado.tabela
|
||||
end
|
||||
|
||||
def resultado
|
||||
@resultado ||= calcular
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def calcular
|
||||
alvo = decompor(@nome)
|
||||
return Resultado.new(confianca: :nenhuma, motivo: 'nome vazio') if alvo[:palavras].empty?
|
||||
|
||||
candidatas = (@tabelas || Operacao.nomes_validos).map do |tabela|
|
||||
[tabela, pontuar(alvo, decompor(Operacao.label(tabela)))]
|
||||
end.reject { |_, ponto| ponto.nil? }
|
||||
|
||||
return Resultado.new(confianca: :nenhuma, motivo: 'nenhuma operação com o mesmo mês/ano') if candidatas.empty?
|
||||
|
||||
melhor = candidatas.max_by(&:last)
|
||||
empate = candidatas.count { |_, ponto| ponto == melhor.last }
|
||||
if empate > 1
|
||||
return Resultado.new(confianca: :ambigua,
|
||||
motivo: "#{empate} operações batem igualmente — escolha na lista")
|
||||
end
|
||||
|
||||
Resultado.new(tabela: melhor.first, confianca: :alta, motivo: 'mês, ano e palavras batem')
|
||||
end
|
||||
|
||||
# "UBS SUL E LESTE AGOSTO 2026" -> { palavras: #{ubs sul leste}, mes: 8, ano: 2026 }
|
||||
def decompor(texto)
|
||||
tokens = normalizar(texto)
|
||||
ano = tokens.find { |t| t.match?(/\A(19|20)\d{2}\z/) }
|
||||
mes = mes_de(tokens, ano)
|
||||
|
||||
palavras = tokens - [ano, mes[:token]].compact
|
||||
palavras = palavras.reject { |t| RUIDO.include?(t) }
|
||||
|
||||
{ palavras: palavras.to_set, mes: mes[:numero], ano: ano&.to_i }
|
||||
end
|
||||
|
||||
# Aceita "agosto", "ago" e o mês NUMÉRICO de "09.2026" — este último só quando
|
||||
# vem colado ao ano, senão o "3" de "AVULSAS 3 EMAD" viraria março.
|
||||
def mes_de(tokens, ano)
|
||||
por_nome = tokens.find { |t| Operacao.mes_numero(t) }
|
||||
return { token: por_nome, numero: Operacao.mes_numero(por_nome) } if por_nome
|
||||
|
||||
return { token: nil, numero: nil } unless ano
|
||||
|
||||
idx = tokens.index(ano)
|
||||
anterior = idx&.positive? ? tokens[idx - 1] : nil
|
||||
if anterior&.match?(/\A\d{1,2}\z/) && (1..12).cover?(anterior.to_i)
|
||||
{ token: anterior, numero: anterior.to_i }
|
||||
else
|
||||
{ token: nil, numero: nil }
|
||||
end
|
||||
end
|
||||
|
||||
def normalizar(texto)
|
||||
ActiveSupport::Inflector.transliterate(texto.to_s)
|
||||
.downcase
|
||||
.split(/[^a-z0-9]+/) # "09.2026" e "EMAD/SUDESTE" quebram aqui
|
||||
.reject(&:empty?)
|
||||
end
|
||||
|
||||
# nil = descartada. Mês e ano são ELIMINATÓRIOS: é o que impede "EMAD 09.2026"
|
||||
# de casar com a tabela de agosto, que tem exatamente as mesmas palavras.
|
||||
def pontuar(alvo, candidata)
|
||||
return nil if alvo[:mes] && candidata[:mes] && alvo[:mes] != candidata[:mes]
|
||||
return nil if alvo[:ano] && candidata[:ano] && alvo[:ano] != candidata[:ano]
|
||||
return nil if alvo[:palavras].empty? || candidata[:palavras].empty?
|
||||
|
||||
comuns = (alvo[:palavras] & candidata[:palavras]).size
|
||||
return nil if comuns.zero?
|
||||
|
||||
sobrando = (alvo[:palavras] ^ candidata[:palavras]).size
|
||||
# Palavra em comum soma 2; palavra que só um lado tem tira 1. "UBS SUL LESTE"
|
||||
# contra "UBS SUL" ainda casa, mas perde para "UBS SUL LESTE" exato.
|
||||
(comuns * 2) - sobrando
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user