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>
119 lines
4.9 KiB
Ruby
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
|