mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
- 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
78 lines
2.6 KiB
TypeScript
78 lines
2.6 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const phoneMocks = vi.hoisted(() => ({
|
|
normalizeBrazilianPhone: vi.fn<(value: string) => string | null>(),
|
|
}))
|
|
|
|
vi.mock('@/lib/phone', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('@/lib/phone')>()
|
|
phoneMocks.normalizeBrazilianPhone.mockImplementation(actual.normalizeBrazilianPhone)
|
|
|
|
return {
|
|
...actual,
|
|
normalizeBrazilianPhone: phoneMocks.normalizeBrazilianPhone,
|
|
}
|
|
})
|
|
|
|
import { Contact } from '@/collections/Contact'
|
|
import { leadershipCreateSchema } from '@/lib/schemas/leadership'
|
|
|
|
import { stub } from '../helpers/stub'
|
|
|
|
const contactPhoneHook = Contact.hooks?.beforeValidate?.[0]
|
|
|
|
type ContactPhoneHookArgs = Parameters<NonNullable<typeof contactPhoneHook>>[0]
|
|
|
|
const runContactPhoneHook = async (phones: unknown) => {
|
|
if (typeof contactPhoneHook !== 'function') {
|
|
throw new Error('Expected Contact.beforeValidate phone hook.')
|
|
}
|
|
|
|
const data: { phones?: unknown } = { phones }
|
|
await contactPhoneHook(stub<ContactPhoneHookArgs>({ data }))
|
|
return data.phones
|
|
}
|
|
|
|
describe('Contact phone normalization boundary', () => {
|
|
beforeEach(() => {
|
|
phoneMocks.normalizeBrazilianPhone.mockClear()
|
|
})
|
|
|
|
it('normalizes once before leadership dedupe and skips canonical Contact input', async () => {
|
|
const parsed = leadershipCreateSchema.parse({
|
|
municipalities: [1],
|
|
name: 'Liderança normalizada',
|
|
phones: ['+55 (71) 99999-1234'],
|
|
})
|
|
|
|
expect(parsed.phones).toEqual(['71999991234'])
|
|
expect(await runContactPhoneHook([{ value: parsed.phones[0] }])).toEqual([
|
|
{ value: '71999991234' },
|
|
])
|
|
expect(phoneMocks.normalizeBrazilianPhone).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('normalizes a formatted phone sent directly to the Contact API boundary', async () => {
|
|
expect(await runContactPhoneHook([{ value: '+55 (71) 99999-1234' }])).toEqual([
|
|
{ value: '71999991234' },
|
|
])
|
|
expect(phoneMocks.normalizeBrazilianPhone).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('rejects an invalid canonical-length direct API phone without re-normalizing it', async () => {
|
|
await expect(runContactPhoneHook([{ value: '00000000000' }])).rejects.toThrow(
|
|
'Celular brasileiro inválido.',
|
|
)
|
|
expect(phoneMocks.normalizeBrazilianPhone).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('drops empty entries and rejects a duplicate within the same ficha', async () => {
|
|
expect(await runContactPhoneHook([{ value: '' }, { value: null }])).toEqual([])
|
|
|
|
await expect(
|
|
runContactPhoneHook([{ value: '71999991234' }, { value: '71999991234' }]),
|
|
).rejects.toThrow('Telefone repetido na ficha.')
|
|
})
|
|
})
|