iachat/spec/builders/v2/reports/conversion_funnel_builder_spec.rb
Rodribm10 d831ee4d33 feat(reports): Painel Diretoria — Onda 1A (leitura)
Primeira onda do roadmap de indicadores executivos do Grupo Nova. Mede
ADOÇÃO DO CANAL DIGITAL, não a operação total — banner explícito alerta
que reservas fechadas manualmente na recepção ainda não estão capturadas
(Onda 1B vai adicionar marcação manual via botão na conversa).

Backend:
- V2::Reports::ConversionFunnelBuilder — leads (novo/retorno/total),
  reservas (criadas != draft, pagas in active/completed/confirmed),
  taxas de conversão. Filtro opcional por inbox.
- V2::Reports::InboxBenchmarkingBuilder — uma linha por inbox com
  brand_name (via Captain::UnitInbox -> Unit -> Brand)
- Endpoints GET /reports/conversion_funnel e /reports/inbox_benchmarking
- RSpec do ConversionFunnelBuilder

Frontend:
- Rota top-level Reports → Painel Diretoria
- DirectoryDashboard.vue: banner de adoção + filtros + cards + funil + tabela
  benchmarking agrupada por marca com variação vs média
- API client getConversionFunnel + getInboxBenchmarking
- i18n EN + PT

Memórias suporte: feedback_metricas_adocao_canal.md + project_painel_diretoria_roadmap.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:44:59 -03:00

119 lines
4.9 KiB
Ruby

require 'rails_helper'
RSpec.describe V2::Reports::ConversionFunnelBuilder do
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:other_inbox) { create(:inbox, account: account) }
let(:since_t) { 30.days.ago.beginning_of_day }
let(:until_t) { Time.current.end_of_day }
let(:base_params) do
{
since: since_t.to_i.to_s,
until: until_t.to_i.to_s
}
end
def build(params = base_params)
described_class.new(account, params).metrics
end
describe '#metrics' do
context 'with empty period' do
it 'returns zeroed metrics' do
result = build
expect(result[:leads]).to eq({ total: 0, new: 0, returning: 0 })
expect(result[:reservations]).to eq({ created: 0, paid: 0 })
expect(result[:conversion_rates][:lead_to_paid_reservation]).to eq(0)
end
end
context 'with leads only (no reservations)' do
it 'counts conversations as leads' do
contact = create(:contact, account: account)
create(:conversation, account: account, inbox: inbox, contact: contact, created_at: 1.day.ago)
result = build
expect(result[:leads][:total]).to eq(1)
expect(result[:leads][:new]).to eq(1)
expect(result[:reservations][:paid]).to eq(0)
end
end
context 'when classifying new vs returning leads' do
it 'separates contacts with prior history' do
contact = create(:contact, account: account)
create(:conversation, account: account, inbox: inbox, contact: contact, created_at: 5.days.ago)
create(:conversation, account: account, inbox: inbox, contact: contact, created_at: 1.day.ago)
result = build
expect(result[:leads][:total]).to eq(2)
expect(result[:leads][:new]).to eq(1)
expect(result[:leads][:returning]).to eq(1)
end
end
context 'when classifying reservation statuses' do
let(:contact) { create(:contact, account: account) }
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: inbox) }
it 'ignores draft reservations from "created"' do
create(:captain_reservation, account: account, inbox: inbox, contact: contact,
contact_inbox: contact_inbox, status: :draft, created_at: 1.day.ago)
result = build
expect(result[:reservations][:created]).to eq(0)
end
it 'counts non-draft as created' do
create(:captain_reservation, account: account, inbox: inbox, contact: contact,
contact_inbox: contact_inbox, status: :pending_payment, created_at: 1.day.ago)
result = build
expect(result[:reservations][:created]).to eq(1)
expect(result[:reservations][:paid]).to eq(0)
end
%i[active completed confirmed].each do |paid_status|
it "counts #{paid_status} as paid" do
create(:captain_reservation, account: account, inbox: inbox, contact: contact,
contact_inbox: contact_inbox, status: paid_status, created_at: 1.day.ago)
result = build
expect(result[:reservations][:paid]).to eq(1)
end
end
end
context 'with inbox_id filter' do
it 'restricts both conversations and reservations to that inbox' do
contact = create(:contact, account: account)
contact_inbox = create(:contact_inbox, contact: contact, inbox: inbox)
contact_inbox_other = create(:contact_inbox, contact: contact, inbox: other_inbox)
create(:conversation, account: account, inbox: inbox, contact: contact, created_at: 1.day.ago)
create(:conversation, account: account, inbox: other_inbox, contact: contact, created_at: 1.day.ago)
create(:captain_reservation, account: account, inbox: inbox, contact: contact,
contact_inbox: contact_inbox, status: :confirmed, created_at: 1.day.ago)
create(:captain_reservation, account: account, inbox: other_inbox, contact: contact,
contact_inbox: contact_inbox_other, status: :confirmed, created_at: 1.day.ago)
result = build(base_params.merge(inbox_id: inbox.id))
expect(result[:leads][:total]).to eq(1)
expect(result[:reservations][:paid]).to eq(1)
end
end
context 'when computing conversion rates' do
it 'rounds to one decimal' do
contact = create(:contact, account: account)
contact_inbox = create(:contact_inbox, contact: contact, inbox: inbox)
3.times { create(:conversation, account: account, inbox: inbox, contact: contact, created_at: 1.day.ago) }
create(:captain_reservation, account: account, inbox: inbox, contact: contact,
contact_inbox: contact_inbox, status: :confirmed, created_at: 1.day.ago)
result = build
expect(result[:conversion_rates][:lead_to_paid_reservation]).to eq(33.3)
end
end
end
end