Pular para o conteúdo principal

Por Ranking — Backend: gaps de dados e decisões

Levantado direto contra o DW de produção em 2026-07-29 (conexão real via SUPA_DB_*, introspecção de information_schema + queries de contagem/volume sobre dw_emplacamento_mart.emplacamento_kpi_dia). Complementa requisitos-por-ranking-backend.md e corrige pontos desatualizados de marts-emplacamento.md.

TL;DR: o mart emplacamento_kpi_dia está muito mais completo do que o marts-emplacamento.md (escrito antes) previa — praticamente todos os "blockers" daquele doc já existem como coluna. Os gaps reais que sobraram são de preenchimento (colunas existem mas vêm majoritariamente vazias/nulas na origem), não de ausência de coluna.


1. Estado real do mart (fonte única do Por Ranking)

dw_emplacamento_mart.emplacamento_kpi_dia43 colunas, grão diário, 2.636.789 linhas, 2025 (ano cheio) + 2026 (até 2026-07-27), volume total 4.035.901. Colunas relevantes ao Por Ranking, todas presentes:

data, ano, mes, fabricante(_codigo), grupomodeloveiculo(_codigo), modelo(_codigo), segmento(_codigo), subsegmento(_codigo), combustivel, ano_fabricacao, municipio(_codigo), estado, local_regiao_abracaf/abcnissan/abrare, regiao_geografica, regiao_metropolitana, regiao_operacional, is_capital, cnpj, razao_social, is_cpf_empresa, montadora, conc_regiao_abracaf/abcnissan/abrare, area_influencia_codigo/nome, grupo_economico_codigo/nome, grupo_empresa_codigo/nome, concessionaria_cadastrada, modalidade, tipo_pessoa, volume.

Consequência: o Por Ranking deve ler só do mart (como o Por Marca), e não do star schema cru (fato_emplacamentos + joins) como o ranking-service.ts atual faz. Isso alinha com as otimizações do Por Marca (cache + react.cache() de dedupe + Promise.all + buildDwFilterClauses).

Já resolvido pelo estado atual do DW (eram "blockers" no marts-emplacamento.md):

  • grupo econômico existe (grupo_economico_codigo/nome)
  • grupo empresa / grupo de concessionária existe (grupo_empresa_codigo/nome) — entidade separada do econômico
  • Incluir Não Cadastrados existe (concessionaria_cadastrada bool)
  • is_capital existe
  • YoY / histórico viável (2025 + 2026 materializados)
  • tipo_pessoa existe como coluna (mas ver gap crítico §2.1)
  • ✅ cnpj, razao_social, area_influencia, regiões — preenchidos

2. Gaps reais (colunas existem, dados faltam na origem)

2.1 🔴 CRÍTICO — tipo_pessoa (PF/PJ) nulo em 86,5% do volume

tipo_pessoavolume%
(null)3.492.49586,5%
PF295.0537,3%
PJ248.3536,2%

Confirmado que não é problema do mart: na fato crua fato_emplacamentos.is_cpf_cliente também é nulo em 3.492.488 linhas (idêntico). A origem (DMS) não classifica PF/PJ em ~86,5% dos emplacamentos.

Impacto no Por Ranking:

  • Colunas Vol PF / Vol PJ (RF027, marcas/modelos/concessionárias) só refletem ~13,5% do volume real → volPF + volPJ ≪ volTotal. As colunas ficam visualmente "quase zeradas" perto do total.
  • Filtro Tipo de Pessoa = Física/Jurídica (RF010) descarta silenciosamente 86,5% do volume. O ranking muda completamente de escala.

✅ Resolvido por decisão de produto: as colunas Vol PF/Vol PJ foram removidas de Marcas e Concessionárias (as únicas que as tinham). O filtro "Tipo de Pessoa" continua funcionando normalmente (restringe o volume da consulta), só a exibição por coluna foi descontinuada — evita induzir a erro com um recorte majoritariamente ausente na origem. Ver requisitos-por-ranking-backend.md §4.1/§4.5. Pendência de dados (preencher tipo_pessoa na origem) continua registrada caso o produto queira reintroduzir as colunas no futuro.

2.2 🟠 grupo_economico_* preenchido em ~0,5% do volume

tem grupo econômico?volume
não4.013.994 (99,5%)
sim21.907 (0,5%)

Filtro/coluna "Por Grupo Econômico" (RF022) e agrupamento com subtotais funcionam tecnicamente, mas com dado quase inexistente o agrupamento é irrelevante hoje. Coluna "Grupo econômico" da tabela de concessionárias virá majoritariamente "—". Pendência: cobrar preenchimento na origem/ETL.

2.3 🟠 grupo_empresa_* (Grupo de Concessionária) preenchido em ~3% do volume

tem grupo empresa?volume
não3.913.847 (97%)
sim122.054 (3%)

Mesma situação da §2.2 para "Por Grupo de Concessionária" (RF021).

Nota: o spec trata "Grupo Econômico" e "Grupo Empresa/de Concessionária" como filtros/colunas separados — e no mart são colunas separadas de fato (grupo_economico_*grupo_empresa_*), ao contrário do que marts-emplacamento.md supôs.

2.4 🟡 concessionaria_cadastrada = false em 88% do volume

cadastradavolume
false3.564.342 (88,3%)
true468.311 (11,6%)
null3.248 (0,1%)

