mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-15 02:23:07 +00:00
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.
315 lines
11 KiB
TypeScript
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)
|
|
})
|
|
})
|