mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-15 10:43:07 +00:00
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
82 lines
2.6 KiB
TypeScript
82 lines
2.6 KiB
TypeScript
import 'server-only'
|
|
|
|
import type { Payload, PayloadRequest } from 'payload'
|
|
|
|
import type { Contact } from '@/payload-types'
|
|
import { acquireContactPhoneLocks } from '@/utilities/contactPhoneLocks'
|
|
import type { PayloadTransactionRequest } from '@/utilities/payloadTransaction'
|
|
|
|
type ContactIdentityRequest = PayloadTransactionRequest | PayloadRequest
|
|
|
|
/**
|
|
* Find-or-create the normalized `Contact` ficha for a person, inside the
|
|
* caller's transaction. With a phone, the advisory lock is acquired HERE and
|
|
* an existing ficha with that phone is reused — but only when the phone is
|
|
* unambiguous (exactly one ficha): that is the phone-matching dedupe C6/C99
|
|
* keeps for re-imports and re-registration of the SAME person. When the phone
|
|
* already belongs to TWO OR MORE fichas there is no person the phone can
|
|
* identify — C111 — so a fresh ficha is created (the identity of the person
|
|
* being created is the ficha just written; never a guess among the existing
|
|
* ones). Without a phone, a fresh name-only ficha is created. Every "cria ou
|
|
* vincula a ficha" path shares one owner for the "BA default + shared-phone"
|
|
* policy.
|
|
*
|
|
* Callers must already be inside an active Payload transaction (the advisory
|
|
* lock requires it) and authorized to own the write (staff creation flows run
|
|
* with the operation already gated; the intentional admin bypass mirrors
|
|
* `createStateDeputyWithContact`).
|
|
*/
|
|
export const findOrCreateContactByPhone = async ({
|
|
payload,
|
|
req,
|
|
phone,
|
|
name,
|
|
email,
|
|
city,
|
|
gender,
|
|
}: {
|
|
payload: Payload
|
|
req: ContactIdentityRequest
|
|
phone: string | null
|
|
name: string
|
|
email?: string | null
|
|
city?: string | null
|
|
gender?: Contact['gender'] | null
|
|
}): Promise<{ contactID: number; reused: boolean }> => {
|
|
if (phone) {
|
|
await acquireContactPhoneLocks(payload, req, [phone])
|
|
|
|
const contacts = await payload.find({
|
|
collection: 'contact',
|
|
where: { phone: { equals: phone } },
|
|
depth: 0,
|
|
limit: 2,
|
|
pagination: false,
|
|
// Intentional admin bypass: the phone match must see every ficha,
|
|
// including ones outside the actor's campaign scope.
|
|
overrideAccess: true,
|
|
req,
|
|
})
|
|
|
|
if (contacts.totalDocs === 1) {
|
|
return { contactID: contacts.docs[0]!.id, reused: true }
|
|
}
|
|
}
|
|
|
|
const contact = await payload.create({
|
|
collection: 'contact',
|
|
data: {
|
|
name,
|
|
phone,
|
|
email: email ?? null,
|
|
state: 'BA' as Contact['state'],
|
|
city: city ?? null,
|
|
gender,
|
|
},
|
|
depth: 0,
|
|
overrideAccess: true,
|
|
req,
|
|
})
|
|
|
|
return { contactID: contact.id, reused: false }
|
|
}
|