teqo/src/utilities/votePledgeData.ts
Francisco Ignácio Fontoura Solla 31fcc20ffd E9 /simplify: consolidate duplicated helpers, dead passthrough, comment drift
Cross-checked by parallel code-quality/performance/reuse reviewers against
the E9 commit. Behavior-preserving:

- laterTimestamp/resolveMunicipalityLastSignalAt were the same "latest of
  two ISO timestamps" written twice; both now delegate to a new
  latestIsoTimestamp in campaignTime.ts, which documents the fixed-width
  UTC invariant that makes the string compare sound.
- Hoisted the frescor comparator's Date.now() out of the per-comparison
  accessor so the sort key stops drifting mid-sort.
- Deleted the unread baseTotal/growthFactor/belowBase passthrough from the
  statewide goal entry point and fixed a comment citing a symbol
  (goalDecompositionBelowBase) that never existed; renamed
  computeStatewideGoalDecomposition -> computeStatewideSuggestedGoals now
  that nothing is proportionally decomposed.
- Collapsed the advisor badge's two copies (desktop table vs mobile card)
  into one AdvisorStatusBadge; they had quietly diverged on which icon
  renders for the "no advisor" state.
- Renamed the sort-menu's stale "Cobertura" label for advisor coverage to
  "Assessoria" (E8 already renamed the list column for the same reason);
  derived the sort-key set from the label record instead of a third
  parallel list.
- Consolidated three zeroed-scenario-record literals into one
  zeroByVoteEstimateScenario() factory in voteEstimate.ts.
- Routed three separate votesByYear[2022] reads through one votesInYear
  helper in municipalityPotential.ts.
- Gated the goal card's empty-state copy on !usesMesaEstimate so it stops
  telling a município with an existing mesa estimate to enter one.
- Swapped a test assertion pinned to a raw Tailwind token for a stable
  data-signal="cold" hook, and tightened the "desert" test's bound to
  check against the actual growth factor (the old bound was true for
  every slug regardless of the formula).

Left alone (roadmap decision, not cleanup): sanityCheckSuggestedGoalsByTerritory
has no production caller yet (deferred to E11/E12); comment shortened
instead of deleting the dead-but-documented code.

Verified: tsc --noEmit, pnpm lint (0 warnings), 337 unit + 366 integration
tests, pnpm build all green.
2026-07-25 02:24:18 -03:00

282 lines
9.4 KiB
TypeScript

