teqo/src/lib/schemas/supporter.ts
Francisco Ignácio Fontoura Solla a18e2f15f5 Simplify C2 supporter paths and register C6 scale follow-ups.
Tighten import/create view models after review, and capture the deferred DRY/import-scale work as roadmap item C6.
2026-07-18 20:33:50 -03:00

101 lines
3.3 KiB
TypeScript

import { z } from 'zod'
import { bahiaMunicipalities } from '@/lib/bahiaTerritories'
import {
brazilianMobile,
optionalPersistedEmail,
positiveRelationshipId,
trimmedNullableText,
} from '@/lib/schemas/primitives'
import { normalizeSearchPhrase } from '@/utilities/wordStartFilter'
export const supporterVoteIntentions = ['certo', 'tende_a_certo', 'indeciso', 'outro'] as const
export type SupporterVoteIntention = (typeof supporterVoteIntentions)[number]
export const isSupporterVoteIntention = (value: unknown): value is SupporterVoteIntention =>
typeof value === 'string' && (supporterVoteIntentions as readonly string[]).includes(value)
const canonicalMunicipalityBySearchValue = new Map(
bahiaMunicipalities.map((city) => [normalizeSearchPhrase(city), city]),
)
export const resolveBahiaMunicipality = (value: string | undefined | null): string | null => {
if (typeof value !== 'string') return null
const trimmed = value.trim()
if (!trimmed) return null
return canonicalMunicipalityBySearchValue.get(normalizeSearchPhrase(trimmed)) ?? null
}
const optionalBahiaCity = z
.union([z.string().trim(), z.literal(''), z.null()])
.optional()
.transform((value, context) => {
if (value === undefined || value === null || value === '') return undefined
const city = resolveBahiaMunicipality(value)
if (!city) {
context.addIssue({
code: 'custom',
message: 'Município não reconhecido na Bahia.',
})
return z.NEVER
}
return city
})
export const supporterCreateSchema = z
.object({
name: z.string().trim().min(2).max(120),
phone: brazilianMobile,
email: optionalPersistedEmail,
city: optionalBahiaCity,
nucleus: positiveRelationshipId.optional(),
voteIntention: z.enum(supporterVoteIntentions).optional(),
consentAccepted: z.literal(true, {
error: 'É necessário aceitar o consentimento de cadastro.',
}),
voteIntentionConsentAccepted: z.boolean().optional(),
})
.superRefine((data, context) => {
if (data.voteIntention && !data.voteIntentionConsentAccepted) {
context.addIssue({
code: 'custom',
path: ['voteIntentionConsentAccepted'],
message: 'É necessário aceitar o consentimento de intenção de voto.',
})
}
})
export const supporterVoteIntentionSchema = z.object({
id: positiveRelationshipId,
voteIntention: z.enum(supporterVoteIntentions),
voteIntentionConsentAccepted: z.literal(true, {
error: 'É necessário aceitar o consentimento de intenção de voto.',
}),
})
export const supporterImportConfirmSchema = z.object({
operatorAttested: z.literal(true, {
error: 'É necessário atestar o consentimento dos apoiadores importados.',
}),
consentNote: trimmedNullableText(2000),
rows: z
.array(
z.object({
nome: z.string().trim().min(2).max(120),
telefone: brazilianMobile,
municipio: optionalBahiaCity,
intencao: z.enum(supporterVoteIntentions).optional(),
}),
)
.min(1)
.max(5000),
})
export const supporterRemoveSchema = z.object({
id: positiveRelationshipId,
})
export type SupporterCreateInput = z.input<typeof supporterCreateSchema>
export type SupporterVoteIntentionInput = z.input<typeof supporterVoteIntentionSchema>
export type SupporterImportConfirmInput = z.input<typeof supporterImportConfirmSchema>