O flag funciona (RF020 "Incluir Não Cadastrados"), mas a maioria do mercado é de concessionárias não cadastradas (esperado: cadastradas ABRACAF são um subconjunto). Decisão de produto necessária: qual o default do ranking de concessionárias?

  • Se default = "não incluir não cadastrados" → tabela mostra só ~11,6% do volume (só as cadastradas).
  • Adotado no backend: respeita o flag; default = só cadastradas (concessionaria_cadastrada = true) conforme leitura literal de "Incluir Não Cadastrados = Não". Confirmar com produto se o default deveria ser o mercado inteiro.

3. Não são gaps (resolvidos / decisões)

  • Categoria (RF005): não há coluna categoria dedicada; o legado do Por Marca já mapeia "Categoria" → segmento (o buildDwFilterClauses filtra m.segmento). Adotado igual aqui. Há 5 segmentos e 23 subsegmentos.
  • Modalidade: 2 valores — Varejo (2.073.321) e Direta (1.962.580). UI relabela Direta → "Venda Direta".
  • Área de Abrangência: todas as colunas existem e são bem preenchidas (estado, municipio, regiao_geografica, regiao_metropolitana, regiao_operacional, area_influencia_nome). Reusa o mesmo parsing prefixado ("cidade:...", "estado:...") do Por Marca.
    • ⚠️ regiao_operacional são códigos (N1, N2, N3, R1...), não nomes geográficos. A coluna "Regional Operacional" (RF020) exibirá esses códigos.
  • YoY (Δ% ano anterior, MS ant., Δ posição): viável com 2025 vs 2026. Só há 2 anos — qualquer requisito de >2 anos fica limitado (não é o caso do Por Ranking, que só compara com o ano imediatamente anterior).
  • Cardinalidade (para dimensionar payload/limites): 132 marcas · 728 grupos de modelo · 3.931 modelos/versão · 5.362 cidades · 3.446 concessionárias.

4. Gaps de wiring (frontend↔backend) — fora do dado, mas bloqueiam "completo"

Descobertos ao mapear os componentes (Erickson fez o frontend; ver requisitos-por-ranking-backend.md §7):

  1. Params divergentes. O RankingFilterBar emite na URL: intervalStart, intervalEnd, categorias, modalidades, area, tab, mes. Mas o page.tsx atual intervalStart, state, group, personType. Ou seja: os filtros que a barra emite (categorias/modalidades/area) não são lidos, e os que o page lê (state/group/personType) não são emitidos pela barra. O backend novo deve ler o vocabulário que a barra realmente emite (+ os que forem adicionados). personType, Grupo e "Incluir Não Cadastrados" ainda não têm controle na barra → filtros existem no backend mas ficam pendentes de UI.
  2. Tabelas 100% mockadas. As 4 ByXxxGridTable consomem RankingGridData/CityRankingData/DealerRankingData/ModelRankingData vindas de _lib/mocked-data.ts. O ranking-service.ts atual produz outra forma (PivotCard/StandardCard) usada só no gráfico. O backend novo passa a produzir os 4 tipos reais e o page.tsx deixa de importar mocked-data.ts.
  3. KPIs mockados no page: card "Ano anterior" = 19.3 hard-coded; card "Acumulado" marcado MOCKED. Substituídos por KPIs reais por aba.
  4. Tooltip do CardRanking tem "Δ% ano ant." e range de data hard-coded.Resolvido: ChartItem agora carrega deltaYoYPct por entidade (calculado no backend de cada aba) e RankingResult.periodLabel traz o intervalo real; o CardRanking renderiza o Δ% real (verde/vermelho/—) e o período no tooltip. CardRanking é usado só pelo Por Ranking.
  5. Controle de acesso RF041 (perfil concessionária loja): exige saber o perfil do usuário e os grupos vinculados a ele — isso depende do modelo de conta/auth, não do DW. Como grupo econômico/empresa está ~0–3% preenchido (§2.2/2.3), a restrição é praticamente inócua hoje. Backend implementa o ponto de validação (rejeita/ignora grupo fora do escopo), mas a fonte dos grupos do usuário fica pendente de definição do modelo de conta.

5. Resumo de decisões adotadas no backend

TemaDecisão
Fontedw_emplacamento_mart.emplacamento_kpi_dia (nunca fato cru)
FiltrosbuildDwFilterClauses (categoria=segmento, modalidade, área, estado, grupo) + tipo_pessoa + concessionaria_cadastrada + grupo econômico/empresa
PF/PJColunas removidas de Marcas/Concessionárias (dado ausente em 86,5%); filtro Tipo de Pessoa mantido (§2.1)
Marcas — colunasMS/MS Ant./Δ% Ano Ant. substituídos por Participação (Mês Atual/Anterior/Δ + Ano Atual/Anterior/Diferença), deltas em variação relativa (%)
ConcessionáriasDefault = só cadastradas; flag "Incluir Não Cadastrados" libera o resto
YoY2025 vs 2026 (Δ% ano ant., MS ant., Δ posição)
CachecacheService.wrap tag RANKING, TTL HISTORICO; react.cache() p/ dedupe do MAX(data)
LimitesTop 100 nas tabelas grandes (concessionárias/modelos); modelos "Todos" = sem limite; cidades cross-tab top-5 marcas

Pendências que dependem de terceiros: §2.1–§2.4 (origem/ETL) e §4.1/§4.5 (frontend + modelo de conta).