import type { Payload, PayloadRequest } from 'payload'
import type { CampaignUser, VotePledge } from '@/payload-types'
import { latestIsoTimestamp } from '@/utilities/campaignTime'
import { relationshipId, requireRelationshipId } from '@/utilities/relationship'
import {
DEFAULT_VOTE_ESTIMATE_SCENARIO,
effectivePledgeVotesForScenario,
resolveMunicipalityStaffVoteTotalForScenario,
toVoteEstimateScenarioViewModel,
VOTE_ESTIMATE_SCENARIOS,
zeroByVoteEstimateScenario,
type VoteEstimateScenario,
type VoteEstimateScenarioFields,
type VoteEstimateScenarioViewModel,
} from '@/utilities/voteEstimate'
/**
* Pledge aggregation helpers. "Effective" votes on staff surfaces are
* `estimated[S] ?? declaredVotes` per scenario; leader-facing surfaces must
* NEVER receive estimated values or effective totals derived from them.
*/
export type { VoteEstimateScenario, VoteEstimateScenarioViewModel }
export type MunicipalityPledgeAggregate = {
declaredTotal: number
effectiveByScenario: Record<VoteEstimateScenario, number>
pledgeCount: number
missingEstimateCount: number
/**
* Most recent `declaredAt`/`estimatedAt` across the município's pledges —
* E9 freshness: a commitment nobody has touched in weeks is worth less
* (`docs/research`, l. 339), so the allocation queue can order by how cold
* the signal is. `null` when no pledge carries a date.
*/
lastPledgeAt: string | null
}
export const createEmptyMunicipalityPledgeAggregate = (): MunicipalityPledgeAggregate => ({
declaredTotal: 0,
effectiveByScenario: zeroByVoteEstimateScenario(),
pledgeCount: 0,
missingEstimateCount: 0,
lastPledgeAt: null,
})
/** Read-only zero aggregate. Never mutate — use createEmptyMunicipalityPledgeAggregate() when writing. */
export const emptyMunicipalityPledgeAggregate: Readonly<MunicipalityPledgeAggregate> =
createEmptyMunicipalityPledgeAggregate()
export type MunicipalityPledgeCoverageView = {
pledgeCount: number
missingEstimateCount: number
declaredTotal: number
effectiveTotal: number
}
export const toMunicipalityPledgeCoverageView = (
aggregate: MunicipalityPledgeAggregate,
scenario: VoteEstimateScenario = DEFAULT_VOTE_ESTIMATE_SCENARIO,
): MunicipalityPledgeCoverageView | null => {
if (aggregate.pledgeCount === 0) return null
return {
pledgeCount: aggregate.pledgeCount,
missingEstimateCount: aggregate.missingEstimateCount,
declaredTotal: aggregate.declaredTotal,
effectiveTotal: aggregate.effectiveByScenario[scenario],
}
}
/** Staff-facing municipality total: manual expected votes override pledge aggregate when set. */
export const resolveMunicipalityStaffVoteTotal = (
expectedVotes: VoteEstimateScenarioFields | null | undefined,
pledgeEffectiveTotal: number,
scenario: VoteEstimateScenario = DEFAULT_VOTE_ESTIMATE_SCENARIO,
): number =>
resolveMunicipalityStaffVoteTotalForScenario(expectedVotes, pledgeEffectiveTotal, scenario)
export const aggregateMunicipalityPledgesFromRows = (
rows: ReadonlyArray<{
declaredVotes: number
estimatedVotes?: VoteEstimateScenarioFields | null
declaredAt?: string | null
estimatedAt?: string | null
}>,
): MunicipalityPledgeAggregate => {
const aggregate = createEmptyMunicipalityPledgeAggregate()
for (const row of rows) {
const declared = row.declaredVotes ?? 0
aggregate.declaredTotal += declared
for (const scenario of VOTE_ESTIMATE_SCENARIOS) {
aggregate.effectiveByScenario[scenario] += effectivePledgeVotesForScenario(
declared,
row.estimatedVotes,
scenario,
)
}
aggregate.pledgeCount += 1
if (!pledgeHasAnyEstimate(row.estimatedVotes)) aggregate.missingEstimateCount += 1
aggregate.lastPledgeAt = latestIsoTimestamp(
aggregate.lastPledgeAt,
latestIsoTimestamp(row.declaredAt, row.estimatedAt),
)
}
return aggregate
}
export type MunicipalityStaffVoteRollup = {
staffVoteTotal: number
staffVoteTotalByScenario: Record<VoteEstimateScenario, number>
declaredVotesTotal: number
pledgeCount: number
missingEstimateCount: number
}
export const rollupMunicipalityStaffVotes = (
municipalities: ReadonlyArray<{ id: number; expectedVotes?: VoteEstimateScenarioFields | null }>,
pledgeAggregates: Map<number, MunicipalityPledgeAggregate>,
scenario: VoteEstimateScenario = DEFAULT_VOTE_ESTIMATE_SCENARIO,
): MunicipalityStaffVoteRollup => {
let staffVoteTotal = 0
const staffVoteTotalByScenario = zeroByVoteEstimateScenario()
let declaredVotesTotal = 0
let pledgeCount = 0
let missingEstimateCount = 0
for (const municipality of municipalities) {
const aggregate = pledgeAggregates.get(municipality.id) ?? emptyMunicipalityPledgeAggregate
for (const key of VOTE_ESTIMATE_SCENARIOS) {
staffVoteTotalByScenario[key] += resolveMunicipalityStaffVoteTotal(
municipality.expectedVotes,
aggregate.effectiveByScenario[key],
key,
)
}
staffVoteTotal += resolveMunicipalityStaffVoteTotal(
municipality.expectedVotes,
aggregate.effectiveByScenario[scenario],
scenario,
)
declaredVotesTotal += aggregate.declaredTotal
pledgeCount += aggregate.pledgeCount
missingEstimateCount += aggregate.missingEstimateCount
}
return {
staffVoteTotal,
staffVoteTotalByScenario,
declaredVotesTotal,
pledgeCount,
missingEstimateCount,
}
}
const pledgeHasAnyEstimate = (estimated: VoteEstimateScenarioFields | null | undefined): boolean =>
estimated?.pessimistic != null || estimated?.central != null || estimated?.optimistic != null
/**
* Staff-only aggregate over an already access-checked municipality id set.
* Intentional admin bypass: callers pass municipality ids the actor may read.
*/
export const aggregatePledgesByMunicipality = async (
payload: Pick<Payload, 'find'>,
municipalityIDs: number[],
req?: PayloadRequest,
): Promise<Map<number, MunicipalityPledgeAggregate>> => {
const aggregates = new Map<number, MunicipalityPledgeAggregate>()
if (municipalityIDs.length === 0) return aggregates
const result = await payload.find({
collection: 'votePledge',
where: { municipality: { in: municipalityIDs } },
depth: 0,
limit: 0,
pagination: false,
select: {
municipality: true,
declaredVotes: true,
estimatedVotes: {
pessimistic: true,
central: true,
optimistic: true,
},
// E9 freshness (`lastPledgeAt`) — the queue orders by how cold the signal is.
declaredAt: true,
estimatedAt: true,
},
overrideAccess: true,
...(req ? { req } : {}),
})
for (const doc of result.docs) {
// `select` narrows the runtime shape, not the static type Payload returns.
const pledge = doc as VotePledge
const municipalityID = relationshipId(pledge.municipality)
if (municipalityID === null) continue
const declared = pledge.declaredVotes ?? 0
const estimated = pledge.estimatedVotes
const current = aggregates.get(municipalityID) ?? createEmptyMunicipalityPledgeAggregate()
current.declaredTotal += declared
for (const scenario of VOTE_ESTIMATE_SCENARIOS) {
current.effectiveByScenario[scenario] += effectivePledgeVotesForScenario(
declared,
estimated,
scenario,
)
}
current.pledgeCount += 1
if (!pledgeHasAnyEstimate(estimated)) current.missingEstimateCount += 1
current.lastPledgeAt = latestIsoTimestamp(
current.lastPledgeAt,
latestIsoTimestamp(pledge.declaredAt, pledge.estimatedAt),
)
aggregates.set(municipalityID, current)
}
return aggregates
}
export type StaffPledgeRow = {
id: number
leadershipID: number
contactName: string
declaredVotes: number
declaredAt: string | null
estimatedVotes: VoteEstimateScenarioViewModel
estimateNote: string | null
estimatedAt: string | null
}
/** Staff view of a municipality's pledges with leadership contact names. */
export const loadMunicipalityPledges = async (
payload: Payload,
user: CampaignUser,
municipalityID: number,
): Promise<StaffPledgeRow[]> => {
const pledges = await payload.find({
collection: 'votePledge',
where: { municipality: { equals: municipalityID } },
depth: 0,
limit: 0,
pagination: false,
sort: '-declaredVotes',
user,
overrideAccess: false,
})
if (pledges.docs.length === 0) return []
const leadershipIDs = [
...new Set(pledges.docs.map((pledge) => requireRelationshipId(pledge.leadership))),
]
const leaderships = await payload.find({
collection: 'leadership',
where: { id: { in: leadershipIDs } },
depth: 1,
limit: 0,
pagination: false,
select: { contact: true },
overrideAccess: true,
})
const nameByLeadership = new Map<number, string>()
for (const leadership of leaderships.docs) {
const contact = leadership.contact
const name =
typeof contact === 'object' && contact !== null && 'name' in contact
? (contact.name as string)
: 'Contato'
nameByLeadership.set(leadership.id, name)
}
return pledges.docs.map((pledge) => ({
id: pledge.id,
leadershipID: requireRelationshipId(pledge.leadership),
contactName: nameByLeadership.get(requireRelationshipId(pledge.leadership)) ?? 'Contato',
declaredVotes: pledge.declaredVotes,
declaredAt: pledge.declaredAt ?? null,
estimatedVotes: toVoteEstimateScenarioViewModel(pledge.estimatedVotes),
estimateNote: pledge.estimateNote ?? null,
estimatedAt: pledge.estimatedAt ?? null,
}))
}