teqo-backup/tests/int/municipalityPageData.int.spec.ts
Francisco Ignácio Fontoura Solla 51c13153fc E9: Fila de alocação (déficit como default do staff, frescor, coluna da vergonha)
A lista de municípios vira a fila de decisão, sem rota nova, sem coluna nova
e sem migration.

Fase 0 corrige a meta sintética do E8, que repartia a meta estadual
proporcionalmente só ao teto do campo e ignorava a votação própria (meta 2.911
para Vitória da Conquista, de 5.005 votos em 2022; 813 para Campo Formoso, de
47). `deriveSuggestedGoalsByScenario` substitui `decomposeStateGoal` e ancora a
meta na votação de 2022 por cenário — pessimista = base × (1 − margin), central
= base, otimista = base × (stateGoal ÷ Σ base), com clamp. `goalCoverage` passa
a receber `SuggestedGoalByScenario`, deixando `expectedVotes[S] ??
suggestedGoal[S]` simétrico. Só descrições do global mudaram.

Fase 1 adiciona frescor e ordenações: `declaredAt`/`estimatedAt` no select do
agregado de pledges (`lastPledgeAt`), `lastSignalAt` no view model via
`resolveMunicipalityLastSignalAt` (mesmo helper para o comparador do servidor e
a célula), e as sort keys `deficit` (novo default do staff) e `frescor`,
derivadas em memória no cenário central.

Fase 2 dobra os sinais nas células existentes: "há N dias" na coluna de última
atualização (frio ≥ 21 dias, em âmbar — vermelho já é da prioridade), badge
"sem responsável" em prioridade alta sem assessor, coluna da vergonha no
detalhe da métrica de assessoria, tooltip de cobertura nomeando o cenário e
copy Praças→Municípios.

Cortes: votos em jogo → B13, LQ/captura → E10, coluna dedicada de déficit
desnecessária.
2026-07-25 02:24:17 -03:00

315 lines
11 KiB
TypeScript

