teqo-backup/src/utilities/municipality/municipalityDossierData.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

130 lines
4.7 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 'server-only'
import type { Payload } from 'payload'
import {
demographicsForCode,
type MunicipalityDemographics,
} from '@/lib/bahiaMunicipalityDemographics'
import type { Activity, CampaignUser } from '@/payload-types'
import { buildActivityListWhere } from '@/utilities/activityUi'
import {
activityListSelect,
toActivityListViewModel,
type ActivityListViewModel,
} from '@/utilities/activityViewModels'
import {
loadFreshestMunicipalityLeaderships,
type LeadershipRowViewModel,
} from '@/utilities/leadership/leadershipData'
import { municipalityElectionGeographyForSlug } from '@/utilities/municipality/municipalityElectionGeography'
import {
loadMunicipalityElectoralBaseline,
type MunicipalityElectoralBaseline,
} from '@/utilities/municipality/municipalityElectoralBaseline'
import {
loadMunicipalityGoalAccount,
type MunicipalityGoalAccount,
} from '@/utilities/municipality/municipalityGoalAccount'
import {
loadMunicipalityUpdatesFeed,
type MunicipalityUpdateViewModel,
} from '@/utilities/municipality/municipalityUpdatePageData'
import type { MunicipalityDetailViewModel } from '@/utilities/municipality/municipalityViewModels'
import { loadMunicipalityPledges } from '@/utilities/votePledgeData'
import {
aggregateMunicipalityPledgesFromRows,
type MunicipalityPledgeAggregate,
} from '@/utilities/votePledgeViews'
/**
* E16 dossiê — section caps. The dossier is a 1–2 page pre-visit read, not a
* report: each section shows at most this many rows and links to its full tab.
*/
export const DOSSIER_LEADERSHIP_LIMIT = 8
export const DOSSIER_SIGNAL_LIMIT = 5
const DOSSIER_UPCOMING_ACTIVITY_LIMIT = 3
const DOSSIER_RECENT_ACTIVITY_LIMIT = 2
export type MunicipalityDossierData = {
baseline: MunicipalityElectoralBaseline | null
goalAccount: MunicipalityGoalAccount | null
pledgeAggregate: MunicipalityPledgeAggregate
leaderships: { rows: LeadershipRowViewModel[]; totalCount: number }
signals: { rows: MunicipalityUpdateViewModel[]; totalCount: number }
upcomingActivities: ActivityListViewModel[]
recentActivities: ActivityListViewModel[]
demographics: MunicipalityDemographics | null
}
const loadDossierActivities = async (
payload: Payload,
user: CampaignUser,
municipalityID: number,
): Promise<{ upcoming: ActivityListViewModel[]; recent: ActivityListViewModel[] }> => {
const now = new Date()
const findActivities = (tab: 'proximos' | 'realizados', limit: number, sort: string) =>
payload.find({
collection: 'activity',
where: buildActivityListWhere({ page: 1, tab, municipality: municipalityID }, now),
depth: 0,
limit,
pagination: false,
sort,
select: activityListSelect,
user,
overrideAccess: false,
})
const [upcoming, recent] = await Promise.all([
findActivities('proximos', DOSSIER_UPCOMING_ACTIVITY_LIMIT, 'startAt'),
findActivities('realizados', DOSSIER_RECENT_ACTIVITY_LIMIT, '-startAt'),
])
return {
upcoming: upcoming.docs.map((activity) => toActivityListViewModel(activity as Activity)),
recent: recent.docs.map((activity) => toActivityListViewModel(activity as Activity)),
}
}
/**
* Composes the pre-visit dossier from the same loaders the detail tabs use —
* one pass, no second source of truth (decisão travada do plano E16).
*/
export const loadMunicipalityDossierData = async (
payload: Payload,
user: CampaignUser,
view: MunicipalityDetailViewModel,
): Promise<MunicipalityDossierData> => {
const geography = municipalityElectionGeographyForSlug(view.slug)
const [baseline, pledgesAndGoal, leaderships, feed, activities] = await Promise.all([
geography ? loadMunicipalityElectoralBaseline(user, geography) : null,
loadMunicipalityPledges(payload, user, view.id).then(async (pledges) => {
const pledgeAggregate = aggregateMunicipalityPledgesFromRows(pledges)
const goalAccount = view.strategy
? await loadMunicipalityGoalAccount(
payload,
user,
{ slug: view.slug, expectedVotes: view.strategy.expectedVotes },
pledgeAggregate,
)
: null
return { pledgeAggregate, goalAccount }
}),
loadFreshestMunicipalityLeaderships(payload, user, view.id, DOSSIER_LEADERSHIP_LIMIT),
loadMunicipalityUpdatesFeed(payload, user, view.id, { page: 1 }),
loadDossierActivities(payload, user, view.id),
])
return {
baseline,
goalAccount: pledgesAndGoal.goalAccount,
pledgeAggregate: pledgesAndGoal.pledgeAggregate,
leaderships,
signals: { rows: feed.updates.slice(0, DOSSIER_SIGNAL_LIMIT), totalCount: feed.totalDocs },
upcomingActivities: activities.upcoming,
recentActivities: activities.recent,
demographics: demographicsForCode(view.ibgeCode) ?? null,
}
}