# app/services/romaneios/enriquecimento_gade.rb # # Preenche APARELHO, TELEFONES e NOME do romaneio a partir da tabela da operação # (gade_entregas_*). É o que dispensa a SEGUNDA PLANILHA do programa Python: lá o # operador subia um .xlsx só com NOTA FISCAL + STATUS para marcar quem é NOVO; aqui # o STATUS já está no banco, na própria operação. # # A tabela é SOMENTE LEITURA (apenas SELECT), o nome passa por Operacao.valida? + # quote_table_name e as NFs por conn.quote. module Romaneios class EnriquecimentoGade # chave usada no SELECT => coluna real na gade. # # ⚠️ `status` PRECISA sair aliasado: o espelho de rastreio também tem uma coluna # `status` (o estado da visita), e qualquer query que junte as duas trocaria uma # pela outra em silêncio. Analytics::OperacaoMetricas já faz esse mesmo alias # (GADE_OPCIONAIS) pelo mesmo motivo. COLUNAS = { 'nota_fiscal' => 'nota_fiscal', 'nome_completo' => 'nome_completo', 'telefones' => 'telefones', 'status' => 'status_gade' }.freeze # Sem essas, não há o que enriquecer. ESSENCIAIS = %w[nota_fiscal].freeze attr_reader :tabela def initialize(tabela) @tabela = tabela.to_s raise ArgumentError, "Operação inválida: #{@tabela}" unless Operacao.valida?(@tabela) end # { '90507' => { aparelho: 'SIM', telefones: '(11) ...', nome: 'CICERA ...' } } # NF que não estiver na operação simplesmente não vem no Hash — é assim que a # tela marca "fora da operação" (plano avulso / de inclusão), mesma semântica de # Analytics::NotasForaOperacao. def para_notas(notas) lista = Array(notas).map { |n| n.to_s.strip }.reject(&:empty?).uniq return {} if lista.empty? registros(lista).each_with_object({}) do |r, memo| nf = r['nota_fiscal'].to_s.strip next if nf.empty? memo[nf] = { aparelho: Normalizador.aparelho_por_status(r['status_gade']), telefones: r['telefones'].to_s.strip, nome: Normalizador.nome(r['nome_completo']) } end end # Colunas que esta operação NÃO tem — a tela precisa DIZER isso. # # O esquema das gade_entregas_* varia por operação (só `nota_fiscal` é # garantida). Quando falta `status`, o APARELHO sai vazio para TODO MUNDO — e um # romaneio em que ninguém recebe aparelho, entregue calado, é pior do que um # aviso feio na tela. def colunas_ausentes @colunas_ausentes ||= COLUNAS.keys - existentes end def utilizavel? (ESSENCIAIS - existentes).empty? end def label Operacao.label(@tabela) end private def conn ActiveRecord::Base.connection end def existentes @existentes ||= conn.columns(@tabela).map(&:name) end def registros(lista) return [] unless utilizavel? valores = lista.map { |n| conn.quote(n) }.join(', ') sql = "SELECT #{selects} FROM #{conn.quote_table_name(@tabela)} g " \ "WHERE g.nota_fiscal IN (#{valores})" conn.select_all(sql).to_a end # Coluna que não existir vira NULL com o mesmo alias, preservando a forma do # resultado — mesmo padrão de SimpliRoute::PlanilhaCarga#selects. As chaves de # COLUNAS são whitelist fixa (não há entrada de usuário no SQL). def selects COLUNAS.map do |coluna, apelido| existentes.include?(coluna) ? "g.#{coluna} AS #{apelido}" : "CAST(NULL AS text) AS #{apelido}" end.join(', ') end end end