teqo/src/utilities/campaignInviteRedemption.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

293 lines
9.2 KiB
TypeScript

import 'server-only'
import type { Payload } from 'payload'
import {
campaignInviteAutofillSchema,
campaignInviteLoginSchema,
type CampaignInviteAutofillInput,
type CampaignInviteLoginInput,
} from '@/lib/schemas/invite'
import type { CampaignUser, Leadership } from '@/payload-types'
import { requireLeadershipConsent, resolveInviteConsent } from '@/utilities/campaignConsent'
import {
acquireCampaignInviteAccountLocks,
acquireCampaignInviteRedemptionContactLock,
consumeCampaignInvite,
findSameContactAccountIDs,
INVALID_CAMPAIGN_INVITE_MESSAGE,
requireCampaignInvitePostgres,
} from '@/utilities/campaignInviteRepository'
import {
assertContactPhoneAvailable,
contactPhoneLockKeys,
} from '@/utilities/contactPhoneInvariant'
import type { PayloadTransactionRequest } from '@/utilities/payloadTransaction'
import { withPayloadTransaction } from '@/utilities/payloadTransaction'
import { relationshipId, requireRelationshipId } from '@/utilities/relationship'
const MISSING_CONSENT_MESSAGE = 'Consentimento ainda não configurado.'
type ProfileInput = {
name: string
phone: string
email?: string | null
gender?: 'feminino' | 'masculino' | 'outro' | 'nao_informado' | null
sector?: Leadership['sector']
sectorNotes?: string | null
}
const profileContactData = (data: ProfileInput) => ({
name: data.name,
phone: data.phone,
...(data.email !== undefined ? { email: data.email } : {}),
...(data.gender !== undefined ? { gender: data.gender } : {}),
})
const profileLeadershipData = (data: ProfileInput) => ({
...(data.sector !== undefined ? { sector: data.sector } : {}),
...(data.sectorNotes !== undefined ? { sectorNotes: data.sectorNotes } : {}),
})
const consentUpdateData = (
resolution: ReturnType<typeof resolveInviteConsent>,
): {
consent?: number
consentContentHash?: string
consentedAt?: string
} =>
resolution.shouldUpdate
? {
consent: resolution.consentID,
consentContentHash: resolution.consentContentHash,
consentedAt: resolution.consentedAt,
}
: {}
const findReusableLeadershipAccount = async (
payload: Payload,
req: PayloadTransactionRequest,
leadership: { contact: unknown; user?: unknown },
): Promise<CampaignUser | null> => {
const contactID = requireRelationshipId(leadership.contact)
await acquireCampaignInviteRedemptionContactLock(payload, req, contactID)
let accountID = relationshipId(leadership.user)
if (accountID === null) {
const linkedAccountIDs = await findSameContactAccountIDs(payload, req, contactID)
if (linkedAccountIDs.length > 1) throw new Error(INVALID_CAMPAIGN_INVITE_MESSAGE)
accountID = linkedAccountIDs[0] ?? null
}
if (accountID === null) return null
return payload.findByID({
collection: 'campaignUser',
id: accountID,
depth: 0,
overrideAccess: true,
req,
})
}
export const redeemCampaignInviteAutofillRecord = async (
payload: Payload,
input: CampaignInviteAutofillInput,
): Promise<{ ok: true }> => {
const data = campaignInviteAutofillSchema.parse(input)
return withPayloadTransaction(
payload,
async ({ req }) => {
requireCampaignInvitePostgres(payload)
const leadershipID = await consumeCampaignInvite(
payload,
req,
data.token,
'autopreenchimento',
)
// The consumed single-use token scopes this restricted self-service transaction.
const [leadership, consent] = await Promise.all([
payload.findByID({
collection: 'leadership',
id: leadershipID,
depth: 0,
overrideAccess: true,
req,
}),
requireLeadershipConsent(payload, req, MISSING_CONSENT_MESSAGE),
])
const consentResolution = resolveInviteConsent({
existingConsentID: relationshipId(leadership.consent),
existingConsentContentHash: leadership.consentContentHash,
configuredConsentID: consent.id,
configuredConsentContentHash: consent.contentHash,
consentAccepted: data.consentAccepted,
})
const contactID = requireRelationshipId(leadership.contact)
const originalContact = await payload.findByID({
collection: 'contact',
id: contactID,
depth: 0,
overrideAccess: true,
req,
})
await acquireCampaignInviteAccountLocks(
payload,
req,
contactPhoneLockKeys([originalContact.phone, data.phone]),
)
await assertContactPhoneAvailable(
payload,
req,
data.phone,
contactID,
INVALID_CAMPAIGN_INVITE_MESSAGE,
)
await payload.update({
collection: 'contact',
id: contactID,
data: profileContactData(data),
depth: 0,
overrideAccess: true,
req,
})
await payload.update({
collection: 'leadership',
id: leadership.id,
data: {
...profileLeadershipData(data),
...consentUpdateData(consentResolution),
},
depth: 0,
overrideAccess: true,
req,
})
return { ok: true }
},
{ beginFailureMessage: 'Não foi possível iniciar a transação do convite.' },
)
}
export const redeemCampaignInviteLoginRecord = async (
payload: Payload,
input: CampaignInviteLoginInput,
): Promise<{ token: string }> => {
const data = campaignInviteLoginSchema.parse(input)
return withPayloadTransaction(
payload,
async ({ req }) => {
requireCampaignInvitePostgres(payload)
const leadershipID = await consumeCampaignInvite(payload, req, data.token, 'login')
// The consumed single-use token scopes all trusted account and profile writes below.
const leadership = await payload.findByID({
collection: 'leadership',
id: leadershipID,
depth: 0,
overrideAccess: true,
req,
})
if (leadership.supportStatus !== 'engajado') {
throw new Error(INVALID_CAMPAIGN_INVITE_MESSAGE)
}
const configuredConsent = await requireLeadershipConsent(
payload,
req,
MISSING_CONSENT_MESSAGE,
)
const consentResolution = resolveInviteConsent({
existingConsentID: relationshipId(leadership.consent),
existingConsentContentHash: leadership.consentContentHash,
configuredConsentID: configuredConsent.id,
configuredConsentContentHash: configuredConsent.contentHash,
consentAccepted: data.consentAccepted,
})
const originalContact = await payload.findByID({
collection: 'contact',
id: requireRelationshipId(leadership.contact),
depth: 0,
overrideAccess: true,
req,
})
let account = await findReusableLeadershipAccount(payload, req, leadership)
await acquireCampaignInviteAccountLocks(payload, req, [
...contactPhoneLockKeys([originalContact.phone, data.phone]),
...(account?.username ? [`account-username:${account.username}`] : []),
`account-username:${data.phone}`,
...(account ? [`invite-redemption-user:${account.id}`] : []),
])
await assertContactPhoneAvailable(
payload,
req,
data.phone,
originalContact.id,
INVALID_CAMPAIGN_INVITE_MESSAGE,
)
const accounts = await payload.find({
collection: 'campaignUser',
where: { username: { equals: data.phone } },
depth: 0,
limit: 2,
pagination: false,
overrideAccess: true,
req,
})
if (accounts.totalDocs > 1) {
throw new Error('Existe mais de uma conta para este celular.')
}
const usernameOwner = accounts.docs[0]
if (usernameOwner && usernameOwner.id !== account?.id) {
throw new Error(INVALID_CAMPAIGN_INVITE_MESSAGE)
}
await payload.update({
collection: 'contact',
id: originalContact.id,
data: profileContactData(data),
depth: 0,
overrideAccess: true,
req,
})
const accountData = {
name: data.name,
phone: data.phone,
username: data.phone,
password: data.password,
...(data.email !== undefined ? { email: data.email } : {}),
}
account = account
? await payload.update({
collection: 'campaignUser',
id: account.id,
data: accountData,
depth: 0,
overrideAccess: true,
req,
})
: await payload.create({
collection: 'campaignUser',
data: { ...accountData, role: 'leader' },
depth: 0,
overrideAccess: true,
req,
})
await payload.update({
collection: 'leadership',
id: leadership.id,
data: {
...profileLeadershipData(data),
...consentUpdateData(consentResolution),
user: account.id,
},
depth: 0,
overrideAccess: true,
req,
})
const login = await payload.login({
collection: 'campaignUser',
data: { username: data.phone, password: data.password },
req,
})
if (!login.token) throw new Error('Não foi possível autenticar a conta criada.')
return { token: login.token }
},
{ beginFailureMessage: 'Não foi possível iniciar a transação do convite.' },
)
}