teqo-backup/src/lib/electionResults.ts
Francisco Ignácio Fontoura Solla 4806c417c8 Engineering Pass 3: consolidate patterns and lock them with guards.
Fold duplicated campanha access, loaders, list/filter/form shells, and scripts into shared modules; reorganize utilities by domain; stabilize e2e; and document prevention-first audit workflow so the same smells cannot creep back in.
2026-07-29 06:11:29 -03:00

323 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { bahiaMunicipalities } from '@/lib/bahiaTerritories'
import { ELECTION_YEAR_2014, ELECTION_YEAR_2018, ELECTION_YEAR_2022 } from '@/lib/electionYears'
import { resolveMunicipalityName } from '@/lib/municipalityNameAliases'
export { ELECTION_YEAR_2014, ELECTION_YEAR_2018, ELECTION_YEAR_2022 } from '@/lib/electionYears'
/** Years loaded for prior-cycle comparison before 2022 (detail baseline query). */
const HISTORICAL_PRIOR_SERIES_YEARS = [ELECTION_YEAR_2014, ELECTION_YEAR_2018] as const
/** Years shown in the federal candidate vote series (E2). */
export const HISTORICAL_SERIES_YEARS = [
...HISTORICAL_PRIOR_SERIES_YEARS,
ELECTION_YEAR_2022,
] as const
export const ELECTION_OFFICE_OPTIONS = [
{ label: 'Presidente', value: 'presidente' },
{ label: 'Governador', value: 'governador' },
{ label: 'Deputado federal', value: 'deputado_federal' },
{ label: 'Deputado estadual', value: 'deputado_estadual' },
] as const
export const ELECTION_OFFICES = ELECTION_OFFICE_OPTIONS.map((o) => o.value)
export type ElectionOffice = (typeof ELECTION_OFFICES)[number]
export const FEDERAL_DEPUTY_OFFICE = 'deputado_federal' as const satisfies ElectionOffice
export const ELECTION_TURN_OPTIONS = [
{ label: '1º turno', value: '1' },
{ label: '2º turno', value: '2' },
] as const
const ELECTION_TURNS = ELECTION_TURN_OPTIONS.map((o) => o.value)
export type ElectionTurn = (typeof ELECTION_TURNS)[number]
export const ELECTION_VOTE_TYPE_OPTIONS = [
{ label: 'Nominal', value: 'nominal' },
{ label: 'Legenda', value: 'legenda' },
] as const
type ElectionVoteType = (typeof ELECTION_VOTE_TYPE_OPTIONS)[number]['value']
export const ELECTION_ELECTED_BY_OPTIONS = [
{ label: 'Quociente partidário', value: 'QP' },
{ label: 'Média', value: 'média' },
{ label: '2º turno', value: '2º turno' },
] as const
type ElectionElectedBy = (typeof ELECTION_ELECTED_BY_OPTIONS)[number]['value']
export const ELECTION_RUNNING_AGAIN_OPTIONS = [
{ label: 'Sim', value: 'sim' },
{ label: 'Não', value: 'nao' },
{ label: 'Desconhecido', value: 'desconhecido' },
] as const
type ElectionRunningAgain = (typeof ELECTION_RUNNING_AGAIN_OPTIONS)[number]['value']
/**
* Campaign ticket for the electoral baseline, keyed by role.
* Swap this block (and the election year) for a future cycle — aggregation/UI stay put.
* 2022: Jorge Solla (dep. federal BA nº 1313) / Lula (pres. nº 13) / Jerônimo (gov. nº 13).
*/
export const BASELINE_TICKET_2022 = {
candidate: {
candidateNumber: 1313,
office: 'deputado_federal',
name: 'Jorge Solla',
party: 'PT',
officeLabel: 'Dep. Federal',
},
president: { candidateNumber: 13, name: 'Lula', party: 'PT' },
governor: { candidateNumber: 13, name: 'Jerônimo Rodrigues', party: 'PT' },
} as const
/** TSE CD_CARGO → office enum used in collections. */
const TSE_CARGO_TO_OFFICE: Readonly<Record<string, ElectionOffice>> = {
'1': 'presidente',
'3': 'governador',
'6': 'deputado_federal',
'7': 'deputado_estadual',
}
export type ParsedElectedStatus = {
elected: boolean
electedBy: ElectionElectedBy | null
}
/**
* Map TSE DS_SIT_TOT_TURNO to elected flag + electedBy.
* Values observed in 2022 consulta_cand / votacao datasets.
* When `turn` is `2` and the candidate is elected, electedBy defaults to `2º turno`
* (TSE often writes plain "ELEITO" on the second-turn file).
*/
export const parseElectedStatus = (
dsSitTotTurno: string | null | undefined,
turn?: ElectionTurn | null,
): ParsedElectedStatus => {
const value = (dsSitTotTurno ?? '').trim().toUpperCase()
if (!value || value === '#NULO#' || value === 'NÃO ELEITO' || value === 'NAO ELEITO') {
return { elected: false, electedBy: null }
}
if (value.includes('2º TURNO') || value.includes('2O TURNO') || value.includes('2° TURNO')) {
// "ELEITO … 2º TURNO" counts as elected; bare "2º TURNO" means advanced only.
if (value.startsWith('ELEITO')) {
return { elected: true, electedBy: '2º turno' }
}
return { elected: false, electedBy: null }
}
if (value.includes('MÉDIA') || value.includes('MEDIA')) {
return { elected: true, electedBy: 'média' }
}
if (value.startsWith('ELEITO')) {
return { elected: true, electedBy: turn === '2' ? '2º turno' : 'QP' }
}
return { elected: false, electedBy: null }
}
/** Fold accents and case for municipality matching. */
export const normalizeMunicipalityKey = (value: string): string =>
value
.normalize('NFD')
.replace(/\p{M}/gu, '')
.replace(/['’`]/g, '')
.replace(/\s+/g, ' ')
.trim()
.toUpperCase()
/**
* Curated variant spellings (TSE open data, typed CSVs) live in
* `@/lib/municipalityNameAliases` — THE single fold both this pipeline and the
* supporter import resolve through (P3-H). Aliases point at the catalog at
* module load there, so this file no longer validates its own table.
*/
export class UnknownMunicipalityError extends Error {
constructor(public readonly tseName: string) {
super(`Município TSE sem mapeamento canônico: "${tseName}"`)
this.name = 'UnknownMunicipalityError'
}
}
/** Cache raw TSE NM_MUNICIPIO → canonical (millions of repeated lookups on seed). */
const municipalityResolveCache = new Map<string, string>()
/**
* Map TSE NM_MUNICIPIO (usually ALL CAPS) to the canonical Bahia municipality name
* used by bahiaTerritories / municipality.city. Fails closed on unknown names.
*/
export const canonicalizeMunicipalityName = (tseMunicipalityName: string): string => {
const cached = municipalityResolveCache.get(tseMunicipalityName)
if (cached) return cached
const canonical = resolveMunicipalityName(tseMunicipalityName)
if (!canonical) {
throw new UnknownMunicipalityError(tseMunicipalityName)
}
municipalityResolveCache.set(tseMunicipalityName, canonical)
return canonical
}
const isElectionTurn = (value: string): value is ElectionTurn =>
(ELECTION_TURNS as readonly string[]).includes(value)
export type CandidateVoteRow = {
year: number
office: ElectionOffice
turn: ElectionTurn
state: string
cityCode: string
cityName: string
zoneNumber: number
candidateNumber: number
candidateName: string
coalition: string | null
party: string | null
voteType: ElectionVoteType
votes: number
}
/** Winner fields as stored on electionTally (null when no nominal votes in that cell). */
type TallyWinner = {
winnerCandidateNumber: number | null
winnerCandidateName: string | null
winnerVotes: number | null
winnerCoalition: string | null
winnerParty: string | null
}
type ResolvedWinner = {
winnerCandidateNumber: number
winnerCandidateName: string
winnerVotes: number
winnerCoalition: string | null
winnerParty: string | null
}
const EMPTY_WINNER: TallyWinner = {
winnerCandidateNumber: null,
winnerCandidateName: null,
winnerVotes: null,
winnerCoalition: null,
winnerParty: null,
}
export const winnerKey = (
cityCode: string,
zoneNumber: number,
office: ElectionOffice,
turn: ElectionTurn,
) => `${cityCode}|${zoneNumber}|${office}|${turn}`
/**
* Compute the local winner (highest nominal votes) for each city+zone+office+turn.
* Ties: first seen candidate with the max vote count wins (stable for import order).
*/
export const computeWinnersByScope = (
votes: readonly CandidateVoteRow[],
): Map<string, ResolvedWinner> => {
const winners = new Map<string, ResolvedWinner>()
for (const row of votes) {
if (row.voteType !== 'nominal' || row.votes <= 0) continue
const key = winnerKey(row.cityCode, row.zoneNumber, row.office, row.turn)
const current = winners.get(key)
if (!current || current.winnerVotes < row.votes) {
winners.set(key, {
winnerCandidateNumber: row.candidateNumber,
winnerCandidateName: row.candidateName,
winnerVotes: row.votes,
winnerCoalition: row.coalition,
winnerParty: row.party,
})
}
}
return winners
}
export type TseDetalheApuracaoRow = {
year: number
office: ElectionOffice
turn: ElectionTurn
state: string
cityCode: string
cityName: string
zoneNumber: number
aptos: number
comparecimento: number
abstencoes: number
votosValidos: number
votosNominaisValidos: number
votosLegenda: number
votosBranco: number
votosNulo: number
votosAnulados: number
}
export type ElectionTallyRow = TseDetalheApuracaoRow & TallyWinner
export type ElectionCandidateRow = {
year: number
office: ElectionOffice
turn: ElectionTurn
state: string
candidateNumber: number
urnaName: string
completeName: string | null
party: string | null
coalition: string | null
candidateStatus: string | null
elected: boolean
electedBy: ElectionElectedBy | null
totalVotesState: number
identityKey: string
runningAgain2026: ElectionRunningAgain
}
export const mergeTallyWithWinners = (
tallies: readonly TseDetalheApuracaoRow[],
winners: ReadonlyMap<string, ResolvedWinner>,
): ElectionTallyRow[] =>
tallies.map((tally) => {
const winner =
winners.get(winnerKey(tally.cityCode, tally.zoneNumber, tally.office, tally.turn)) ??
EMPTY_WINNER
return { ...tally, ...winner }
})
/** Parse a TSE integer cell that may be "#NULO#" / empty. */
export const parseTseInt = (value: string | undefined | null, fallback = 0): number => {
if (value == null) return fallback
const trimmed = value.trim()
if (!trimmed || trimmed === '#NULO#' || trimmed === '#NE#') return fallback
const n = Number(trimmed)
return Number.isFinite(n) ? n : fallback
}
export const parseTseOptionalText = (value: string | undefined | null): string | null => {
if (value == null) return null
const trimmed = value.trim()
if (!trimmed || trimmed === '#NULO#' || trimmed === '#NE#') return null
return trimmed
}
export const officeFromTseCargo = (cdCargo: string): ElectionOffice | null => {
const office = TSE_CARGO_TO_OFFICE[cdCargo.trim()]
return office ?? null
}
export const turnFromTse = (nrTurno: string): ElectionTurn | null => {
const turn = nrTurno.trim()
return isElectionTurn(turn) ? turn : null
}
export const assertAllCanonicalMunicipalitiesResolvable = (): void => {
for (const name of bahiaMunicipalities) {
const resolved = canonicalizeMunicipalityName(name)
if (resolved !== name) {
throw new Error(`canonicalizeMunicipalityName drifted for "${name}" → "${resolved}"`)
}
}
}