Snapshot of uncommitted work: landing, bots, prod infra, partner cabinet, backoffice stage 1
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
45d7512d2b
commit
d452824c62
314 files changed
+44394
-121
No files matched your search
@@ -0,0 +1,22 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Order" ADD COLUMN "priceBreakdown" JSONB;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Provider" ADD COLUMN "payoutPresetId" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PayoutPreset" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"role" "ProviderRole" NOT NULL,
|
||||
"rules" JSONB NOT NULL,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "PayoutPreset_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Provider" ADD CONSTRAINT "Provider_payoutPresetId_fkey" FOREIGN KEY ("payoutPresetId") REFERENCES "PayoutPreset"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,54 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WebRequestStatus" AS ENUM ('NEW', 'LINKED', 'ORDERED', 'CANCELLED');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WebRequest" (
|
||||
"id" TEXT NOT NULL,
|
||||
"number" SERIAL NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"serviceId" TEXT NOT NULL,
|
||||
"params" JSONB NOT NULL,
|
||||
"preferredAt" TIMESTAMP(3) NOT NULL,
|
||||
"durationMinutes" INTEGER NOT NULL,
|
||||
"districtId" TEXT NOT NULL,
|
||||
"addressText" TEXT NOT NULL,
|
||||
"note" TEXT,
|
||||
"phone" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"locale" "Locale" NOT NULL,
|
||||
"totalPrice" INTEGER NOT NULL,
|
||||
"consentAt" TIMESTAMP(3) NOT NULL,
|
||||
"consentVersion" TEXT NOT NULL,
|
||||
"status" "WebRequestStatus" NOT NULL DEFAULT 'NEW',
|
||||
"clientId" TEXT,
|
||||
"orderId" TEXT,
|
||||
"adminNote" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "WebRequest_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WebRequest_number_key" ON "WebRequest"("number");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WebRequest_token_key" ON "WebRequest"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WebRequest_orderId_key" ON "WebRequest"("orderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "WebRequest_status_createdAt_idx" ON "WebRequest"("status", "createdAt");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WebRequest" ADD CONSTRAINT "WebRequest_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WebRequest" ADD CONSTRAINT "WebRequest_districtId_fkey" FOREIGN KEY ("districtId") REFERENCES "District"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WebRequest" ADD CONSTRAINT "WebRequest_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "Client"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WebRequest" ADD CONSTRAINT "WebRequest_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,38 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "LeadStatus" AS ENUM ('NEW', 'IN_WORK', 'DONE', 'CANCELLED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "LeadContactType" AS ENUM ('PHONE', 'TELEGRAM');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Lead" (
|
||||
"id" TEXT NOT NULL,
|
||||
"number" SERIAL NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"contact" TEXT NOT NULL,
|
||||
"contactType" "LeadContactType" NOT NULL,
|
||||
"locale" "Locale" NOT NULL,
|
||||
"agency" TEXT,
|
||||
"utm" JSONB,
|
||||
"moved" BOOLEAN,
|
||||
"moveWhen" TEXT,
|
||||
"moveDate" TIMESTAMP(3),
|
||||
"package" TEXT,
|
||||
"options" JSONB,
|
||||
"submittedAt" TIMESTAMP(3),
|
||||
"consentAt" TIMESTAMP(3) NOT NULL,
|
||||
"consentVersion" TEXT NOT NULL,
|
||||
"status" "LeadStatus" NOT NULL DEFAULT 'NEW',
|
||||
"adminNote" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Lead_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Lead_number_key" ON "Lead"("number");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Lead_status_createdAt_idx" ON "Lead"("status", "createdAt");
|
||||
@@ -0,0 +1,106 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ClientSegment" AS ENUM ('MOVING', 'LIVING');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "LeadStepStatus" AS ENUM ('TODO', 'IN_PROGRESS', 'DONE', 'SKIPPED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ClientRequestKind" AS ENUM ('SUBSCRIPTION', 'SERVICE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ClientRequestStatus" AS ENUM ('NEW', 'DONE', 'CANCELLED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ConciergeAuthor" AS ENUM ('CLIENT', 'OPERATOR');
|
||||
|
||||
-- AlterEnum
|
||||
ALTER TYPE "SubscriptionPlan" ADD VALUE 'ELITE';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Category" ADD COLUMN "eliteDiscountPct" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Client" ADD COLUMN "segment" "ClientSegment";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Lead" ADD COLUMN "checklist" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
ADD COLUMN "clientId" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "LeadStep" (
|
||||
"id" TEXT NOT NULL,
|
||||
"leadId" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"sortOrder" INTEGER NOT NULL,
|
||||
"status" "LeadStepStatus" NOT NULL DEFAULT 'TODO',
|
||||
"note" TEXT,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "LeadStep_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ClientRequest" (
|
||||
"id" TEXT NOT NULL,
|
||||
"number" SERIAL NOT NULL,
|
||||
"clientId" TEXT NOT NULL,
|
||||
"kind" "ClientRequestKind" NOT NULL,
|
||||
"plan" "SubscriptionPlan",
|
||||
"serviceCode" TEXT,
|
||||
"comment" TEXT,
|
||||
"status" "ClientRequestStatus" NOT NULL DEFAULT 'NEW',
|
||||
"adminNote" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ClientRequest_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ConciergeMessage" (
|
||||
"id" TEXT NOT NULL,
|
||||
"clientId" TEXT NOT NULL,
|
||||
"author" "ConciergeAuthor" NOT NULL,
|
||||
"adminUserId" TEXT,
|
||||
"text" TEXT NOT NULL,
|
||||
"readAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ConciergeMessage_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "LeadStep_leadId_code_key" ON "LeadStep"("leadId", "code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ClientRequest_number_key" ON "ClientRequest"("number");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ClientRequest_status_createdAt_idx" ON "ClientRequest"("status", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ClientRequest_clientId_idx" ON "ClientRequest"("clientId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ConciergeMessage_clientId_createdAt_idx" ON "ConciergeMessage"("clientId", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ConciergeMessage_author_readAt_idx" ON "ConciergeMessage"("author", "readAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Lead_clientId_idx" ON "Lead"("clientId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Lead" ADD CONSTRAINT "Lead_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "Client"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LeadStep" ADD CONSTRAINT "LeadStep_leadId_fkey" FOREIGN KEY ("leadId") REFERENCES "Lead"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ClientRequest" ADD CONSTRAINT "ClientRequest_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "Client"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ConciergeMessage" ADD CONSTRAINT "ConciergeMessage_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "Client"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- Elite: скидки по направлениям как на главной («до −35%»)
|
||||
UPDATE "Category" SET "eliteDiscountPct" = 35 WHERE "code" IN ('HOME', 'DOCS', 'LEARNING');
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RelocationItemKind" AS ENUM ('STEP', 'DOC');
|
||||
|
||||
-- AlterEnum
|
||||
ALTER TYPE "ConciergeAuthor" ADD VALUE 'PROVIDER';
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "ConciergeMessage" DROP CONSTRAINT "ConciergeMessage_clientId_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "AdminUser" ADD COLUMN "telegramId" BIGINT,
|
||||
ADD COLUMN "tgLinkToken" TEXT,
|
||||
ADD COLUMN "tgNotify" BOOLEAN NOT NULL DEFAULT true;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ConciergeMessage" ADD COLUMN "providerId" TEXT,
|
||||
ALTER COLUMN "clientId" DROP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Lead" DROP COLUMN "checklist";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "LeadStep" ADD COLUMN "title" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "RelocationItem" (
|
||||
"id" TEXT NOT NULL,
|
||||
"kind" "RelocationItemKind" NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"name" JSONB NOT NULL,
|
||||
"hint" JSONB,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"inTurnkey" BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
CONSTRAINT "RelocationItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "LeadDoc" (
|
||||
"id" TEXT NOT NULL,
|
||||
"leadId" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"title" TEXT,
|
||||
"checked" BOOLEAN NOT NULL DEFAULT false,
|
||||
"sortOrder" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "LeadDoc_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "RelocationItem_kind_code_key" ON "RelocationItem"("kind", "code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "LeadDoc_leadId_code_key" ON "LeadDoc"("leadId", "code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AdminUser_telegramId_key" ON "AdminUser"("telegramId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AdminUser_tgLinkToken_key" ON "AdminUser"("tgLinkToken");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ConciergeMessage_providerId_createdAt_idx" ON "ConciergeMessage"("providerId", "createdAt");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LeadDoc" ADD CONSTRAINT "LeadDoc_leadId_fkey" FOREIGN KEY ("leadId") REFERENCES "Lead"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ConciergeMessage" ADD CONSTRAINT "ConciergeMessage_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "Client"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ConciergeMessage" ADD CONSTRAINT "ConciergeMessage_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "Provider"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
|
||||
-- стартовые шаблоны: шаги переезда и чек-лист документов (дальше правятся в админке)
|
||||
INSERT INTO "RelocationItem" ("id", "kind", "code", "name", "hint", "sortOrder", "active", "inTurnkey") VALUES
|
||||
(gen_random_uuid()::text, 'STEP', 'consult', '{"ru": "Консультация по переезду", "en": "Relocation consultation", "zh": "搬家咨询", "hy": "Տեղափոխության խորհրդատվություն", "es": "Consulta de mudanza"}'::jsonb, NULL, 0, true, false),
|
||||
(gen_random_uuid()::text, 'STEP', 'airport', '{"ru": "Встреча в аэропорту", "en": "Airport pickup", "zh": "机场接机", "hy": "Դիմավորում օդանավակայանում", "es": "Recogida en el aeropuerto"}'::jsonb, NULL, 1, true, true),
|
||||
(gen_random_uuid()::text, 'STEP', 'stay', '{"ru": "Жильё на первое время", "en": "A place for the first days", "zh": "初期住宿", "hy": "Բնակություն առաջին օրերի համար", "es": "Alojamiento para empezar"}'::jsonb, NULL, 2, true, true),
|
||||
(gen_random_uuid()::text, 'STEP', 'flat', '{"ru": "Поиск квартиры", "en": "Flat search", "zh": "找房", "hy": "Բնակարանի որոնում", "es": "Búsqueda de piso"}'::jsonb, NULL, 3, true, true),
|
||||
(gen_random_uuid()::text, 'STEP', 'registration', '{"ru": "Регистрация", "en": "Registration", "zh": "登记", "hy": "Գրանցում", "es": "Empadronamiento"}'::jsonb, NULL, 4, true, true),
|
||||
(gen_random_uuid()::text, 'STEP', 'socialCard', '{"ru": "Соцкарта", "en": "Social card", "zh": "社保卡", "hy": "Սոցքարտ", "es": "Tarjeta social"}'::jsonb, NULL, 5, true, true),
|
||||
(gen_random_uuid()::text, 'STEP', 'bankCard', '{"ru": "Банковская карта", "en": "Bank card", "zh": "银行卡", "hy": "Բանկային քարտ", "es": "Tarjeta bancaria"}'::jsonb, NULL, 6, true, true),
|
||||
(gen_random_uuid()::text, 'STEP', 'residence', '{"ru": "ВНЖ", "en": "Residence permit", "zh": "居留许可", "hy": "Կացության քարտ", "es": "Permiso de residencia"}'::jsonb, NULL, 7, true, true),
|
||||
(gen_random_uuid()::text, 'DOC', 'passport', '{"ru": "Загранпаспорт", "en": "International passport", "zh": "护照", "hy": "Արտասահմանյան անձնագիր", "es": "Pasaporte"}'::jsonb, '{"ru": "Действует ещё минимум 6 месяцев после переезда", "en": "Valid for at least 6 months after the move", "zh": "搬家后至少还有 6 个月有效期", "hy": "Վավեր տեղափոխվելուց հետո առնվազն 6 ամիս", "es": "Vigente al menos 6 meses después de la mudanza"}'::jsonb, 0, true, false),
|
||||
(gen_random_uuid()::text, 'DOC', 'passportCopies', '{"ru": "Копии паспорта", "en": "Passport copies", "zh": "护照复印件", "hy": "Անձնագրի պատճեններ", "es": "Copias del pasaporte"}'::jsonb, '{"ru": "2–3 копии страницы с фото", "en": "2–3 copies of the photo page", "zh": "照片页复印 2–3 份", "hy": "Լուսանկարով էջի 2–3 պատճեն", "es": "2–3 copias de la página con foto"}'::jsonb, 1, true, false),
|
||||
(gen_random_uuid()::text, 'DOC', 'photos', '{"ru": "Фото 3×4", "en": "3×4 photos", "zh": "3×4 照片", "hy": "3×4 լուսանկարներ", "es": "Fotos 3×4"}'::jsonb, '{"ru": "4–6 штук для ВНЖ и соцкарты", "en": "4–6 for the residence permit and social card", "zh": "4–6 张,用于居留许可和社保卡", "hy": "4–6 հատ՝ կացության քարտի և սոցքարտի համար", "es": "4–6 para la residencia y la tarjeta social"}'::jsonb, 2, true, false),
|
||||
(gen_random_uuid()::text, 'DOC', 'birthCertificate', '{"ru": "Свидетельство о рождении", "en": "Birth certificate", "zh": "出生证明", "hy": "Ծննդյան վկայական", "es": "Partida de nacimiento"}'::jsonb, '{"ru": "С апостилем — нужно для ВНЖ", "en": "Apostilled — needed for the residence permit", "zh": "需海牙认证,办理居留许可时使用", "hy": "Ապոստիլով՝ պետք է կացության քարտի համար", "es": "Apostillada: se necesita para la residencia"}'::jsonb, 3, true, false),
|
||||
(gen_random_uuid()::text, 'DOC', 'marriageCertificate', '{"ru": "Свидетельство о браке", "en": "Marriage certificate", "zh": "结婚证", "hy": "Ամուսնության վկայական", "es": "Certificado de matrimonio"}'::jsonb, '{"ru": "С апостилем, если переезжаете семьёй", "en": "Apostilled, if moving as a family", "zh": "需海牙认证,全家搬迁时需要", "hy": "Ապոստիլով, եթե տեղափոխվում եք ընտանիքով", "es": "Apostillado, si os mudáis en familia"}'::jsonb, 4, true, false),
|
||||
(gen_random_uuid()::text, 'DOC', 'diploma', '{"ru": "Диплом", "en": "Diploma", "zh": "学历证书", "hy": "Դիպլոմ", "es": "Título universitario"}'::jsonb, '{"ru": "С апостилем — пригодится для работы и ВНЖ", "en": "Apostilled — useful for work and the residence permit", "zh": "需海牙认证,工作和居留许可时有用", "hy": "Ապոստիլով՝ պետք կգա աշխատանքի և կացության քարտի համար", "es": "Apostillado: útil para trabajar y para la residencia"}'::jsonb, 5, true, false),
|
||||
(gen_random_uuid()::text, 'DOC', 'criminalRecord', '{"ru": "Справка о несудимости", "en": "Criminal record certificate", "zh": "无犯罪记录证明", "hy": "Դատվածության բացակայության տեղեկանք", "es": "Certificado de antecedentes penales"}'::jsonb, '{"ru": "С апостилем, действует 3 месяца", "en": "Apostilled, valid for 3 months", "zh": "需海牙认证,有效期 3 个月", "hy": "Ապոստիլով, վավեր է 3 ամիս", "es": "Apostillado, válido 3 meses"}'::jsonb, 6, true, false),
|
||||
(gen_random_uuid()::text, 'DOC', 'insurance', '{"ru": "Медицинская страховка", "en": "Health insurance", "zh": "医疗保险", "hy": "Բժշկական ապահովագրություն", "es": "Seguro médico"}'::jsonb, '{"ru": "На первые месяцы, пока нет ВНЖ", "en": "For the first months before the residence permit", "zh": "覆盖办下居留许可前的头几个月", "hy": "Առաջին ամիսների համար, մինչև կացության քարտը", "es": "Para los primeros meses, hasta tener la residencia"}'::jsonb, 7, true, false)
|
||||
ON CONFLICT ("kind", "code") DO NOTHING;
|
||||
@@ -0,0 +1,81 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "OfferLimit" AS ENUM ('NONE', 'ONCE', 'WEEK', 'MONTH');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PartnerRole" AS ENUM ('OWNER', 'STAFF');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "CardUsage" ADD COLUMN "level" TEXT,
|
||||
ADD COLUMN "memberUserId" TEXT,
|
||||
ADD COLUMN "offerId" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Partner" ADD COLUMN "ownerInviteCode" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PartnerOffer" (
|
||||
"id" TEXT NOT NULL,
|
||||
"partnerId" TEXT NOT NULL,
|
||||
"title" JSONB NOT NULL,
|
||||
"minLevel" "SubscriptionPlan",
|
||||
"limit" "OfferLimit" NOT NULL DEFAULT 'NONE',
|
||||
"limitCount" INTEGER NOT NULL DEFAULT 1,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT "PartnerOffer_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PartnerMember" (
|
||||
"id" TEXT NOT NULL,
|
||||
"partnerId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"role" "PartnerRole" NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "PartnerMember_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PartnerInvite" (
|
||||
"id" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"partnerId" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"usedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "PartnerInvite_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PartnerMember_partnerId_userId_key" ON "PartnerMember"("partnerId", "userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PartnerInvite_code_key" ON "PartnerInvite"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "CardUsage_clientId_offerId_createdAt_idx" ON "CardUsage"("clientId", "offerId", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "CardUsage_partnerId_createdAt_idx" ON "CardUsage"("partnerId", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Partner_ownerInviteCode_key" ON "Partner"("ownerInviteCode");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PartnerOffer" ADD CONSTRAINT "PartnerOffer_partnerId_fkey" FOREIGN KEY ("partnerId") REFERENCES "Partner"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PartnerMember" ADD CONSTRAINT "PartnerMember_partnerId_fkey" FOREIGN KEY ("partnerId") REFERENCES "Partner"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PartnerMember" ADD CONSTRAINT "PartnerMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PartnerInvite" ADD CONSTRAINT "PartnerInvite_partnerId_fkey" FOREIGN KEY ("partnerId") REFERENCES "Partner"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CardUsage" ADD CONSTRAINT "CardUsage_offerId_fkey" FOREIGN KEY ("offerId") REFERENCES "PartnerOffer"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "StaffRole" AS ENUM ('SUPERADMIN', 'SUPPORT', 'CONCIERGE', 'SALES');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "AdminUser" ADD COLUMN "roles" "StaffRole"[] DEFAULT ARRAY[]::"StaffRole"[],
|
||||
ALTER COLUMN "email" DROP NOT NULL,
|
||||
ALTER COLUMN "passwordHash" DROP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Client" ADD COLUMN "adminNote" TEXT,
|
||||
ADD COLUMN "tags" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Subscription" ADD COLUMN "agencyId" TEXT,
|
||||
ADD COLUMN "amount" INTEGER,
|
||||
ADD COLUMN "cancelledAt" TIMESTAMP(3),
|
||||
ADD COLUMN "grantedByAdminId" TEXT,
|
||||
ADD COLUMN "note" TEXT,
|
||||
ADD COLUMN "payMethod" TEXT,
|
||||
ADD COLUMN "refundAmount" INTEGER,
|
||||
ADD COLUMN "refundedAt" TIMESTAMP(3);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AdminLoginRequest" (
|
||||
"id" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"pollKey" TEXT NOT NULL,
|
||||
"adminUserId" TEXT,
|
||||
"approvedAt" TIMESTAMP(3),
|
||||
"consumedAt" TIMESTAMP(3),
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "AdminLoginRequest_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SavedSegment" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"filters" JSONB NOT NULL,
|
||||
"adminUserId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "SavedSegment_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AdminLoginRequest_code_key" ON "AdminLoginRequest"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Subscription_createdAt_idx" ON "Subscription"("createdAt");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AdminLoginRequest" ADD CONSTRAINT "AdminLoginRequest_adminUserId_fkey" FOREIGN KEY ("adminUserId") REFERENCES "AdminUser"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SavedSegment" ADD CONSTRAINT "SavedSegment_adminUserId_fkey" FOREIGN KEY ("adminUserId") REFERENCES "AdminUser"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- существующие админы получают роли новой админки
|
||||
UPDATE "AdminUser" SET "roles" = ARRAY['SUPERADMIN']::"StaffRole"[] WHERE "role" = 'OWNER';
|
||||
UPDATE "AdminUser" SET "roles" = ARRAY['SUPPORT','CONCIERGE']::"StaffRole"[] WHERE "role" = 'OPERATOR';
|
||||
-- подарки агентств: запоминаем агентство
|
||||
UPDATE "Subscription" s SET "agencyId" = c."agencyId" FROM "Client" c WHERE s."clientId" = c.id AND s.source = 'AGENCY_GIFT';
|
||||
@@ -107,8 +107,17 @@ enum AdminRole {
|
||||
OPERATOR
|
||||
}
|
||||
|
||||
/// Роли новой админки (apps/backoffice). У одного человека может быть несколько.
|
||||
enum StaffRole {
|
||||
SUPERADMIN
|
||||
SUPPORT
|
||||
CONCIERGE
|
||||
SALES
|
||||
}
|
||||
|
||||
enum SubscriptionPlan {
|
||||
BAREV_PLUS
|
||||
ELITE
|
||||
}
|
||||
|
||||
enum SubscriptionSource {
|
||||
@@ -147,6 +156,15 @@ enum TaskStatus {
|
||||
DONE
|
||||
}
|
||||
|
||||
/// Заявка с сайта: NEW — человек ещё не открыл бота; LINKED — открыл, но заказ не создан (нужен звонок оператора);
|
||||
/// ORDERED — стала заказом; CANCELLED — отменена оператором
|
||||
enum WebRequestStatus {
|
||||
NEW
|
||||
LINKED
|
||||
ORDERED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
enum PayoutStatus {
|
||||
DRAFT
|
||||
APPROVED
|
||||
@@ -170,6 +188,7 @@ model User {
|
||||
client Client?
|
||||
provider Provider?
|
||||
dispatcher CompanyDispatcher[]
|
||||
partnerMembers PartnerMember[]
|
||||
messages Message[]
|
||||
}
|
||||
|
||||
@@ -189,6 +208,12 @@ model Client {
|
||||
giftUsedAt DateTime?
|
||||
/// короткий код клубной карты (Этап 3)
|
||||
cardCode String @unique
|
||||
/// сценарий приложения: переезжает или уже живёт в Армении; null — ещё не выбрал
|
||||
segment ClientSegment?
|
||||
/// метки команды: VIP, проблемный…
|
||||
tags String[] @default([])
|
||||
/// заметка команды о клиенте (клиент не видит)
|
||||
adminNote String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -197,6 +222,10 @@ model Client {
|
||||
ratings Rating[]
|
||||
cardUsages CardUsage[]
|
||||
promoUses PromoCodeUse[]
|
||||
webRequests WebRequest[]
|
||||
leads Lead[]
|
||||
requests ClientRequest[]
|
||||
concierge ConciergeMessage[]
|
||||
|
||||
@@index([agencyId])
|
||||
}
|
||||
@@ -254,6 +283,9 @@ model Provider {
|
||||
onShift Boolean @default(false)
|
||||
active Boolean @default(true)
|
||||
inviteCode String @unique
|
||||
/// пресет ставок (джун / мидл / прайват); если задан — заменяет rateType/rateValue
|
||||
payoutPresetId String?
|
||||
payoutPreset PayoutPreset? @relation(fields: [payoutPresetId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -265,14 +297,30 @@ model Provider {
|
||||
payouts Payout[]
|
||||
|
||||
@@index([role, active])
|
||||
concierge ConciergeMessage[]
|
||||
}
|
||||
|
||||
/// Пресет ставок исполнителя: для каждой строки цены — фикс или % (см. packages/core/src/payout.ts)
|
||||
model PayoutPreset {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
role ProviderRole
|
||||
rules Json
|
||||
sortOrder Int @default(0)
|
||||
active Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
providers Provider[]
|
||||
}
|
||||
|
||||
model District {
|
||||
id String @id @default(cuid())
|
||||
code String @unique
|
||||
sortOrder Int @default(0)
|
||||
providers ProviderDistrict[]
|
||||
orders Order[]
|
||||
providers ProviderDistrict[]
|
||||
orders Order[]
|
||||
webRequests WebRequest[]
|
||||
}
|
||||
|
||||
model ProviderDistrict {
|
||||
@@ -304,6 +352,8 @@ model Category {
|
||||
sortOrder Int @default(0)
|
||||
/// скидка Barev+ на направление, %
|
||||
barevPlusDiscountPct Int @default(0)
|
||||
/// скидка Barev Elite на направление, %
|
||||
eliteDiscountPct Int @default(0)
|
||||
active Boolean @default(true)
|
||||
services Service[]
|
||||
}
|
||||
@@ -330,8 +380,9 @@ model Service {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
checklist ChecklistItem[]
|
||||
orders Order[]
|
||||
checklist ChecklistItem[]
|
||||
orders Order[]
|
||||
webRequests WebRequest[]
|
||||
}
|
||||
|
||||
/// Шаблон пункта чек-листа услуги
|
||||
@@ -379,6 +430,8 @@ model Order {
|
||||
/// язык клиента на момент заказа
|
||||
locale Locale
|
||||
basePrice Int
|
||||
/// разбивка цены на момент заказа (основа, комнаты, санузлы, доп. услуги) — для расчёта выплат
|
||||
priceBreakdown Json?
|
||||
discountPct Int @default(0)
|
||||
discountAmount Int @default(0)
|
||||
totalPrice Int
|
||||
@@ -413,6 +466,7 @@ model Order {
|
||||
payments Payment[]
|
||||
commission AgencyCommission?
|
||||
tasks OperatorTask[]
|
||||
webRequest WebRequest?
|
||||
|
||||
@@index([status])
|
||||
@@index([scheduledAt])
|
||||
@@ -644,9 +698,22 @@ model Subscription {
|
||||
startsAt DateTime
|
||||
endsAt DateTime
|
||||
remindedAt DateTime?
|
||||
/// оплачено клиентом, AMD (PAID); для подарков пусто
|
||||
amount Int?
|
||||
/// cash | transfer | card | link
|
||||
payMethod String?
|
||||
/// кто из команды начислил / продал
|
||||
grantedByAdminId String?
|
||||
/// агентство, подарившее подписку (AGENCY_GIFT)
|
||||
agencyId String?
|
||||
note String?
|
||||
cancelledAt DateTime?
|
||||
refundAmount Int?
|
||||
refundedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([clientId, endsAt])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
// ───────────── Партнёры и клубная карта (Этап 3) ─────────────
|
||||
@@ -661,18 +728,90 @@ model Partner {
|
||||
terms Json?
|
||||
address String?
|
||||
active Boolean @default(true)
|
||||
/// одноразовая ссылка для собственника: t.me/<рабочий бот>?start=po-<код> (выдаёт админка)
|
||||
ownerInviteCode String? @unique
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
usages CardUsage[]
|
||||
offers PartnerOffer[]
|
||||
members PartnerMember[]
|
||||
invites PartnerInvite[]
|
||||
}
|
||||
|
||||
enum OfferLimit {
|
||||
/// без ограничений
|
||||
NONE
|
||||
/// только первое посещение
|
||||
ONCE
|
||||
/// N раз в календарную неделю
|
||||
WEEK
|
||||
/// N раз в календарный месяц
|
||||
MONTH
|
||||
}
|
||||
|
||||
/// Предложение партнёра: что положено клиенту с определённым уровнем и как часто
|
||||
model PartnerOffer {
|
||||
id String @id @default(cuid())
|
||||
partnerId String
|
||||
partner Partner @relation(fields: [partnerId], references: [id], onDelete: Cascade)
|
||||
/// { ru, en, zh, hy, es }
|
||||
title Json
|
||||
/// минимальный уровень: пусто — любой член клуба, BAREV_PLUS — Barev+ и Elite, ELITE — только Elite
|
||||
minLevel SubscriptionPlan?
|
||||
limit OfferLimit @default(NONE)
|
||||
limitCount Int @default(1)
|
||||
active Boolean @default(true)
|
||||
sortOrder Int @default(0)
|
||||
|
||||
usages CardUsage[]
|
||||
}
|
||||
|
||||
enum PartnerRole {
|
||||
OWNER
|
||||
STAFF
|
||||
}
|
||||
|
||||
/// Собственник или сотрудник партнёра — работает в рабочем боте: проверяет QR клиентов
|
||||
model PartnerMember {
|
||||
id String @id @default(cuid())
|
||||
partnerId String
|
||||
partner Partner @relation(fields: [partnerId], references: [id], onDelete: Cascade)
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
role PartnerRole
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([partnerId, userId])
|
||||
}
|
||||
|
||||
/// Приглашение сотрудника: собственник показывает QR, сотрудник сканирует — t.me/<рабочий бот>?start=pi-<код>
|
||||
model PartnerInvite {
|
||||
id String @id @default(cuid())
|
||||
code String @unique
|
||||
partnerId String
|
||||
partner Partner @relation(fields: [partnerId], references: [id], onDelete: Cascade)
|
||||
expiresAt DateTime
|
||||
usedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
/// Проверка клубной карты у партнёра (offerId пусто) или списание лимитного предложения
|
||||
model CardUsage {
|
||||
id String @id @default(cuid())
|
||||
clientId String
|
||||
client Client @relation(fields: [clientId], references: [id])
|
||||
partnerId String?
|
||||
partner Partner? @relation(fields: [partnerId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
id String @id @default(cuid())
|
||||
clientId String
|
||||
client Client @relation(fields: [clientId], references: [id])
|
||||
partnerId String?
|
||||
partner Partner? @relation(fields: [partnerId], references: [id])
|
||||
offerId String?
|
||||
offer PartnerOffer? @relation(fields: [offerId], references: [id], onDelete: SetNull)
|
||||
/// кто из сотрудников партнёра проверил
|
||||
memberUserId String?
|
||||
/// уровень клиента на момент проверки
|
||||
level String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([clientId, offerId, createdAt])
|
||||
@@index([partnerId, createdAt])
|
||||
}
|
||||
|
||||
// ───────────── Финансы ─────────────
|
||||
@@ -694,17 +833,53 @@ model Payout {
|
||||
|
||||
model AdminUser {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
/// вход по паролю — только старая админка; в новой вход через Telegram
|
||||
email String? @unique
|
||||
name String
|
||||
passwordHash String
|
||||
passwordHash String?
|
||||
role AdminRole @default(OPERATOR)
|
||||
/// роли в новой админке
|
||||
roles StaffRole[] @default([])
|
||||
totpSecret String?
|
||||
totpEnabled Boolean @default(false)
|
||||
active Boolean @default(true)
|
||||
/// Telegram оператора — уведомления приходят через рабочий бот
|
||||
telegramId BigInt? @unique
|
||||
/// одноразовый код ссылки t.me/<рабочий бот>?start=adm-<код>
|
||||
tgLinkToken String? @unique
|
||||
/// присылать уведомления в Telegram
|
||||
tgNotify Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
sessions AdminSession[]
|
||||
audit AuditLog[]
|
||||
sessions AdminSession[]
|
||||
audit AuditLog[]
|
||||
loginRequests AdminLoginRequest[]
|
||||
segments SavedSegment[]
|
||||
}
|
||||
|
||||
/// Вход в новую админку через Telegram: страница показывает ссылку t.me/<рабочий бот>?start=lg-<code>,
|
||||
/// бот подтверждает по telegramId, страница опрашивает статус и получает сессию
|
||||
model AdminLoginRequest {
|
||||
id String @id @default(cuid())
|
||||
code String @unique
|
||||
/// секрет браузера — по нему страница забирает сессию (code виден в ссылке)
|
||||
pollKey String
|
||||
adminUserId String?
|
||||
adminUser AdminUser? @relation(fields: [adminUserId], references: [id], onDelete: Cascade)
|
||||
approvedAt DateTime?
|
||||
consumedAt DateTime?
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
/// Сохранённая выборка клиентов (фильтры списка)
|
||||
model SavedSegment {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
filters Json
|
||||
adminUserId String
|
||||
adminUser AdminUser @relation(fields: [adminUserId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model AdminSession {
|
||||
@@ -747,3 +922,212 @@ model StartToken {
|
||||
utm Json?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
/// Заявка на услугу с сайта (barevclub.com/clean). Попадает в админку сразу; когда человек открывает бота
|
||||
/// по ссылке с token, заявка привязывается к клиенту и, если выбранное время свободно, становится заказом.
|
||||
model WebRequest {
|
||||
id String @id @default(cuid())
|
||||
number Int @unique @default(autoincrement())
|
||||
/// id StartToken — payload ссылки в бота
|
||||
token String @unique
|
||||
serviceId String
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
params Json
|
||||
/// желаемое время, оператор подтверждает
|
||||
preferredAt DateTime
|
||||
durationMinutes Int
|
||||
districtId String
|
||||
district District @relation(fields: [districtId], references: [id])
|
||||
addressText String
|
||||
note String?
|
||||
phone String
|
||||
name String?
|
||||
locale Locale
|
||||
/// цена на момент заявки, без скидки Barev+
|
||||
totalPrice Int
|
||||
consentAt DateTime
|
||||
consentVersion String
|
||||
status WebRequestStatus @default(NEW)
|
||||
clientId String?
|
||||
client Client? @relation(fields: [clientId], references: [id])
|
||||
orderId String? @unique
|
||||
order Order? @relation(fields: [orderId], references: [id])
|
||||
adminNote String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([status, createdAt])
|
||||
}
|
||||
|
||||
enum LeadStatus {
|
||||
NEW
|
||||
IN_WORK
|
||||
DONE
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
enum LeadContactType {
|
||||
PHONE
|
||||
TELEGRAM
|
||||
}
|
||||
|
||||
/// Заявка из квиза на главной («Переезжаю в Армению»). Создаётся сразу после шага с контактом,
|
||||
/// дальше дополняется ответами; submittedAt — человек дошёл до конца.
|
||||
model Lead {
|
||||
id String @id @default(cuid())
|
||||
number Int @unique @default(autoincrement())
|
||||
/// секрет для дозаписи ответов из браузера (id видят в URL логов, ключ — нет)
|
||||
key String
|
||||
name String
|
||||
contact String
|
||||
contactType LeadContactType
|
||||
locale Locale
|
||||
agency String?
|
||||
utm Json?
|
||||
/// уже живёт в Армении — квиз отправил на /live
|
||||
moved Boolean?
|
||||
/// month | quarter | later | date
|
||||
moveWhen String?
|
||||
moveDate DateTime?
|
||||
/// CONSULT | TURNKEY | CUSTOM
|
||||
package String?
|
||||
/// конструктор: { airport, stay, stayBudgetFrom, stayBudgetTo, flat, socialCard, bankCard, registration, residence }
|
||||
options Json?
|
||||
submittedAt DateTime?
|
||||
consentAt DateTime
|
||||
consentVersion String
|
||||
status LeadStatus @default(NEW)
|
||||
adminNote String?
|
||||
/// клиент в приложении — заявка из приложения или найдена по @username из квиза
|
||||
clientId String?
|
||||
client Client? @relation(fields: [clientId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
steps LeadStep[]
|
||||
docs LeadDoc[]
|
||||
|
||||
@@index([status, createdAt])
|
||||
@@index([clientId])
|
||||
}
|
||||
|
||||
enum ClientSegment {
|
||||
MOVING
|
||||
LIVING
|
||||
}
|
||||
|
||||
enum LeadStepStatus {
|
||||
TODO
|
||||
IN_PROGRESS
|
||||
DONE
|
||||
SKIPPED
|
||||
}
|
||||
|
||||
/// Шаг плана переезда (аэропорт, жильё, соцкарта…) — статус двигает оператор, клиент видит прогресс
|
||||
model LeadStep {
|
||||
id String @id @default(cuid())
|
||||
leadId String
|
||||
lead Lead @relation(fields: [leadId], references: [id], onDelete: Cascade)
|
||||
/// код из шаблона (RelocationItem) или custom-… для шага, добавленного только этому клиенту
|
||||
code String
|
||||
/// своё название для этого клиента; пусто — название из шаблона
|
||||
title String?
|
||||
sortOrder Int
|
||||
status LeadStepStatus @default(TODO)
|
||||
/// комментарий оператора для клиента: «просмотры в среду в 15:00»
|
||||
note String?
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([leadId, code])
|
||||
}
|
||||
|
||||
enum RelocationItemKind {
|
||||
STEP
|
||||
DOC
|
||||
}
|
||||
|
||||
/// Общий список шагов переезда и документов — редактируется в админке; у клиента копия, которую можно менять отдельно
|
||||
model RelocationItem {
|
||||
id String @id @default(cuid())
|
||||
kind RelocationItemKind
|
||||
code String
|
||||
/// { ru, en, zh, hy, es }
|
||||
name Json
|
||||
/// подсказка под названием (для документов)
|
||||
hint Json?
|
||||
sortOrder Int @default(0)
|
||||
active Boolean @default(true)
|
||||
/// шаг входит в пакет «под ключ»
|
||||
inTurnkey Boolean @default(true)
|
||||
|
||||
@@unique([kind, code])
|
||||
}
|
||||
|
||||
/// Документ в чек-листе конкретного клиента
|
||||
model LeadDoc {
|
||||
id String @id @default(cuid())
|
||||
leadId String
|
||||
lead Lead @relation(fields: [leadId], references: [id], onDelete: Cascade)
|
||||
code String
|
||||
title String?
|
||||
checked Boolean @default(false)
|
||||
sortOrder Int
|
||||
|
||||
@@unique([leadId, code])
|
||||
}
|
||||
|
||||
enum ClientRequestKind {
|
||||
SUBSCRIPTION
|
||||
SERVICE
|
||||
}
|
||||
|
||||
enum ClientRequestStatus {
|
||||
NEW
|
||||
DONE
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
/// Заявка из приложения оператору: «хочу Elite», «перезвоните по ремонту»
|
||||
model ClientRequest {
|
||||
id String @id @default(cuid())
|
||||
number Int @unique @default(autoincrement())
|
||||
clientId String
|
||||
client Client @relation(fields: [clientId], references: [id])
|
||||
kind ClientRequestKind
|
||||
plan SubscriptionPlan?
|
||||
serviceCode String?
|
||||
comment String?
|
||||
status ClientRequestStatus @default(NEW)
|
||||
adminNote String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([status, createdAt])
|
||||
@@index([clientId])
|
||||
}
|
||||
|
||||
enum ConciergeAuthor {
|
||||
CLIENT
|
||||
PROVIDER
|
||||
OPERATOR
|
||||
}
|
||||
|
||||
/// Переписка из админки: с клиентом (клиентский бот) или с исполнителем (рабочий бот); ответ приходит в тот же чат с ботом
|
||||
model ConciergeMessage {
|
||||
id String @id @default(cuid())
|
||||
clientId String?
|
||||
client Client? @relation(fields: [clientId], references: [id])
|
||||
providerId String?
|
||||
provider Provider? @relation(fields: [providerId], references: [id])
|
||||
author ConciergeAuthor
|
||||
/// кто из операторов ответил
|
||||
adminUserId String?
|
||||
text String
|
||||
/// оператор прочитал сообщение клиента
|
||||
readAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([clientId, createdAt])
|
||||
@@index([providerId, createdAt])
|
||||
@@index([author, readAt])
|
||||
}
|
||||
Reference in new issue
Block a user