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

@@ -0,0 +1,63 @@
-- CreateEnum
CREATE TYPE "PartnerDeal" AS ENUM ('PROFIT_SHARE', 'COMMISSION');
-- DropForeignKey
ALTER TABLE "WebRequest" DROP CONSTRAINT "WebRequest_districtId_fkey";
-- AlterTable
ALTER TABLE "Agency" ADD COLUMN "giftMonths" INTEGER NOT NULL DEFAULT 1,
ADD COLUMN "giftPlan" "SubscriptionPlan" DEFAULT 'BAREV_PLUS';
-- AlterTable
ALTER TABLE "Provider" ADD COLUMN "maxHoursPerDay" INTEGER,
ADD COLUMN "maxOrdersPerDay" INTEGER;
-- AlterTable
ALTER TABLE "ProviderCompany" ADD COLUMN "autoAssign" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "commissionPct" INTEGER NOT NULL DEFAULT 30,
ADD COLUMN "deal" "PartnerDeal" NOT NULL DEFAULT 'COMMISSION',
ADD COLUMN "fotPct" INTEGER,
ADD COLUMN "note" TEXT,
ADD COLUMN "profitSharePct" INTEGER NOT NULL DEFAULT 50,
ADD COLUMN "taxPct" INTEGER,
ADD COLUMN "travelGapMinutes" INTEGER NOT NULL DEFAULT 60;
-- AlterTable
ALTER TABLE "WebRequest" ALTER COLUMN "preferredAt" DROP NOT NULL,
ALTER COLUMN "districtId" DROP NOT NULL,
ALTER COLUMN "addressText" DROP NOT NULL;
-- CreateTable
CREATE TABLE "PartnerEarning" (
"id" TEXT NOT NULL,
"companyId" TEXT NOT NULL,
"orderId" TEXT NOT NULL,
"deal" "PartnerDeal" NOT NULL,
"paid" INTEGER NOT NULL,
"fot" INTEGER NOT NULL DEFAULT 0,
"tax" INTEGER NOT NULL DEFAULT 0,
"pct" INTEGER NOT NULL,
"amount" INTEGER NOT NULL,
"period" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PartnerEarning_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "PartnerEarning_orderId_key" ON "PartnerEarning"("orderId");
-- CreateIndex
CREATE INDEX "PartnerEarning_companyId_period_idx" ON "PartnerEarning"("companyId", "period");
-- AddForeignKey
ALTER TABLE "PartnerEarning" ADD CONSTRAINT "PartnerEarning_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "ProviderCompany"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PartnerEarning" ADD CONSTRAINT "PartnerEarning_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WebRequest" ADD CONSTRAINT "WebRequest_districtId_fkey" FOREIGN KEY ("districtId") REFERENCES "District"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- Действующие компании: их ставка «% компании от заказа» превращается в нашу комиссию
UPDATE "ProviderCompany" SET "commissionPct" = GREATEST(0, LEAST(100, 100 - "rateValue")) WHERE "rateType" = 'PERCENT';
@@ -0,0 +1,26 @@
-- CreateTable
CREATE TABLE "ProWebSession" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"codeHash" TEXT,
"codeExpiresAt" TIMESTAMP(3),
"tokenHash" TEXT,
"expiresAt" TIMESTAMP(3) NOT NULL,
"lastUsedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ProWebSession_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "ProWebSession_codeHash_key" ON "ProWebSession"("codeHash");
-- CreateIndex
CREATE UNIQUE INDEX "ProWebSession_tokenHash_key" ON "ProWebSession"("tokenHash");
-- CreateIndex
CREATE INDEX "ProWebSession_userId_idx" ON "ProWebSession"("userId");
-- AddForeignKey
ALTER TABLE "ProWebSession" ADD CONSTRAINT "ProWebSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,267 @@
-- CreateEnum
CREATE TYPE "PointsKind" AS ENUM ('ORDER', 'BONUS');
-- CreateEnum
CREATE TYPE "PointsStatus" AS ENUM ('PENDING', 'ACTIVE', 'SPENT', 'EXPIRED', 'REVERSED');
-- CreateEnum
CREATE TYPE "PointsChannel" AS ENUM ('WEB', 'BOT', 'CSV', 'AUTO', 'CLIENT');
-- CreateEnum
CREATE TYPE "RedemptionStatus" AS ENUM ('REQUESTED', 'CONFIRMED', 'REJECTED', 'REVERSED');
-- CreateEnum
CREATE TYPE "SuggestionStatus" AS ENUM ('OPEN', 'ACCEPTED', 'REJECTED');
-- AlterTable
ALTER TABLE "Agency" ADD COLUMN "housewarmingDays" INTEGER NOT NULL DEFAULT 60,
ADD COLUMN "housewarmingPoints" INTEGER NOT NULL DEFAULT 0;
-- AlterTable
ALTER TABLE "Client" ADD COLUMN "pointsActive" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "pointsActivityAt" TIMESTAMP(3),
ADD COLUMN "pointsDebt" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "pointsPending" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "pointsWarn14At" TIMESTAMP(3),
ADD COLUMN "pointsWarn3At" TIMESTAMP(3),
ADD COLUMN "streakCount" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "streakLastAt" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "Order" ADD COLUMN "pointsUsed" INTEGER NOT NULL DEFAULT 0;
-- AlterTable
ALTER TABLE "Service" ADD COLUMN "pointsCategoryId" TEXT;
-- CreateTable
CREATE TABLE "PointsCategory" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" JSONB NOT NULL,
"rateBarev" INTEGER NOT NULL,
"ratePlus" INTEGER NOT NULL,
"rateElite" INTEGER NOT NULL,
"active" BOOLEAN NOT NULL DEFAULT true,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "PointsCategory_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PointsBonusType" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" JSONB NOT NULL,
"defaultAmount" INTEGER NOT NULL,
"ttlDays" INTEGER,
"active" BOOLEAN NOT NULL DEFAULT true,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "PointsBonusType_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PointsAccrual" (
"id" TEXT NOT NULL,
"clientId" TEXT NOT NULL,
"kind" "PointsKind" NOT NULL,
"categoryId" TEXT,
"bonusTypeId" TEXT,
"orderId" TEXT,
"orderRef" TEXT,
"orderAmount" INTEGER,
"excludedAmount" INTEGER NOT NULL DEFAULT 0,
"paidWithPoints" INTEGER NOT NULL DEFAULT 0,
"tier" TEXT,
"ratePct" INTEGER,
"streakBonusPct" INTEGER NOT NULL DEFAULT 0,
"suggestedAmount" INTEGER,
"amount" INTEGER NOT NULL,
"remaining" INTEGER NOT NULL,
"expiredAmount" INTEGER NOT NULL DEFAULT 0,
"status" "PointsStatus" NOT NULL,
"serviceDate" TIMESTAMP(3) NOT NULL,
"availableAt" TIMESTAMP(3) NOT NULL,
"expiresAt" TIMESTAMP(3),
"reason" TEXT,
"commentForClient" TEXT,
"createdById" TEXT,
"channel" "PointsChannel" NOT NULL,
"idempotencyKey" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PointsAccrual_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PointsRedemption" (
"id" TEXT NOT NULL,
"clientId" TEXT NOT NULL,
"orderId" TEXT,
"orderRef" TEXT,
"orderAmount" INTEGER,
"amount" INTEGER NOT NULL,
"status" "RedemptionStatus" NOT NULL,
"channel" "PointsChannel" NOT NULL,
"note" TEXT,
"requestedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expiresAt" TIMESTAMP(3),
"confirmedById" TEXT,
"confirmedAt" TIMESTAMP(3),
"reason" TEXT,
"idempotencyKey" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PointsRedemption_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PointsRedemptionItem" (
"id" TEXT NOT NULL,
"redemptionId" TEXT NOT NULL,
"accrualId" TEXT NOT NULL,
"amount" INTEGER NOT NULL,
CONSTRAINT "PointsRedemptionItem_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PointsReversal" (
"id" TEXT NOT NULL,
"targetType" TEXT NOT NULL,
"targetId" TEXT NOT NULL,
"clientId" TEXT NOT NULL,
"amount" INTEGER NOT NULL,
"debt" INTEGER NOT NULL DEFAULT 0,
"createdById" TEXT,
"channel" "PointsChannel" NOT NULL,
"reason" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PointsReversal_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PointsSuggestion" (
"id" TEXT NOT NULL,
"clientId" TEXT NOT NULL,
"mechanic" TEXT NOT NULL,
"dedupeKey" TEXT NOT NULL,
"payload" JSONB NOT NULL,
"suggestedAmount" INTEGER NOT NULL,
"status" "SuggestionStatus" NOT NULL DEFAULT 'OPEN',
"resolvedById" TEXT,
"resolvedAt" TIMESTAMP(3),
"reason" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PointsSuggestion_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "PointsCategory_code_key" ON "PointsCategory"("code");
-- CreateIndex
CREATE UNIQUE INDEX "PointsBonusType_code_key" ON "PointsBonusType"("code");
-- CreateIndex
CREATE UNIQUE INDEX "PointsAccrual_idempotencyKey_key" ON "PointsAccrual"("idempotencyKey");
-- CreateIndex
CREATE INDEX "PointsAccrual_clientId_status_idx" ON "PointsAccrual"("clientId", "status");
-- CreateIndex
CREATE INDEX "PointsAccrual_status_availableAt_idx" ON "PointsAccrual"("status", "availableAt");
-- CreateIndex
CREATE INDEX "PointsAccrual_orderId_idx" ON "PointsAccrual"("orderId");
-- CreateIndex
CREATE INDEX "PointsAccrual_createdAt_idx" ON "PointsAccrual"("createdAt");
-- CreateIndex
CREATE UNIQUE INDEX "PointsRedemption_idempotencyKey_key" ON "PointsRedemption"("idempotencyKey");
-- CreateIndex
CREATE INDEX "PointsRedemption_clientId_status_idx" ON "PointsRedemption"("clientId", "status");
-- CreateIndex
CREATE INDEX "PointsRedemption_status_requestedAt_idx" ON "PointsRedemption"("status", "requestedAt");
-- CreateIndex
CREATE INDEX "PointsRedemptionItem_accrualId_idx" ON "PointsRedemptionItem"("accrualId");
-- CreateIndex
CREATE INDEX "PointsReversal_targetType_targetId_idx" ON "PointsReversal"("targetType", "targetId");
-- CreateIndex
CREATE INDEX "PointsReversal_clientId_idx" ON "PointsReversal"("clientId");
-- CreateIndex
CREATE INDEX "PointsReversal_createdAt_idx" ON "PointsReversal"("createdAt");
-- CreateIndex
CREATE UNIQUE INDEX "PointsSuggestion_dedupeKey_key" ON "PointsSuggestion"("dedupeKey");
-- CreateIndex
CREATE INDEX "PointsSuggestion_status_createdAt_idx" ON "PointsSuggestion"("status", "createdAt");
-- AddForeignKey
ALTER TABLE "Service" ADD CONSTRAINT "Service_pointsCategoryId_fkey" FOREIGN KEY ("pointsCategoryId") REFERENCES "PointsCategory"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PointsAccrual" ADD CONSTRAINT "PointsAccrual_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "Client"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PointsAccrual" ADD CONSTRAINT "PointsAccrual_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "PointsCategory"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PointsAccrual" ADD CONSTRAINT "PointsAccrual_bonusTypeId_fkey" FOREIGN KEY ("bonusTypeId") REFERENCES "PointsBonusType"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PointsAccrual" ADD CONSTRAINT "PointsAccrual_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PointsRedemption" ADD CONSTRAINT "PointsRedemption_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "Client"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PointsRedemption" ADD CONSTRAINT "PointsRedemption_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PointsRedemptionItem" ADD CONSTRAINT "PointsRedemptionItem_redemptionId_fkey" FOREIGN KEY ("redemptionId") REFERENCES "PointsRedemption"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PointsRedemptionItem" ADD CONSTRAINT "PointsRedemptionItem_accrualId_fkey" FOREIGN KEY ("accrualId") REFERENCES "PointsAccrual"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PointsSuggestion" ADD CONSTRAINT "PointsSuggestion_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "Client"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- номер заказа уникален среди неотменённых начислений; автоначисление — один раз на заказ
CREATE UNIQUE INDEX "PointsAccrual_orderRef_live_key" ON "PointsAccrual"("orderRef") WHERE "orderRef" IS NOT NULL AND "status" <> 'REVERSED';
CREATE UNIQUE INDEX "PointsAccrual_orderId_live_key" ON "PointsAccrual"("orderId") WHERE "orderId" IS NOT NULL AND "status" <> 'REVERSED';
-- категории кэшбека (ставки по ТЗ; Elite = Barev+ +2)
INSERT INTO "PointsCategory" ("id", "code", "name", "rateBarev", "ratePlus", "rateElite", "sortOrder") VALUES
('pc_cleaning', 'cleaning', '{"ru":"Клининг","en":"Cleaning","zh":"保洁","hy":"Մաքրում","es":"Limpieza"}', 5, 8, 10, 1),
('pc_documents', 'documents', '{"ru":"Документы","en":"Documents","zh":"证件","hy":"Փաստաթղթեր","es":"Documentos"}', 5, 10, 12, 2),
('pc_learning', 'learning', '{"ru":"Обучение и карьера","en":"Learning and career","zh":"学习与职业","hy":"Ուսում և կարիերա","es":"Formación y carrera"}', 3, 4, 6, 3),
('pc_repair', 'repair', '{"ru":"Ремонт","en":"Repairs","zh":"维修","hy":"Վերանորոգում","es":"Reparaciones"}', 2, 4, 6, 4),
('pc_transfer', 'transfer', '{"ru":"Трансфер","en":"Transfer","zh":"接送","hy":"Տրանսֆեր","es":"Traslado"}', 2, 4, 6, 5);
UPDATE "Service" SET "pointsCategoryId" = 'pc_cleaning' WHERE "code" = 'cleaning';
UPDATE "Service" SET "pointsCategoryId" = 'pc_repair' WHERE "code" = 'repair';
UPDATE "Service" SET "pointsCategoryId" = 'pc_transfer' WHERE "code" = 'transfer';
UPDATE "Service" s SET "pointsCategoryId" = 'pc_documents' FROM "Category" c WHERE c."id" = s."categoryId" AND c."code" = 'DOCS';
UPDATE "Service" s SET "pointsCategoryId" = 'pc_learning' FROM "Category" c WHERE c."id" = s."categoryId" AND c."code" = 'LEARNING';
-- типы бонусов
INSERT INTO "PointsBonusType" ("id", "code", "name", "defaultAmount", "ttlDays", "sortOrder") VALUES
('pb_housewarming', 'housewarming', '{"ru":"Новоселье","en":"Housewarming","zh":"乔迁之喜","hy":"Բնակարանամուտ","es":"Mudanza"}', 5000, 60, 1),
('pb_route', 'first_month_route', '{"ru":"Маршрут первого месяца","en":"First-month route","zh":"首月路线","hy":"Առաջին ամսվա երթուղի","es":"Ruta del primer mes"}', 3000, NULL, 2),
('pb_ref_inviter', 'referral_inviter', '{"ru":"Пригласил друга","en":"Invited a friend","zh":"邀请好友","hy":"Հրավիրել է ընկերոջ","es":"Invitó a un amigo"}', 3000, NULL, 3),
('pb_ref_invitee', 'referral_invitee', '{"ru":"По приглашению друга","en":"Invited by a friend","zh":"好友邀请","hy":"Ընկերոջ հրավերով","es":"Invitado por un amigo"}', 3000, NULL, 4),
('pb_photo', 'photo_review', '{"ru":"Отзыв с фото","en":"Review with a photo","zh":"带图评价","hy":"Կարծիք լուսանկարով","es":"Reseña con foto"}', 500, NULL, 5),
('pb_lease', 'lease_renewal', '{"ru":"Продление аренды","en":"Lease renewal","zh":"续租","hy":"Վարձակալության երկարաձգում","es":"Renovación del alquiler"}', 2000, 60, 6),
('pb_complaint', 'complaint', '{"ru":"Компенсация","en":"Compensation","zh":"补偿","hy":"Փոխհատուցում","es":"Compensación"}', 1000, NULL, 7),
('pb_promo', 'promo', '{"ru":"Промо","en":"Promo","zh":"促销","hy":"Պրոմո","es":"Promoción"}', 1000, 30, 8);
@@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "OrderChecklistItem" ADD COLUMN "checkedByAdminId" TEXT,
ADD COLUMN "photoWaivedReason" TEXT;
+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])
}