Add initial Prisma migration and catalog-based product seed

This commit is contained in:
Ruslan Bakiev
2026-03-31 12:29:21 +07:00
parent 9e379985ad
commit 705d76c597
3 changed files with 385 additions and 18 deletions

View File

@@ -0,0 +1 @@
provider = "postgresql"

View File

@@ -0,0 +1,293 @@
-- CreateSchema
CREATE SCHEMA IF NOT EXISTS "public";
-- CreateEnum
CREATE TYPE "UserRole" AS ENUM ('CLIENT', 'MANAGER');
-- CreateEnum
CREATE TYPE "RegistrationStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED');
-- CreateEnum
CREATE TYPE "MessengerType" AS ENUM ('TELEGRAM', 'MAX');
-- CreateEnum
CREATE TYPE "OrderKind" AS ENUM ('READY', 'CALCULATION');
-- CreateEnum
CREATE TYPE "OrderStatus" AS ENUM ('NEW', 'MANAGER_PROCESSING', 'WAITING_DOUBLE_CONFIRM', 'CLIENT_REJECTED', 'MANAGER_REJECTED', 'MANAGER_BLOCKED', 'CONFIRMED', 'IN_PROGRESS', 'COMPLETED');
-- CreateEnum
CREATE TYPE "WithdrawalStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED');
-- CreateTable
CREATE TABLE "Company" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"inn" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Company_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"fullName" TEXT NOT NULL,
"role" "UserRole" NOT NULL,
"companyId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RegistrationRequest" (
"id" TEXT NOT NULL,
"companyName" TEXT NOT NULL,
"inn" TEXT,
"contactName" TEXT NOT NULL,
"email" TEXT NOT NULL,
"status" "RegistrationStatus" NOT NULL DEFAULT 'PENDING',
"rejectionReason" TEXT,
"requesterId" TEXT,
"reviewedById" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "RegistrationRequest_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Invitation" (
"id" TEXT NOT NULL,
"token" TEXT NOT NULL,
"email" TEXT NOT NULL,
"companyName" TEXT NOT NULL,
"managerId" TEXT NOT NULL,
"acceptedById" TEXT,
"expiresAt" TIMESTAMP(3) NOT NULL,
"acceptedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Invitation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "MessengerConnection" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" "MessengerType" NOT NULL,
"channelId" TEXT NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "MessengerConnection_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Product" (
"id" TEXT NOT NULL,
"sku" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"isCustomizable" BOOLEAN NOT NULL DEFAULT false,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Product_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Warehouse" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Warehouse_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProductStock" (
"id" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"warehouseId" TEXT NOT NULL,
"availableQty" DECIMAL(14,3) NOT NULL,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProductStock_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Order" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"kind" "OrderKind" NOT NULL,
"customerId" TEXT NOT NULL,
"managerId" TEXT,
"status" "OrderStatus" NOT NULL DEFAULT 'NEW',
"clientApproved" BOOLEAN,
"managerApproved" BOOLEAN,
"blockReason" TEXT,
"deliveryTerms" TEXT,
"deliveryFee" DECIMAL(14,2),
"totalPrice" DECIMAL(14,2),
"calculationPayload" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Order_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OrderItem" (
"id" TEXT NOT NULL,
"orderId" TEXT NOT NULL,
"productId" TEXT,
"productName" TEXT NOT NULL,
"quantity" DECIMAL(14,3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "OrderItem_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OrderStatusEvent" (
"id" TEXT NOT NULL,
"orderId" TEXT NOT NULL,
"status" "OrderStatus" NOT NULL,
"actorUserId" TEXT NOT NULL,
"note" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "OrderStatusEvent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ReferralLink" (
"id" TEXT NOT NULL,
"referrerId" TEXT NOT NULL,
"refereeId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ReferralLink_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BonusTransaction" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"amount" DECIMAL(14,2) NOT NULL,
"reason" TEXT NOT NULL,
"orderId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "BonusTransaction_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RewardWithdrawalRequest" (
"id" TEXT NOT NULL,
"requesterId" TEXT NOT NULL,
"amount" DECIMAL(14,2) NOT NULL,
"status" "WithdrawalStatus" NOT NULL DEFAULT 'PENDING',
"reviewedById" TEXT,
"reviewComment" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "RewardWithdrawalRequest_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Company_name_key" ON "Company"("name");
-- CreateIndex
CREATE UNIQUE INDEX "Company_inn_key" ON "Company"("inn");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Invitation_token_key" ON "Invitation"("token");
-- CreateIndex
CREATE UNIQUE INDEX "MessengerConnection_userId_type_channelId_key" ON "MessengerConnection"("userId", "type", "channelId");
-- CreateIndex
CREATE UNIQUE INDEX "Product_sku_key" ON "Product"("sku");
-- CreateIndex
CREATE UNIQUE INDEX "Warehouse_code_key" ON "Warehouse"("code");
-- CreateIndex
CREATE UNIQUE INDEX "ProductStock_productId_warehouseId_key" ON "ProductStock"("productId", "warehouseId");
-- CreateIndex
CREATE UNIQUE INDEX "Order_code_key" ON "Order"("code");
-- CreateIndex
CREATE UNIQUE INDEX "ReferralLink_referrerId_refereeId_key" ON "ReferralLink"("referrerId", "refereeId");
-- AddForeignKey
ALTER TABLE "User" ADD CONSTRAINT "User_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RegistrationRequest" ADD CONSTRAINT "RegistrationRequest_requesterId_fkey" FOREIGN KEY ("requesterId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RegistrationRequest" ADD CONSTRAINT "RegistrationRequest_reviewedById_fkey" FOREIGN KEY ("reviewedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Invitation" ADD CONSTRAINT "Invitation_managerId_fkey" FOREIGN KEY ("managerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Invitation" ADD CONSTRAINT "Invitation_acceptedById_fkey" FOREIGN KEY ("acceptedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "MessengerConnection" ADD CONSTRAINT "MessengerConnection_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProductStock" ADD CONSTRAINT "ProductStock_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProductStock" ADD CONSTRAINT "ProductStock_warehouseId_fkey" FOREIGN KEY ("warehouseId") REFERENCES "Warehouse"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Order" ADD CONSTRAINT "Order_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Order" ADD CONSTRAINT "Order_managerId_fkey" FOREIGN KEY ("managerId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OrderItem" ADD CONSTRAINT "OrderItem_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OrderItem" ADD CONSTRAINT "OrderItem_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OrderStatusEvent" ADD CONSTRAINT "OrderStatusEvent_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OrderStatusEvent" ADD CONSTRAINT "OrderStatusEvent_actorUserId_fkey" FOREIGN KEY ("actorUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ReferralLink" ADD CONSTRAINT "ReferralLink_referrerId_fkey" FOREIGN KEY ("referrerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ReferralLink" ADD CONSTRAINT "ReferralLink_refereeId_fkey" FOREIGN KEY ("refereeId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BonusTransaction" ADD CONSTRAINT "BonusTransaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RewardWithdrawalRequest" ADD CONSTRAINT "RewardWithdrawalRequest_requesterId_fkey" FOREIGN KEY ("requesterId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RewardWithdrawalRequest" ADD CONSTRAINT "RewardWithdrawalRequest_reviewedById_fkey" FOREIGN KEY ("reviewedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -2,9 +2,98 @@ import 'dotenv/config';
import { prisma } from '../src/prisma-client.js'; import { prisma } from '../src/prisma-client.js';
const CATALOG_PAGES = ['https://fregat.org/catalog/', 'https://fregat.org/catalog/page/2/'];
const managerEmail = 'manager@fregat.local'; const managerEmail = 'manager@fregat.local';
const clientEmail = 'client@fregat.local'; const clientEmail = 'client@fregat.local';
function decodeHtmlEntities(value) {
return value
.replaceAll(' ', ' ')
.replaceAll('&', '&')
.replaceAll('"', '"')
.replaceAll(''', "'")
.replaceAll('«', '«')
.replaceAll('»', '»')
.replaceAll('—', '—')
.replaceAll('–', '-');
}
function normalizeText(value) {
return decodeHtmlEntities(value.replaceAll(/<[^>]*>/g, ' ').replaceAll(/\s+/g, ' ')).trim();
}
function skuFromHref(href, index) {
const pathname = href.replace(/^https?:\/\/[^/]+/i, '').split('?')[0] || '';
const segments = pathname.split('/').filter(Boolean);
const slug = segments[segments.length - 1] ?? '';
if (!slug) {
return `FREGAT-CATALOG-${String(index + 1).padStart(3, '0')}`;
}
const normalized = slug.toUpperCase().replaceAll(/[^A-Z0-9]+/g, '-').replace(/^-+|-+$/g, '');
return `FREGAT-${normalized || String(index + 1).padStart(3, '0')}`;
}
function parseCatalogCards(html) {
const cards = [];
const cardRegex =
/<a[^>]*class="[^"]*product-card[^"]*"[^>]*href="([^"]+)"[^>]*>[\s\S]*?<div class="product-card__title">([\s\S]*?)<\/div>\s*<div class="product-card__type">([\s\S]*?)<\/div>/g;
let match;
while ((match = cardRegex.exec(html)) !== null) {
const href = match[1];
const title = normalizeText(match[2]);
const type = normalizeText(match[3]);
if (!title) {
continue;
}
cards.push({ href, title, type });
}
return cards;
}
async function loadCatalogProducts() {
const parsed = [];
for (const pageUrl of CATALOG_PAGES) {
const response = await fetch(pageUrl, { redirect: 'follow' });
if (!response.ok) {
throw new Error(`Catalog request failed: ${pageUrl} (${response.status})`);
}
const html = await response.text();
const cards = parseCatalogCards(html);
if (!cards.length) {
throw new Error(`No product cards found at ${pageUrl}`);
}
parsed.push(...cards);
}
const deduped = new Map();
for (const [index, card] of parsed.entries()) {
const sku = skuFromHref(card.href, index);
if (deduped.has(sku)) {
continue;
}
const descriptionParts = [];
if (card.type) {
descriptionParts.push(card.type);
}
descriptionParts.push(`Источник: ${card.href}`);
deduped.set(sku, {
sku,
name: card.title,
description: descriptionParts.join('. '),
isCustomizable: /логотип|печать/i.test(`${card.title} ${card.type}`),
qtyMain: 300 + index * 17,
qtyReserve: 120 + index * 9,
});
}
return [...deduped.values()];
}
const company = await prisma.company.upsert({ const company = await prisma.company.upsert({
where: { inn: '7701000000' }, where: { inn: '7701000000' },
update: {}, update: {},
@@ -47,24 +136,8 @@ const warehouseReserve = await prisma.warehouse.upsert({
create: { code: 'SPB-01', name: 'Reserve warehouse' }, create: { code: 'SPB-01', name: 'Reserve warehouse' },
}); });
const products = [ const products = await loadCatalogProducts();
{ console.log(`Loaded ${products.length} products from fregat.org catalog.`);
sku: 'FILM-100',
name: 'Пленка прозрачная 100мкм',
description: 'Готовая продукция для стандартной упаковки',
isCustomizable: false,
qtyMain: 1250,
qtyReserve: 420,
},
{
sku: 'FILM-200-COLOR',
name: 'Пленка цветная 200мкм',
description: 'Продукция с дополнительной окраской',
isCustomizable: true,
qtyMain: 860,
qtyReserve: 230,
},
];
for (const product of products) { for (const product of products) {
const dbProduct = await prisma.product.upsert({ const dbProduct = await prisma.product.upsert({