advanced
politica
ingestao
diff

Ingestao de Politica de Cidades

Como o Levy puxa o feed de politica publicado por cada cidade, valida com Zod, atalha em sha256 inalterado, materializa geofences e expoe o fluxo de diff + auditoria + ativacao.

Equipe Levy FleetsMay 18, 202616 min read

Ingestao de Politica de Cidades

Cidades publicam sua politica via API MDS 2.0 Policy. O Levy consulta esse feed a cada minuto, valida, compara com o ultimo conhecido, materializa os geofences resultantes e programa para ativacao. Todo o fluxo e idempotente -- executar duas vezes no mesmo payload de feed e um no-op.

Um feed por jurisdicao

Cada linha mds_jurisdictions aponta para exatamente uma URL de feed de politica. Se uma cidade publica multiplos feeds (por exemplo, um para patinetes, um para e-bikes), crie uma jurisdicao por feed. O slug e o que os desambigua.

A pipeline em resumo

+-------------------+   60s   +----------------------+    +-------------------+
| mds-policy-poll   |-------->| fetchPolicyFeed      |--->| hash sha256       |
| cron              |         |                      |    +---------+---------+
+-------------------+         +----------------------+              |
                                                                    | inalterado?
                                                                    v sair
                                                          +---------+---------+
                                                          | parsePolicyFeed   |
                                                          | (validacao Zod)   |
                                                          +---------+---------+
                                                                    |
                                                                    v
                                                          +---------+---------+
                                                          | ingestPolicies    |
                                                          | upsert policies   |
                                                          | replace rules     |
                                                          | materialize zones |
                                                          | mirror into zones |
                                                          +---------+---------+
                                                                    |
                                                                    v
                                                          +---------+---------+
                                                          | mds_policy_audit  |
                                                          | run_id + diff     |
                                                          +-------------------+

Passo 1 -- Buscar + hashear

fetchPolicyFeed(jurisdiction) emite um GET para a policy_feed_url configurada, anexando o policy_feed_auth_token opcional como header Bearer. O corpo completo da resposta e hasheado com sha256.

Se o novo hash e igual ao ultimo hash conhecido na linha da jurisdicao, o ingester atalha e sai. Sem parse, sem diff, sem entrada de auditoria. Isso mantem o custo de polling em regime estavel baixo e evita spam no log de auditoria com runs no-op.

Se o hash difere (ou nenhum hash conhecido existe), o processamento continua.

Passo 2 -- Parse + validar

parsePolicyFeed(raw) roda o JSON bruto atraves de esquemas Zod modelados na spec MDS 2.0 Policy:

CampoEsquema
policies[].policy_iduuid
policies[].namestring
policies[].start_dateRFC3339
policies[].end_dateRFC3339, nullable
policies[].published_dateRFC3339
policies[].rules[]array de regras, cada uma com rule_type, geographies, rule_units, vehicle_types[], days[], start_time, end_time
geographies[].geography_iduuid
geographies[].geography_jsonFeature GeoJSON

Uma segunda passada (parseGeographyFeed) resolve cada geography_id referenciado de rules[].geographies[] contra o feed /geographies da cidade. Referencias nao resolvidas sao logadas como avisos no run de auditoria, mas nao bloqueiam a ingestao (a regra problematica e pulada).

Falhas de validacao sao registradas em mds_policy_audit com o caminho do erro Zod. O estado anterior da politica permanece em vigor -- nunca aplicamos parcialmente um feed mal formado.

Passo 3 -- Upsert + substituir

ingestPolicies() roda em uma unica transacao:

  1. Politicas: INSERT ... ON CONFLICT (jurisdiction_id, policy_external_id) DO UPDATE em mds_policies. A coluna raw_json armazena o payload parseado para o visualizador de diff.
  2. Regras: delete-then-insert contra mds_policy_rules para as politicas afetadas (regras sao muito emaranhadas para upsert limpo).
  3. Geofences: materializa linhas policy_geofences das geografias referenciadas por cada regra. Uma linha por (rule_id, geography_id). O campo geojson armazena a Feature resolvida; a coluna PostGIS geometry atualmente fica nula e e populada pelo checker pointInPolygon em JS (veja Prioridade de Geofences Empilhados e as notas de implementacao).
  4. Espelhamento em zones: para compatibilidade retroativa com o motor de enforcement de zona existente, cada policy geofence tambem escreve uma linha na tabela zones legada marcada com source = 'city', source_jurisdiction_id e source_policy_rule_id. Um indice parcial unico em source_policy_rule_id torna o upsert idempotente.

raw_json e feed_hash sao atualizados na linha da jurisdicao no final da transacao -- apenas apos cada escrita downstream ter sucesso.

Passo 4 -- Auditoria + diff

Toda execucao de ingestao escreve uma linha em mds_policy_audit:

