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:
zabudko-ruandClaude Opus 5.5 committed 2026-10-17 19:45:51 +04:00
commit 45d7512d2b
64 files changed
+9483

No files matched your search

+29
View File
@@ -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"
}
}
+11
View File
@@ -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"
+749
View File
@@ -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())
}
+21
View File
@@ -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;
}
+351
View File
@@ -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());
+1
View File
@@ -0,0 +1 @@
{ "extends": "../../tsconfig.base.json", "compilerOptions": { "types": ["node"] }, "include": ["src", "prisma.config.ts"] }