Files
Reem-Notas/spec/models/consolidacao_spec.rb

340 lines
16 KiB
Ruby

require 'rails_helper'
RSpec.describe Consolidacao, type: :model do
describe 'validação de período' do
it 'rejeita data_fim anterior a data_inicio' do
c = build(:consolidacao, data_inicio: Date.new(2026, 6, 10), data_fim: Date.new(2026, 6, 1))
expect(c).not_to be_valid
expect(c.errors[:data_fim]).to be_present
end
it 'aceita período válido' do
expect(build(:consolidacao)).to be_valid
end
end
describe '#recalcular_valor_total (before_save)' do
it 'soma os valores dos motoristas da consolidação' do
consolidacao = create(:consolidacao)
create(:consolidacao_motorista, consolidacao: consolidacao, motorista_nome: 'A')
create(:consolidacao_motorista, consolidacao: consolidacao, motorista_nome: 'B')
consolidacao.consolidacao_motoristas.update_all(valor_total: 30)
consolidacao.save!
expect(consolidacao.reload.valor_total).to eq(60)
end
end
describe '#finalizar!' do
let(:consolidacao) { create(:consolidacao) }
let(:gerente) { create(:gerente) }
it 'não finaliza quando há entregas não classificadas' do
allow(consolidacao).to receive(:todas_entregas_classificadas?).and_return(false)
expect(consolidacao.finalizar!(gerente)).to be_falsey
expect(consolidacao.reload).to be_rascunho
end
it 'finaliza e registra finalizador quando tudo está classificado' do
allow(consolidacao).to receive(:todas_entregas_classificadas?).and_return(true)
consolidacao.finalizar!(gerente)
consolidacao.reload
expect(consolidacao).to be_finalizada
expect(consolidacao.finalizado_por).to eq(gerente.id)
expect(consolidacao.finalizado_em).to be_present
end
end
describe '#arquivar!' do
it 'marca deleted_at, status arquivada e registra quem arquivou' do
gerente = create(:gerente)
consolidacao = create(:consolidacao)
consolidacao.arquivar!(gerente)
consolidacao.reload
expect(consolidacao).to be_arquivada
expect(consolidacao.deleted_at).to be_present
expect(consolidacao.arquivado_por).to eq(gerente.id)
expect(consolidacao.arquivado_em).to be_present
expect(consolidacao.arquivador).to eq(gerente)
end
end
describe '#todas_entregas_classificadas? (gate por motorista)' do
let(:consolidacao) { create(:consolidacao) }
before do
create(:consolidacao_motorista, consolidacao: consolidacao, motorista_nome: 'A')
create(:consolidacao_motorista, consolidacao: consolidacao, motorista_nome: 'B')
end
it 'é falso quando UM motorista tem pendências, mesmo que outro tenha entregas de sobra' do
allow(consolidacao).to receive(:entregas_elegiveis_count).and_return(2)
# A: 0 classificadas | B: 4 classificadas. A soma agregada (4 >= 4) passaria,
# mas a checagem por motorista deve reprovar.
create_list(:consolidacao_entrega, 4, consolidacao: consolidacao, motorista_nome: 'B')
expect(consolidacao.todas_entregas_classificadas?).to be false
end
it 'é verdadeiro quando cada motorista está completo' do
allow(consolidacao).to receive(:entregas_elegiveis_count).and_return(1)
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A')
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'B')
expect(consolidacao.todas_entregas_classificadas?).to be true
end
it 'ignora motoristas sem entregas elegíveis (total zero)' do
allow(consolidacao).to receive(:entregas_elegiveis_count).and_return(0)
expect(consolidacao.todas_entregas_classificadas?).to be true
end
end
describe '#classificadas_count (ignora apontamentos manuais)' do
let(:consolidacao) { create(:consolidacao) }
it 'conta entregas elegíveis e ignora as manuais (não infla o progresso)' do
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A', tracking_id: 'T1')
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A', tracking_id: 'T2')
# Apontamento manual — fora do conjunto elegível do período.
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A',
tracking_id: 'T3', manual: true)
expect(consolidacao.classificadas_count('A')).to eq(2)
end
it 'não ultrapassa o total elegível por causa de apontamentos (gate continua válido)' do
create(:consolidacao_motorista, consolidacao: consolidacao, motorista_nome: 'A')
allow(consolidacao).to receive(:entregas_elegiveis_count).and_return(1)
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A', tracking_id: 'T1')
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A',
tracking_id: 'T2', manual: true)
expect(consolidacao.classificadas_count('A')).to eq(1)
expect(consolidacao.todas_entregas_classificadas?).to be true
end
end
describe '#adicionar_apontamento! (apontamento manual multi-tipo)' do
let(:consolidacao) { create(:consolidacao) }
let(:user) { create(:gerente) }
let(:entrega) { Struct.new(:tracking_id, :driver).new('TRK-AV', 'Steferson Lucas') }
before { allow(ConsolidacaoEntrega).to receive(:valor_para).and_return(15.0) }
it 'inclui o motorista e cria um pilar manual por tipo, somando o valor' do
consolidacao.adicionar_apontamento!(entrega, %w[entrega_normal bonus], user)
cm = consolidacao.consolidacao_motoristas.find_by(motorista_nome: 'Steferson Lucas')
expect(cm).to be_present
pilares = consolidacao.consolidacao_entregas.where(tracking_id: 'TRK-AV')
expect(pilares.count).to eq(2)
expect(pilares.all?(&:manual?)).to be true
expect(cm.reload.valor_total).to eq(30.0)
end
it 'é idempotente por (tracking, tipo) e exige ao menos um tipo válido' do
consolidacao.adicionar_apontamento!(entrega, %w[entrega_normal], user)
consolidacao.adicionar_apontamento!(entrega, %w[entrega_normal], user)
expect(consolidacao.consolidacao_entregas.where(tracking_id: 'TRK-AV').count).to eq(1)
expect { consolidacao.adicionar_apontamento!(entrega, [], user) }.to raise_error(ArgumentError)
end
end
describe '#adicionar_apontamento_manual! (100% manual, sem rastreio)' do
let(:consolidacao) { create(:consolidacao) }
let(:user) { create(:gerente) }
before { allow(ConsolidacaoEntrega).to receive(:valor_para).and_return(15.0) }
it 'cria pilares manuais com nf/obs, inclui o motorista e soma o valor' do
consolidacao.adicionar_apontamento_manual!(
motorista: 'Motorista A', tipos: %w[entrega_normal bonus],
nf: '12345', obs: 'UBS Sacomã', user: user
)
cm = consolidacao.consolidacao_motoristas.find_by(motorista_nome: 'Motorista A')
expect(cm).to be_present
pilares = consolidacao.consolidacao_entregas.where(motorista_nome: 'Motorista A')
expect(pilares.count).to eq(2)
expect(pilares.all?(&:manual?)).to be true
expect(pilares.map(&:nf_manual).uniq).to eq(['12345'])
expect(pilares.map(&:obs_manual).uniq).to eq(['UBS Sacomã'])
# Mesmo tracking_id sintético agrupa os pilares como UMA entrega.
expect(pilares.map(&:tracking_id).uniq.size).to eq(1)
expect(cm.reload.valor_total).to eq(30.0)
end
it 'não conta no gate (manual) mas exige motorista e ao menos um tipo' do
allow(consolidacao).to receive(:entregas_elegiveis_count).and_return(0)
consolidacao.adicionar_apontamento_manual!(motorista: 'Motorista A', tipos: %w[entrega_normal], user: user)
expect(consolidacao.classificadas_count('Motorista A')).to eq(0)
expect { consolidacao.adicionar_apontamento_manual!(motorista: '', tipos: %w[entrega_normal], user: user) }
.to raise_error(ArgumentError)
expect { consolidacao.adicionar_apontamento_manual!(motorista: 'X', tipos: [], user: user) }
.to raise_error(ArgumentError)
end
it 'usa valor_extra para a Extraordinária quando informado' do
consolidacao.adicionar_apontamento_manual!(
motorista: 'B', tipos: %w[extraordinaria], valor_extra: 99.0, user: user
)
pilar = consolidacao.consolidacao_entregas.find_by(motorista_nome: 'B', tipo: :extraordinaria)
expect(pilar.valor_aplicado).to eq(99.0)
end
end
describe 'arquivamento de motorista (reversível)' do
let(:consolidacao) { create(:consolidacao) }
let(:gerente) { create(:gerente) }
before do
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A',
tipo: :entrega_normal, valor_aplicado: 20)
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'B',
tipo: :entrega_normal, valor_aplicado: 30)
@cm_a = create(:consolidacao_motorista, consolidacao: consolidacao, motorista_nome: 'A')
@cm_b = create(:consolidacao_motorista, consolidacao: consolidacao, motorista_nome: 'B')
end
it 'tira o motorista arquivado do total e do gate, e restaura ao desarquivar' do
consolidacao.update_column(:valor_total, consolidacao.consolidacao_motoristas.ativos.sum(:valor_total))
expect(consolidacao.reload.valor_total).to eq(50)
@cm_a.arquivar!(gerente)
consolidacao.recalcular_motorista!('A')
expect(@cm_a.reload).to be_arquivado
expect(consolidacao.reload.valor_total).to eq(30) # só B conta
expect(ConsolidacaoMotorista.ativos).to contain_exactly(@cm_b)
expect(ConsolidacaoMotorista.arquivados).to contain_exactly(@cm_a)
# Gate ignora o motorista arquivado (A estava sem classificação completa).
allow(consolidacao).to receive(:entregas_elegiveis_count).and_return(0)
allow(consolidacao).to receive(:entregas_elegiveis_count).with('B').and_return(1)
expect(consolidacao.todas_entregas_classificadas?).to be true
@cm_a.desarquivar!(gerente)
consolidacao.recalcular_motorista!('A')
expect(consolidacao.reload.valor_total).to eq(50)
end
end
describe 'escopos de soft delete' do
it 'separa ativas de arquivadas' do
ativa = create(:consolidacao)
arquivada = create(:consolidacao)
arquivada.arquivar!(create(:gerente))
expect(Consolidacao.ativas).to include(ativa)
expect(Consolidacao.ativas).not_to include(arquivada)
expect(Consolidacao.arquivadas).to include(arquivada)
end
end
describe '#sincronizar_veiculos! (via recalcular_motorista!)' do
let(:consolidacao) { create(:consolidacao) }
before { create(:consolidacao_motorista, consolidacao: consolidacao, motorista_nome: 'A') }
it 'cria uma linha por veículo classificado, somando o valor do veículo' do
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A',
vehicle: 'GADE_1', tipo: :entrega_normal, valor_aplicado: 15)
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A',
vehicle: 'GADE_1', tipo: :bonus, valor_aplicado: 10)
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A',
vehicle: 'GADE_2', tipo: :entrega_normal, valor_aplicado: 20)
consolidacao.recalcular_motorista!('A')
veics = consolidacao.consolidacao_veiculos.where(motorista_nome: 'A').order(:vehicle)
expect(veics.map(&:vehicle)).to eq(%w[GADE_1 GADE_2])
expect(veics.find_by(vehicle: 'GADE_1').valor_total).to eq(25)
expect(veics.find_by(vehicle: 'GADE_2').valor_total).to eq(20)
end
it 'remove o veículo aberto que ficou sem pilares, mas preserva o fechado' do
e1 = create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A',
vehicle: 'GADE_1', valor_aplicado: 15)
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A',
vehicle: 'GADE_2', valor_aplicado: 20)
consolidacao.recalcular_motorista!('A')
consolidacao.consolidacao_veiculos.find_by(vehicle: 'GADE_2').fechar!(create(:gerente))
e1.destroy
consolidacao.recalcular_motorista!('A')
veics = consolidacao.consolidacao_veiculos.where(motorista_nome: 'A')
expect(veics.pluck(:vehicle)).to contain_exactly('GADE_2') # GADE_1 (aberto e vazio) removido
end
end
describe 'gate: veículo arquivado libera a finalização' do
let(:consolidacao) { create(:consolidacao) }
before { create(:consolidacao_motorista, consolidacao: consolidacao, motorista_nome: 'A') }
it 'desconta as elegíveis do veículo arquivado' do
allow(consolidacao).to receive(:entregas_elegiveis_count).with('A').and_return(3)
allow(consolidacao).to receive(:entregas_elegiveis_count_veiculo).with('A', 'GADE_2').and_return(1)
# 2 classificadas (GADE_1). Falta 1 (GADE_2) => incompleto.
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A',
vehicle: 'GADE_1', tracking_id: 'T1')
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A',
vehicle: 'GADE_1', tracking_id: 'T2', tipo: :bonus)
expect(consolidacao.motorista_completo?('A')).to be false
# Arquiva GADE_2 (o carro sem classificar): total cai p/ 2 => completo.
cv = consolidacao.consolidacao_veiculos.create!(motorista_nome: 'A', vehicle: 'GADE_2')
cv.arquivar!(create(:gerente))
expect(consolidacao.motorista_completo?('A')).to be true
end
end
describe '#arquivar_veiculos_pendentes!' do
let(:consolidacao) { create(:consolidacao) }
let(:gerente) { create(:gerente) }
before { create(:consolidacao_motorista, consolidacao: consolidacao, motorista_nome: 'A') }
it 'arquiva só os veículos pendentes (com elegíveis e < 100%)' do
allow(consolidacao).to receive(:veiculos_do_motorista).with('A').and_return([
{ vehicle: 'GADE_1', eligible: 5, classificadas: 5, completo: true, fechado: true, arquivado: false },
{ vehicle: 'GADE_2', eligible: 3, classificadas: 1, completo: false, fechado: false, arquivado: false },
{ vehicle: '(sem veículo)', eligible: 0, classificadas: 0, completo: false, fechado: false, arquivado: false }
])
arquivados = consolidacao.arquivar_veiculos_pendentes!(gerente)
expect(arquivados).to eq(['A · GADE_2'])
expect(consolidacao.consolidacao_veiculos.find_by(motorista_nome: 'A', vehicle: 'GADE_2')).to be_arquivado
# não mexe no consolidado (GADE_1) nem no bucket sem elegíveis
expect(consolidacao.consolidacao_veiculos.where(vehicle: 'GADE_1')).not_to exist
expect(consolidacao.consolidacao_veiculos.where(vehicle: '(sem veículo)')).not_to exist
end
end
describe '#finalizar! fecha os veículos ainda abertos' do
let(:consolidacao) { create(:consolidacao) }
let(:gerente) { create(:gerente) }
before { create(:consolidacao_motorista, consolidacao: consolidacao, motorista_nome: 'A') }
it 'consolida os veículos abertos ao finalizar' do
create(:consolidacao_entrega, consolidacao: consolidacao, motorista_nome: 'A',
vehicle: 'GADE_1', valor_aplicado: 15)
consolidacao.recalcular_motorista!('A')
cv = consolidacao.consolidacao_veiculos.find_by(vehicle: 'GADE_1')
expect(cv).not_to be_fechado
allow(consolidacao).to receive(:todas_entregas_classificadas?).and_return(true)
consolidacao.finalizar!(gerente)
expect(cv.reload).to be_fechado
expect(cv.fechado_por).to eq(gerente.id)
end
end
end