teste #11
@@ -76,32 +76,40 @@ class DashboardController < ApplicationController
|
||||
@operacoes_agrupadas = Operacao.agrupadas_por_mes
|
||||
@operacao_filtro = Operacao.sanitizar(params[:operacoes])
|
||||
|
||||
# PAGAS pela DATA REAL (checkout) — base de toda a análise financeira por data.
|
||||
pagas = Entrega.da_conta_gade.pagas.no_periodo_checkout(@periodo_inicio, @periodo_fim)
|
||||
pagas = pagas.da_operacoes(@operacao_filtro) if @operacao_filtro.present?
|
||||
# ATENDIDAS pela DATA REAL (checkout) — base de toda a análise financeira.
|
||||
#
|
||||
# Atendida = o motorista FOI ao local e fechou a visita, com sucesso
|
||||
# (`completed`) OU insucesso (`failed`). É exatamente o conjunto que a
|
||||
# consolidação considera elegível e paga (ver Entrega::STATUS_ATENDIDO,
|
||||
# Entrega.contar_atendidas e Consolidacao#veiculos_do_motorista), por isso o
|
||||
# dashboard usa o MESMO recorte — usava só `pagas` (concluídas) e mostrava
|
||||
# um valor menor que o do fechamento.
|
||||
atendidas = Entrega.da_conta_gade.atendidas.no_periodo_checkout(@periodo_inicio, @periodo_fim)
|
||||
atendidas = atendidas.da_operacoes(@operacao_filtro) if @operacao_filtro.present?
|
||||
|
||||
# PENDENTES (em aberto, nem concluídas nem falhadas) pela data planejada.
|
||||
# PENDENTES (em aberto, nem concluídas nem falhadas) pela data planejada —
|
||||
# é o único recorte sem checkout, então planned_date é o eixo correto aqui.
|
||||
pendentes = Entrega.da_conta_gade.pendentes.no_periodo(@periodo_inicio, @periodo_fim)
|
||||
pendentes = pendentes.da_operacoes(@operacao_filtro) if @operacao_filtro.present?
|
||||
|
||||
# FALHADAS (insucesso) pela data planejada — também não têm checkout.
|
||||
falhadas = Entrega.da_conta_gade.falhadas.no_periodo(@periodo_inicio, @periodo_fim)
|
||||
falhadas = falhadas.da_operacoes(@operacao_filtro) if @operacao_filtro.present?
|
||||
|
||||
# Totais gerais
|
||||
@entregas_pagas = pagas.count
|
||||
# Quebra do total entre sucesso e insucesso (ambos já dentro de `atendidas`).
|
||||
# ⚠️ Falhada TEM checkout: o motorista foi ao local e fechou a visita com
|
||||
# motivo de insucesso. Por isso o eixo é checkout, igual às concluídas — antes
|
||||
# as falhas eram filtradas por planned_date e caíam no período errado.
|
||||
@entregas_pagas = atendidas.concluidas.count
|
||||
@entregas_falhadas = atendidas.falhadas.count
|
||||
@entregas_atendidas = @entregas_pagas + @entregas_falhadas
|
||||
@entregas_pendentes = pendentes.count
|
||||
@entregas_falhadas = falhadas.count
|
||||
@total_entregas = @entregas_pagas + @entregas_pendentes + @entregas_falhadas
|
||||
@total_entregas = @entregas_atendidas + @entregas_pendentes
|
||||
|
||||
# Configurações de preço
|
||||
config = Configuracao.mapa_de_precos
|
||||
|
||||
# Valor estimado total
|
||||
@valor_estimado = (@entregas_pagas * config[:entrega]).round(2)
|
||||
@valor_estimado = (@entregas_atendidas * config[:entrega]).round(2)
|
||||
|
||||
# Por motorista (todos)
|
||||
@motoristas = pagas.group(:driver)
|
||||
@motoristas = atendidas.group(:driver)
|
||||
.count
|
||||
.sort_by { |_, v| -v }
|
||||
.map do |driver, qtd|
|
||||
@@ -113,14 +121,14 @@ class DashboardController < ApplicationController
|
||||
end
|
||||
|
||||
# Por local de entrega (contact_name)
|
||||
@por_operacao = pagas.group(:contact_name)
|
||||
@por_operacao = atendidas.group(:contact_name)
|
||||
.count
|
||||
.sort_by { |_, v| -v }
|
||||
.first(6)
|
||||
.to_h
|
||||
|
||||
# Evolução diária no período (para Chart.js) — por data real (checkout)
|
||||
@grafico_diario = build_grafico_diario(pagas, config[:entrega])
|
||||
@grafico_diario = build_grafico_diario(atendidas, config[:entrega])
|
||||
|
||||
# Consolidações cujo período (data_inicio..data_fim) cruza o período do dashboard
|
||||
# — independe de quando foram criadas (created_at).
|
||||
@@ -221,20 +229,21 @@ class DashboardController < ApplicationController
|
||||
.sort_by { |_, v| -v }
|
||||
end
|
||||
|
||||
# `pagas` já vem filtrado por checkout no período. Agrupa pela DATA REAL
|
||||
# (DATE(checkout)) — DATE() sobre timestamp não faz conversão de fuso.
|
||||
def build_grafico_diario(pagas, preco_entrega)
|
||||
# `atendidas` já vem filtrado por checkout no período (concluídas + falhadas).
|
||||
# Agrupa pela DATA REAL (DATE(checkout)) — DATE() sobre timestamp não faz
|
||||
# conversão de fuso.
|
||||
def build_grafico_diario(atendidas, preco_entrega)
|
||||
dias = (@periodo_inicio..@periodo_fim).to_a
|
||||
|
||||
if dias.size <= MAX_DIAS_GRAFICO
|
||||
# Granularidade diária (períodos curtos)
|
||||
contagem = pagas.group("DATE(checkout)").count.transform_keys(&:to_s)
|
||||
contagem = atendidas.group("DATE(checkout)").count.transform_keys(&:to_s)
|
||||
labels = dias.map { |d| d.strftime('%d/%m') }
|
||||
valores = dias.map { |d| ((contagem[d.to_s] || 0) * preco_entrega).round(2) }
|
||||
qtds = dias.map { |d| contagem[d.to_s] || 0 }
|
||||
else
|
||||
# Período longo: agrega por mês para o gráfico não ficar vazio nem poluído.
|
||||
contagem = pagas.group("TO_CHAR(checkout, 'YYYY-MM')").count
|
||||
contagem = atendidas.group("TO_CHAR(checkout, 'YYYY-MM')").count
|
||||
meses = meses_no_periodo
|
||||
labels = meses.map { |m| Date.strptime(m, '%Y-%m').strftime('%m/%Y') }
|
||||
valores = meses.map { |m| ((contagem[m] || 0) * preco_entrega).round(2) }
|
||||
|
||||
@@ -121,12 +121,20 @@ class Entrega < ApplicationRecord
|
||||
|
||||
# Veículos usados pelo(s) motorista(s) selecionado(s) no período — alimenta o
|
||||
# filtro dinâmico de veículos na criação de consolidação.
|
||||
#
|
||||
# ⚠️ Precisa usar EXATAMENTE o mesmo recorte da elegibilidade
|
||||
# (`atendidas` + `no_periodo_checkout`, ver contar_atendidas e
|
||||
# Consolidacao#veiculos_do_motorista). Usava `no_periodo` (planned_date), e o
|
||||
# descasamento sumia com veículo do filtro: entrega planejada em 31/07 e
|
||||
# entregue em 01/08 é elegível numa consolidação 01/08–14/08 (checkout dentro),
|
||||
# mas o veículo dela não aparecia (planned_date fora) — sem opção de marcar,
|
||||
# a entrega ficava de fora do fechamento SEM aviso, e o financeiro não batia.
|
||||
def self.veiculos_de_motoristas(motoristas, inicio: nil, fim: nil)
|
||||
nomes = Array(motoristas).reject(&:blank?)
|
||||
return [] if nomes.empty?
|
||||
|
||||
base = da_conta_gade.where(driver: nomes)
|
||||
base = base.no_periodo(inicio, fim) if inicio.present? && fim.present?
|
||||
base = base.atendidas.no_periodo_checkout(inicio, fim) if inicio.present? && fim.present?
|
||||
base.distinct.order(:vehicle).pluck(:vehicle).compact.reject(&:empty?)
|
||||
end
|
||||
|
||||
|
||||
@@ -134,8 +134,14 @@
|
||||
<p class="text-3xl lg:text-4xl font-black text-white mb-1 break-words">
|
||||
<%= moeda(@valor_estimado) %>
|
||||
</p>
|
||||
<%# "Atendidas" = concluídas + falhadas (motorista foi ao local e fechou
|
||||
a visita). É a mesma base que a consolidação paga, por isso o rótulo
|
||||
NÃO pode voltar a dizer "concluídas": o valor acima inclui as falhas. %>
|
||||
<p class="text-orange-200 text-sm">
|
||||
<%= @entregas_pagas %> entregas concluídas
|
||||
<%= @entregas_atendidas %> entregas atendidas
|
||||
</p>
|
||||
<p class="text-orange-200/80 text-xs">
|
||||
<%= @entregas_pagas %> concluídas · <%= @entregas_falhadas %> falhadas
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
252
bin/entregas_do_motorista
Executable file
252
bin/entregas_do_motorista
Executable file
@@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
#
|
||||
# Lista, DIRETO NA API do SimpliRoute, todas as visitas de um motorista dentro
|
||||
# de um período.
|
||||
#
|
||||
# ⚠️ POR QUE É DIA A DIA (e não uma chamada só):
|
||||
# A API NÃO tem filtro de intervalo. Testado contra a API real em 21/07/2026
|
||||
# (bin/sondar_busca_nf): `planned_date_from/to`, `planned_date_after/before`,
|
||||
# `__gte/__lte`, `since/until`, `start_date/end_date`, `date_from/to` — TODOS
|
||||
# ignorados em silêncio (200 + o conjunto padrão inteiro). E SEM `planned_date`
|
||||
# a API devolve um conjunto padrão (~2469 visitas) que NÃO cobre o histórico.
|
||||
# Logo: `planned_date` é obrigatório e o período vira N requisições, uma por dia,
|
||||
# disparadas em paralelo.
|
||||
#
|
||||
# ⚠️ Não filtramos motorista no servidor: `search=` só foi comprovado para NF, e
|
||||
# parâmetro que a API não registra é IGNORADO em silêncio (devolve o dia inteiro
|
||||
# com HTTP 200). Então baixamos o dia inteiro e filtramos AQUI — é lento
|
||||
# (~3,8 MB / ~9 s por dia) mas é a única forma correta.
|
||||
#
|
||||
# SÓ LEITURA: o script faz apenas GET. Não altera nada.
|
||||
#
|
||||
# Rode NO SERVIDOR (é lá que vive o SIMPLIROUTE_TOKEN):
|
||||
#
|
||||
# SIMPLIROUTE_TOKEN=xxx bin/entregas_do_motorista \
|
||||
# --motorista "JOSE DA SILVA" --de 2026-08-01 --ate 2026-08-14
|
||||
#
|
||||
# ... --csv /tmp/entregas.csv grava o resultado em CSV
|
||||
# ... --listar-motoristas só mostra quem aparece no período (sem filtrar)
|
||||
# ... --campos mostra as chaves cruas de uma visita (diagnóstico)
|
||||
#
|
||||
# Alternativa MUITO mais barata quando não precisa do dado ao vivo: o espelho
|
||||
# local (model Entrega, tabela db_reem_simplerout_2026) já tem a coluna `driver`
|
||||
# e responde isso numa query só:
|
||||
#
|
||||
# Entrega.no_periodo('2026-08-01', '2026-08-14').do_motorista('JOSE DA SILVA')
|
||||
#
|
||||
# O espelho sincroniza depois da API, então só use este script quando precisar
|
||||
# do estado ATUAL ou desconfiar da sync.
|
||||
|
||||
require 'net/http'
|
||||
require 'json'
|
||||
require 'uri'
|
||||
require 'date'
|
||||
require 'csv'
|
||||
|
||||
BASE = ENV.fetch('SIMPLIROUTE_BASE_URL', 'https://api.simpliroute.com')
|
||||
TOKEN = ENV['SIMPLIROUTE_TOKEN'].to_s
|
||||
THREADS = 6 # mesmo lote usado no sondar_busca_nf; acima disso a API estrangula
|
||||
|
||||
def sair(msg)
|
||||
warn msg
|
||||
exit 1
|
||||
end
|
||||
|
||||
sair('Defina SIMPLIROUTE_TOKEN no ambiente.') if TOKEN.empty?
|
||||
|
||||
motorista = nil
|
||||
de = nil
|
||||
ate = nil
|
||||
csv_path = nil
|
||||
listar = false
|
||||
campos = false
|
||||
ARGV.each_with_index do |a, i|
|
||||
case a
|
||||
when '--motorista' then motorista = ARGV[i + 1]
|
||||
when '--de' then de = ARGV[i + 1]
|
||||
when '--ate', '--até' then ate = ARGV[i + 1]
|
||||
when '--csv' then csv_path = ARGV[i + 1]
|
||||
when '--listar-motoristas' then listar = true
|
||||
when '--campos' then campos = true
|
||||
end
|
||||
end
|
||||
|
||||
sair('Uso: bin/entregas_do_motorista --motorista "NOME" --de YYYY-MM-DD --ate YYYY-MM-DD') if de.to_s.empty? || ate.to_s.empty?
|
||||
sair('Informe --motorista, ou use --listar-motoristas.') if motorista.to_s.empty? && !listar && !campos
|
||||
|
||||
inicio = begin
|
||||
Date.parse(de)
|
||||
rescue ArgumentError
|
||||
sair("Data inválida em --de: #{de}")
|
||||
end
|
||||
fim = begin
|
||||
Date.parse(ate)
|
||||
rescue ArgumentError
|
||||
sair("Data inválida em --ate: #{ate}")
|
||||
end
|
||||
sair('--de é depois de --ate.') if inicio > fim
|
||||
|
||||
DIAS = (inicio..fim).to_a
|
||||
|
||||
# Normaliza nome para comparar: sem acento, sem espaço duplo, maiúsculo.
|
||||
# O nome no SimpliRoute é digitado por gente — "José da Silva" e "JOSE DA SILVA"
|
||||
# são o mesmo motorista.
|
||||
def normalizar(nome)
|
||||
nome.to_s
|
||||
.unicode_normalize(:nfd).gsub(/\p{Mn}/, '')
|
||||
.upcase.gsub(/\s+/, ' ').strip
|
||||
end
|
||||
|
||||
def requisicao(caminho)
|
||||
uri = URI.join(BASE, caminho)
|
||||
req = Net::HTTP::Get.new(uri)
|
||||
req['Authorization'] = "Token #{TOKEN}"
|
||||
req['Accept'] = 'application/json'
|
||||
|
||||
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https',
|
||||
open_timeout: 10, read_timeout: 180) { |h| h.request(req) }
|
||||
return [res.code.to_i, nil] unless res.code.to_i == 200
|
||||
|
||||
[200, (res.body.to_s.empty? ? nil : JSON.parse(res.body))]
|
||||
rescue StandardError => e
|
||||
[0, "ERRO: #{e.class}: #{e.message}"]
|
||||
end
|
||||
|
||||
# A resposta pode ser lista crua OU paginada ({count, next, results}).
|
||||
def itens(corpo)
|
||||
return corpo if corpo.is_a?(Array)
|
||||
return Array(corpo['results']) if corpo.is_a?(Hash) && corpo.key?('results')
|
||||
|
||||
[]
|
||||
end
|
||||
|
||||
# Onde está o nome do motorista numa visita. A lista-por-data às vezes traz o
|
||||
# campo achatado ('driver'), às vezes só o id, às vezes um hash aninhado — por
|
||||
# isso procuramos em várias formas em vez de fixar uma. Se nada casar, o script
|
||||
# avisa e o --campos mostra o formato real.
|
||||
CHAVES_MOTORISTA = %w[driver driver_name route_driver driver_full_name].freeze
|
||||
|
||||
def nome_do_motorista(visita)
|
||||
CHAVES_MOTORISTA.each do |k|
|
||||
v = visita[k]
|
||||
next if v.nil? || v == '' || v.is_a?(Numeric)
|
||||
|
||||
return v if v.is_a?(String)
|
||||
return (v['name'] || v['full_name'] ||
|
||||
[v['first_name'], v['last_name']].compact.join(' ')) if v.is_a?(Hash)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
puts "Base: #{BASE}"
|
||||
puts "Período: #{inicio.strftime('%d/%m/%Y')} a #{fim.strftime('%d/%m/%Y')} (#{DIAS.size} dia(s), #{THREADS} em paralelo)"
|
||||
puts "Motorista: #{motorista}" if motorista
|
||||
puts '(a API não filtra por intervalo nem por motorista — baixando dia a dia; pode levar ~1 min)'
|
||||
puts
|
||||
|
||||
# ── Varredura dia a dia, em lotes ────────────────────────────────────────────
|
||||
t0 = Time.now
|
||||
visitas = []
|
||||
falhas = []
|
||||
mutex = Mutex.new
|
||||
|
||||
DIAS.each_slice(THREADS) do |lote|
|
||||
lote.map do |dia|
|
||||
Thread.new do
|
||||
cod, corpo = requisicao("/v1/routes/visits/?planned_date=#{dia.iso8601}")
|
||||
lista = cod == 200 ? itens(corpo) : []
|
||||
mutex.synchronize do
|
||||
if cod == 200
|
||||
# planned_date às vezes não volta na visita — garantimos o dia pedido.
|
||||
lista.each { |v| v['_dia'] = dia.iso8601 }
|
||||
visitas.concat(lista)
|
||||
print '.'
|
||||
else
|
||||
falhas << [dia, cod, corpo]
|
||||
print 'x'
|
||||
end
|
||||
end
|
||||
end
|
||||
end.each(&:join)
|
||||
end
|
||||
puts "\nBaixadas #{visitas.size} visita(s) em #{(Time.now - t0).round(1)}s"
|
||||
falhas.each { |d, c, e| warn " ⚠️ #{d}: HTTP #{c} #{e.inspect[0, 120]}" }
|
||||
sair('Nenhuma visita voltou — período sem operação ou token sem acesso.') if visitas.empty?
|
||||
|
||||
# ── Diagnóstico do formato ───────────────────────────────────────────────────
|
||||
if campos
|
||||
puts "\nChaves de uma visita crua:"
|
||||
puts visitas.first.keys.sort.inspect
|
||||
puts "\nExemplo (recortado):"
|
||||
puts JSON.pretty_generate(visitas.first)[0, 2000]
|
||||
exit 0
|
||||
end
|
||||
|
||||
com_nome = visitas.count { |v| nome_do_motorista(v) }
|
||||
if com_nome.zero?
|
||||
warn "\n⚠️ NENHUMA visita trouxe nome de motorista nas chaves #{CHAVES_MOTORISTA.inspect}."
|
||||
warn ' A lista-por-data pode estar devolvendo só o id da rota. Rode com --campos'
|
||||
warn ' para ver o formato real, ou use o espelho local (Entrega#driver).'
|
||||
exit 1
|
||||
end
|
||||
warn "\n⚠️ #{visitas.size - com_nome} visita(s) sem motorista preenchido (ficam de fora do filtro)." if com_nome < visitas.size
|
||||
|
||||
# ── Só listar quem apareceu ──────────────────────────────────────────────────
|
||||
if listar
|
||||
puts "\nMotoristas no período:"
|
||||
visitas.group_by { |v| nome_do_motorista(v) }
|
||||
.reject { |k, _| k.nil? }
|
||||
.sort_by { |_, vs| -vs.size }
|
||||
.each { |nome, vs| puts " #{vs.size.to_s.rjust(5)} #{nome}" }
|
||||
exit 0
|
||||
end
|
||||
|
||||
# ── Filtro pelo motorista ────────────────────────────────────────────────────
|
||||
alvo = normalizar(motorista)
|
||||
dele = visitas.select { |v| normalizar(nome_do_motorista(v)) == alvo }
|
||||
|
||||
if dele.empty?
|
||||
puts "\nNenhuma visita de #{motorista.inspect} no período."
|
||||
parecidos = visitas.map { |v| nome_do_motorista(v) }.compact.uniq
|
||||
.select { |n| normalizar(n).include?(alvo) || alvo.include?(normalizar(n)) }
|
||||
puts "Nomes parecidos que existem: #{parecidos.inspect}" if parecidos.any?
|
||||
puts 'Use --listar-motoristas para ver a grafia exata.'
|
||||
exit 0
|
||||
end
|
||||
|
||||
dele.sort_by! { |v| [v['_dia'].to_s, v['checkout_time'].to_s] }
|
||||
|
||||
puts "\n#{'─' * 100}"
|
||||
puts "#{dele.size} visita(s) de #{motorista}"
|
||||
puts '─' * 100
|
||||
printf("%-11s %-10s %-9s %-19s %-40s\n", 'DATA', 'NF', 'STATUS', 'CHECKOUT', 'DESTINO')
|
||||
dele.each do |v|
|
||||
printf("%-11s %-10s %-9s %-19s %-40s\n",
|
||||
Date.parse(v['_dia']).strftime('%d/%m/%Y'),
|
||||
v['reference'].to_s[0, 10],
|
||||
v['status'].to_s[0, 9],
|
||||
v['checkout_time'].to_s[0, 19].tr('T', ' '),
|
||||
v['title'].to_s[0, 40])
|
||||
end
|
||||
|
||||
puts '─' * 100
|
||||
por_status = dele.group_by { |v| v['status'].to_s }.transform_values(&:size)
|
||||
puts "Total: #{dele.size} " + por_status.sort_by { |_, n| -n }.map { |s, n| "#{s}=#{n}" }.join(' ')
|
||||
atendidas = dele.count { |v| %w[completed failed].include?(v['status'].to_s) && v['checkout_time'].to_s != '' }
|
||||
puts "Atendidas (foi ao local, com checkout): #{atendidas}"
|
||||
puts "Dias com entrega: #{dele.map { |v| v['_dia'] }.uniq.size}"
|
||||
|
||||
# ── CSV ──────────────────────────────────────────────────────────────────────
|
||||
if csv_path
|
||||
CSV.open(csv_path, 'w') do |csv|
|
||||
csv << ['Data', 'NF', 'Tracking', 'ID visita', 'Motorista', 'Status',
|
||||
'Checkout', 'Título', 'Endereço', 'Motivo (UUID)', 'Comentário']
|
||||
dele.each do |v|
|
||||
csv << [Date.parse(v['_dia']).strftime('%d/%m/%Y'), v['reference'], v['tracking_id'], v['id'],
|
||||
nome_do_motorista(v), v['status'], v['checkout_time'], v['title'], v['address'],
|
||||
v['checkout_observation'], v['checkout_comment']]
|
||||
end
|
||||
end
|
||||
puts "\nCSV gravado em #{csv_path}"
|
||||
end
|
||||
118
lib/tasks/entregas_motorista.rake
Normal file
118
lib/tasks/entregas_motorista.rake
Normal file
@@ -0,0 +1,118 @@
|
||||
# lib/tasks/entregas_motorista.rake
|
||||
#
|
||||
# Lista as entregas de um motorista num período lendo o ESPELHO LOCAL
|
||||
# (model Entrega / tabela db_reem_simplerout_2026) — sem tocar na API do
|
||||
# SimpliRoute, logo sem precisar de SIMPLIROUTE_TOKEN.
|
||||
#
|
||||
# Por que aqui e não na API: a API não tem filtro de intervalo nem de motorista
|
||||
# (ver bin/entregas_do_motorista), então um período vira N requisições de vários
|
||||
# MB. No espelho é uma query só. O preço é a defasagem da sync — se precisar do
|
||||
# estado AO VIVO, use bin/entregas_do_motorista.
|
||||
#
|
||||
# SÓ LEITURA: Entrega#readonly? é true; nada aqui escreve.
|
||||
#
|
||||
# rake "entregas:motorista[JOSE DA SILVA,2026-08-01,2026-08-14]"
|
||||
# rake "entregas:motorista[JOSE DA SILVA,2026-08-01,2026-08-14,/tmp/saida.csv]"
|
||||
# rake "entregas:motoristas[2026-08-01,2026-08-14]" # quem operou no período
|
||||
#
|
||||
# ⚠️ O nome tem que ser a grafia EXATA da coluna `driver` (do_motorista usa
|
||||
# igualdade). Rode `entregas:motoristas` primeiro para ver a lista; se o nome
|
||||
# passado não bater exatamente, a task sugere os parecidos em vez de devolver
|
||||
# vazio silenciosamente.
|
||||
#
|
||||
# ⚠️ Período pelo `planned_date` (dia planejado da visita), que é o mesmo eixo
|
||||
# do dashboard. As análises FINANCEIRAS usam `checkout` (data real da entrega) —
|
||||
# são recortes diferentes; passe FONTE=checkout para usar esse outro eixo.
|
||||
|
||||
namespace :entregas do
|
||||
desc 'Entregas de um motorista no período, lidas do espelho local (sem API)'
|
||||
task :motorista, %i[nome inicio fim csv] => :environment do |_t, args|
|
||||
nome = args[:nome].to_s.strip
|
||||
inicio = args[:inicio].to_s.strip
|
||||
fim = args[:fim].to_s.strip
|
||||
|
||||
if nome.empty? || inicio.empty? || fim.empty?
|
||||
abort 'Uso: rake "entregas:motorista[NOME,YYYY-MM-DD,YYYY-MM-DD,/caminho/opcional.csv]"'
|
||||
end
|
||||
|
||||
d1 = Date.parse(inicio)
|
||||
d2 = Date.parse(fim)
|
||||
abort 'Início é depois do fim.' if d1 > d2
|
||||
|
||||
# planned_date = dia planejado; checkout = dia em que a entrega ocorreu.
|
||||
por_checkout = ENV['FONTE'].to_s.casecmp('checkout').zero?
|
||||
base = Entrega.da_conta_gade
|
||||
base = por_checkout ? base.no_periodo_checkout(d1, d2) : base.no_periodo(d1, d2)
|
||||
|
||||
entregas = base.do_motorista(nome).order(:planned_date, :checkout).to_a
|
||||
|
||||
if entregas.empty?
|
||||
puts "Nenhuma entrega de #{nome.inspect} entre #{d1.strftime('%d/%m/%Y')} e #{d2.strftime('%d/%m/%Y')}."
|
||||
# Nome quase certo é o erro mais comum aqui — mostrar o vizinho evita
|
||||
# concluir "o motorista não rodou" quando na verdade a grafia divergiu.
|
||||
alvo = nome.upcase
|
||||
parecidos = Entrega.motoristas_ativos(inicio: d1, fim: d2)
|
||||
.select { |n| n.upcase.include?(alvo) || alvo.include?(n.upcase) }
|
||||
puts "Nomes parecidos no período: #{parecidos.inspect}" if parecidos.any?
|
||||
puts 'Rode: rake "entregas:motoristas[' + "#{d1},#{d2}" + ']" para ver a grafia exata.'
|
||||
next
|
||||
end
|
||||
|
||||
eixo = por_checkout ? 'checkout (data real)' : 'planned_date (dia planejado)'
|
||||
puts "#{'─' * 104}"
|
||||
puts "#{entregas.size} entrega(s) de #{nome} · #{d1.strftime('%d/%m/%Y')} a #{d2.strftime('%d/%m/%Y')} · eixo: #{eixo}"
|
||||
puts '─' * 104
|
||||
printf("%-11s %-10s %-10s %-10s %-17s %-38s\n", 'DATA', 'NF', 'VEÍCULO', 'STATUS', 'CHECKOUT', 'DESTINO')
|
||||
entregas.each do |e|
|
||||
printf("%-11s %-10s %-10s %-10s %-17s %-38s\n",
|
||||
e.planned_date&.strftime('%d/%m/%Y').to_s,
|
||||
e.reference_id.to_s[0, 10],
|
||||
e.vehicle.to_s[0, 10],
|
||||
e.status.to_s[0, 10],
|
||||
e.checkout&.strftime('%d/%m %H:%M').to_s,
|
||||
e.local.to_s[0, 38])
|
||||
end
|
||||
puts '─' * 104
|
||||
|
||||
por_status = entregas.group_by { |e| e.status.to_s }.transform_values(&:size)
|
||||
puts "Total: #{entregas.size} " + por_status.sort_by { |_, n| -n }.map { |s, n| "#{s}=#{n}" }.join(' ')
|
||||
# "Atendidas" = foi ao local (sucesso OU insucesso) com checkout — é o
|
||||
# conjunto elegível da consolidação, e não o mesmo que "entregues".
|
||||
puts "Entregues (completed + checkout) ..... #{entregas.count { |e| e.concluida? && e.checkout_registrado? }}"
|
||||
puts "Insucessos (failed) .................. #{entregas.count(&:falhada?)}"
|
||||
puts "Atendidas (foi ao local c/ checkout) . #{entregas.count { |e| Entrega::STATUS_ATENDIDO.include?(e.status) && e.checkout_registrado? }}"
|
||||
puts "Dias com entrega ..................... #{entregas.map { |e| e.planned_date&.to_date }.compact.uniq.size}"
|
||||
puts "Veículos ............................. #{entregas.map(&:vehicle).compact.reject(&:empty?).uniq.join(', ')}"
|
||||
|
||||
if args[:csv].present?
|
||||
require 'csv'
|
||||
CSV.open(args[:csv], 'w') do |csv|
|
||||
csv << ['Data planejada', 'NF', 'Tracking', 'Motorista', 'Veículo', 'Status', 'Checkout', 'Local', 'Endereço', 'Motivo']
|
||||
entregas.each do |e|
|
||||
csv << [e.planned_date, e.reference_id, e.tracking_id, e.driver, e.vehicle,
|
||||
e.status, e.checkout, e.local, e.try(:address), e.try(:observation)]
|
||||
end
|
||||
end
|
||||
puts "\nCSV gravado em #{args[:csv]}"
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Motoristas que aparecem no espelho dentro do período (grafia exata + volume)'
|
||||
task :motoristas, %i[inicio fim] => :environment do |_t, args|
|
||||
abort 'Uso: rake "entregas:motoristas[YYYY-MM-DD,YYYY-MM-DD]"' if args[:inicio].blank? || args[:fim].blank?
|
||||
|
||||
d1 = Date.parse(args[:inicio])
|
||||
d2 = Date.parse(args[:fim])
|
||||
|
||||
contagem = Entrega.da_conta_gade.no_periodo(d1, d2)
|
||||
.where.not(driver: [nil, '']).group(:driver).count
|
||||
|
||||
if contagem.empty?
|
||||
puts "Nenhuma entrega no espelho entre #{d1.strftime('%d/%m/%Y')} e #{d2.strftime('%d/%m/%Y')}."
|
||||
next
|
||||
end
|
||||
|
||||
puts "Motoristas · #{d1.strftime('%d/%m/%Y')} a #{d2.strftime('%d/%m/%Y')} (#{contagem.values.sum} entregas)"
|
||||
contagem.sort_by { |_, n| -n }.each { |nome, n| puts " #{n.to_s.rjust(5)} #{nome}" }
|
||||
end
|
||||
end
|
||||
137
spec/models/entrega_spec.rb
Normal file
137
spec/models/entrega_spec.rb
Normal file
@@ -0,0 +1,137 @@
|
||||
require 'rails_helper'
|
||||
|
||||
# Trava o EIXO DE DATA do espelho de rastreio.
|
||||
#
|
||||
# O sistema tem dois eixos e eles NÃO são intercambiáveis:
|
||||
# planned_date → dia planejado da visita (scope :no_periodo)
|
||||
# checkout → dia REAL da entrega (scope :no_periodo_checkout)
|
||||
#
|
||||
# A elegibilidade da consolidação — o que vira dinheiro — é sempre pelo
|
||||
# `checkout`. Um filtro que use `planned_date` some com entrega elegível na
|
||||
# virada de mês e o fechamento não bate. Foi exatamente o bug do seletor de
|
||||
# veículos da nova consolidação (Entrega.veiculos_de_motoristas usava
|
||||
# :no_periodo), por isso o caso da virada está fixado aqui.
|
||||
#
|
||||
# A tabela do espelho é EXTERNA (sem migration, sem factory) — quem a cria e
|
||||
# derruba no banco de teste é o harness em spec/support/espelho_rastreio.rb, que
|
||||
# também expõe o helper `semear`.
|
||||
RSpec.describe Entrega, type: :model do
|
||||
include_context 'espelho de rastreio'
|
||||
|
||||
describe '.veiculos_de_motoristas' do
|
||||
# Período de fechamento usado em todos os exemplos.
|
||||
let(:inicio) { Date.new(2026, 8, 1) }
|
||||
let(:fim) { Date.new(2026, 8, 14) }
|
||||
|
||||
it 'inclui o veículo de entrega PLANEJADA fora do período mas ENTREGUE dentro' do
|
||||
# A regressão: planejada em 31/07, entregue em 01/08. É elegível (checkout
|
||||
# dentro), logo o veículo TEM que aparecer — senão não há como marcá-lo no
|
||||
# filtro e a entrega sai do fechamento sem aviso.
|
||||
semear([{ driver: 'JOSE DA SILVA', vehicle: 'GADE_001', status: 'completed',
|
||||
planned_date: Time.zone.local(2026, 7, 31, 8, 0),
|
||||
checkout: Time.zone.local(2026, 8, 1, 9, 30) }])
|
||||
|
||||
expect(described_class.veiculos_de_motoristas(['JOSE DA SILVA'], inicio: inicio, fim: fim))
|
||||
.to eq(['GADE_001'])
|
||||
end
|
||||
|
||||
it 'exclui o veículo de entrega PLANEJADA dentro do período mas ENTREGUE fora' do
|
||||
# O espelho do caso acima: planejada em 14/08, entregue em 20/08. Não é
|
||||
# elegível neste fechamento, então o veículo não pode aparecer.
|
||||
semear([{ driver: 'JOSE DA SILVA', vehicle: 'GADE_002', status: 'completed',
|
||||
planned_date: Time.zone.local(2026, 8, 14, 8, 0),
|
||||
checkout: Time.zone.local(2026, 8, 20, 9, 30) }])
|
||||
|
||||
expect(described_class.veiculos_de_motoristas(['JOSE DA SILVA'], inicio: inicio, fim: fim))
|
||||
.to be_empty
|
||||
end
|
||||
|
||||
it 'inclui o último dia do período inteiro (checkout às 23h de 14/08)' do
|
||||
# no_periodo_checkout usa `< fim + 1`; um `<= fim` cortaria o último dia.
|
||||
semear([{ driver: 'JOSE DA SILVA', vehicle: 'GADE_003', status: 'completed',
|
||||
planned_date: Time.zone.local(2026, 8, 14, 7, 0),
|
||||
checkout: Time.zone.local(2026, 8, 14, 23, 40) }])
|
||||
|
||||
expect(described_class.veiculos_de_motoristas(['JOSE DA SILVA'], inicio: inicio, fim: fim))
|
||||
.to eq(['GADE_003'])
|
||||
end
|
||||
|
||||
it 'inclui veículo de INSUCESSO (failed conta como atendida)' do
|
||||
# Atendida = o motorista foi ao local, com sucesso OU insucesso. Insucesso
|
||||
# é elegível e precisa ser classificado no wizard.
|
||||
semear([{ driver: 'JOSE DA SILVA', vehicle: 'GADE_004', status: 'failed',
|
||||
planned_date: Time.zone.local(2026, 8, 5, 8, 0),
|
||||
checkout: Time.zone.local(2026, 8, 5, 16, 0) }])
|
||||
|
||||
expect(described_class.veiculos_de_motoristas(['JOSE DA SILVA'], inicio: inicio, fim: fim))
|
||||
.to eq(['GADE_004'])
|
||||
end
|
||||
|
||||
it 'exclui veículo que só tem entrega PENDENTE (sem checkout) no período' do
|
||||
# Pendente não é elegível; listar o veículo só sujaria o filtro com um
|
||||
# carro de zero elegíveis.
|
||||
semear([{ driver: 'JOSE DA SILVA', vehicle: 'GADE_005', status: 'pending',
|
||||
planned_date: Time.zone.local(2026, 8, 6, 8, 0), checkout: nil }])
|
||||
|
||||
expect(described_class.veiculos_de_motoristas(['JOSE DA SILVA'], inicio: inicio, fim: fim))
|
||||
.to be_empty
|
||||
end
|
||||
|
||||
it 'não vaza veículo de outro motorista' do
|
||||
semear([
|
||||
{ driver: 'JOSE DA SILVA', vehicle: 'GADE_006', status: 'completed',
|
||||
planned_date: Time.zone.local(2026, 8, 4, 8, 0), checkout: Time.zone.local(2026, 8, 4, 10, 0) },
|
||||
{ driver: 'MARIA SOUZA', vehicle: 'GADE_007', status: 'completed',
|
||||
planned_date: Time.zone.local(2026, 8, 4, 8, 0), checkout: Time.zone.local(2026, 8, 4, 11, 0) }
|
||||
])
|
||||
|
||||
expect(described_class.veiculos_de_motoristas(['JOSE DA SILVA'], inicio: inicio, fim: fim))
|
||||
.to eq(['GADE_006'])
|
||||
end
|
||||
|
||||
# A invariante que realmente protege o financeiro: o seletor de veículos do
|
||||
# formulário e o conjunto elegível da consolidação têm que enxergar o MESMO
|
||||
# recorte. Se alguém mexer no eixo de data de um dos dois, este exemplo cai.
|
||||
it 'lista exatamente os veículos que têm entrega elegível no período' do
|
||||
semear([
|
||||
# elegíveis (checkout dentro)
|
||||
{ driver: 'JOSE DA SILVA', vehicle: 'GADE_010', status: 'completed',
|
||||
planned_date: Time.zone.local(2026, 7, 30, 8, 0), checkout: Time.zone.local(2026, 8, 2, 10, 0) },
|
||||
{ driver: 'JOSE DA SILVA', vehicle: 'GADE_011', status: 'failed',
|
||||
planned_date: Time.zone.local(2026, 8, 9, 8, 0), checkout: Time.zone.local(2026, 8, 9, 10, 0) },
|
||||
# não elegíveis
|
||||
{ driver: 'JOSE DA SILVA', vehicle: 'GADE_012', status: 'completed',
|
||||
planned_date: Time.zone.local(2026, 8, 10, 8, 0), checkout: Time.zone.local(2026, 9, 1, 10, 0) },
|
||||
{ driver: 'JOSE DA SILVA', vehicle: 'GADE_013', status: 'pending',
|
||||
planned_date: Time.zone.local(2026, 8, 11, 8, 0), checkout: nil }
|
||||
])
|
||||
|
||||
elegiveis = described_class.atendidas.da_conta_gade
|
||||
.no_periodo_checkout(inicio, fim)
|
||||
.do_motorista('JOSE DA SILVA')
|
||||
.distinct.pluck(:vehicle).compact.sort
|
||||
|
||||
expect(described_class.veiculos_de_motoristas(['JOSE DA SILVA'], inicio: inicio, fim: fim).sort)
|
||||
.to eq(elegiveis)
|
||||
expect(elegiveis).to eq(%w[GADE_010 GADE_011])
|
||||
end
|
||||
|
||||
it 'devolve vazio sem motorista informado (não toca o banco)' do
|
||||
expect(described_class.veiculos_de_motoristas([], inicio: inicio, fim: fim)).to eq([])
|
||||
expect(described_class.veiculos_de_motoristas(nil, inicio: inicio, fim: fim)).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
# Os dois scopes são a origem da confusão — vale fixar que cada um olha para a
|
||||
# coluna que promete, para nenhum refactor trocar um pelo outro em silêncio.
|
||||
describe 'eixos de período' do
|
||||
it ':no_periodo filtra por planned_date e :no_periodo_checkout por checkout' do
|
||||
expect(described_class.no_periodo(Date.new(2026, 8, 1), Date.new(2026, 8, 14)).to_sql)
|
||||
.to include('planned_date')
|
||||
|
||||
sql_checkout = described_class.no_periodo_checkout(Date.new(2026, 8, 1), Date.new(2026, 8, 14)).to_sql
|
||||
expect(sql_checkout).to include('checkout')
|
||||
expect(sql_checkout).not_to include('planned_date')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -12,3 +12,111 @@ RSpec.describe 'Dashboard', type: :request do
|
||||
expect(response).to redirect_to(motorista_dashboard_path)
|
||||
end
|
||||
end
|
||||
|
||||
# Trava a CONTABILIZAÇÃO DAS FALHAS no dashboard principal.
|
||||
#
|
||||
# A base financeira do dashboard é `atendidas` (concluídas + falhadas, com
|
||||
# checkout) — o MESMO conjunto que a consolidação considera elegível e paga.
|
||||
# Antes usava só `pagas` (concluídas) e o valor da tela ficava menor que o do
|
||||
# fechamento; e as falhas eram filtradas por `planned_date`, caindo no período
|
||||
# errado. Os dois casos estão fixados aqui.
|
||||
#
|
||||
# ⚠️ Falhada TEM checkout: o motorista foi ao local e fechou a visita com motivo
|
||||
# de insucesso. É o que torna `checkout` o eixo correto também para elas.
|
||||
RSpec.describe 'Dashboard — contabilização de falhas', type: :request do
|
||||
include_context 'espelho de rastreio'
|
||||
|
||||
# Preço redondo para os valores esperados serem óbvios na leitura do spec.
|
||||
let(:preco) { 10.0 }
|
||||
let(:inicio) { '2026-08-01' }
|
||||
let(:fim) { '2026-08-14' }
|
||||
|
||||
before do
|
||||
create(:configuracao, chave: 'preco_entrega', valor: preco.to_s)
|
||||
sign_in create(:gerente)
|
||||
end
|
||||
|
||||
def abrir_dashboard
|
||||
get dashboard_path, params: { inicio: inicio, fim: fim }
|
||||
expect(response).to have_http_status(:ok)
|
||||
response.body
|
||||
end
|
||||
|
||||
it 'soma a falhada ao valor estimado e ao total de atendidas' do
|
||||
semear([
|
||||
{ driver: 'JOSE DA SILVA', vehicle: 'GADE_001', status: 'completed',
|
||||
planned_date: Time.zone.local(2026, 8, 5, 8, 0), checkout: Time.zone.local(2026, 8, 5, 10, 0) },
|
||||
{ driver: 'JOSE DA SILVA', vehicle: 'GADE_001', status: 'failed',
|
||||
planned_date: Time.zone.local(2026, 8, 6, 8, 0), checkout: Time.zone.local(2026, 8, 6, 11, 0) }
|
||||
])
|
||||
|
||||
corpo = abrir_dashboard
|
||||
|
||||
# 2 atendidas × R$ 10,00 — com a base antiga (`pagas`) seria 1 e R$ 10,00.
|
||||
expect(corpo).to include('2 entregas atendidas')
|
||||
expect(corpo).to include('1 concluídas · 1 falhadas')
|
||||
expect(corpo).to include('R$ 20,00')
|
||||
end
|
||||
|
||||
it 'conta a falhada pela data do CHECKOUT, não pela planejada' do
|
||||
# Planejada em 31/07 (fora), entregue em 01/08 (dentro) → entra.
|
||||
semear([
|
||||
{ driver: 'JOSE DA SILVA', vehicle: 'GADE_002', status: 'failed',
|
||||
planned_date: Time.zone.local(2026, 7, 31, 8, 0), checkout: Time.zone.local(2026, 8, 1, 9, 0) }
|
||||
])
|
||||
|
||||
corpo = abrir_dashboard
|
||||
|
||||
expect(corpo).to include('1 entregas atendidas')
|
||||
expect(corpo).to include('0 concluídas · 1 falhadas')
|
||||
end
|
||||
|
||||
it 'exclui a falhada planejada dentro do período mas com checkout fora' do
|
||||
# O espelho do caso acima: planejada em 10/08 (dentro), entregue em 01/09
|
||||
# (fora) → não entra. Com o eixo antigo (planned_date) entrava.
|
||||
semear([
|
||||
{ driver: 'JOSE DA SILVA', vehicle: 'GADE_003', status: 'failed',
|
||||
planned_date: Time.zone.local(2026, 8, 10, 8, 0), checkout: Time.zone.local(2026, 9, 1, 9, 0) }
|
||||
])
|
||||
|
||||
corpo = abrir_dashboard
|
||||
|
||||
expect(corpo).to include('0 entregas atendidas')
|
||||
expect(corpo).to include('0 concluídas · 0 falhadas')
|
||||
end
|
||||
|
||||
it 'mantém a pendente fora do valor, contando-a só como pendente' do
|
||||
# Pendente não tem checkout: continua no eixo planned_date e não vira R$.
|
||||
semear([
|
||||
{ driver: 'JOSE DA SILVA', vehicle: 'GADE_004', status: 'pending',
|
||||
planned_date: Time.zone.local(2026, 8, 7, 8, 0), checkout: nil }
|
||||
])
|
||||
|
||||
corpo = abrir_dashboard
|
||||
|
||||
expect(corpo).to include('0 entregas atendidas')
|
||||
expect(corpo).to include('1 pendentes')
|
||||
end
|
||||
|
||||
it 'inclui a falhada no ranking por motorista' do
|
||||
semear([
|
||||
# JOSE: 1 concluída + 1 falhada = 2 atendidas → R$ 20,00
|
||||
{ driver: 'JOSE DA SILVA', vehicle: 'GADE_005', status: 'completed',
|
||||
planned_date: Time.zone.local(2026, 8, 4, 8, 0), checkout: Time.zone.local(2026, 8, 4, 10, 0) },
|
||||
{ driver: 'JOSE DA SILVA', vehicle: 'GADE_005', status: 'failed',
|
||||
planned_date: Time.zone.local(2026, 8, 4, 8, 0), checkout: Time.zone.local(2026, 8, 4, 15, 0) },
|
||||
# MARIA: 1 concluída = 1 atendida → R$ 10,00
|
||||
{ driver: 'MARIA SOUZA', vehicle: 'GADE_006', status: 'completed',
|
||||
planned_date: Time.zone.local(2026, 8, 5, 8, 0), checkout: Time.zone.local(2026, 8, 5, 10, 0) }
|
||||
])
|
||||
|
||||
corpo = abrir_dashboard
|
||||
|
||||
expect(corpo).to include('3 entregas atendidas')
|
||||
expect(corpo).to include('JOSE DA SILVA')
|
||||
# R$ 20,00 do JOSE só existe se a falhada dele entrou na conta do ranking
|
||||
# (com a base antiga ele apareceria com R$ 10,00, igual à MARIA).
|
||||
expect(corpo).to include('R$ 20,00')
|
||||
expect(corpo).to include('R$ 30,00') # total estimado do período
|
||||
end
|
||||
end
|
||||
|
||||
102
spec/support/espelho_rastreio.rb
Normal file
102
spec/support/espelho_rastreio.rb
Normal file
@@ -0,0 +1,102 @@
|
||||
# Harness da tabela do espelho de rastreio (db_reem_simplerout_2026) para os
|
||||
# specs.
|
||||
#
|
||||
# A tabela é EXTERNA: não tem migration (o model Entrega proíbe explicitamente)
|
||||
# e não tem factory. Sem este harness, qualquer teste que dependa dela só
|
||||
# poderia mockar o método sob teste — o que não pega regressão de SQL, que é
|
||||
# justamente onde moram os bugs de eixo de data (planned_date × checkout).
|
||||
#
|
||||
# Uso:
|
||||
#
|
||||
# RSpec.describe Entrega do
|
||||
# include_context 'espelho de rastreio'
|
||||
#
|
||||
# it '...' do
|
||||
# semear([{ driver: 'FULANO', vehicle: 'GADE_001', status: 'completed',
|
||||
# planned_date: ..., checkout: ... }])
|
||||
# end
|
||||
# end
|
||||
module EspelhoRastreio
|
||||
TABELA = 'db_reem_simplerout_2026'.freeze
|
||||
|
||||
# `Entrega.da_conta_gade` filtra por DB_EXISTING_ACCOUNT_ID (default '95907').
|
||||
# Gravamos nas linhas o primeiro valor configurado: se estiver 'all' o scope
|
||||
# não filtra e o valor é indiferente; se estiver '95907' as linhas casam.
|
||||
# Assim o harness não depende do ENV da máquina nem precisa stubar ENV.
|
||||
CONTA = ENV.fetch('DB_EXISTING_ACCOUNT_ID', '95907').split(',').first.to_s.strip.presence || '95907'
|
||||
|
||||
class << self
|
||||
def criar!
|
||||
# ⚠️ GUARDA OBRIGATÓRIA. O model do espelho proíbe DROP porque a tabela
|
||||
# real é a base de rastreio da operação. Aqui só criamos uma tabela de
|
||||
# mesmo nome no banco de TESTE; se ela já existir COM DADOS, estamos
|
||||
# apontados para dados de verdade e o spec tem que abortar em vez de
|
||||
# destruí-los.
|
||||
raise "Espelho de teste exige RAILS_ENV=test (está em #{Rails.env})" unless Rails.env.test?
|
||||
return if @criada
|
||||
|
||||
if conexao.table_exists?(TABELA)
|
||||
linhas = conexao.select_value("SELECT COUNT(*) FROM #{nome_citado}").to_i
|
||||
if linhas.positive?
|
||||
raise "#{TABELA} já existe no banco de teste com #{linhas} linha(s) — " \
|
||||
'o spec NÃO vai derrubá-la. Aponte RAILS_ENV=test para um banco descartável.'
|
||||
end
|
||||
|
||||
conexao.drop_table(TABELA)
|
||||
end
|
||||
|
||||
conexao.create_table(TABELA, id: false) do |t|
|
||||
t.string :tracking_id, null: false
|
||||
t.string :account_id
|
||||
t.bigint :reference_id
|
||||
t.string :driver
|
||||
t.string :vehicle
|
||||
t.string :status
|
||||
t.datetime :planned_date
|
||||
t.datetime :checkout
|
||||
t.string :contact_name
|
||||
t.string :address
|
||||
end
|
||||
conexao.execute("ALTER TABLE #{nome_citado} ADD PRIMARY KEY (tracking_id)")
|
||||
@criada = true
|
||||
end
|
||||
|
||||
# Só derruba o que este harness criou.
|
||||
def derrubar!
|
||||
return unless @criada
|
||||
|
||||
conexao.drop_table(TABELA, if_exists: true)
|
||||
@criada = false
|
||||
end
|
||||
|
||||
def limpar!
|
||||
conexao.execute("DELETE FROM #{nome_citado}") if @criada
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def conexao
|
||||
ActiveRecord::Base.connection
|
||||
end
|
||||
|
||||
def nome_citado
|
||||
conexao.quote_table_name(TABELA)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_context 'espelho de rastreio' do
|
||||
before(:all) { EspelhoRastreio.criar! }
|
||||
after(:all) { EspelhoRastreio.derrubar! }
|
||||
before { EspelhoRastreio.limpar! }
|
||||
|
||||
# O model é readonly? = true, então insert_all (que não instancia registro) é
|
||||
# a única via de gravação — e vale só para o banco de teste.
|
||||
def semear(linhas)
|
||||
Entrega.insert_all(
|
||||
linhas.map.with_index do |l, i|
|
||||
{ tracking_id: "TRK-#{i}-#{SecureRandom.hex(3)}", account_id: EspelhoRastreio::CONTA }.merge(l)
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user