teqo-backup/src/utilities/contactPhoneLocks.ts
Francisco Solla 162a518416 feat(C111): telefone não único — duas pessoas podem compartilhar o mesmo celular
Contact.phone deixa de ser único em enforcement (nunca houve constraint DB):
- remove o hook enforceUniqueContactPhone e os asserts assertContactPhone*
  (conflicts/ambiguity) — os 5 fluxos de escrita de pessoa salvam com número
  compartilhado; rename contactPhoneInvariant.ts -> contactPhoneLocks.ts
  (locks passam a significar serialização, não unicidade)
- findOrCreateContactByPhone: 0 fichas cria, 1 reusa (dedupe C6/C99),
  2+ cria ficha nova (a identidade da pessoa em criação é a ficha nova)
- convite autofill/login: ficha ancorada pode compartilhar o phone; o lado
  conta permanece fail-closed (username DB-unique; check de 2+ contas)
- import CSV: preview sinaliza telefone_compartilhado (resolução manual);
  confirm/bulk abortam fail-closed se um phone ganhar 2+ fichas (race)
- fix de achado de execução: supporter_import_batch.actor_id NOT NULL com
  FK ON DELETE set null derrubava o delete de campaignUser com batch
  órfão (23502) — CampaignUser.beforeDelete limpa batches + fixture
  cleanup descobre/deleta batches dos accounts owned
2026-08-10 23:03:05 -03:00

25 lines
1 KiB
TypeScript

import 'server-only'
import type { Payload, PayloadRequest } from 'payload'
import type { PayloadTransactionRequest } from '@/utilities/payloadTransaction'
import { acquireTextAdvisoryLocks } from '@/utilities/postgresTransactionLocks'
type ContactPhonePayload = Pick<Payload, 'db'>
type ContactPhoneRequest = PayloadTransactionRequest | PayloadRequest
/**
* Advisory locks keyed by phone (C111 — the phone is a contact channel, not a
* unique person identity, so these serialize read-modify-write flows that
* look contacts up by phone: find-or-create, CSV import, invite redemption,
* LGPD tombstone). They no longer guard uniqueness — they prevent two
* concurrent flows from racing the same phone key.
*/
export const contactPhoneLockKeys = (phones: string[]): string[] =>
[...new Set(phones)].sort().map((phone) => `contact-phone:${phone}`)
export const acquireContactPhoneLocks = (
payload: ContactPhonePayload,
req: ContactPhoneRequest,
phones: string[],
): Promise<void> => acquireTextAdvisoryLocks(payload, req, contactPhoneLockKeys(phones))