- ChatThread per client/provider and channel; messages get files, internal notes, source (bot/mini app) and delivery errors; reply templates. Migration moves old concierge messages into threads. - Routing: subscribers write to the concierge, others to support; /support and /concierge in the client bot; staff can take, transfer, close and move threads. - Telegram: photos/documents from clients and providers, replies with the channel header, files sent from storage, notifications only to the assignee or the channel's staff, "Reply" into a specific thread. - New admin: /chats with list, conversation, notes, files, templates, client panel; "Message" button on the client card; dashboard counts threads awaiting reply. - Client mini app: in-app chat screen sharing the same conversation. - Old admin keeps working on the same data (notes hidden there). - Tests for routing, notes, assign/close/move, lists, recipients; separate test DB via TEST_DB_NAME; dev:chats-demo script. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
131 lines
5.4 KiB
SQL
131 lines
5.4 KiB
SQL
-- CreateEnum
|
|
CREATE TYPE "ChatChannel" AS ENUM ('SUPPORT', 'CONCIERGE');
|
|
|
|
-- CreateEnum
|
|
CREATE TYPE "ChatStatus" AS ENUM ('OPEN', 'CLOSED');
|
|
|
|
-- AlterEnum
|
|
-- This migration adds more than one value to an enum.
|
|
-- With PostgreSQL versions 11 and earlier, this is not possible
|
|
-- in a single migration. This can be worked around by creating
|
|
-- multiple migrations, each migration adding only one value to
|
|
-- the enum.
|
|
|
|
|
|
ALTER TYPE "ConciergeAuthor" ADD VALUE 'BOT';
|
|
ALTER TYPE "ConciergeAuthor" ADD VALUE 'SYSTEM';
|
|
|
|
-- AlterTable
|
|
ALTER TABLE "Client" ADD COLUMN "chatChannel" "ChatChannel";
|
|
|
|
-- AlterTable
|
|
ALTER TABLE "ConciergeMessage" ADD COLUMN "deliveryError" TEXT,
|
|
ADD COLUMN "fileKey" TEXT,
|
|
ADD COLUMN "fileName" TEXT,
|
|
ADD COLUMN "internal" BOOLEAN NOT NULL DEFAULT false,
|
|
ADD COLUMN "mime" TEXT,
|
|
ADD COLUMN "size" INTEGER,
|
|
ADD COLUMN "source" TEXT NOT NULL DEFAULT 'BOT',
|
|
ADD COLUMN "tgBot" TEXT,
|
|
ADD COLUMN "tgFileId" TEXT,
|
|
ADD COLUMN "threadId" TEXT;
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "ChatThread" (
|
|
"id" TEXT NOT NULL,
|
|
"channel" "ChatChannel" NOT NULL,
|
|
"clientId" TEXT,
|
|
"providerId" TEXT,
|
|
"status" "ChatStatus" NOT NULL DEFAULT 'OPEN',
|
|
"assigneeId" TEXT,
|
|
"botActive" BOOLEAN NOT NULL DEFAULT false,
|
|
"lastMessageAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"waitingSince" TIMESTAMP(3),
|
|
"closedAt" TIMESTAMP(3),
|
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT "ChatThread_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "ChatTemplate" (
|
|
"id" TEXT NOT NULL,
|
|
"channel" "ChatChannel",
|
|
"title" TEXT NOT NULL,
|
|
"text" TEXT NOT NULL,
|
|
"createdById" TEXT,
|
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
|
|
CONSTRAINT "ChatTemplate_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "ChatThread_channel_status_lastMessageAt_idx" ON "ChatThread"("channel", "status", "lastMessageAt");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "ChatThread_assigneeId_status_idx" ON "ChatThread"("assigneeId", "status");
|
|
|
|
-- CreateIndex
|
|
CREATE UNIQUE INDEX "ChatThread_clientId_channel_key" ON "ChatThread"("clientId", "channel");
|
|
|
|
-- CreateIndex
|
|
CREATE UNIQUE INDEX "ChatThread_providerId_channel_key" ON "ChatThread"("providerId", "channel");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "ConciergeMessage_threadId_createdAt_idx" ON "ConciergeMessage"("threadId", "createdAt");
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "ConciergeMessage" ADD CONSTRAINT "ConciergeMessage_threadId_fkey" FOREIGN KEY ("threadId") REFERENCES "ChatThread"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "ChatThread" ADD CONSTRAINT "ChatThread_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "Client"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "ChatThread" ADD CONSTRAINT "ChatThread_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "Provider"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "ChatThread" ADD CONSTRAINT "ChatThread_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "ChatTemplate" ADD CONSTRAINT "ChatTemplate_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
|
|
-- ───── Перенос старой переписки: у каждого собеседника появляется диалог ─────
|
|
-- клиент с активной подпиской → консьерж, остальные → поддержка; есть непрочитанные — диалог открыт
|
|
INSERT INTO "ChatThread" ("id", "channel", "clientId", "status", "lastMessageAt", "waitingSince", "closedAt", "createdAt")
|
|
SELECT
|
|
'cht' || substr(md5('c:' || m."clientId"), 1, 22),
|
|
CASE WHEN EXISTS (
|
|
SELECT 1 FROM "Subscription" s
|
|
WHERE s."clientId" = m."clientId" AND s."status" = 'ACTIVE' AND s."startsAt" <= now() AND s."endsAt" > now()
|
|
) THEN 'CONCIERGE'::"ChatChannel" ELSE 'SUPPORT'::"ChatChannel" END,
|
|
m."clientId",
|
|
CASE WHEN bool_or(m."author" <> 'OPERATOR' AND m."readAt" IS NULL) THEN 'OPEN'::"ChatStatus" ELSE 'CLOSED'::"ChatStatus" END,
|
|
max(m."createdAt"),
|
|
min(m."createdAt") FILTER (WHERE m."author" <> 'OPERATOR' AND m."readAt" IS NULL),
|
|
CASE WHEN bool_or(m."author" <> 'OPERATOR' AND m."readAt" IS NULL) THEN NULL ELSE max(m."createdAt") END,
|
|
min(m."createdAt")
|
|
FROM "ConciergeMessage" m
|
|
WHERE m."clientId" IS NOT NULL
|
|
GROUP BY m."clientId";
|
|
|
|
-- исполнители пишут в поддержку
|
|
INSERT INTO "ChatThread" ("id", "channel", "providerId", "status", "lastMessageAt", "waitingSince", "closedAt", "createdAt")
|
|
SELECT
|
|
'cht' || substr(md5('p:' || m."providerId"), 1, 22),
|
|
'SUPPORT'::"ChatChannel",
|
|
m."providerId",
|
|
CASE WHEN bool_or(m."author" <> 'OPERATOR' AND m."readAt" IS NULL) THEN 'OPEN'::"ChatStatus" ELSE 'CLOSED'::"ChatStatus" END,
|
|
max(m."createdAt"),
|
|
min(m."createdAt") FILTER (WHERE m."author" <> 'OPERATOR' AND m."readAt" IS NULL),
|
|
CASE WHEN bool_or(m."author" <> 'OPERATOR' AND m."readAt" IS NULL) THEN NULL ELSE max(m."createdAt") END,
|
|
min(m."createdAt")
|
|
FROM "ConciergeMessage" m
|
|
WHERE m."providerId" IS NOT NULL
|
|
GROUP BY m."providerId";
|
|
|
|
UPDATE "ConciergeMessage" m SET "threadId" = t."id"
|
|
FROM "ChatThread" t
|
|
WHERE (m."clientId" IS NOT NULL AND t."clientId" = m."clientId") OR (m."providerId" IS NOT NULL AND t."providerId" = m."providerId");
|
|
|