From 932d9bb72a07a8a733faaa8f1fc8271c75ce6033 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Mon, 4 May 2026 04:10:28 +0000 Subject: [PATCH] fix(GRO-982): look up or create client by phone before inserting conversation Fixes FK constraint violation where clientId was set to businessSettings.id or a random UUID. Now looks up clients.phone = clientPhone first; if no match, creates a placeholder client with phone as name and a placeholder email. --- .../messaging/__tests__/inbound.test.ts | 28 +++++++++++++++++++ apps/api/src/services/messaging/inbound.ts | 23 +++++++++++---- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/apps/api/src/services/messaging/__tests__/inbound.test.ts b/apps/api/src/services/messaging/__tests__/inbound.test.ts index 826566c..f75c1dd 100644 --- a/apps/api/src/services/messaging/__tests__/inbound.test.ts +++ b/apps/api/src/services/messaging/__tests__/inbound.test.ts @@ -13,6 +13,7 @@ vi.mock("@groombook/db", () => ({ conversations: { id: "", businessId: "", clientId: "", externalNumber: "", businessNumber: "", channel: "", lastMessageAt: null, status: "", createdAt: null, updatedAt: null }, messages: { id: "", conversationId: "", direction: "", body: "", status: "", providerMessageId: "", sentByStaffId: null, createdAt: null, deliveredAt: null, readByClientAt: null }, businessSettings: { id: "", messagingPhoneNumber: "" }, + clients: { id: "", name: "", email: "", phone: "", status: "" }, eq: vi.fn(), and: vi.fn(), sql: vi.fn(), @@ -127,6 +128,33 @@ describe("findOrCreateConversation", () => { const result = await findOrCreateConversation("biz-1", "+1555111", "+1555222"); expect(result.id).toBe("conv-2"); }); + + it("creates placeholder client for unknown phone then creates conversation", async () => { + mockDb.select + .mockReturnValueOnce({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + limit: vi.fn().mockReturnValue([]), + }), + }), + }) + .mockReturnValueOnce({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + limit: vi.fn().mockReturnValue([]), + }), + }), + }); + mockDb.insert.mockReturnValue({ + values: vi.fn().mockReturnValue({ + returning: vi.fn().mockReturnValue([{ id: "conv-3", clientId: "client-3" }]), + }), + }); + + const result = await findOrCreateConversation("biz-1", "+1555111", "+1555222"); + expect(result.id).toBe("conv-3"); + expect(result.clientId).toBe("client-3"); + }); }); describe("upsertMessage", () => { diff --git a/apps/api/src/services/messaging/inbound.ts b/apps/api/src/services/messaging/inbound.ts index b5ab37b..de9d0e4 100644 --- a/apps/api/src/services/messaging/inbound.ts +++ b/apps/api/src/services/messaging/inbound.ts @@ -1,4 +1,5 @@ -import { getDb, conversations, messages, businessSettings, eq, and, sql } from "@groombook/db"; +import { getDb, conversations, messages, businessSettings, clients, eq, and } from "@groombook/db"; +import { v4 as uuidv4 } from "uuid"; export interface TelnyxMessageReceivedPayload { data: { @@ -41,13 +42,23 @@ export async function findOrCreateConversation( return { id: existing.id, clientId: existing.clientId }; } - const [business] = await db - .select({ primaryClientId: sql`${businessSettings.id}` }) - .from(businessSettings) - .where(eq(businessSettings.id, businessId)) + const [existingClient] = await db + .select({ id: clients.id }) + .from(clients) + .where(eq(clients.phone, clientPhone)) .limit(1); - const clientId = business?.primaryClientId ?? crypto.randomUUID(); + const clientId = existingClient?.id ?? uuidv4(); + + if (!existingClient) { + await db.insert(clients).values({ + id: clientId, + name: clientPhone, + email: `sms-${uuidv4()}@placeholder.local`, + phone: clientPhone, + status: "active", + }); + } const [created] = await db .insert(conversations)