Apps, web, i18n, UI, partners, payouts, backoffice, bariki, prod infra
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
45d7512d2b
commit
3489e0b816
319 files changed
+45483
-121
No files matched your search
@@ -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