diff --git a/prisma/migration_lock.toml b/prisma/migration_lock.toml new file mode 100644 index 0000000..2fe25d8 --- /dev/null +++ b/prisma/migration_lock.toml @@ -0,0 +1 @@ +provider = "postgresql" diff --git a/prisma/migrations/0001_init/migration.sql b/prisma/migrations/0001_init/migration.sql new file mode 100644 index 0000000..e57c598 --- /dev/null +++ b/prisma/migrations/0001_init/migration.sql @@ -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; + diff --git a/scripts/seed.js b/scripts/seed.js index 47767f5..7a0e74d 100644 --- a/scripts/seed.js +++ b/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 = + /]*class="[^"]*product-card[^"]*"[^>]*href="([^"]+)"[^>]*>[\s\S]*?
([\s\S]*?)<\/div>\s*
([\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({