mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-15 10:43:07 +00:00
Closes #352 - POST /campanha/municipios/advisors aceita body de criação (name) na union; createMunicipalityAdvisorRecord cria campaignUser advisor (stub <slug>@criado.invalid, senha aleatória) e atribui ao município na mesma transação (cap 10 com rollback, lock municipality-advisors:{id}) - MunicipalityAdvisorCreateProvider compartilha o assessor criado entre linhas; opção "+ Criar assessor" e chip otimista com id temporário no control - advisorNameSchema unificado em primitives.ts; predicado de placeholder cobre @criado.invalid (reset de senha e célula de e-mail tratam stubs inline) - Destrava o spec e2e de municipios (falhas pré-existentes em main): key warning do fragment trailing de MunicipalityFilters; settle-wait da animação do Drawer no teste B136
51 lines
1.7 KiB
TypeScript
51 lines
1.7 KiB
TypeScript
import { z } from 'zod'
|
||
|
||
import { BRAZILIAN_PHONE_INVALID_MESSAGE, normalizeBrazilianPhone } from '@/lib/phone'
|
||
|
||
/**
|
||
* The assessor display-name rule, shared by the B19 account screen, the B154
|
||
* inline create and the wire contract of `POST /campanha/municipios/advisors`.
|
||
* Owned here (a leaf module) because `schemas/advisor.ts` already imports from
|
||
* `schemas/municipality.ts`, so the reverse import would close a cycle.
|
||
*/
|
||
export const advisorNameSchema = z.string().trim().min(2).max(160)
|
||
|
||
export const positiveRelationshipId = z.number().int().positive()
|
||
|
||
export const trimmedOptionalText = (maximum: number) =>
|
||
z
|
||
.string()
|
||
.trim()
|
||
.max(maximum)
|
||
.optional()
|
||
.transform((value) => (value === '' ? undefined : value))
|
||
|
||
export const trimmedNullableText = (maximum: number) =>
|
||
z
|
||
.union([z.string().trim().max(maximum), z.null()])
|
||
.optional()
|
||
.transform((value) => (value === '' ? null : value))
|
||
|
||
export const brazilianMobile = z.string().transform((value, context) => {
|
||
const phone = normalizeBrazilianPhone(value)
|
||
if (phone) return phone
|
||
|
||
context.addIssue({
|
||
code: 'custom',
|
||
message: BRAZILIAN_PHONE_INVALID_MESSAGE,
|
||
})
|
||
return z.NEVER
|
||
})
|
||
|
||
export const optionalPersistedEmail = z
|
||
.union([z.email('E-mail inválido.'), z.literal('')])
|
||
.optional()
|
||
.transform((value) => (value === '' ? undefined : value))
|
||
|
||
export const nullablePersistedEmail = z
|
||
.union([z.email('E-mail inválido.'), z.literal(''), z.null()])
|
||
.optional()
|
||
.transform((value) => (value === '' ? null : value))
|
||
|
||
/** Upper bound for any vote count field — one bound, two validation layers (P3-K, was 1_000_000 spelled ×6). */
|
||
export const MAX_VOTE_COUNT = 1_000_000
|