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', sts: 'STS PENHA', checkout: '2026-08-10 09:00:00'), row(nf: 100, status: 'completed', driver: 'Carlos', sts: 'STS PENHA', checkout: '2026-08-12 09:00:00'), row(nf: 101, status: 'completed', driver: 'Pedro', sts: 'STS PENHA', checkout: '2026-08-11 09:00:00'), row(nf: 102, status: 'failed', driver: 'Pedro', sts: 'STS SAPOPEMBA', checkout: '2026-08-13 09:00:00', obs: 'ÓBITO'), row(nf: 103, status: 'pending', driver: nil, sts: 'STS SAPOPEMBA', planned: '2026-08-20') ] 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 '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 it 'traduz o resultado da última visita' do linha = fora.linhas.find { |r| r['reference_id'] == 103 } expect(fora.resultado(linha)).to eq('Em aberto') expect(fora.sucesso?(linha)).to be(false) end # Regressão do dado real (24/08/2026): `title` traz "NF 89096 - KAIQUE TAUAN DA # SILVA" — o destinatário, não o plano. Agrupar por ele daria um grupo por NF. context 'quando o espelho só tem `title` (destinatário)' do let(:visitas) do [ row(nf: 100, status: 'completed', driver: 'Thiago', sts: 'STS PENHA', checkout: '2026-08-12 09:00:00', title: 'NF 100 - KAIQUE TAUAN DA SILVA'), row(nf: 101, status: 'pending', driver: nil, sts: 'STS PENHA', planned: '2026-08-03', title: 'NF 101 - MARIA APARECIDA') ] end it 'não trata o título como plano' do expect(fora.plano(fora.linhas.first)).to be_nil expect(fora.titulo(fora.linhas.first)).to start_with('NF ') expect(fora.plano_identificado?).to be(false) end it 'cai para o agrupamento por unidade' do rotulo, grupos = fora.agrupamento expect(rotulo).to eq('Unidade') expect(grupos).to eq([{ nome: 'STS PENHA', total: 2 }]) end end context 'quando o espelho traz o plano' do let(:visitas) do [ row(nf: 100, status: 'completed', driver: 'Carlos', sts: 'STS PENHA', checkout: '2026-08-12 09:00:00', notes: '(Avulsa)'), row(nf: 101, status: 'completed', driver: 'Pedro', sts: 'STS PENHA', checkout: '2026-08-11 09:00:00', notes: 'INCLUSÃO'), row(nf: 102, status: 'failed', driver: 'Pedro', sts: 'STS PENHA', checkout: '2026-08-13 09:00:00', notes: 'INCLUSÃO') ] end it 'separa (Avulsa) de INCLUSÃO' do rotulo, grupos = fora.agrupamento expect(rotulo).to eq('Plano') expect(grupos).to eq([{ nome: 'INCLUSÃO', total: 2 }, { nome: '(Avulsa)', total: 1 }]) end 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.agrupamento.last).to eq([]) end end def row(nf:, status:, driver:, sts: nil, checkout: nil, planned: nil, title: nil, notes: nil, obs: nil) { 'reference_id' => nf, 'status' => status, 'driver' => driver, 'checkout' => checkout, 'planned_date' => planned, 'observation' => obs, 'contact_name' => sts, 'title' => title, 'notes' => notes, 'comments' => nil, 'route_id' => nil } end end