// 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 } enum SubscriptionPlan { BAREV_PLUS } 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 } 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[] 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 createdAt DateTime @default(now()) updatedAt DateTime @updatedAt orders Order[] subscriptions Subscription[] ratings Rating[] cardUsages CardUsage[] promoUses PromoCodeUse[] @@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 createdAt DateTime @default(now()) updatedAt DateTime @updatedAt districts ProviderDistrict[] schedule ProviderSchedule[] orders Order[] offers OrderOffer[] ratings Rating[] payouts Payout[] @@index([role, active]) } model District { id String @id @default(cuid()) code String @unique sortOrder Int @default(0) providers ProviderDistrict[] orders Order[] } 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) 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[] } /// Шаблон пункта чек-листа услуги 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 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[] @@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? createdAt DateTime @default(now()) @@index([clientId, endsAt]) } // ───────────── Партнёры и клубная карта (Этап 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) createdAt DateTime @default(now()) usages CardUsage[] } 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()) } // ───────────── Финансы ───────────── 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()) email String @unique name String passwordHash String role AdminRole @default(OPERATOR) totpSecret String? totpEnabled Boolean @default(false) active Boolean @default(true) createdAt DateTime @default(now()) sessions AdminSession[] audit AuditLog[] } 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()) }