// @vitest-environment node
import type { Payload } from 'payload'
import { getPayload } from 'payload'
import { beforeAll, describe, expect, it } from 'vitest'
import config from '@/payload.config'
import { loadMunicipalityListPageBundle } from '@/utilities/municipalityPageData'
import {
aggregatePledgesByMunicipality,
rollupMunicipalityStaffVotes,
} from '@/utilities/votePledgeData'
import { installCampaignFixtures } from '../helpers/campaignFixtures'
let payload: Payload
const campaignFixtures = installCampaignFixtures({
getPayload: () => payload,
setPayload: (nextPayload) => {
payload = nextPayload
},
})
describe('loadMunicipalityListPageBundle', () => {
beforeAll(async () => {
payload = await getPayload({ config: await config })
})
it('returns null overview when the filtered municipality set is empty', async () => {
const coordinator = await campaignFixtures().createCampaignUser('coordinator')
const bundle = await loadMunicipalityListPageBundle(payload, coordinator, { q: 'zzznomatch' })
expect(bundle.municipalities).toHaveLength(0)
expect(bundle.overview).toBeNull()
})
it('rolls up staffVoteTotal from expectedVotes when no pledge overrides apply', async () => {
const fixtures = campaignFixtures()
const coordinator = await fixtures.createCampaignUser('coordinator')
const municipality = await fixtures.getMunicipality()
fixtures.touchMunicipality(municipality.id)
await payload.update({
collection: 'municipality',
id: municipality.id,
data: { expectedVotes: { pessimistic: null, central: 1_500, optimistic: null } },
depth: 0,
overrideAccess: true,
})
const contact = await fixtures.createContact()
const leadership = await fixtures.createLeadership({
contact: contact.id,
municipalities: [municipality.id],
supportStatus: 'engajado',
})
await fixtures.createVotePledge({
leadership: leadership.id,
municipality: municipality.id,
declaredVotes: 80,
estimatedVotes: { pessimistic: null, central: 120, optimistic: null },
})
const refreshedMunicipality = await payload.findByID({
collection: 'municipality',
id: municipality.id,
depth: 0,
overrideAccess: true,
})
const aggregates = await aggregatePledgesByMunicipality(payload, [municipality.id])
const rollup = rollupMunicipalityStaffVotes([refreshedMunicipality], aggregates)
expect(rollup.staffVoteTotal).toBe(1_500)
const bundle = await loadMunicipalityListPageBundle(payload, coordinator, {
q: municipality.name,
})
expect(bundle.overview).not.toBeNull()
expect(bundle.overview!.staffVoteTotalByScenario.central).toBeGreaterThanOrEqual(1_500)
const row = bundle.municipalities.find((item) => item.slug === municipality.slug)
expect(row?.votePosition2022).not.toBeNull()
expect(row!.votePosition2022!.totalUnits).toBe(435)
expect(row!.votePosition2022!.rank).toBeGreaterThanOrEqual(1)
})
it('orders the filtered set by 2022 votes when sort=votos', async () => {
const fixtures = campaignFixtures()
const coordinator = await fixtures.createCampaignUser('coordinator')
const first = await fixtures.getMunicipality()
const second = await fixtures.getMunicipality()
fixtures.touchMunicipality(first.id)
fixtures.touchMunicipality(second.id)
const marker = `a11-votos-${Date.now()}`
await payload.update({
collection: 'municipality',
id: first.id,
data: { name: `${marker}-alpha` },
depth: 0,
overrideAccess: true,
})
await payload.update({
collection: 'municipality',
id: second.id,
data: { name: `${marker}-beta` },
depth: 0,
overrideAccess: true,
})
const bundle = await loadMunicipalityListPageBundle(payload, coordinator, {
q: marker,
sort: 'votos',
})
expect(bundle.municipalities.length).toBeGreaterThanOrEqual(2)
const votes = bundle.municipalities.map((row) => row.votePosition2022?.votes ?? 0)
const nonZero = votes.filter((value) => value > 0)
for (let index = 1; index < nonZero.length; index += 1) {
expect(nonZero[index - 1]!).toBeGreaterThanOrEqual(nonZero[index]!)
}
// Zeros (if any) must trail the non-zero block.
const firstZero = votes.findIndex((value) => value === 0)
if (firstZero >= 0) {
expect(votes.slice(firstZero).every((value) => value === 0)).toBe(true)
}
})
/**
* E9 allocation queue: the default ordering. Uses `expectedVotes` (mesa
* goals) so the deficits are pinned by the fixture instead of by whatever
* the 2022 artifact says for the allocated slugs.
*/
it('orders by uncovered deficit (default sort), biggest gap first', async () => {
const fixtures = campaignFixtures()
const coordinator = await fixtures.createCampaignUser('coordinator')
const wideGap = await fixtures.getMunicipality()
const nearlyCovered = await fixtures.getMunicipality()
const marker = `e9-deficit-${Date.now()}`
await payload.update({
collection: 'municipality',
id: wideGap.id,
data: {
name: `${marker}-wide`,
expectedVotes: { pessimistic: null, central: 5_000, optimistic: null },
},
depth: 0,
overrideAccess: true,
})
fixtures.touchMunicipality(wideGap.id)
await payload.update({
collection: 'municipality',
id: nearlyCovered.id,
data: {
name: `${marker}-near`,
expectedVotes: { pessimistic: null, central: 1_000, optimistic: null },
},
depth: 0,
overrideAccess: true,
})
fixtures.touchMunicipality(nearlyCovered.id)
const contact = await fixtures.createContact()
const leadership = await fixtures.createLeadership({
contact: contact.id,
municipalities: [nearlyCovered.id],
supportStatus: 'engajado',
})
await fixtures.createVotePledge({
leadership: leadership.id,
municipality: nearlyCovered.id,
declaredVotes: 900,
estimatedVotes: { pessimistic: null, central: 900, optimistic: null },
})
const descending = await loadMunicipalityListPageBundle(payload, coordinator, { q: marker })
expect(descending.municipalities.map((row) => row.slug)).toEqual([
wideGap.slug,
nearlyCovered.slug,
])
expect(descending.municipalities[0]!.goalCoverageByScenario.central.deficit).toBe(5_000)
expect(descending.municipalities[1]!.goalCoverageByScenario.central.deficit).toBe(100)
const ascending = await loadMunicipalityListPageBundle(payload, coordinator, {
q: marker,
sort: 'deficit',
dir: 'asc',
})
expect(ascending.municipalities.map((row) => row.slug)).toEqual([
nearlyCovered.slug,
wideGap.slug,
])
})
it('orders by frescor with "never had a signal" ahead of the oldest signal', async () => {
const fixtures = campaignFixtures()
const coordinator = await fixtures.createCampaignUser('coordinator')
const silent = await fixtures.getMunicipality()
const touched = await fixtures.getMunicipality()
const marker = `e9-frescor-${Date.now()}`
await payload.update({
collection: 'municipality',
id: silent.id,
data: { name: `${marker}-silent`, lastUpdateAt: null },
depth: 0,
overrideAccess: true,
})
fixtures.touchMunicipality(silent.id)
await payload.update({
collection: 'municipality',
id: touched.id,
data: { name: `${marker}-touched` },
depth: 0,
overrideAccess: true,
})
// The collection hook recomputes `lastUpdateAt` from this update.
await fixtures.createMunicipalityUpdate({
municipality: touched.id,
author: coordinator.id,
})
const coldestFirst = await loadMunicipalityListPageBundle(payload, coordinator, {
q: marker,
sort: 'frescor',
})
expect(coldestFirst.municipalities.map((row) => row.slug)).toEqual([silent.slug, touched.slug])
expect(coldestFirst.municipalities[0]!.lastSignalAt).toBeNull()
expect(coldestFirst.municipalities[1]!.lastSignalAt).not.toBeNull()
const freshestFirst = await loadMunicipalityListPageBundle(payload, coordinator, {
q: marker,
sort: 'frescor',
dir: 'asc',
})
expect(freshestFirst.municipalities.map((row) => row.slug)).toEqual([touched.slug, silent.slug])
})
it('counts a pledge date as a signal even when no staff update exists', async () => {
const fixtures = campaignFixtures()
const coordinator = await fixtures.createCampaignUser('coordinator')
const municipality = await fixtures.getMunicipality()
fixtures.touchMunicipality(municipality.id)
const marker = `e9-pledge-signal-${Date.now()}`
await payload.update({
collection: 'municipality',
id: municipality.id,
data: { name: marker, lastUpdateAt: null },
depth: 0,
overrideAccess: true,
})
const contact = await fixtures.createContact()
const leadership = await fixtures.createLeadership({
contact: contact.id,
municipalities: [municipality.id],
supportStatus: 'engajado',
})
// `declaredAt` is stamped by the collection's own beforeChange hook, so the
// fixture cannot pin it — read it back and assert the signal matches.
const pledge = await fixtures.createVotePledge({
leadership: leadership.id,
municipality: municipality.id,
declaredVotes: 50,
})
expect(pledge.declaredAt).toBeTruthy()
const bundle = await loadMunicipalityListPageBundle(payload, coordinator, { q: marker })
const row = bundle.municipalities.find((item) => item.slug === municipality.slug)
expect(row?.lastUpdateAt).toBeNull()
expect(row?.lastSignalAt).toBe(pledge.declaredAt)
expect(row?.pledges.lastPledgeAt).toBe(pledge.declaredAt)
})
it('keeps advisor access and applies URL filters on top', async () => {
const fixtures = campaignFixtures()
const advisor = await fixtures.createCampaignUser('advisor')
const administered = await fixtures.getMunicipality()
await fixtures.assignMunicipalityAdvisors(administered.id, [advisor.id])
const included = await loadMunicipalityListPageBundle(payload, advisor, {
q: administered.name,
})
expect(included.overview).not.toBeNull()
expect(included.municipalities.some((row) => row.slug === administered.slug)).toBe(true)
const excluded =
administered.kind === 'zona'
? await loadMunicipalityListPageBundle(payload, advisor, { kind: 'municipio' })
: await loadMunicipalityListPageBundle(payload, advisor, { kind: 'zona' })
expect(excluded.overview).toBeNull()
expect(excluded.municipalities).toHaveLength(0)
})
it('returns an empty bundle for leaders (municipality list lockdown)', async () => {
const fixtures = campaignFixtures()
const municipality = await fixtures.getMunicipality()
const leader = await fixtures.createCampaignUser('leader')
const contact = await fixtures.createContact()
await fixtures.createLeadership({
contact: contact.id,
municipalities: [municipality.id],
user: leader.id,
supportStatus: 'engajado',
})
const bundle = await loadMunicipalityListPageBundle(payload, leader, { q: municipality.name })
expect(bundle.overview).toBeNull()
expect(bundle.municipalities).toHaveLength(0)
expect(bundle.scopeTotal).toBe(0)
})
})