correção de bugs
This commit is contained in:
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