Correção de alguns valores no dash principal

This commit is contained in:
2026-08-24 12:07:56 -03:00
parent 7b3ec3e407
commit 04b78bc7de
12 changed files with 690 additions and 50 deletions

View File

@@ -0,0 +1,67 @@
require 'rails_helper'
RSpec.describe Analytics::NotasForaOperacao do
subject(:fora) { described_class.new(inicio: Date.new(2026, 8, 1), fim: Date.new(2026, 8, 31)) }
# Visitas já filtradas pelo SQL (NFs que não estão em nenhuma gade_entregas_*).
let(:visitas) do
[
row(nf: 100, status: 'failed', driver: 'Carlos', checkout: '2026-08-10 09:00:00', title: 'NF 100 - FULANO (Avulsa)'),
row(nf: 100, status: 'completed', driver: 'Carlos', checkout: '2026-08-12 09:00:00', title: 'NF 100 - FULANO (Avulsa)'),
row(nf: 101, status: 'completed', driver: 'Pedro', checkout: '2026-08-11 09:00:00', title: 'INCLUSÃO'),
row(nf: 102, status: 'failed', driver: 'Pedro', checkout: '2026-08-13 09:00:00', title: 'INCLUSÃO', obs: 'ÓBITO'),
row(nf: 103, status: 'pending', driver: 'Marcos', planned: '2026-08-20', title: nil)
]
end
before { allow(fora).to receive(:visitas).and_return(visitas) }
it 'conta uma linha por NF, pelo status da última visita' do
expect(fora.total).to eq(4)
expect(fora.entregues).to eq(2)
expect(fora.nao_entregues).to eq(1)
expect(fora.pendentes).to eq(1)
end
it 'agrupa por plano de origem (Avulsa x INCLUSÃO)' do
expect(fora.por_plano).to contain_exactly(
{ nome: 'INCLUSÃO', total: 2 },
{ nome: 'NF 100 - FULANO (Avulsa)', total: 1 },
{ nome: 'SEM PLANO IDENTIFICADO', total: 1 }
)
end
it 'ordena a listagem da visita mais recente para a mais antiga' do
expect(fora.listagem.map { |r| r['reference_id'] }).to eq([102, 100, 101, 103])
end
# NOT IN com NULL na subquery devolve zero linhas — por isso cada SELECT da
# união precisa filtrar nota_fiscal IS NOT NULL.
it 'protege o NOT IN contra nota_fiscal NULL' do
allow(Operacao).to receive(:nomes_validos).and_return(%w[gade_entregas_teste])
sql = nil
allow(ActiveRecord::Base.connection).to receive(:select_all) { |consulta| sql = consulta; [] }
allow(ActiveRecord::Base.connection).to receive(:columns).and_return([])
described_class.new(inicio: Date.new(2026, 8, 1), fim: Date.new(2026, 8, 31)).send(:carregar)
expect(sql).to include('nota_fiscal IS NOT NULL')
expect(sql).to include('NOT IN')
end
context 'sem nenhuma nota fora da operação' do
let(:visitas) { [] }
it 'não quebra e informa vazio' do
expect(fora.total).to eq(0)
expect(fora.any?).to be(false)
expect(fora.por_plano).to eq([])
end
end
def row(nf:, status:, driver:, checkout: nil, planned: nil, title: nil, obs: nil)
{ 'reference_id' => nf, 'status' => status, 'driver' => driver,
'checkout' => checkout, 'planned_date' => planned, 'observation' => obs,
'contact_name' => nil, 'title' => title, 'notes' => nil, 'comments' => nil, 'route_id' => nil }
end
end

View File

