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_diaestá muito mais completo do que omarts-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_dia — 43 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_cadastradabool) - ✅ 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_pessoa | volume | % |
|---|---|---|
| (null) | 3.492.495 | 86,5% |
| PF | 295.053 | 7,3% |
| PJ | 248.353 | 6,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ão | 4.013.994 (99,5%) |
| sim | 21.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ão | 3.913.847 (97%) |
| sim | 122.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 quemarts-emplacamento.mdsupôs.
2.4 🟡 concessionaria_cadastrada = false em 88% do volume
| cadastrada | volume |
|---|---|
| false | 3.564.342 (88,3%) |
| true | 468.311 (11,6%) |
| null | 3.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
categoriadedicada; o legado do Por Marca já mapeia "Categoria" →segmento(obuildDwFilterClausesfiltram.segmento). Adotado igual aqui. Há 5 segmentos e 23 subsegmentos. - Modalidade: 2 valores —
Varejo(2.073.321) eDireta(1.962.580). UI relabelaDireta→ "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_operacionalsã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):
- Params divergentes. O
RankingFilterBaremite na URL:intervalStart, intervalEnd, categorias, modalidades, area, tab, mes. Mas opage.tsxatual lê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,Grupoe "Incluir Não Cadastrados" ainda não têm controle na barra → filtros existem no backend mas ficam pendentes de UI. - Tabelas 100% mockadas. As 4
ByXxxGridTableconsomemRankingGridData/CityRankingData/DealerRankingData/ModelRankingDatavindas de_lib/mocked-data.ts. Oranking-service.tsatual produz outra forma (PivotCard/StandardCard) usada só no gráfico. O backend novo passa a produzir os 4 tipos reais e opage.tsxdeixa de importarmocked-data.ts. - KPIs mockados no page: card "Ano anterior" =
19.3hard-coded; card "Acumulado" marcadoMOCKED. Substituídos por KPIs reais por aba. Tooltip do✅ Resolvido:CardRankingtem "Δ% ano ant." e range de data hard-coded.ChartItemagora carregadeltaYoYPctpor entidade (calculado no backend de cada aba) eRankingResult.periodLabeltraz o intervalo real; oCardRankingrenderiza o Δ% real (verde/vermelho/—) e o período no tooltip.CardRankingé usado só pelo Por Ranking.- 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
| Tema | Decisão |
|---|---|
| Fonte | Só dw_emplacamento_mart.emplacamento_kpi_dia (nunca fato cru) |
| Filtros | buildDwFilterClauses (categoria=segmento, modalidade, área, estado, grupo) + tipo_pessoa + concessionaria_cadastrada + grupo econômico/empresa |
| PF/PJ | Colunas removidas de Marcas/Concessionárias (dado ausente em 86,5%); filtro Tipo de Pessoa mantido (§2.1) |
| Marcas — colunas | MS/MS Ant./Δ% Ano Ant. substituídos por Participação (Mês Atual/Anterior/Δ + Ano Atual/Anterior/Diferença), deltas em variação relativa (%) |
| Concessionárias | Default = só cadastradas; flag "Incluir Não Cadastrados" libera o resto |
| YoY | 2025 vs 2026 (Δ% ano ant., MS ant., Δ posição) |
| Cache | cacheService.wrap tag RANKING, TTL HISTORICO; react.cache() p/ dedupe do MAX(data) |
| Limites | Top 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).