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:
| Campo | Esquema |
|---|---|
policies[].policy_id | uuid |
policies[].name | string |
policies[].start_date | RFC3339 |
policies[].end_date | RFC3339, nullable |
policies[].published_date | RFC3339 |
policies[].rules[] | array de regras, cada uma com rule_type, geographies, rule_units, vehicle_types[], days[], start_time, end_time |
geographies[].geography_id | uuid |
geographies[].geography_json | Feature 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:
- Politicas:
INSERT ... ON CONFLICT (jurisdiction_id, policy_external_id) DO UPDATEemmds_policies. A colunaraw_jsonarmazena o payload parseado para o visualizador de diff. - Regras: delete-then-insert contra
mds_policy_rulespara as politicas afetadas (regras sao muito emaranhadas para upsert limpo). - Geofences: materializa linhas
policy_geofencesdas geografias referenciadas por cada regra. Uma linha por(rule_id, geography_id). O campogeojsonarmazena a Feature resolvida; a coluna PostGISgeometryatualmente fica nula e e populada pelo checkerpointInPolygonem JS (veja Prioridade de Geofences Empilhados e as notas de implementacao). - Espelhamento em
zones: para compatibilidade retroativa com o motor de enforcement de zona existente, cada policy geofence tambem escreve uma linha na tabelazoneslegada marcada comsource = 'city',source_jurisdiction_idesource_policy_rule_id. Um indice parcial unico emsource_policy_rule_idtorna 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:
| Coluna | Conteudo |
|---|---|
id | UUID para o run |
jurisdiction_id | FK |
applied_at | Timestamp |
feed_hash_before | sha256 do ultimo payload conhecido |
feed_hash_after | sha256 do novo payload |
diff | JSON de {added: [...], removed: [...], modified: [...]} |
errors | Erros Zod, geografias nao resolvidas etc. |
status | success / 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 estado | Gatilho |
|---|---|
pending -> active | start_date <= now() AND status = 'pending' |
active -> expired | end_date IS NOT NULL AND end_date <= now() AND status = 'active' |
pending -> superseded | Uma 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_dateou 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
- Prioridade de Geofences Empilhados -- a escada de prioridade.
- Aplicacao de Velocidade em Tempo Real -- o que acontece na ativacao de regra.
- Solucao de Problemas -- quando polls ou ativacoes nao disparam.