Glossary
OnlineConversation — the database model (packages/prisma/schema.prisma:4116-4163) representing one WhatsApp thread with one contact for one clinic (companyId). Holds the contact's phone and/or bsuid, counts (messagesCount, unseenCount), the last message/activity timestamps, and relations to the matched Patient/Lead record, assigned staff member, tags, and its ConversationMessage history.
bsuid (business-scoped user ID) — an anonymous identifier Meta sends instead of a phone number (wa_id) when a contact has WhatsApp usernames enabled and hasn't recently exchanged messages using their number, or has their number hidden. Looks like "EG.1693676845194969". It is not dialable — it can't be used to send a message directly the way a phone number can — but it reliably identifies the same contact across messages, including after they hide their number. See the code comment at packages/whatsapp-official/src/messages/messages.service.ts:88-92.
Upsert — a single database operation meaning "update this row if it exists, otherwise create it." Prisma's upsert() API is used here, but per The race condition fix, it cannot be compiled into one atomic SQL statement when the write includes nested relation creates (like attaching a new message), which is the root cause this PR works around.
Unique constraint — a database rule guaranteeing no two rows share the same value(s) in a given column (or column combination). OnlineConversation has two: @@unique([companyId, phone]) and @@unique([companyId, bsuid]) (packages/prisma/schema.prisma:4152-4153) — one phone number or one bsuid can belong to at most one conversation per clinic. Violating either raises Prisma error code P2002.
Race condition — a bug that only occurs depending on the relative timing of two operations that were assumed to happen one at a time. Here: two webhook deliveries for the same brand-new contact, both reading "no conversation exists yet" before either has finished writing its own.
Webhook — the HTTP endpoint Meta calls to deliver an incoming WhatsApp message to Dentolize (MessagesController → MessagesService.onMessage, packages/whatsapp-official/src/messages/). The handler responds 200 OK to Meta immediately (messages.service.ts:69), before processing the message, so Meta never retries on a later failure — anything that goes wrong after that point is invisible to Meta and, before this PR, invisible to Dentolize too.
Conversation merge — reconciling two OnlineConversation rows that turn out to represent the same person under two different addresses (phone vs. bsuid) into one. Explicitly not handled by this PR, nor by the pre-existing backfill: false case in findConversationByAddress (packages/whatsapp-official/src/services/online-conversation.service.ts:60-63) — both leave the two rows as-is and call merging a separate job.