teqo-backup/tests/int/campaignMunicipalityLeadershipList.int.spec.ts
Francisco Solla 6de8fad8f6 feat(C112): múltiplos telefones por pessoa — Contact.phones array (ordem = prioridade)
- Contact.phone (text único) → phones (array field Payload, subcampo value):
  fonte única; principal = phones[0]; beforeValidate normaliza, filtra vazios,
  rejeita inválido e duplicado DENTRO da ficha (C111 entre fichas preservado)
- migration add_contact_phones_array: backfill phone → phones[0] (gen_random_uuid)
  antes do drop da coluna + índice em value (hand-edit documentado; snapshot .json
  gerado mantém as colunas C99/C100 já existentes)
- primaryPhoneOf / phoneValuesOf / reorderWithPrimaryPhone em src/lib/phone.ts
  (ordem = prioridade; inline edit = seta o principal preservando o resto)
- findOrCreateContactByPhone aceita phones[] (dedupe pelo primário; reuse faz
  append dos faltantes; locks em todos os números digitados)
- sync conta→ficha (CampaignUser): phone da conta faz APPEND quando a ficha não
  tem (nunca reordena nem remove); tombstone LGPD substitui TODOS os telefones
- convite redemption: telefone digitado vira principal, demais preservados
- import CSV: preview sinaliza compartilhado também por número secundário;
  bulk insere contact_phones (JOIN nas leituras, uuid no insert)
- busca por telefone (apoiadores, pickers, combobox) casa qualquer número
- UI: PhonesFieldEditor compartilhado (repetível mascarado, remover, ↑/↓,
  modo inline com Salvar) nas 5 superfícies — liderança (form/ficha/lista),
  apoiador (form/ficha), dobradinha (ficha), assessor (ficha), wizard
- UI pública (petição/WhatsApp) intocada: zod e forms ficam com phone;
  as ações mapeiam phone → phones[0]
- testes: spec novo campaignMultiPhones (aceite completo), specs C111/C99
  atualizados, e2e wizard/apoiador ajustados
2026-08-11 10:47:02 -03:00

185 lines
7.4 KiB
TypeScript