ColunaConteudo
idUUID para o run
jurisdiction_idFK
applied_atTimestamp
feed_hash_beforesha256 do ultimo payload conhecido
feed_hash_aftersha256 do novo payload
diffJSON de {added: [...], removed: [...], modified: [...]}
errorsErros Zod, geografias nao resolvidas etc.
statussuccess / partial / failed

O diff e computado no nivel de politica -- uma politica que ganhou uma nova regra aparece em modified com os deltas de regra especificos embutidos. O visualizador no dashboard do operador renderiza isso como JSON lado a lado.

Cron de ativacao

Um cron separado, mds-policy-activate, roda a cada minuto e gira maquinas de estado de politica:

Transicao de estadoGatilho
pending -> activestart_date <= now() AND status = 'pending'
active -> expiredend_date IS NOT NULL AND end_date <= now() AND status = 'active'
pending -> supersededUma politica mais nova com o mesmo external_id e regras sobrepostas a substitui

Em pending -> active, o cron de ativacao chama o servico de enforcement para disparar o comando de velocidade/bloqueio especifico do OEM para cada veiculo atualmente dentro da geometria da regra. Veja Aplicacao de Velocidade em Tempo Real.

O cron pula eventos de ativacao mais antigos que 30 segundos -- se o cron parou e um start_date agora esta 5 minutos no passado, ainda aplicamos a regra mas logamos um aviso late_activation. Eventos de enforcement nunca sao retrodatados.

O visualizador de diff

Abra /dashboard/compliance/{jurisdiction-id}/policies/{policy-id}/diff para ver a entrada de auditoria mais recente que tocou uma politica. A pagina renderiza:

  • Cabecalho: hash do feed antes e depois, applied_at, status
  • Painel esquerdo: JSON bruto da ingestao anterior
  • Painel direito: JSON bruto desta ingestao
  • Destaque de diff inline no nivel de campo

Voce pode rolar por entradas de auditoria historicas via o log de auditoria na pagina de detalhe da jurisdicao. Cada entrada e permalinkada por run_id.

O log de auditoria

Vive em /dashboard/compliance/{jurisdiction-id} sob a aba Audit. Uma linha por run de ingestao. Filtravel por status e intervalo de datas. Expandir uma linha mostra o JSON diff bruto e quaisquer erros.

Filtros uteis:

  • Status = failed -- mostra os payloads de feed que nao conseguimos parsear. Na maioria das vezes uma cidade enviou uma politica mal formada e mantivemos o estado anterior.
  • Status = partial -- o feed foi parseado mas pelo menos uma referencia de geografia falhou em resolver. A regra problematica esta listada em errors.
  • Intervalo de datas -- para revisoes trimestrais quando uma cidade pergunta "quando essa politica entrou em vigor do seu lado?".

Resolucao de conflitos no momento da ingestao

O ingester nao resolve conflitos entre zonas do operador e politica municipal no momento da ingestao -- apenas materializa as regras municipais em sua camada de prioridade. A deteccao de conflito acontece no momento de leitura, exposta como:

  • Banner de conflito no indice de compliance -- conta zonas do operador atualmente sombreadas por uma regra municipal.
  • API de conflitos do operador em GET /api/admin/compliance/conflicts -- JSON para cada zona sombreada, com o ID da regra que sombreia.

Essa separacao significa que uma cidade pode publicar uma Policy que sobrepoe suas zonas de operador sem quebrar nada -- suas zonas ainda existem, apenas estao com classificacao inferior. Se a cidade depois aposentar a politica, suas zonas voltam ao topo da fila de prioridade automaticamente.

Veja Prioridade de Geofences Empilhados para a escada.

Soft-delete de uma jurisdicao

Se um operador remove uma jurisdicao (licenca expirada, saida do mercado), a linha e soft-deleted. Retemos dados historicos por 30 dias, depois retornamos HTTP 410 nos endpoints MDS publicos. A ingestao de politica para no soft-delete; o log de auditoria e preservado indefinidamente para registros proprios do operador.

Armadilhas comuns

  • Feed de politica da cidade retorna 404 -- causa mais comum: a URL era provisoria e mudou quando a cidade foi GA. Acesse do seu terminal e confirme. O log de auditoria registra respostas 4xx e 5xx.
  • Feed valida mas nao gera policy_geofences -- toda regra referenciou uma geografia que nao conseguimos resolver. Verifique se a cidade esta publicando tanto o feed de Policy quanto o feed de Geografia.
  • A mesma politica continua aparecendo no diff -- a cidade esta regenerando o feed a cada poll (diferentes published_date ou diferencas de whitespace no JSON). O atalho sha256 dispara apenas em payloads byte-identicos; pecam a cidade para estabilizar sua serializacao.
  • A ativacao acontece mas nenhum veiculo e aplicado -- veiculos fora da geometria ou GPS velho (>5 min). Veja Aplicacao de Velocidade em Tempo Real.

Proximos passos