teqo/src/utilities/supporterListOverviewAggregate.ts
Francisco Ignácio Fontoura Solla 9ccfb3b704 Simplify C6 supporter paths and register C8 scale/DRY follow-ups.
Apply the /simplify cleanup to the C6 commit (identity maps, dead branches,
relationshipId reuse, native base64url, rowCount fix, canManageCampaignUsers
reuse) and register the unimplemented debits as roadmap item C8 with its plan.
2026-07-18 22:34:55 -03:00

147 lines
4.5 KiB
TypeScript

import 'server-only'
import { sql } from '@payloadcms/db-postgres'
import type { Payload } from 'payload'
import type { CampaignUser } from '@/payload-types'
import { isCampaignGeneral } from '@/utilities/campaignAccess'
import { normalizeBrazilianPhone } from '@/utilities/phone'
import type { SupporterListState } from '@/utilities/supporterUi'
import type { SupporterListOverviewViewModel } from '@/utilities/supporterViewModels'
type AccessConstraint =
| { kind: 'all' }
| { kind: 'none' }
| { kind: 'nucleusSet'; ids: number[] }
type PostgresDb = {
execute: (query: ReturnType<typeof sql>) => Promise<unknown>
}
const resultRows = (result: unknown): Array<Record<string, unknown>> => {
if (Array.isArray(result)) return result as Array<Record<string, unknown>>
if (
typeof result === 'object' &&
result !== null &&
'rows' in result &&
Array.isArray((result as { rows: unknown }).rows)
) {
return (result as { rows: Array<Record<string, unknown>> }).rows
}
return []
}
const resolveAccessConstraint = async (
payload: Pick<Payload, 'find'>,
user: CampaignUser,
): Promise<AccessConstraint> => {
if (isCampaignGeneral(user)) return { kind: 'all' }
if (user.role !== 'coordenador') return { kind: 'none' }
const result = await payload.find({
collection: 'electoralNucleus',
where: { coordinators: { contains: user.id } },
depth: 0,
pagination: false,
overrideAccess: true,
})
const ids = result.docs.map((doc) => doc.id)
return { kind: 'nucleusSet', ids }
}
const buildAggregateSql = (
state: SupporterListState,
access: AccessConstraint,
): ReturnType<typeof sql> => {
const conditions: ReturnType<typeof sql>[] = []
const needsContactJoin = Boolean(state.q || state.city)
if (access.kind === 'nucleusSet') {
if (access.ids.length === 0) {
// No accessible nuclei → no rows. Keep a trivially-false condition.
conditions.push(sql`FALSE`)
} else {
conditions.push(
sql`"supporter"."nucleus_id" IN (${sql.join(
access.ids.map((id) => sql`${id}`),
sql`, `,
)})`,
)
}
}
if (state.voteIntention) {
conditions.push(sql`"supporter"."vote_intention" = ${state.voteIntention}`)
}
if (state.nucleus) {
conditions.push(sql`"supporter"."nucleus_id" = ${state.nucleus}`)
}
if (state.city) {
conditions.push(sql`"contact"."city" = ${state.city}`)
}
if (state.q) {
const searchTerms: ReturnType<typeof sql>[] = [
sql`"contact"."name" ILIKE ${`%${state.q}%`}`,
sql`"contact"."city" ILIKE ${`%${state.q}%`}`,
]
const normalizedPhone = normalizeBrazilianPhone(state.q)
if (normalizedPhone) {
searchTerms.push(sql`"contact"."phone" = ${normalizedPhone}`)
} else if (/\d/.test(state.q)) {
const digits = state.q.replace(/\D/g, '')
if (digits) searchTerms.push(sql`"contact"."phone" ILIKE ${`%${digits}%`}`)
}
conditions.push(sql`(${sql.join(searchTerms, sql` OR `)})`)
}
const whereClause =
conditions.length > 0 ? sql` WHERE ${sql.join(conditions, sql` AND `)}` : sql``
const fromClause = needsContactJoin
? sql`FROM "supporter" LEFT JOIN "contact" ON "contact"."id" = "supporter"."contact_id"`
: sql`FROM "supporter"`
return sql`
SELECT
COUNT(*) AS "total",
COUNT(*) FILTER (WHERE "supporter"."vote_intention" IN ('certo', 'tende_a_certo')) AS "certo_and_tende",
COUNT(*) FILTER (WHERE "supporter"."vote_intention" = 'indeciso') AS "indeciso"
${fromClause}${whereClause}
`
}
export const computeSupporterListOverviewAggregate = async (
payload: Payload,
user: CampaignUser,
state: SupporterListState,
): Promise<SupporterListOverviewViewModel | null> => {
const access = await resolveAccessConstraint(payload, user)
if (access.kind === 'none') return null
if (payload.db.name !== 'postgres') {
throw new Error('O overview de apoiadores exige o adaptador PostgreSQL.')
}
const database = payload.db as unknown as { drizzle?: PostgresDb }
const drizzle = database.drizzle
if (!drizzle || typeof drizzle.execute !== 'function') {
throw new Error('A sessão PostgreSQL do overview de apoiadores não está disponível.')
}
const result = await drizzle.execute(buildAggregateSql(state, access))
const row = resultRows(result)[0]
if (!row) return null
const total = Number(row.total)
if (!Number.isSafeInteger(total) || total === 0) return null
return {
total,
certoAndTende: Number(row.certo_and_tende ?? 0),
indeciso: Number(row.indeciso ?? 0),
}
}