Add initial Prisma migration and catalog-based product seed
This commit is contained in:
1
prisma/migration_lock.toml
Normal file
1
prisma/migration_lock.toml
Normal file
@@ -0,0 +1 @@
|
||||
provider = "postgresql"
|
||||
293
prisma/migrations/0001_init/migration.sql
Normal file
293
prisma/migrations/0001_init/migration.sql
Normal 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;
|
||||
|
||||
109
scripts/seed.js
109
scripts/seed.js
@@ -2,9 +2,98 @@ import 'dotenv/config';
|
||||
|
||||
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 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({
|
||||
where: { inn: '7701000000' },
|
||||
update: {},
|
||||
@@ -47,24 +136,8 @@ const warehouseReserve = await prisma.warehouse.upsert({
|
||||
create: { code: 'SPB-01', name: 'Reserve warehouse' },
|
||||
});
|
||||
|
||||
const products = [
|
||||
{
|
||||
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,
|
||||
},
|
||||
];
|
||||
const products = await loadCatalogProducts();
|
||||
console.log(`Loaded ${products.length} products from fregat.org catalog.`);
|
||||
|
||||
for (const product of products) {
|
||||
const dbProduct = await prisma.product.upsert({
|
||||
|
||||
Reference in New Issue
Block a user