Bariki cashback, order edit, and current work in progress

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zabudko-ruandClaude Opus 5.5 committed 2026-10-25 22:57:03 +04:00
1 parent dda917fc71
commit 45477fb018
94 files changed
+10955 -650

No files matched your search

+309 -4
View File
@@ -97,6 +97,14 @@ enum PaymentStatus {
REFUNDED
}
/// Модель сотрудничества с партнёром услуг
enum PartnerDeal {
/// расходы наши: платим ФОТ и налог, партнёру — % от прибыли
PROFIT_SHARE
/// как Uber: забираем свой % с заказа, остальное партнёру, расходы — его
COMMISSION
}
enum RateType {
FIXED
PERCENT
@@ -190,6 +198,7 @@ model User {
dispatcher CompanyDispatcher[]
partnerMembers PartnerMember[]
messages Message[]
proWebSessions ProWebSession[]
}
model Client {
@@ -231,6 +240,25 @@ model Client {
/// null — по умолчанию: подписчик → консьерж, остальные → поддержка
chatChannel ChatChannel?
// ── барики (кэшбек) — кэш баланса; источник правды — журнал PointsAccrual / PointsRedemption ──
/// доступно: остатки активных начислений минус долг
pointsActive Int @default(0)
/// в ожидании (станут доступны позже)
pointsPending Int @default(0)
/// долг после отмены уже потраченного начисления; гасится следующими начислениями
pointsDebt Int @default(0)
/// последнее начисление или списание — от неё считается сгорание
pointsActivityAt DateTime?
/// предупреждения о сгорании уже отправлены (сбрасываются при активности)
pointsWarn14At DateTime?
pointsWarn3At DateTime?
/// серия уборок: сколько подряд с перерывами не больше N дней
streakCount Int @default(0)
streakLastAt DateTime?
pointsAccruals PointsAccrual[]
pointsRedemptions PointsRedemption[]
pointsSuggestions PointsSuggestion[]
@@index([agencyId])
}
@@ -245,11 +273,28 @@ model ProviderCompany {
rateType RateType @default(PERCENT)
rateValue Int @default(70)
contactPhone String?
/// ссылка менеджера: t.me/<рабочий бот>?start=<inviteCode>
inviteCode String @unique
active Boolean @default(true)
/// модель сотрудничества (rateType/rateValue — устаревшая ставка, не используется)
deal PartnerDeal @default(COMMISSION)
/// COMMISSION: наш % с оплаченной суммы заказа
commissionPct Int @default(30)
/// PROFIT_SHARE: % партнёра от (оплачено − ФОТ − налог)
profitSharePct Int @default(50)
/// PROFIT_SHARE: свой ФОТ клинера, % от заказа; пусто — из настроек
fotPct Int?
/// PROFIT_SHARE: свой налог, %; пусто — из настроек
taxPct Int?
/// менеджер включил автораспределение по сотрудникам
autoAssign Boolean @default(false)
/// перерыв между заказами сотрудника на дорогу, мин (не меньше минимума из настроек)
travelGapMinutes Int @default(60)
note String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
earnings PartnerEarning[]
employees Provider[]
dispatchers CompanyDispatcher[]
orders Order[]
@@ -295,6 +340,9 @@ model Provider {
blockedAt DateTime?
/// заметка команды об исполнителе (он не видит)
adminNote String?
/// нагрузка (задаёт менеджер партнёра): максимум заказов и часов в день; пусто — без ограничения
maxOrdersPerDay Int?
maxHoursPerDay Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -391,6 +439,9 @@ model Service {
code String @unique
categoryId String
category Category @relation(fields: [categoryId], references: [id])
/// категория кэшбека «Барики» (ставки Barev / Barev+ / Elite)
pointsCategoryId String?
pointsCategory PointsCategory? @relation(fields: [pointsCategoryId], references: [id])
flow ServiceFlow
providerRole ProviderRole
/// правила цены (см. packages/core/src/pricing)
@@ -462,6 +513,8 @@ model Order {
discountPct Int @default(0)
discountAmount Int @default(0)
totalPrice Int
/// оплачено бариками (к оплате деньгами: totalPrice − pointsUsed)
pointsUsed Int @default(0)
status OrderStatus @default(NEW)
providerId String?
provider Provider? @relation(fields: [providerId], references: [id])
@@ -493,6 +546,9 @@ model Order {
lessons LessonPackage?
payments Payment[]
commission AgencyCommission?
partnerEarning PartnerEarning?
pointsAccruals PointsAccrual[]
pointsRedemptions PointsRedemption[]
tasks OperatorTask[]
webRequest WebRequest?
@@ -548,6 +604,10 @@ model OrderChecklistItem {
checkedAt DateTime?
fileId String? @unique
file OrderFile? @relation(fields: [fileId], references: [id])
/// пункт отметила команда за исполнителя (id сотрудника админки)
checkedByAdminId String?
/// отмечено без фото — причина (команда)
photoWaivedReason String?
@@unique([orderId, code])
}
@@ -662,6 +722,12 @@ model Agency {
name String
/// вознаграждение, % от оплаченных заказов клиентов агентства
commissionPct Int @default(10)
/// подарок новым клиентам: уровень (пусто — без подарка) и срок в месяцах
giftPlan SubscriptionPlan? @default(BAREV_PLUS)
giftMonths Int @default(1)
/// барики на новоселье: сумма (0 — нет) и срок жизни в днях
housewarmingPoints Int @default(0)
housewarmingDays Int @default(60)
contact String?
active Boolean @default(true)
/// продажник, который подключил агентство (бонус в CRM)
@@ -694,6 +760,30 @@ model AgencyCommission {
@@index([agencyId, period])
}
/// Доход партнёра услуг с выполненного заказа — фиксируется при завершении, чтобы смена условий не меняла прошлое
model PartnerEarning {
id String @id @default(cuid())
companyId String
company ProviderCompany @relation(fields: [companyId], references: [id])
orderId String @unique
order Order @relation(fields: [orderId], references: [id])
deal PartnerDeal
/// фактически оплачено клиентом
paid Int
/// PROFIT_SHARE: ФОТ и налог, которые несём мы
fot Int @default(0)
tax Int @default(0)
/// % партнёра (PROFIT_SHARE) или наша комиссия (COMMISSION)
pct Int
/// доход партнёра
amount Int
/// YYYY-MM
period String
createdAt DateTime @default(now())
@@index([companyId, period])
}
model PromoCode {
id String @id @default(cuid())
code String @unique
@@ -998,11 +1088,11 @@ model WebRequest {
service Service @relation(fields: [serviceId], references: [id])
params Json
/// желаемое время, оператор подтверждает
preferredAt DateTime
preferredAt DateTime?
durationMinutes Int
districtId String
district District @relation(fields: [districtId], references: [id])
addressText String
districtId String?
district District? @relation(fields: [districtId], references: [id])
addressText String?
note String?
phone String
name String?
@@ -1435,3 +1525,218 @@ model BroadcastRecipient {
@@unique([broadcastId, clientId])
@@index([broadcastId, status])
}
/// Вход в кабинет менеджера партнёра с компьютера: бот выдаёт одноразовую ссылку (code), браузер меняет её на токен
model ProWebSession {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
/// sha256 одноразового кода из ссылки; после входа обнуляется
codeHash String? @unique
codeExpiresAt DateTime?
/// sha256 токена браузера
tokenHash String? @unique
expiresAt DateTime
lastUsedAt DateTime?
createdAt DateTime @default(now())
@@index([userId])
}
// ───────────── Барики: кэшбек клуба (1 барик = 1 ֏) ─────────────
enum PointsKind {
ORDER
BONUS
}
enum PointsStatus {
/// создано, тратить нельзя до availableAt
PENDING
ACTIVE
/// израсходовано полностью
SPENT
/// сгорело
EXPIRED
/// отменено
REVERSED
}
enum PointsChannel {
WEB
BOT
CSV
AUTO
CLIENT
}
enum RedemptionStatus {
REQUESTED
CONFIRMED
REJECTED
REVERSED
}
enum SuggestionStatus {
OPEN
ACCEPTED
REJECTED
}
/// Категория кэшбека со ставками по уровням, %
model PointsCategory {
id String @id @default(cuid())
code String @unique
/// { ru, en, zh, hy, es }
name Json
rateBarev Int
ratePlus Int
rateElite Int
active Boolean @default(true)
sortOrder Int @default(0)
services Service[]
accruals PointsAccrual[]
}
/// Тип бонуса: новоселье, отзыв, компенсация…
model PointsBonusType {
id String @id @default(cuid())
code String @unique
name Json
defaultAmount Int
/// срок жизни бонуса в днях; пусто — общее правило сгорания
ttlDays Int?
active Boolean @default(true)
sortOrder Int @default(0)
accruals PointsAccrual[]
}
/// Начисление бариков (журнал: не удаляется и не правится, кроме остатка и статуса)
model PointsAccrual {
id String @id @default(cuid())
clientId String
client Client @relation(fields: [clientId], references: [id])
kind PointsKind
categoryId String?
category PointsCategory? @relation(fields: [categoryId], references: [id])
bonusTypeId String?
bonusType PointsBonusType? @relation(fields: [bonusTypeId], references: [id])
/// заказ в системе (автоначисление)
orderId String?
order Order? @relation(fields: [orderId], references: [id])
/// номер заказа для сверки; среди неотменённых начислений уникален (индекс в миграции)
orderRef String?
orderAmount Int?
excludedAmount Int @default(0)
paidWithPoints Int @default(0)
/// BAREV | BAREV_PLUS | ELITE — на дату услуги
tier String?
ratePct Int?
/// надбавка за серию уборок, п.п.
streakBonusPct Int @default(0)
suggestedAmount Int?
amount Int
remaining Int
/// сколько сгорело
expiredAmount Int @default(0)
status PointsStatus
serviceDate DateTime
availableAt DateTime
/// свой срок жизни (бонусы); пусто — общее правило сгорания
expiresAt DateTime?
reason String?
commentForClient String?
createdById String?
channel PointsChannel
idempotencyKey String? @unique
createdAt DateTime @default(now())
items PointsRedemptionItem[]
@@index([clientId, status])
@@index([status, availableAt])
@@index([orderId])
@@index([createdAt])
}
/// Списание (оплата части заказа бариками)
model PointsRedemption {
id String @id @default(cuid())
clientId String
client Client @relation(fields: [clientId], references: [id])
orderId String?
order Order? @relation(fields: [orderId], references: [id])
orderRef String?
orderAmount Int?
amount Int
status RedemptionStatus
channel PointsChannel
/// комментарий клиента к запросу
note String?
requestedAt DateTime @default(now())
expiresAt DateTime?
confirmedById String?
confirmedAt DateTime?
reason String?
idempotencyKey String? @unique
createdAt DateTime @default(now())
items PointsRedemptionItem[]
@@index([clientId, status])
@@index([status, requestedAt])
}
/// Из каких начислений списано (FIFO)
model PointsRedemptionItem {
id String @id @default(cuid())
redemptionId String
redemption PointsRedemption @relation(fields: [redemptionId], references: [id])
accrualId String
accrual PointsAccrual @relation(fields: [accrualId], references: [id])
amount Int
@@index([accrualId])
}
/// Отмена операции — обратная запись со ссылкой на исходную
model PointsReversal {
id String @id @default(cuid())
/// ACCRUAL | REDEMPTION
targetType String
targetId String
clientId String
amount Int
/// сколько ушло в долг (отмена уже потраченного начисления)
debt Int @default(0)
createdById String?
channel PointsChannel
reason String
createdAt DateTime @default(now())
@@index([targetType, targetId])
@@index([clientId])
@@index([createdAt])
}
/// Предложено системой: начислить бонус или подтвердить операцию оператора сверх лимита
model PointsSuggestion {
id String @id @default(cuid())
clientId String
client Client @relation(fields: [clientId], references: [id])
/// route | photo_review | over_limit | …
mechanic String
/// чтобы одна и та же причина не создавала карточку дважды
dedupeKey String @unique
payload Json
suggestedAmount Int
status SuggestionStatus @default(OPEN)
resolvedById String?
resolvedAt DateTime?
reason String?
createdAt DateTime @default(now())
@@index([status, createdAt])
}