Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
750 lines
21 KiB
Plaintext
750 lines
21 KiB
Plaintext
// 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>.*
|
|
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>
|
|
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())
|
|
}
|