@@ -3,14 +3,16 @@ require 'rails_helper'
RSpec.describe Analytics::OperacaoMetricas do
# Linhas simuladas (mesma forma do retorno de select_all) — evita depender das
# tabelas externas no teste. Stubamos #registros para focar nas agregações.
# Uma NF por linha: nesse conjunto nenhuma visita se repete, então as duas
# camadas (visitas x notas) coincidem.
let(:rows) do
[
row(status: 'completed', driver: 'Carlos', sts: 'STS PERUS', gade: 'RECORRENTE', checkout: '2026-06-22 10:00:00', clat: '-23.5', clng: '-46.6'),
row(status: 'completed', driver: 'Carlos', sts: 'STS PERUS', gade: 'RECORRENTE', checkout: '2026-06-22 11:00:00', lat: '-23.6', lng: '-46.7'),
row(status: 'completed', driver: 'Pedro', sts: 'STS PIRITUBA', gade: 'RECORRENTE', checkout: '2026-06-23 09:00:00', clat: '-23.4', clng: '-46.5'),
row(status: 'failed', driver: 'Pedro', sts: 'STS PIRITUBA', gade: 'NOVO', obs: 'ÓBITO', checkout: '2026-06-23 12:00:00'),
row(status: 'failed', driver: 'Carlos', sts: 'STS PERUS', gade: 'NOVO', obs: 'ÓBITO', checkout: '2026-06-22 13:00:00'),
row(status: 'pending', driver: 'Marcos', sts: 'STS PERUS', gade: 'NOVO', planned: '2026-06-24')
row(nf: 1, status: 'completed', driver: 'Carlos', sts: 'STS PERUS', gade: 'RECORRENTE', checkout: '2026-06-22 10:00:00', clat: '-23.5', clng: '-46.6'),
row(nf: 2, status: 'completed', driver: 'Carlos', sts: 'STS PERUS', gade: 'RECORRENTE', checkout: '2026-06-22 11:00:00', lat: '-23.6', lng: '-46.7'),
row(nf: 3, status: 'completed', driver: 'Pedro', sts: 'STS PIRITUBA', gade: 'RECORRENTE', checkout: '2026-06-23 09:00:00', clat: '-23.4', clng: '-46.5'),
row(nf: 4, status: 'failed', driver: 'Pedro', sts: 'STS PIRITUBA', gade: 'NOVO', obs: 'ÓBITO', checkout: '2026-06-23 12:00:00'),
row(nf: 5, status: 'failed', driver: 'Carlos', sts: 'STS PERUS', gade: 'NOVO', obs: 'ÓBITO', checkout: '2026-06-22 13:00:00'),
row(nf: 6, status: 'pending', driver: 'Marcos', sts: 'STS PERUS', gade: 'NOVO', planned: '2026-06-24')
]
end
@@ -31,6 +33,51 @@ RSpec.describe Analytics::OperacaoMetricas do
expect(metricas.pendentes).to eq(1)
end
it 'sem NF repetida, visitas e notas coincidem' do
expect(metricas.total_visitas).to eq(6)
expect(metricas.retentativas).to eq(0)
expect(metricas.notas_reentregues).to eq(0)
end
# A regressão que motivou as duas camadas: a mesma NF visitada duas vezes
# (falhou, depois entregou) contava 2 no dashboard financeiro e 1 aqui, sem
# nada na tela explicando a diferença — e o insucesso desaparecia por completo.
context 'com NF visitada mais de uma vez' do
let(:rows) do
[
row(nf: 10, status: 'failed', driver: 'Carlos', sts: 'STS PERUS', gade: 'NOVO', obs: 'AUSENTE', checkout: '2026-06-10 09:00:00'),
row(nf: 10, status: 'completed', driver: 'Pedro', sts: 'STS PERUS', gade: 'NOVO', checkout: '2026-06-12 09:00:00'),
row(nf: 11, status: 'completed', driver: 'Pedro', sts: 'STS PERUS', gade: 'NOVO', checkout: '2026-06-12 10:00:00')
]
end
it 'conta a NF uma vez, pelo status da ÚLTIMA visita' do
expect(metricas.total).to eq(2)
expect(metricas.sucesso).to eq(2)
expect(metricas.recusas).to eq(0)
end
it 'expõe as idas ao local em vez de descartá-las em silêncio' do
expect(metricas.total_visitas).to eq(3)
expect(metricas.retentativas).to eq(1)
expect(metricas.visitas_insucesso).to eq(1)
expect(metricas.notas_reentregues).to eq(1)
end
it 'visita sem checkout perde para a que tem' do
sem_checkout = [
row(nf: 20, status: 'pending', driver: 'Marcos', sts: 'STS PERUS', gade: 'NOVO', planned: '2026-06-24'),
row(nf: 20, status: 'completed', driver: 'Marcos', sts: 'STS PERUS', gade: 'NOVO', checkout: '2026-06-20 09:00:00')
]
m = described_class.new(tabelas: [])
allow(m).to receive(:registros).and_return(sem_checkout)
expect(m.total).to eq(1)
expect(m.sucesso).to eq(1)
expect(m.pendentes).to eq(0)
end
end
it 'calcula insucessos %' do
ins = metricas.insucessos_pct
expect(ins[:completed]).to eq(3)
@@ -104,9 +151,9 @@ RSpec.describe Analytics::OperacaoMetricas do
end
end
def row(status:, driver:, sts:, gade:, obs: nil, checkout: nil, planned: nil, lat: nil, lng: nil, clat: nil, clng: nil)
def row(status:, driver:, sts:, gade:, nf: nil, obs: nil, checkout: nil, planned: nil, lat: nil, lng: nil, clat: nil, clng: nil)
{
'status' => status, 'driver' => driver, 'contact_name' => sts, 'status_gade' => gade,
'reference_id' => nf, 'status' => status, 'driver' => driver, 'contact_name' => sts, 'status_gade' => gade,
'observation' => obs, 'checkout' => checkout, 'planned_date' => planned,
'latitude' => lat, 'longitude' => lng,
'checkout_latitude' => clat, 'checkout_longitude' => clng