Monorepo skeleton, Prisma schema, core domain and services with tests
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
45d7512d2b
64 files changed
+9483
No files matched your search
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@barev/db",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": { ".": "./src/index.ts" },
|
||||
"scripts": {
|
||||
"generate": "prisma generate",
|
||||
"migrate": "prisma migrate dev",
|
||||
"deploy": "prisma migrate deploy",
|
||||
"reset": "prisma migrate reset --force",
|
||||
"seed": "tsx src/seed/index.ts",
|
||||
"studio": "prisma studio",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"postinstall": "prisma generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/adapter-pg": "7.10.1",
|
||||
"@prisma/client": "7.10.1",
|
||||
"pg": "^8.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@barev/core": "workspace:*",
|
||||
"@node-rs/argon2": "^2.0.2",
|
||||
"dotenv": "^17.0.0",
|
||||
"prisma": "7.10.1",
|
||||
"tsx": "^4.20.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import path from 'node:path';
|
||||
import { config } from 'dotenv';
|
||||
import { defineConfig, env } from 'prisma/config';
|
||||
|
||||
config({ path: path.resolve(import.meta.dirname, '../../.env'), quiet: true });
|
||||
|
||||
export default defineConfig({
|
||||
schema: 'prisma/schema.prisma',
|
||||
migrations: { path: 'prisma/migrations', seed: 'tsx src/seed/index.ts' },
|
||||
datasource: { url: env('DATABASE_URL') },
|
||||
});
|
||||
@@ -0,0 +1,824 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Locale" AS ENUM ('ru', 'en', 'zh', 'hy', 'es');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "CategoryCode" AS ENUM ('HOME', 'DOCS', 'LEARNING', 'PERKS');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ServiceFlow" AS ENUM ('CLEANING', 'REPAIR', 'TRANSFER', 'LANGUAGE', 'CAREER', 'DOCUMENTS');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ProviderRole" AS ENUM ('CLEANER', 'HANDYMAN', 'DRIVER', 'TEACHER', 'CAREER_CONSULTANT', 'DOCS_AGENT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "OrderStatus" AS ENUM ('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');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ActorType" AS ENUM ('CLIENT', 'PROVIDER', 'DISPATCHER', 'ADMIN', 'SYSTEM');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "OfferStatus" AS ENUM ('OFFERED', 'ACCEPTED', 'DECLINED', 'EXPIRED', 'REVOKED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PaymentStatus" AS ENUM ('UNPAID', 'PAID', 'REFUNDED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RateType" AS ENUM ('FIXED', 'PERCENT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AdminRole" AS ENUM ('OWNER', 'OPERATOR');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SubscriptionPlan" AS ENUM ('BAREV_PLUS');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SubscriptionSource" AS ENUM ('AGENCY_GIFT', 'MANUAL', 'PAID');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SubscriptionStatus" AS ENUM ('ACTIVE', 'EXPIRED', 'CANCELLED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PromoKind" AS ENUM ('AGENCY_GIFT', 'DISCOUNT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "FileKind" AS ENUM ('PHOTO_REPORT', 'CLIENT_PHOTO', 'DOCUMENT_SCAN', 'RESUME', 'OTHER');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "EstimateStatus" AS ENUM ('PENDING', 'ACCEPTED', 'REJECTED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TaskStatus" AS ENUM ('OPEN', 'DONE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PayoutStatus" AS ENUM ('DRAFT', 'APPROVED', 'PAID');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
"telegramId" BIGINT NOT NULL,
|
||||
"username" TEXT,
|
||||
"firstName" TEXT,
|
||||
"lastName" TEXT,
|
||||
"phone" TEXT,
|
||||
"locale" "Locale" NOT NULL DEFAULT 'ru',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Client" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"consentAt" TIMESTAMP(3),
|
||||
"consentVersion" TEXT,
|
||||
"agencyId" TEXT,
|
||||
"agencyAttachedAt" TIMESTAMP(3),
|
||||
"source" TEXT NOT NULL DEFAULT 'organic',
|
||||
"utm" JSONB,
|
||||
"giftUsedAt" TIMESTAMP(3),
|
||||
"cardCode" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Client_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ProviderCompany" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"roles" "ProviderRole"[],
|
||||
"rateType" "RateType" NOT NULL DEFAULT 'PERCENT',
|
||||
"rateValue" INTEGER NOT NULL DEFAULT 70,
|
||||
"contactPhone" TEXT,
|
||||
"inviteCode" TEXT NOT NULL,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ProviderCompany_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CompanyDispatcher" (
|
||||
"id" TEXT NOT NULL,
|
||||
"companyId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "CompanyDispatcher_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Provider" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT,
|
||||
"name" TEXT NOT NULL,
|
||||
"phone" TEXT,
|
||||
"role" "ProviderRole" NOT NULL,
|
||||
"companyId" TEXT,
|
||||
"languages" "Locale"[],
|
||||
"rateType" "RateType" NOT NULL DEFAULT 'PERCENT',
|
||||
"rateValue" INTEGER NOT NULL DEFAULT 70,
|
||||
"ratingSum" INTEGER NOT NULL DEFAULT 0,
|
||||
"ratingCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"onShift" BOOLEAN NOT NULL DEFAULT false,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"inviteCode" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Provider_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "District" (
|
||||
"id" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT "District_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ProviderDistrict" (
|
||||
"providerId" TEXT NOT NULL,
|
||||
"districtId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "ProviderDistrict_pkey" PRIMARY KEY ("providerId","districtId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ProviderSchedule" (
|
||||
"id" TEXT NOT NULL,
|
||||
"providerId" TEXT NOT NULL,
|
||||
"weekday" INTEGER NOT NULL,
|
||||
"startMinute" INTEGER NOT NULL,
|
||||
"endMinute" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "ProviderSchedule_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Category" (
|
||||
"id" TEXT NOT NULL,
|
||||
"code" "CategoryCode" NOT NULL,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"barevPlusDiscountPct" INTEGER NOT NULL DEFAULT 0,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Service" (
|
||||
"id" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"categoryId" TEXT NOT NULL,
|
||||
"flow" "ServiceFlow" NOT NULL,
|
||||
"providerRole" "ProviderRole" NOT NULL,
|
||||
"pricing" JSONB NOT NULL,
|
||||
"paramsSchema" JSONB NOT NULL,
|
||||
"slotConfig" JSONB NOT NULL,
|
||||
"lateCancelFeePct" INTEGER NOT NULL DEFAULT 30,
|
||||
"isOnline" BOOLEAN NOT NULL DEFAULT false,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Service_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ChecklistItem" (
|
||||
"id" TEXT NOT NULL,
|
||||
"serviceId" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"requiresPhoto" BOOLEAN NOT NULL DEFAULT false,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT "ChecklistItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PaymentMethod" (
|
||||
"id" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"name" JSONB NOT NULL,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT "PaymentMethod_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Order" (
|
||||
"id" TEXT NOT NULL,
|
||||
"number" SERIAL NOT NULL,
|
||||
"clientId" TEXT NOT NULL,
|
||||
"serviceId" TEXT NOT NULL,
|
||||
"params" JSONB NOT NULL,
|
||||
"scheduledAt" TIMESTAMP(3) NOT NULL,
|
||||
"durationMinutes" INTEGER NOT NULL,
|
||||
"isOnline" BOOLEAN NOT NULL DEFAULT false,
|
||||
"addressText" TEXT,
|
||||
"districtId" TEXT,
|
||||
"lat" DOUBLE PRECISION,
|
||||
"lng" DOUBLE PRECISION,
|
||||
"note" TEXT,
|
||||
"locale" "Locale" NOT NULL,
|
||||
"basePrice" INTEGER NOT NULL,
|
||||
"discountPct" INTEGER NOT NULL DEFAULT 0,
|
||||
"discountAmount" INTEGER NOT NULL DEFAULT 0,
|
||||
"totalPrice" INTEGER NOT NULL,
|
||||
"status" "OrderStatus" NOT NULL DEFAULT 'NEW',
|
||||
"providerId" TEXT,
|
||||
"companyId" TEXT,
|
||||
"paymentMethodId" TEXT,
|
||||
"paymentStatus" "PaymentStatus" NOT NULL DEFAULT 'UNPAID',
|
||||
"paidAt" TIMESTAMP(3),
|
||||
"cancelledAt" TIMESTAMP(3),
|
||||
"cancelFee" INTEGER NOT NULL DEFAULT 0,
|
||||
"cancelReason" TEXT,
|
||||
"completedAt" TIMESTAMP(3),
|
||||
"repeatOfId" TEXT,
|
||||
"reminded24hAt" TIMESTAMP(3),
|
||||
"reminded1hAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Order_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OrderStatusHistory" (
|
||||
"id" TEXT NOT NULL,
|
||||
"orderId" TEXT NOT NULL,
|
||||
"fromStatus" "OrderStatus",
|
||||
"toStatus" "OrderStatus" NOT NULL,
|
||||
"event" TEXT,
|
||||
"actorType" "ActorType" NOT NULL,
|
||||
"actorId" TEXT,
|
||||
"comment" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "OrderStatusHistory_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OrderOffer" (
|
||||
"id" TEXT NOT NULL,
|
||||
"orderId" TEXT NOT NULL,
|
||||
"providerId" TEXT,
|
||||
"companyId" TEXT,
|
||||
"status" "OfferStatus" NOT NULL DEFAULT 'OFFERED',
|
||||
"expiresAt" TIMESTAMP(3),
|
||||
"respondedAt" TIMESTAMP(3),
|
||||
"createdById" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "OrderOffer_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OrderChecklistItem" (
|
||||
"id" TEXT NOT NULL,
|
||||
"orderId" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"requiresPhoto" BOOLEAN NOT NULL DEFAULT false,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"checkedAt" TIMESTAMP(3),
|
||||
"fileId" TEXT,
|
||||
|
||||
CONSTRAINT "OrderChecklistItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OrderFile" (
|
||||
"id" TEXT NOT NULL,
|
||||
"orderId" TEXT NOT NULL,
|
||||
"kind" "FileKind" NOT NULL,
|
||||
"storageKey" TEXT NOT NULL,
|
||||
"mime" TEXT NOT NULL,
|
||||
"size" INTEGER NOT NULL,
|
||||
"uploadedBy" "ActorType" NOT NULL,
|
||||
"uploadedById" TEXT,
|
||||
"deleteAfter" TIMESTAMP(3),
|
||||
"deletedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "OrderFile_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "FileAccessLog" (
|
||||
"id" TEXT NOT NULL,
|
||||
"fileId" TEXT NOT NULL,
|
||||
"actorType" "ActorType" NOT NULL,
|
||||
"actorId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "FileAccessLog_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Estimate" (
|
||||
"id" TEXT NOT NULL,
|
||||
"orderId" TEXT NOT NULL,
|
||||
"items" JSONB NOT NULL,
|
||||
"total" INTEGER NOT NULL,
|
||||
"status" "EstimateStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"respondedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Estimate_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Message" (
|
||||
"id" TEXT NOT NULL,
|
||||
"orderId" TEXT NOT NULL,
|
||||
"fromType" "ActorType" NOT NULL,
|
||||
"fromUserId" TEXT,
|
||||
"text" TEXT,
|
||||
"fileId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Message_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Rating" (
|
||||
"id" TEXT NOT NULL,
|
||||
"orderId" TEXT NOT NULL,
|
||||
"clientId" TEXT NOT NULL,
|
||||
"providerId" TEXT,
|
||||
"score" INTEGER NOT NULL,
|
||||
"comment" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Rating_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "LessonPackage" (
|
||||
"id" TEXT NOT NULL,
|
||||
"orderId" TEXT NOT NULL,
|
||||
"total" INTEGER NOT NULL,
|
||||
"completed" INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT "LessonPackage_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Payment" (
|
||||
"id" TEXT NOT NULL,
|
||||
"orderId" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"amount" INTEGER NOT NULL,
|
||||
"status" "PaymentStatus" NOT NULL,
|
||||
"externalId" TEXT,
|
||||
"markedById" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Payment_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OperatorTask" (
|
||||
"id" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"orderId" TEXT,
|
||||
"status" "TaskStatus" NOT NULL DEFAULT 'OPEN',
|
||||
"note" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"doneAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "OperatorTask_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Agency" (
|
||||
"id" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"commissionPct" INTEGER NOT NULL DEFAULT 10,
|
||||
"contact" TEXT,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Agency_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AgencyCommission" (
|
||||
"id" TEXT NOT NULL,
|
||||
"agencyId" TEXT NOT NULL,
|
||||
"orderId" TEXT NOT NULL,
|
||||
"pct" INTEGER NOT NULL,
|
||||
"amount" INTEGER NOT NULL,
|
||||
"period" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "AgencyCommission_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PromoCode" (
|
||||
"id" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"kind" "PromoKind" NOT NULL,
|
||||
"agencyId" TEXT,
|
||||
"discountPct" INTEGER,
|
||||
"maxUses" INTEGER,
|
||||
"usedCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"validFrom" TIMESTAMP(3),
|
||||
"validUntil" TIMESTAMP(3),
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "PromoCode_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PromoCodeUse" (
|
||||
"id" TEXT NOT NULL,
|
||||
"promoId" TEXT NOT NULL,
|
||||
"clientId" TEXT NOT NULL,
|
||||
"orderId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "PromoCodeUse_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Subscription" (
|
||||
"id" TEXT NOT NULL,
|
||||
"clientId" TEXT NOT NULL,
|
||||
"plan" "SubscriptionPlan" NOT NULL DEFAULT 'BAREV_PLUS',
|
||||
"source" "SubscriptionSource" NOT NULL,
|
||||
"status" "SubscriptionStatus" NOT NULL DEFAULT 'ACTIVE',
|
||||
"startsAt" TIMESTAMP(3) NOT NULL,
|
||||
"endsAt" TIMESTAMP(3) NOT NULL,
|
||||
"remindedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Subscription_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Partner" (
|
||||
"id" TEXT NOT NULL,
|
||||
"category" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"offer" JSONB NOT NULL,
|
||||
"terms" JSONB,
|
||||
"address" TEXT,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Partner_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CardUsage" (
|
||||
"id" TEXT NOT NULL,
|
||||
"clientId" TEXT NOT NULL,
|
||||
"partnerId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "CardUsage_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Payout" (
|
||||
"id" TEXT NOT NULL,
|
||||
"providerId" TEXT,
|
||||
"companyId" TEXT,
|
||||
"periodStart" TIMESTAMP(3) NOT NULL,
|
||||
"periodEnd" TIMESTAMP(3) NOT NULL,
|
||||
"amount" INTEGER NOT NULL,
|
||||
"status" "PayoutStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Payout_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AdminUser" (
|
||||
"id" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"passwordHash" TEXT NOT NULL,
|
||||
"role" "AdminRole" NOT NULL DEFAULT 'OPERATOR',
|
||||
"totpSecret" TEXT,
|
||||
"totpEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "AdminUser_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AdminSession" (
|
||||
"id" TEXT NOT NULL,
|
||||
"adminUserId" TEXT NOT NULL,
|
||||
"tokenHash" TEXT NOT NULL,
|
||||
"verified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "AdminSession_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AuditLog" (
|
||||
"id" TEXT NOT NULL,
|
||||
"adminUserId" TEXT,
|
||||
"action" TEXT NOT NULL,
|
||||
"entity" TEXT NOT NULL,
|
||||
"entityId" TEXT,
|
||||
"data" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Setting" (
|
||||
"key" TEXT NOT NULL,
|
||||
"value" JSONB NOT NULL,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Setting_pkey" PRIMARY KEY ("key")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "StartToken" (
|
||||
"id" TEXT NOT NULL,
|
||||
"locale" "Locale",
|
||||
"agency" TEXT,
|
||||
"utm" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "StartToken_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_telegramId_key" ON "User"("telegramId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Client_userId_key" ON "Client"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Client_cardCode_key" ON "Client"("cardCode");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Client_agencyId_idx" ON "Client"("agencyId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ProviderCompany_inviteCode_key" ON "ProviderCompany"("inviteCode");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "CompanyDispatcher_companyId_userId_key" ON "CompanyDispatcher"("companyId", "userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Provider_userId_key" ON "Provider"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Provider_inviteCode_key" ON "Provider"("inviteCode");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Provider_role_active_idx" ON "Provider"("role", "active");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "District_code_key" ON "District"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ProviderSchedule_providerId_weekday_idx" ON "ProviderSchedule"("providerId", "weekday");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Category_code_key" ON "Category"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Service_code_key" ON "Service"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ChecklistItem_serviceId_code_key" ON "ChecklistItem"("serviceId", "code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PaymentMethod_code_key" ON "PaymentMethod"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Order_number_key" ON "Order"("number");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Order_status_idx" ON "Order"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Order_scheduledAt_idx" ON "Order"("scheduledAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Order_clientId_idx" ON "Order"("clientId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Order_providerId_scheduledAt_idx" ON "Order"("providerId", "scheduledAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "OrderStatusHistory_orderId_createdAt_idx" ON "OrderStatusHistory"("orderId", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "OrderOffer_orderId_status_idx" ON "OrderOffer"("orderId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "OrderOffer_providerId_status_idx" ON "OrderOffer"("providerId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "OrderOffer_companyId_status_idx" ON "OrderOffer"("companyId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OrderChecklistItem_fileId_key" ON "OrderChecklistItem"("fileId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OrderChecklistItem_orderId_code_key" ON "OrderChecklistItem"("orderId", "code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OrderFile_storageKey_key" ON "OrderFile"("storageKey");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Message_orderId_createdAt_idx" ON "Message"("orderId", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Rating_orderId_key" ON "Rating"("orderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "LessonPackage_orderId_key" ON "LessonPackage"("orderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Agency_code_key" ON "Agency"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AgencyCommission_orderId_key" ON "AgencyCommission"("orderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AgencyCommission_agencyId_period_idx" ON "AgencyCommission"("agencyId", "period");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PromoCode_code_key" ON "PromoCode"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PromoCodeUse_promoId_clientId_key" ON "PromoCodeUse"("promoId", "clientId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Subscription_clientId_endsAt_idx" ON "Subscription"("clientId", "endsAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AdminUser_email_key" ON "AdminUser"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AdminSession_tokenHash_key" ON "AdminSession"("tokenHash");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AuditLog_entity_entityId_idx" ON "AuditLog"("entity", "entityId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Client" ADD CONSTRAINT "Client_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Client" ADD CONSTRAINT "Client_agencyId_fkey" FOREIGN KEY ("agencyId") REFERENCES "Agency"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CompanyDispatcher" ADD CONSTRAINT "CompanyDispatcher_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "ProviderCompany"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CompanyDispatcher" ADD CONSTRAINT "CompanyDispatcher_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Provider" ADD CONSTRAINT "Provider_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Provider" ADD CONSTRAINT "Provider_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "ProviderCompany"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ProviderDistrict" ADD CONSTRAINT "ProviderDistrict_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "Provider"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ProviderDistrict" ADD CONSTRAINT "ProviderDistrict_districtId_fkey" FOREIGN KEY ("districtId") REFERENCES "District"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ProviderSchedule" ADD CONSTRAINT "ProviderSchedule_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "Provider"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Service" ADD CONSTRAINT "Service_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ChecklistItem" ADD CONSTRAINT "ChecklistItem_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Order" ADD CONSTRAINT "Order_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "Client"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Order" ADD CONSTRAINT "Order_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Order" ADD CONSTRAINT "Order_districtId_fkey" FOREIGN KEY ("districtId") REFERENCES "District"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Order" ADD CONSTRAINT "Order_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "Provider"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Order" ADD CONSTRAINT "Order_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "ProviderCompany"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Order" ADD CONSTRAINT "Order_paymentMethodId_fkey" FOREIGN KEY ("paymentMethodId") REFERENCES "PaymentMethod"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OrderStatusHistory" ADD CONSTRAINT "OrderStatusHistory_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OrderOffer" ADD CONSTRAINT "OrderOffer_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OrderOffer" ADD CONSTRAINT "OrderOffer_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "Provider"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OrderOffer" ADD CONSTRAINT "OrderOffer_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "ProviderCompany"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OrderChecklistItem" ADD CONSTRAINT "OrderChecklistItem_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OrderChecklistItem" ADD CONSTRAINT "OrderChecklistItem_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "OrderFile"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OrderFile" ADD CONSTRAINT "OrderFile_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "FileAccessLog" ADD CONSTRAINT "FileAccessLog_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "OrderFile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Estimate" ADD CONSTRAINT "Estimate_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Message" ADD CONSTRAINT "Message_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Message" ADD CONSTRAINT "Message_fromUserId_fkey" FOREIGN KEY ("fromUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Rating" ADD CONSTRAINT "Rating_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Rating" ADD CONSTRAINT "Rating_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "Client"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Rating" ADD CONSTRAINT "Rating_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "Provider"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LessonPackage" ADD CONSTRAINT "LessonPackage_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Payment" ADD CONSTRAINT "Payment_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OperatorTask" ADD CONSTRAINT "OperatorTask_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AgencyCommission" ADD CONSTRAINT "AgencyCommission_agencyId_fkey" FOREIGN KEY ("agencyId") REFERENCES "Agency"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AgencyCommission" ADD CONSTRAINT "AgencyCommission_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PromoCode" ADD CONSTRAINT "PromoCode_agencyId_fkey" FOREIGN KEY ("agencyId") REFERENCES "Agency"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PromoCodeUse" ADD CONSTRAINT "PromoCodeUse_promoId_fkey" FOREIGN KEY ("promoId") REFERENCES "PromoCode"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PromoCodeUse" ADD CONSTRAINT "PromoCodeUse_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "Client"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "Client"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CardUsage" ADD CONSTRAINT "CardUsage_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "Client"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CardUsage" ADD CONSTRAINT "CardUsage_partnerId_fkey" FOREIGN KEY ("partnerId") REFERENCES "Partner"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Payout" ADD CONSTRAINT "Payout_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "Provider"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Payout" ADD CONSTRAINT "Payout_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "ProviderCompany"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AdminSession" ADD CONSTRAINT "AdminSession_adminUserId_fkey" FOREIGN KEY ("adminUserId") REFERENCES "AdminUser"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_adminUserId_fkey" FOREIGN KEY ("adminUserId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -0,0 +1,749 @@
|
||||
// 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())
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { PrismaPg } from '@prisma/adapter-pg';
|
||||
import { PrismaClient, type Prisma } from './generated/prisma/client.ts';
|
||||
|
||||
export * from './generated/prisma/client.ts';
|
||||
|
||||
export type Db = PrismaClient;
|
||||
/** Клиент внутри транзакции */
|
||||
export type Tx = Prisma.TransactionClient;
|
||||
|
||||
let instance: PrismaClient | undefined;
|
||||
|
||||
export function createDb(url = process.env.DATABASE_URL): PrismaClient {
|
||||
if (!url) throw new Error('DATABASE_URL is not set');
|
||||
return new PrismaClient({ adapter: new PrismaPg({ connectionString: url }) });
|
||||
}
|
||||
|
||||
/** Общий клиент процесса */
|
||||
export function getDb(): PrismaClient {
|
||||
instance ??= createDb();
|
||||
return instance;
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
/**
|
||||
* Seed для локальной разработки и staging: справочники, исполнители, компания, агентства,
|
||||
* тестовые клиенты и заказы во всех колонках канбана. Полностью пересоздаёт данные.
|
||||
*
|
||||
* pnpm db:seed
|
||||
*/
|
||||
import path from 'node:path';
|
||||
import { config } from 'dotenv';
|
||||
import { hash } from '@node-rs/argon2';
|
||||
import {
|
||||
CATALOG,
|
||||
DEFAULT_CATEGORY_DISCOUNTS,
|
||||
DEFAULT_SETTINGS,
|
||||
YEREVAN_DISTRICTS,
|
||||
agencyCommission,
|
||||
fromYerevan,
|
||||
quote,
|
||||
toYerevan,
|
||||
yerevanMonthKey,
|
||||
type CategoryCode,
|
||||
} from '@barev/core';
|
||||
import { createDb, type Locale, type OrderStatus } from '../index.ts';
|
||||
|
||||
config({ path: path.resolve(import.meta.dirname, '../../../../.env'), quiet: true });
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && process.env.SEED_FORCE !== '1') {
|
||||
console.error('Seed пересоздаёт данные. В production запустите с SEED_FORCE=1, если уверены.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const db = createDb();
|
||||
const HOUR = 3600_000;
|
||||
|
||||
/** Telegram ID тестовых пользователей — ими можно войти в Mini App через pnpm dev:initdata */
|
||||
export const SEED_TG = {
|
||||
clientAnna: 100000001n,
|
||||
clientJohn: 100000002n,
|
||||
clientLi: 100000003n,
|
||||
cleanerMaria: 200000001n,
|
||||
cleanerAni: 200000002n,
|
||||
cleanerElena: 200000003n,
|
||||
companyLusine: 200000011n,
|
||||
driverArman: 200000021n,
|
||||
dispatcherKaren: 300000001n,
|
||||
} as const;
|
||||
|
||||
async function wipe() {
|
||||
const tables = await db.$queryRaw<{ tablename: string }[]>`
|
||||
SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename <> '_prisma_migrations'`;
|
||||
const list = tables.map((t) => `"public"."${t.tablename}"`).join(', ');
|
||||
if (list) await db.$executeRawUnsafe(`TRUNCATE ${list} RESTART IDENTITY CASCADE`);
|
||||
}
|
||||
|
||||
/** Ереванское время: сегодня + dayOffset, hh:mm */
|
||||
function at(dayOffset: number, hour: number, minute = 0): Date {
|
||||
const p = toYerevan(new Date(Date.now() + dayOffset * 24 * HOUR));
|
||||
return fromYerevan(p.year, p.month, p.day, hour * 60 + minute);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await wipe();
|
||||
|
||||
// ── настройки ──
|
||||
await db.setting.createMany({ data: Object.entries(DEFAULT_SETTINGS).map(([key, value]) => ({ key, value })) });
|
||||
|
||||
// ── районы ──
|
||||
await db.district.createMany({ data: YEREVAN_DISTRICTS.map((code, i) => ({ code, sortOrder: i })) });
|
||||
const districts = Object.fromEntries((await db.district.findMany()).map((d) => [d.code, d.id])) as Record<(typeof YEREVAN_DISTRICTS)[number], string>;
|
||||
|
||||
// ── направления и услуги ──
|
||||
const categoryOrder: CategoryCode[] = ['HOME', 'DOCS', 'LEARNING', 'PERKS'];
|
||||
const categories: Record<string, string> = {};
|
||||
for (const [i, code] of categoryOrder.entries()) {
|
||||
const c = await db.category.create({ data: { code, sortOrder: i, barevPlusDiscountPct: DEFAULT_CATEGORY_DISCOUNTS[code] } });
|
||||
categories[code] = c.id;
|
||||
}
|
||||
const services: Record<string, { id: string }> = {};
|
||||
for (const [i, s] of CATALOG.entries()) {
|
||||
services[s.code] = await db.service.create({
|
||||
data: {
|
||||
code: s.code,
|
||||
categoryId: categories[s.category]!,
|
||||
flow: s.flow,
|
||||
providerRole: s.providerRole,
|
||||
pricing: s.pricing,
|
||||
paramsSchema: s.paramsSchema,
|
||||
slotConfig: s.slotConfig,
|
||||
isOnline: s.isOnline,
|
||||
lateCancelFeePct: 30,
|
||||
active: s.stage === 1,
|
||||
sortOrder: i,
|
||||
checklist: { create: s.checklist.map((c, j) => ({ code: c.code, requiresPhoto: !!c.requiresPhoto, sortOrder: j })) },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ── способы оплаты (PAY-1) ──
|
||||
const cash = await db.paymentMethod.create({
|
||||
data: { code: 'cash', sortOrder: 0, name: { ru: 'Наличными исполнителю', en: 'Cash to the specialist', zh: '现金支付给服务人员', hy: 'Կանխիկ՝ կատարողին', es: 'En efectivo al especialista' } },
|
||||
});
|
||||
await db.paymentMethod.create({
|
||||
data: { code: 'transfer', sortOrder: 1, name: { ru: 'Перевод на карту', en: 'Card transfer', zh: '银行卡转账', hy: 'Փոխանցում քարտին', es: 'Transferencia a tarjeta' } },
|
||||
});
|
||||
|
||||
// ── агентства ──
|
||||
const domik = await db.agency.create({ data: { code: 'DOMIK', name: 'Domik Realty', commissionPct: 10, contact: '+374 00 000000' } });
|
||||
await db.agency.create({ data: { code: 'YERHOME', name: 'Yerevan Home', commissionPct: 10 } });
|
||||
await db.agency.create({ data: { code: 'OLDAGENCY', name: 'Закрытое агентство', active: false } });
|
||||
|
||||
// ── админка ──
|
||||
const owner = await db.adminUser.create({
|
||||
data: {
|
||||
email: process.env.SEED_OWNER_EMAIL ?? 'owner@barev.local',
|
||||
name: 'Владелец',
|
||||
role: 'OWNER',
|
||||
passwordHash: await hash(process.env.SEED_OWNER_PASSWORD ?? 'barev-owner-123'),
|
||||
},
|
||||
});
|
||||
await db.adminUser.create({
|
||||
data: { email: 'operator@barev.local', name: 'Оператор', role: 'OPERATOR', passwordHash: await hash('barev-operator-123') },
|
||||
});
|
||||
|
||||
// ── пользователи Telegram ──
|
||||
const user = (telegramId: bigint, firstName: string, locale: Locale, lastName?: string) =>
|
||||
db.user.create({ data: { telegramId, firstName, lastName, locale, username: `seed_${telegramId}` } });
|
||||
|
||||
// ── исполнители ──
|
||||
const workweek = [0, 1, 2, 3, 4, 5].map((weekday) => ({ weekday, startMinute: 9 * 60, endMinute: 20 * 60 }));
|
||||
const everyday = [0, 1, 2, 3, 4, 5, 6].map((weekday) => ({ weekday, startMinute: 8 * 60, endMinute: 21 * 60 }));
|
||||
async function provider(p: {
|
||||
tg?: bigint;
|
||||
name: string;
|
||||
role: 'CLEANER' | 'DRIVER';
|
||||
languages: Locale[];
|
||||
districts: (keyof typeof districts)[];
|
||||
companyId?: string;
|
||||
rate?: { type: 'FIXED' | 'PERCENT'; value: number };
|
||||
onShift?: boolean;
|
||||
rating?: [number, number];
|
||||
schedule?: typeof workweek;
|
||||
invite: string;
|
||||
}) {
|
||||
const u = p.tg ? await user(p.tg, p.name.split(' ')[0]!, p.languages[0]!) : null;
|
||||
return db.provider.create({
|
||||
data: {
|
||||
userId: u?.id,
|
||||
name: p.name,
|
||||
role: p.role,
|
||||
companyId: p.companyId,
|
||||
languages: p.languages,
|
||||
rateType: p.rate?.type ?? 'PERCENT',
|
||||
rateValue: p.rate?.value ?? 70,
|
||||
onShift: p.onShift ?? true,
|
||||
ratingSum: p.rating?.[0] ?? 0,
|
||||
ratingCount: p.rating?.[1] ?? 0,
|
||||
inviteCode: p.invite,
|
||||
districts: { create: p.districts.map((d) => ({ districtId: districts[d] })) },
|
||||
schedule: { create: p.schedule ?? workweek },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const maria = await provider({ tg: SEED_TG.cleanerMaria, name: 'Мария Петросян', role: 'CLEANER', languages: ['ru', 'hy', 'en'], districts: ['kentron', 'arabkir'], rating: [47, 10], invite: 'PRO-MARIA' });
|
||||
const ani = await provider({ tg: SEED_TG.cleanerAni, name: 'Ани Саргсян', role: 'CLEANER', languages: ['hy', 'ru'], districts: ['shengavit', 'erebuni', 'malatia_sebastia'], rate: { type: 'FIXED', value: 9000 }, rating: [22, 5], invite: 'PRO-ANI' });
|
||||
const elena = await provider({ tg: SEED_TG.cleanerElena, name: 'Elena Gómez', role: 'CLEANER', languages: ['es', 'en', 'ru'], districts: ['kentron', 'arabkir', 'nor_nork'], schedule: everyday, invite: 'PRO-ELENA' });
|
||||
await provider({ tg: SEED_TG.driverArman, name: 'Арман Мкртчян', role: 'DRIVER', languages: ['ru', 'hy', 'en'], districts: ['kentron'], schedule: everyday, rate: { type: 'FIXED', value: 7000 }, invite: 'PRO-ARMAN' });
|
||||
|
||||
const company = await db.providerCompany.create({
|
||||
data: { name: 'Чисто Ереван', roles: ['CLEANER'], rateType: 'PERCENT', rateValue: 65, contactPhone: '+374 00 111111', inviteCode: 'CO-CHISTO' },
|
||||
});
|
||||
const karen = await user(SEED_TG.dispatcherKaren, 'Карен', 'ru');
|
||||
await db.companyDispatcher.create({ data: { companyId: company.id, userId: karen.id } });
|
||||
const lusine = await provider({ tg: SEED_TG.companyLusine, name: 'Лусине Акопян', role: 'CLEANER', languages: ['hy', 'ru'], districts: ['kentron', 'nor_nork', 'avan'], companyId: company.id, invite: 'PRO-LUSINE' });
|
||||
await provider({ name: 'Гаяне Арутюнян', role: 'CLEANER', languages: ['ru', 'en'], districts: ['arabkir', 'ajapnyak', 'davtashen'], companyId: company.id, onShift: false, invite: 'PRO-GAYANE' });
|
||||
|
||||
// ── клиенты ──
|
||||
const now = new Date();
|
||||
async function client(tg: bigint, name: string, locale: Locale, opts: { agency?: string; gift?: boolean; card: string; attachedMonthsAgo?: number }) {
|
||||
const u = await user(tg, name, locale);
|
||||
const attachedAt = opts.agency ? new Date(now.getTime() - (opts.attachedMonthsAgo ?? 0) * 30 * 24 * HOUR - HOUR) : undefined;
|
||||
const c = await db.client.create({
|
||||
data: {
|
||||
userId: u.id,
|
||||
consentAt: now,
|
||||
consentVersion: DEFAULT_SETTINGS['consent.version'],
|
||||
agencyId: opts.agency,
|
||||
agencyAttachedAt: attachedAt,
|
||||
source: opts.agency ? 'deeplink' : 'organic',
|
||||
utm: opts.agency ? { utm_source: 'qr', utm_campaign: 'lease' } : undefined,
|
||||
cardCode: opts.card,
|
||||
giftUsedAt: opts.gift ? attachedAt : undefined,
|
||||
},
|
||||
});
|
||||
if (opts.gift) {
|
||||
await db.subscription.create({
|
||||
data: { clientId: c.id, source: 'AGENCY_GIFT', startsAt: attachedAt!, endsAt: new Date(attachedAt!.getTime() + 30 * 24 * HOUR) },
|
||||
});
|
||||
}
|
||||
return c;
|
||||
}
|
||||
const anna = await client(SEED_TG.clientAnna, 'Анна', 'ru', { agency: domik.id, gift: true, card: 'ANNA2026' });
|
||||
const john = await client(SEED_TG.clientJohn, 'John', 'en', { card: 'JOHN2026' });
|
||||
const li = await client(SEED_TG.clientLi, '李明', 'zh', { agency: domik.id, gift: false, card: 'LIMING26' });
|
||||
|
||||
// ── заказы клининга ──
|
||||
const cleaning = CATALOG.find((s) => s.code === 'cleaning')!;
|
||||
const checklistCodes = cleaning.checklist;
|
||||
async function order(o: {
|
||||
client: { id: string };
|
||||
locale: Locale;
|
||||
when: Date;
|
||||
status: OrderStatus;
|
||||
params: { cleaningType: string; rooms: number };
|
||||
district: keyof typeof districts;
|
||||
address: string;
|
||||
providerId?: string;
|
||||
companyId?: string;
|
||||
discount: boolean;
|
||||
checked?: number;
|
||||
paid?: boolean;
|
||||
note?: string;
|
||||
history: [OrderStatus | null, OrderStatus, 'CLIENT' | 'SYSTEM' | 'PROVIDER' | 'ADMIN' | 'DISPATCHER', string?][];
|
||||
}) {
|
||||
const q = quote(cleaning.pricing, o.params, o.discount ? DEFAULT_CATEGORY_DISCOUNTS.HOME : 0);
|
||||
const created = await db.order.create({
|
||||
data: {
|
||||
clientId: o.client.id,
|
||||
serviceId: services.cleaning!.id,
|
||||
params: o.params,
|
||||
scheduledAt: o.when,
|
||||
durationMinutes: q.durationMinutes,
|
||||
addressText: o.address,
|
||||
districtId: districts[o.district],
|
||||
note: o.note,
|
||||
locale: o.locale,
|
||||
basePrice: q.basePrice,
|
||||
discountPct: q.discountPct,
|
||||
discountAmount: q.discountAmount,
|
||||
totalPrice: q.totalPrice,
|
||||
status: o.status,
|
||||
providerId: o.providerId,
|
||||
companyId: o.companyId,
|
||||
paymentMethodId: cash.id,
|
||||
paymentStatus: o.paid ? 'PAID' : 'UNPAID',
|
||||
paidAt: o.paid ? o.when : undefined,
|
||||
completedAt: o.status === 'DONE' ? new Date(o.when.getTime() + q.durationMinutes * 60_000) : undefined,
|
||||
cancelledAt: o.status === 'CANCELLED' ? now : undefined,
|
||||
checklist: {
|
||||
create: checklistCodes.map((c, i) => ({
|
||||
code: c.code,
|
||||
requiresPhoto: !!c.requiresPhoto,
|
||||
sortOrder: i,
|
||||
checkedAt: i < (o.checked ?? 0) && !c.requiresPhoto ? now : undefined,
|
||||
})),
|
||||
},
|
||||
history: {
|
||||
create: o.history.map(([from, to, actor, event], i) => ({
|
||||
fromStatus: from,
|
||||
toStatus: to,
|
||||
actorType: actor,
|
||||
event,
|
||||
createdAt: new Date(now.getTime() - (o.history.length - i) * 600_000),
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
return created;
|
||||
}
|
||||
|
||||
// Новый, без исполнителя — колонка «Новые»
|
||||
await order({
|
||||
client: anna, locale: 'ru', when: at(1, 11), status: 'NEW', params: { cleaningType: 'general', rooms: 2 },
|
||||
district: 'kentron', address: 'ул. Пушкина, 10, кв. 15', discount: true, note: 'Есть кот, он дружелюбный',
|
||||
history: [[null, 'NEW', 'CLIENT']],
|
||||
});
|
||||
// Предложен Марии, ждёт подтверждения — колонка «Назначены»
|
||||
const offered = await order({
|
||||
client: john, locale: 'en', when: at(1, 15), status: 'NEW', params: { cleaningType: 'regular', rooms: 3 },
|
||||
district: 'arabkir', address: 'Komitas Ave 25, apt 7', discount: false, history: [[null, 'NEW', 'CLIENT']],
|
||||
});
|
||||
await db.orderOffer.create({ data: { orderId: offered.id, providerId: maria.id, status: 'OFFERED', expiresAt: new Date(now.getTime() + 15 * 60_000), createdById: owner.id } });
|
||||
// Передан компании — «Назначен на компанию»
|
||||
const toCompany = await order({
|
||||
client: li, locale: 'zh', when: at(2, 12), status: 'NEW', params: { cleaningType: 'renovation', rooms: 2 },
|
||||
district: 'nor_nork', address: 'Gai Ave 3/1, apt 40', discount: false, history: [[null, 'NEW', 'CLIENT']],
|
||||
});
|
||||
await db.order.update({ where: { id: toCompany.id }, data: { companyId: company.id } });
|
||||
await db.orderOffer.create({ data: { orderId: toCompany.id, companyId: company.id, status: 'OFFERED', createdById: owner.id } });
|
||||
// Назначен Марии на сегодня — для демонстрации шагов исполнителя
|
||||
const todayHour = Math.min(Math.max(toYerevan(now).hour + 2, 9), 17);
|
||||
await order({
|
||||
client: anna, locale: 'ru', when: at(0, todayHour), status: 'ASSIGNED', params: { cleaningType: 'regular', rooms: 2 },
|
||||
district: 'kentron', address: 'ул. Абовяна, 22, кв. 5', providerId: maria.id, discount: true, note: 'Домофон 5#',
|
||||
history: [[null, 'NEW', 'CLIENT'], ['NEW', 'ASSIGNED', 'SYSTEM', 'accepted']],
|
||||
});
|
||||
// Идёт уборка у Елены — колонка «В работе»
|
||||
await order({
|
||||
client: li, locale: 'zh', when: at(0, Math.max(9, todayHour - 2)), status: 'CLEANING', params: { cleaningType: 'regular', rooms: 1 },
|
||||
district: 'arabkir', address: 'Baghramyan Ave 50, apt 12', providerId: elena.id, discount: false, checked: 2,
|
||||
history: [[null, 'NEW', 'CLIENT'], ['NEW', 'ASSIGNED', 'SYSTEM', 'accepted'], ['ASSIGNED', 'EN_ROUTE', 'PROVIDER', 'depart'], ['EN_ROUTE', 'EN_ROUTE', 'PROVIDER', 'arrived'], ['EN_ROUTE', 'CLEANING', 'PROVIDER', 'startCleaning']],
|
||||
});
|
||||
// Назначен Ани на послезавтра
|
||||
await order({
|
||||
client: john, locale: 'en', when: at(2, 10), status: 'ASSIGNED', params: { cleaningType: 'general', rooms: 1 },
|
||||
district: 'shengavit', address: 'Arshakunyats Ave 8', providerId: ani.id, discount: false,
|
||||
history: [[null, 'NEW', 'CLIENT'], ['NEW', 'ASSIGNED', 'SYSTEM', 'accepted']],
|
||||
});
|
||||
// Сотрудник компании — назначен диспетчером
|
||||
await order({
|
||||
client: anna, locale: 'ru', when: at(3, 14), status: 'ASSIGNED', params: { cleaningType: 'regular', rooms: 4 },
|
||||
district: 'kentron', address: 'ул. Туманяна, 5', providerId: lusine.id, companyId: company.id, discount: true,
|
||||
history: [[null, 'NEW', 'CLIENT'], ['NEW', 'ASSIGNED', 'DISPATCHER', 'dispatcherAssigned']],
|
||||
});
|
||||
// Готов и оплачен — с вознаграждением агентству
|
||||
const done = await order({
|
||||
client: anna, locale: 'ru', when: at(-3, 10), status: 'DONE', params: { cleaningType: 'general', rooms: 2 },
|
||||
district: 'kentron', address: 'ул. Пушкина, 10, кв. 15', providerId: maria.id, discount: true, checked: 5, paid: true,
|
||||
history: [[null, 'NEW', 'CLIENT'], ['NEW', 'ASSIGNED', 'SYSTEM', 'accepted'], ['ASSIGNED', 'EN_ROUTE', 'PROVIDER', 'depart'], ['EN_ROUTE', 'CLEANING', 'PROVIDER', 'startCleaning'], ['CLEANING', 'DONE', 'PROVIDER', 'finishCleaning']],
|
||||
});
|
||||
await db.payment.create({ data: { orderId: done.id, provider: 'manual', amount: done.totalPrice, status: 'PAID', markedById: owner.id } });
|
||||
await db.agencyCommission.create({
|
||||
data: { agencyId: domik.id, orderId: done.id, pct: domik.commissionPct, amount: agencyCommission(done.totalPrice, domik.commissionPct), period: yerevanMonthKey(done.scheduledAt) },
|
||||
});
|
||||
// Отменён клиентом
|
||||
await order({
|
||||
client: john, locale: 'en', when: at(4, 9), status: 'CANCELLED', params: { cleaningType: 'regular', rooms: 1 },
|
||||
district: 'kentron', address: 'Amiryan St 4', discount: false, history: [[null, 'NEW', 'CLIENT'], ['NEW', 'CANCELLED', 'CLIENT']],
|
||||
});
|
||||
|
||||
// ── партнёры (Этап 3, для каталога) ──
|
||||
await db.partner.createMany({
|
||||
data: [
|
||||
{ category: 'cafe', name: 'Coffee Lab', offer: { ru: '−10% на кофе', en: '−10% on coffee' }, address: 'Abovyan St 3' },
|
||||
{ category: 'restaurant', name: 'Lavash', offer: { ru: 'Десерт в подарок', en: 'Free dessert' }, address: 'Tumanyan St 21' },
|
||||
{ category: 'other', name: 'Yerevan Books', offer: { ru: '−15% на книги', en: '−15% on books' }, address: 'Nalbandyan St 7' },
|
||||
],
|
||||
});
|
||||
|
||||
console.log('Seed готов.');
|
||||
console.log(` Админка: ${owner.email} / ${process.env.SEED_OWNER_PASSWORD ?? 'barev-owner-123'} (владелец), operator@barev.local / barev-operator-123`);
|
||||
console.log(' Агентства: DOMIK, YERHOME (OLDAGENCY — неактивно)');
|
||||
console.log(' Инвайты исполнителей: PRO-GAYANE (не привязан), компания CO-CHISTO');
|
||||
console.log(' Telegram ID для pnpm dev:initdata:', Object.entries(SEED_TG).map(([k, v]) => `${k}=${v}`).join(', '));
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => db.$disconnect());
|
||||
@@ -0,0 +1 @@
|
||||
{ "extends": "../../tsconfig.base.json", "compilerOptions": { "types": ["node"] }, "include": ["src", "prisma.config.ts"] }
|
||||
Reference in new issue
Block a user