// Barev Club — модель данных (PRD, раздел 7). // Схема заложена сразу целиком; часть сущностей используется начиная с Этапов 2–3. generator client { provider = "prisma-client" output = "../src/generated/prisma" moduleFormat = "esm" } datasource db { provider = "postgresql" } // ───────────── Справочные перечисления ───────────── enum Locale { ru en zh hy es } /// Направление (раздел 2) enum CategoryCode { HOME DOCS LEARNING PERKS } /// Сценарий заказа — определяет state machine и шаги исполнителя enum ServiceFlow { CLEANING REPAIR TRANSFER LANGUAGE CAREER DOCUMENTS } enum ProviderRole { CLEANER HANDYMAN DRIVER TEACHER CAREER_CONSULTANT DOCS_AGENT } /// Все статусы всех сценариев. Допустимые переходы — в packages/core (flows). enum OrderStatus { NEW ASSIGNED EN_ROUTE CLEANING ON_SITE ESTIMATE_APPROVED REPAIRING DRIVER_WAITING RIDE_IN_PROGRESS ENROLLED LESSON_SCHEDULED LESSON_IN_PROGRESS LESSON_DONE CONSULTANT_ASSIGNED RESUME_RECEIVED MEETING PLAN_SENT DOCS_REVIEW NEEDS_MORE_DOCS SUBMITTED DONE COMPLETED CANCELLED } enum ActorType { CLIENT PROVIDER DISPATCHER ADMIN SYSTEM } enum OfferStatus { OFFERED ACCEPTED DECLINED EXPIRED REVOKED } enum PaymentStatus { UNPAID PAID REFUNDED } enum RateType { FIXED PERCENT } enum AdminRole { OWNER OPERATOR } /// Роли новой админки (apps/backoffice). У одного человека может быть несколько. enum StaffRole { SUPERADMIN SUPPORT CONCIERGE SALES } enum SubscriptionPlan { BAREV_PLUS ELITE } enum SubscriptionSource { AGENCY_GIFT MANUAL PAID } enum SubscriptionStatus { ACTIVE EXPIRED CANCELLED } enum PromoKind { AGENCY_GIFT DISCOUNT } enum FileKind { PHOTO_REPORT CLIENT_PHOTO DOCUMENT_SCAN RESUME OTHER } enum EstimateStatus { PENDING ACCEPTED REJECTED } enum TaskStatus { OPEN DONE } /// Заявка с сайта: NEW — человек ещё не открыл бота; LINKED — открыл, но заказ не создан (нужен звонок оператора); /// ORDERED — стала заказом; CANCELLED — отменена оператором enum WebRequestStatus { NEW LINKED ORDERED CANCELLED } enum PayoutStatus { DRAFT APPROVED PAID } // ───────────── Пользователи ───────────── /// Пользователь Telegram. Один и тот же человек может быть клиентом и исполнителем. model User { id String @id @default(cuid()) telegramId BigInt @unique username String? firstName String? lastName String? phone String? locale Locale @default(ru) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt client Client? provider Provider? dispatcher CompanyDispatcher[] partnerMembers PartnerMember[] messages Message[] } model Client { id String @id @default(cuid()) userId String @unique user User @relation(fields: [userId], references: [id]) consentAt DateTime? consentVersion String? agencyId String? agency Agency? @relation(fields: [agencyId], references: [id]) agencyAttachedAt DateTime? /// источник: deeplink | manual_code | organic source String @default("organic") utm Json? /// подарочный месяц уже использован (один раз на клиента) giftUsedAt DateTime? /// короткий код клубной карты (Этап 3) cardCode String @unique /// сценарий приложения: переезжает или уже живёт в Армении; null — ещё не выбрал segment ClientSegment? /// метки команды: VIP, проблемный… tags String[] @default([]) /// заметка команды о клиенте (клиент не видит) adminNote String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt orders Order[] subscriptions Subscription[] ratings Rating[] cardUsages CardUsage[] promoUses PromoCodeUse[] webRequests WebRequest[] leads Lead[] requests ClientRequest[] concierge ConciergeMessage[] chatThreads ChatThread[] /// куда сейчас пишет клиент: SUPPORT или CONCIERGE (переключается командой бота или командой поддержки); /// null — по умолчанию: подписчик → консьерж, остальные → поддержка chatChannel ChatChannel? @@index([agencyId]) } // ───────────── Исполнители ───────────── model ProviderCompany { id String @id @default(cuid()) name String /// какие роли исполнителей есть в компании (какие услуги она берёт) roles ProviderRole[] /// договорная ставка компании rateType RateType @default(PERCENT) rateValue Int @default(70) contactPhone String? inviteCode String @unique active Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt employees Provider[] dispatchers CompanyDispatcher[] orders Order[] offers OrderOffer[] payouts Payout[] } model CompanyDispatcher { id String @id @default(cuid()) companyId String company ProviderCompany @relation(fields: [companyId], references: [id]) userId String user User @relation(fields: [userId], references: [id]) createdAt DateTime @default(now()) @@unique([companyId, userId]) } model Provider { id String @id @default(cuid()) /// заполняется, когда исполнитель открыл бота по инвайт-коду userId String? @unique user User? @relation(fields: [userId], references: [id]) name String phone String? role ProviderRole companyId String? company ProviderCompany? @relation(fields: [companyId], references: [id]) languages Locale[] /// ставка фрилансера; для сотрудников компании не используется rateType RateType @default(PERCENT) rateValue Int @default(70) ratingSum Int @default(0) ratingCount Int @default(0) 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 districts ProviderDistrict[] schedule ProviderSchedule[] orders Order[] offers OrderOffer[] ratings Rating[] payouts Payout[] @@index([role, active]) concierge ConciergeMessage[] chatThreads ChatThread[] } /// Пресет ставок исполнителя: для каждой строки цены — фикс или % (см. 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[] webRequests WebRequest[] } model ProviderDistrict { providerId String provider Provider @relation(fields: [providerId], references: [id], onDelete: Cascade) districtId String district District @relation(fields: [districtId], references: [id], onDelete: Cascade) @@id([providerId, districtId]) } /// Недельный график: 0 = понедельник … 6 = воскресенье, минуты от полуночи по Asia/Yerevan model ProviderSchedule { id String @id @default(cuid()) providerId String provider Provider @relation(fields: [providerId], references: [id], onDelete: Cascade) weekday Int startMinute Int endMinute Int @@index([providerId, weekday]) } // ───────────── Каталог ───────────── model Category { id String @id @default(cuid()) code CategoryCode @unique sortOrder Int @default(0) /// скидка Barev+ на направление, % barevPlusDiscountPct Int @default(0) /// скидка Barev Elite на направление, % eliteDiscountPct Int @default(0) active Boolean @default(true) services Service[] } model Service { id String @id @default(cuid()) /// код услуги; тексты берутся из i18n по ключу services..* code String @unique categoryId String category Category @relation(fields: [categoryId], references: [id]) flow ServiceFlow providerRole ProviderRole /// правила цены (см. packages/core/src/pricing) pricing Json /// описание параметров формы заказа (см. packages/core/src/params) paramsSchema Json /// окно записи и шаг слотов (см. packages/core/src/slots) slotConfig Json /// удержание при поздней отмене, % lateCancelFeePct Int @default(30) isOnline Boolean @default(false) active Boolean @default(true) sortOrder Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt checklist ChecklistItem[] orders Order[] webRequests WebRequest[] } /// Шаблон пункта чек-листа услуги model ChecklistItem { id String @id @default(cuid()) serviceId String service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade) /// код пункта; подпись — i18n checklist. code String requiresPhoto Boolean @default(false) sortOrder Int @default(0) @@unique([serviceId, code]) } model PaymentMethod { id String @id @default(cuid()) code String @unique /// { ru, en, zh, hy, es } name Json active Boolean @default(true) sortOrder Int @default(0) orders Order[] } // ───────────── Заказы ───────────── model Order { id String @id @default(cuid()) number Int @unique @default(autoincrement()) clientId String client Client @relation(fields: [clientId], references: [id]) serviceId String service Service @relation(fields: [serviceId], references: [id]) params Json scheduledAt DateTime durationMinutes Int isOnline Boolean @default(false) addressText String? districtId String? district District? @relation(fields: [districtId], references: [id]) lat Float? lng Float? note String? /// язык клиента на момент заказа locale Locale basePrice Int /// разбивка цены на момент заказа (основа, комнаты, санузлы, доп. услуги) — для расчёта выплат priceBreakdown Json? discountPct Int @default(0) discountAmount Int @default(0) totalPrice Int status OrderStatus @default(NEW) providerId String? provider Provider? @relation(fields: [providerId], references: [id]) /// заказ передан компании; пока providerId пуст — «Назначен на компанию» companyId String? company ProviderCompany? @relation(fields: [companyId], references: [id]) paymentMethodId String? paymentMethod PaymentMethod? @relation(fields: [paymentMethodId], references: [id]) paymentStatus PaymentStatus @default(UNPAID) paidAt DateTime? cancelledAt DateTime? cancelFee Int @default(0) cancelReason String? completedAt DateTime? repeatOfId String? reminded24hAt DateTime? reminded1hAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt history OrderStatusHistory[] checklist OrderChecklistItem[] offers OrderOffer[] files OrderFile[] messages Message[] estimates Estimate[] rating Rating? lessons LessonPackage? payments Payment[] commission AgencyCommission? tasks OperatorTask[] webRequest WebRequest? @@index([status]) @@index([scheduledAt]) @@index([clientId]) @@index([providerId, scheduledAt]) } model OrderStatusHistory { id String @id @default(cuid()) orderId String order Order @relation(fields: [orderId], references: [id], onDelete: Cascade) fromStatus OrderStatus? toStatus OrderStatus /// событие без смены статуса (например «Я на месте») или код действия event String? actorType ActorType actorId String? comment String? createdAt DateTime @default(now()) @@index([orderId, createdAt]) } /// Предложение заказа исполнителю или компании (принять / отклонить с таймаутом) model OrderOffer { id String @id @default(cuid()) orderId String order Order @relation(fields: [orderId], references: [id], onDelete: Cascade) providerId String? provider Provider? @relation(fields: [providerId], references: [id]) companyId String? company ProviderCompany? @relation(fields: [companyId], references: [id]) status OfferStatus @default(OFFERED) expiresAt DateTime? respondedAt DateTime? createdById String? createdAt DateTime @default(now()) @@index([orderId, status]) @@index([providerId, status]) @@index([companyId, status]) } model OrderChecklistItem { id String @id @default(cuid()) orderId String order Order @relation(fields: [orderId], references: [id], onDelete: Cascade) code String requiresPhoto Boolean @default(false) sortOrder Int @default(0) checkedAt DateTime? fileId String? @unique file OrderFile? @relation(fields: [fileId], references: [id]) @@unique([orderId, code]) } model OrderFile { id String @id @default(cuid()) orderId String order Order @relation(fields: [orderId], references: [id], onDelete: Cascade) kind FileKind storageKey String @unique mime String size Int uploadedBy ActorType uploadedById String? /// дата автоудаления (документы — через N дней после завершения) deleteAfter DateTime? deletedAt DateTime? createdAt DateTime @default(now()) checklistItem OrderChecklistItem? accessLog FileAccessLog[] } model FileAccessLog { id String @id @default(cuid()) fileId String file OrderFile @relation(fields: [fileId], references: [id], onDelete: Cascade) actorType ActorType actorId String? createdAt DateTime @default(now()) } /// Смета ремонта (Этап 2) model Estimate { id String @id @default(cuid()) orderId String order Order @relation(fields: [orderId], references: [id], onDelete: Cascade) items Json total Int status EstimateStatus @default(PENDING) respondedAt DateTime? createdAt DateTime @default(now()) } /// Чат клиент ↔ исполнитель через бота (Этап 2) model Message { id String @id @default(cuid()) orderId String order Order @relation(fields: [orderId], references: [id], onDelete: Cascade) fromType ActorType fromUserId String? fromUser User? @relation(fields: [fromUserId], references: [id]) text String? fileId String? createdAt DateTime @default(now()) @@index([orderId, createdAt]) } model Rating { id String @id @default(cuid()) orderId String @unique order Order @relation(fields: [orderId], references: [id], onDelete: Cascade) clientId String client Client @relation(fields: [clientId], references: [id]) providerId String? provider Provider? @relation(fields: [providerId], references: [id]) score Int comment String? createdAt DateTime @default(now()) } /// Пакет уроков (Этап 2) model LessonPackage { id String @id @default(cuid()) orderId String @unique order Order @relation(fields: [orderId], references: [id], onDelete: Cascade) total Int completed Int @default(0) } model Payment { id String @id @default(cuid()) orderId String order Order @relation(fields: [orderId], references: [id], onDelete: Cascade) /// manual | <будущий провайдер> provider String amount Int status PaymentStatus externalId String? markedById String? createdAt DateTime @default(now()) } model OperatorTask { id String @id @default(cuid()) type String orderId String? order Order? @relation(fields: [orderId], references: [id], onDelete: Cascade) status TaskStatus @default(OPEN) note String? createdAt DateTime @default(now()) doneAt DateTime? } // ───────────── Агентства, промокоды, членство ───────────── model Agency { id String @id @default(cuid()) /// код для deep-link и ручного ввода (латиница/цифры) code String @unique name String /// вознаграждение, % от оплаченных заказов клиентов агентства commissionPct Int @default(10) contact String? active Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt clients Client[] commissions AgencyCommission[] promoCodes PromoCode[] } model AgencyCommission { id String @id @default(cuid()) agencyId String agency Agency @relation(fields: [agencyId], references: [id]) orderId String @unique order Order @relation(fields: [orderId], references: [id]) pct Int amount Int /// YYYY-MM period String createdAt DateTime @default(now()) @@index([agencyId, period]) } model PromoCode { id String @id @default(cuid()) code String @unique kind PromoKind agencyId String? agency Agency? @relation(fields: [agencyId], references: [id]) discountPct Int? maxUses Int? usedCount Int @default(0) validFrom DateTime? validUntil DateTime? active Boolean @default(true) createdAt DateTime @default(now()) uses PromoCodeUse[] } model PromoCodeUse { id String @id @default(cuid()) promoId String promo PromoCode @relation(fields: [promoId], references: [id]) clientId String client Client @relation(fields: [clientId], references: [id]) orderId String? createdAt DateTime @default(now()) @@unique([promoId, clientId]) } model Subscription { id String @id @default(cuid()) clientId String client Client @relation(fields: [clientId], references: [id]) plan SubscriptionPlan @default(BAREV_PLUS) source SubscriptionSource status SubscriptionStatus @default(ACTIVE) 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) ───────────── model Partner { id String @id @default(cuid()) /// cafe | restaurant | other category String name String /// { ru, en, ... } offer Json 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]) 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]) } // ───────────── Финансы ───────────── model Payout { id String @id @default(cuid()) providerId String? provider Provider? @relation(fields: [providerId], references: [id]) companyId String? company ProviderCompany? @relation(fields: [companyId], references: [id]) periodStart DateTime periodEnd DateTime amount Int status PayoutStatus @default(DRAFT) createdAt DateTime @default(now()) } // ───────────── Админка ───────────── model AdminUser { id String @id @default(cuid()) /// вход по паролю — только старая админка; в новой вход через Telegram email String? @unique name 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[] loginRequests AdminLoginRequest[] segments SavedSegment[] chatThreads ChatThread[] chatTemplates ChatTemplate[] } /// Вход в новую админку через Telegram: страница показывает ссылку t.me/<рабочий бот>?start=lg-, /// бот подтверждает по 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 { id String @id @default(cuid()) adminUserId String adminUser AdminUser @relation(fields: [adminUserId], references: [id], onDelete: Cascade) /// sha256 токена сессии tokenHash String @unique /// сессия прошла 2FA verified Boolean @default(false) expiresAt DateTime createdAt DateTime @default(now()) } model AuditLog { id String @id @default(cuid()) adminUserId String? adminUser AdminUser? @relation(fields: [adminUserId], references: [id]) action String entity String entityId String? data Json? createdAt DateTime @default(now()) @@index([entity, entityId]) } /// Настройки (ключ → значение). Список ключей и дефолты — packages/core/src/settings.ts model Setting { key String @id value Json updatedAt DateTime @updatedAt } /// Короткий токен для deep-link с лендинга, когда payload не влезает в 64 символа (UTM) model StartToken { id String @id locale Locale? agency String? 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 /// ИИ-консьерж (на будущее): отвечает сам, человек может перехватить диалог BOT /// служебная строка в ленте: «взял диалог», «передал», «закрыл» SYSTEM } /// Сообщение в чате. Клиент пишет боту или в мини-аппе — всё попадает в один диалог (ChatThread). /// Ответ команды приходит клиенту в чат с ботом (клиентский бот, для исполнителя — рабочий). model ConciergeMessage { id String @id @default(cuid()) /// диалог; у старых сообщений до этапа 2 проставлен миграцией threadId String? thread ChatThread? @relation(fields: [threadId], references: [id], onDelete: Cascade) clientId String? client Client? @relation(fields: [clientId], references: [id]) providerId String? provider Provider? @relation(fields: [providerId], references: [id]) author ConciergeAuthor /// кто из команды написал (author = OPERATOR / SYSTEM) adminUserId String? text String /// внутренняя заметка команды — клиент её не видит internal Boolean @default(false) /// откуда пришло: BOT | MINIAPP | ADMIN source String @default("BOT") /// файл: ключ в приватном хранилище (загрузила команда / мини-апп)… fileKey String? /// …или file_id в Telegram (прислал клиент боту); бот — client | pro tgFileId String? tgBot String? fileName String? mime String? size Int? /// не удалось доставить в Telegram (клиент заблокировал бота и т.п.) deliveryError String? /// оператор прочитал сообщение клиента readAt DateTime? createdAt DateTime @default(now()) @@index([clientId, createdAt]) @@index([providerId, createdAt]) @@index([author, readAt]) @@index([threadId, createdAt]) } enum ChatChannel { /// поддержка — все клиенты и исполнители SUPPORT /// консьерж — только подписчики Barev+ / Elite CONCIERGE } enum ChatStatus { OPEN CLOSED } /// Диалог: один на собеседника в каждом канале. Закрытый диалог открывается заново, когда клиент пишет снова. model ChatThread { id String @id @default(cuid()) channel ChatChannel clientId String? client Client? @relation(fields: [clientId], references: [id]) providerId String? provider Provider? @relation(fields: [providerId], references: [id]) status ChatStatus @default(OPEN) /// кто из команды ведёт диалог assigneeId String? assignee AdminUser? @relation(fields: [assigneeId], references: [id]) /// диалог ведёт ИИ-консьерж (задел на будущее); человек может перехватить botActive Boolean @default(false) lastMessageAt DateTime @default(now()) /// с какого момента клиент ждёт ответа; null — ответили waitingSince DateTime? closedAt DateTime? createdAt DateTime @default(now()) messages ConciergeMessage[] @@unique([clientId, channel]) @@unique([providerId, channel]) @@index([channel, status, lastMessageAt]) @@index([assigneeId, status]) } /// Шаблон быстрого ответа в чате. {name} подставляется именем клиента. model ChatTemplate { id String @id @default(cuid()) /// для какого канала; null — для обоих channel ChatChannel? title String text String createdById String? createdBy AdminUser? @relation(fields: [createdById], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }