teqo-backup/src/collections/VotePledge.ts
Francisco Ignácio Fontoura Solla cf13c441aa Remodel /campanha around predefined Praças replacing electoral nuclei.
436 seeded Praças (municipality-based; per-TSE-zone in Salvador/Camaçari) become
the operational unit; roles renamed to coordinator/advisor/leader with advisors
scoped to administered Praças. Leadership is one record per person across many
Praças/organizations; vote pledges split leader-declared vs staff-estimated
(estimates never serialized to leaders). Adds organizations, demand workflow
with staff-only cost/receipts, action-plan results with deputy presence, manual
political trend, multi-candidate comparison and the diverging red/white/blue
map mode. Migration 20260721_020109_remodel_plazas deliberately resets the
campaign vertical in production (public site untouched); test suite ported to
the new model (unit 187 / int 306 / e2e 9 green).
2026-07-21 02:28:00 -03:00

214 lines
5.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 type { CollectionBeforeChangeHook, CollectionConfig } from 'payload'
import { APIError } from 'payload'
import {
canCreateVotePledge,
canDeleteVotePledge,
canManageCampaignStaffField,
canReadCampaignStaffField,
canReadVotePledge,
canSetCampaignSystemField,
canUpdateVotePledge,
} from '@/utilities/campaignAccess'
import { relationshipId } from '@/utilities/relationship'
/**
* Vote pledge per leadership × plaza. The leader DECLARES how many votes they
* are bringing (`declaredVotes` — the only number they ever see); staff record
* their own ESTIMATE of the real value (`estimatedVotes`), which is never
* serialized to the leader (field access denies read). Plaza aggregates use
* `estimatedVotes ?? declaredVotes` on staff surfaces only.
*/
const validatePledgeIntegrity: CollectionBeforeChangeHook = async ({
data,
operation,
originalDoc,
req,
}) => {
const leadershipID = relationshipId(data.leadership ?? originalDoc?.leadership)
const plazaID = relationshipId(data.plaza ?? originalDoc?.plaza)
if (!leadershipID || !plazaID) {
throw new APIError('Informe a liderança e a Praça do compromisso de votos.', 400)
}
if (operation === 'update') {
const previousLeadership = relationshipId(originalDoc?.leadership)
const previousPlaza = relationshipId(originalDoc?.plaza)
if (
(data.leadership !== undefined && leadershipID !== previousLeadership) ||
(data.plaza !== undefined && plazaID !== previousPlaza)
) {
throw new APIError('A liderança e a Praça do compromisso não podem ser alteradas.', 409)
}
}
const leadership = await req.payload.findByID({
collection: 'leadership',
id: leadershipID,
depth: 0,
overrideAccess: true,
req,
})
const linkedPlazaIDs = (Array.isArray(leadership.plazas) ? leadership.plazas : [])
.map(relationshipId)
.filter((id): id is number => id !== null)
if (!linkedPlazaIDs.includes(plazaID)) {
throw new APIError('A liderança precisa estar vinculada à Praça para declarar votos nela.', 409)
}
return data
}
const deriveVotePledgeAudit: CollectionBeforeChangeHook = ({ data, originalDoc, req }) => {
const actorID = req.user?.collection === 'campaignUser' ? req.user.id : null
if (data.declaredVotes !== undefined && data.declaredVotes !== originalDoc?.declaredVotes) {
data.declaredAt = new Date().toISOString()
if (actorID) data.declaredBy = actorID
}
const estimateChanged =
(data.estimatedVotes !== undefined && data.estimatedVotes !== originalDoc?.estimatedVotes) ||
(data.estimateNote !== undefined && data.estimateNote !== originalDoc?.estimateNote)
if (estimateChanged) {
data.estimatedAt = new Date().toISOString()
if (actorID) data.estimatedBy = actorID
}
return data
}
export const VotePledge: CollectionConfig = {
slug: 'votePledge',
labels: {
singular: 'Votos declarados',
plural: 'Votos declarados',
},
admin: {
group: 'Campanha',
useAsTitle: 'id',
defaultColumns: ['leadership', 'plaza', 'declaredVotes', 'estimatedVotes', 'updatedAt'],
},
access: {
create: canCreateVotePledge,
read: canReadVotePledge,
update: canUpdateVotePledge,
delete: canDeleteVotePledge,
},
indexes: [
{
fields: ['leadership', 'plaza'],
unique: true,
},
],
hooks: {
beforeChange: [validatePledgeIntegrity, deriveVotePledgeAudit],
},
fields: [
{
name: 'leadership',
type: 'relationship',
relationTo: 'leadership',
label: 'Liderança',
required: true,
index: true,
},
{
name: 'plaza',
type: 'relationship',
relationTo: 'plaza',
label: 'Praça',
required: true,
index: true,
},
{
name: 'declaredVotes',
type: 'number',
label: 'Votos declarados pela liderança',
required: true,
min: 0,
index: true,
},
{
name: 'declaredAt',
type: 'date',
label: 'Declarado em',
admin: {
readOnly: true,
},
access: {
create: canSetCampaignSystemField,
update: canSetCampaignSystemField,
},
},
{
name: 'declaredBy',
type: 'relationship',
relationTo: 'campaignUser',
label: 'Declarado por',
admin: {
readOnly: true,
},
access: {
create: canSetCampaignSystemField,
update: canSetCampaignSystemField,
},
},
{
name: 'estimatedVotes',
type: 'number',
label: 'Votos estimados pelo assessor',
min: 0,
index: true,
admin: {
description: 'Estimativa interna do valor real. A liderança nunca vê este número.',
},
access: {
create: canManageCampaignStaffField,
read: canReadCampaignStaffField,
update: canManageCampaignStaffField,
},
},
{
name: 'estimateNote',
type: 'textarea',
label: 'Justificativa da estimativa',
maxLength: 1000,
access: {
create: canManageCampaignStaffField,
read: canReadCampaignStaffField,
update: canManageCampaignStaffField,
},
},
{
name: 'estimatedBy',
type: 'relationship',
relationTo: 'campaignUser',
label: 'Estimado por',
admin: {
readOnly: true,
},
access: {
create: canSetCampaignSystemField,
read: canReadCampaignStaffField,
update: canSetCampaignSystemField,
},
},
{
name: 'estimatedAt',
type: 'date',
label: 'Estimado em',
admin: {
readOnly: true,
},
access: {
create: canSetCampaignSystemField,
read: canReadCampaignStaffField,
update: canSetCampaignSystemField,
},
},
],
}