// @vitest-environment node
import type { Payload } from 'payload'
import { getPayload } from 'payload'
import { beforeAll, describe, expect, it } from 'vitest'
import {
createLeadershipRecord,
createMunicipalityLeadershipRecord,
} from '@/app/(campaign)/campanha/actions/leadership'
import { relationshipId } from '@/lib/relationship'
import config from '@/payload.config'
import { loadMunicipalityListPageBundle } from '@/utilities/municipality/municipalityPageData'
import {
getEligibleLeadershipOptions,
loadMunicipalityLeadershipSummaries,
} from '@/utilities/municipality/municipalityViewModels'
import { installCampaignFixtures } from '../helpers/campaignFixtures'
let payload: Payload
const campaignFixtures = installCampaignFixtures({
getPayload: () => payload,
setPayload: (nextPayload) => {
payload = nextPayload
},
})
describe('B155 — lideranças na lista de municípios (leitura reversa + opções)', () => {
beforeAll(async () => {
payload = await getPayload({ config: await config })
})
it('summarizes the leaderships of the given municípios with contact names', async () => {
const fixtures = campaignFixtures()
const coordinator = await fixtures.createCampaignUser('coordinator')
const first = await fixtures.getMunicipality()
const second = await fixtures.getMunicipality()
const contactA = await fixtures.createContact({ name: 'Maria de Jesus' })
const leadershipA = await fixtures.createLeadership({
contact: contactA,
municipalities: [first],
})
const contactB = await fixtures.createContact({ name: 'João do Brejo' })
const leadershipB = await fixtures.createLeadership({
contact: contactB,
municipalities: [first, second],
})
const { leadershipIDsByMunicipality, summariesById } =
await loadMunicipalityLeadershipSummaries(payload, coordinator, [first.id, second.id])
expect([...summariesById.keys()].sort()).toEqual([leadershipA.id, leadershipB.id].sort())
expect(summariesById.get(leadershipA.id)?.name).toBe('Maria de Jesus')
expect(summariesById.get(leadershipB.id)?.name).toBe('João do Brejo')
expect(leadershipIDsByMunicipality.get(first.id)?.sort()).toEqual(
[leadershipA.id, leadershipB.id].sort(),
)
expect(leadershipIDsByMunicipality.get(second.id)).toEqual([leadershipB.id])
})
it('returns empty maps for an empty id list', async () => {
const fixtures = campaignFixtures()
const coordinator = await fixtures.createCampaignUser('coordinator')
const { leadershipIDsByMunicipality, summariesById } =
await loadMunicipalityLeadershipSummaries(payload, coordinator, [])
expect(leadershipIDsByMunicipality.size).toBe(0)
expect(summariesById.size).toBe(0)
})
it('scopes the summaries and options to an advisor portfolio', async () => {
const fixtures = campaignFixtures()
const advisor = await fixtures.createCampaignUser('advisor')
const administered = await fixtures.getMunicipality()
const stranger = await fixtures.getMunicipality()
await fixtures.assignMunicipalityAdvisors(administered, [advisor])
const ownContact = await fixtures.createContact({ name: 'Própria' })
const own = await fixtures.createLeadership({
contact: ownContact,
municipalities: [administered],
})
const alienContact = await fixtures.createContact({ name: 'Alheia' })
const alien = await fixtures.createLeadership({
contact: alienContact,
municipalities: [stranger],
})
const { summariesById } = await loadMunicipalityLeadershipSummaries(payload, advisor, [
administered.id,
stranger.id,
])
expect(summariesById.has(own.id)).toBe(true)
expect(summariesById.has(alien.id)).toBe(false)
const options = await getEligibleLeadershipOptions(payload, advisor)
expect(options.map((option) => option.id)).toContain(own.id)
expect(options.map((option) => option.id)).not.toContain(alien.id)
})
it('lists every leadership for unrestricted staff and names them by contact', async () => {
const fixtures = campaignFixtures()
const coordinator = await fixtures.createCampaignUser('coordinator')
const municipality = await fixtures.getMunicipality()
const contact = await fixtures.createContact({ name: 'Fulana dos Anjos' })
const leadership = await fixtures.createLeadership({ contact, municipalities: [municipality] })
const options = await getEligibleLeadershipOptions(payload, coordinator)
const found = options.find((option) => option.id === leadership.id)
expect(found).toMatchObject({ id: leadership.id, name: 'Fulana dos Anjos' })
})
it('carries leadershipIDs and names in the list page bundle for staff only', async () => {
const fixtures = campaignFixtures()
const coordinator = await fixtures.createCampaignUser('coordinator')
const municipality = await fixtures.getMunicipality()
const contact = await fixtures.createContact({ name: 'Chica da Roça' })
const leadership = await fixtures.createLeadership({ contact, municipalities: [municipality] })
const bundle = await loadMunicipalityListPageBundle(payload, coordinator, {
slug: municipality.slug,
})
const row = bundle.municipalities.find((item) => item.id === municipality.id)
expect(row?.leadershipIDs).toContain(leadership.id)
expect(bundle.leadershipNamesById.get(leadership.id)?.name).toBe('Chica da Roça')
const leader = await fixtures.createCampaignUser('leader')
const leaderBundle = await loadMunicipalityListPageBundle(payload, leader, {})
expect(leaderBundle.municipalities).toEqual([])
expect(leaderBundle.leadershipNamesById.size).toBe(0)
})
it('creates an inline leadership with name only and exposes it through the column read', async () => {
const fixtures = campaignFixtures()
const coordinator = await fixtures.createCampaignUser('coordinator')
const municipality = await fixtures.getMunicipality()
const created = await createMunicipalityLeadershipRecord(payload, coordinator, {
municipalityId: municipality.id,
name: 'Criada na Reunião',
})
const contactID = relationshipId(created.leadership.contact)
if (contactID === null) throw new Error('A liderança criada deve referenciar um contato.')
const contact = await payload.findByID({
collection: 'contact',
id: contactID,
depth: 0,
overrideAccess: true,
})
expect(contact.phones).toEqual([])
const { leadershipIDsByMunicipality, summariesById } =
await loadMunicipalityLeadershipSummaries(payload, coordinator, [municipality.id])
expect(leadershipIDsByMunicipality.get(municipality.id)).toContain(created.leadership.id)
expect(summariesById.get(created.leadership.id)?.name).toBe('Criada na Reunião')
})
it('keeps phone dedup on the complete leadership create path', async () => {
const fixtures = campaignFixtures()
const coordinator = await fixtures.createCampaignUser('coordinator')
const municipality = await fixtures.getMunicipality()
const phone = fixtures.phone()
const first = await createLeadershipRecord(payload, coordinator, {
municipalities: [municipality.id],
name: 'Nome Original',
phones: [phone],
})
await expect(
createLeadershipRecord(payload, coordinator, {
municipalities: [municipality.id],
name: 'Outro Nome',
phones: [phone],
}),
).rejects.toThrow('Esta pessoa já está cadastrada como liderança')
expect(first.contactReused).toBe(false)
})
})