feat: auto-run SQL migrations and one-time seed on boot

This commit is contained in:
mahmood
2026-09-10 17:15:17 +03:30
parent a4c5c9259f
commit f19f49daeb
12 changed files with 2569 additions and 0 deletions
+186
View File
@@ -77552,6 +77552,190 @@ var require_app_module = __commonJS({
}
});
// dist/prisma/migrate.js
var require_migrate = __commonJS({
"dist/prisma/migrate.js"(exports2) {
"use strict";
Object.defineProperty(exports2, "__esModule", { value: true });
exports2.splitSqlStatements = splitSqlStatements;
exports2.runMigrations = runMigrations;
exports2.runOnce = runOnce;
var fs_1 = require("fs");
var path_1 = require("path");
var TRACKING_TABLE = "_goldbank_migrations";
function resolveMigrationsDir() {
const candidates = [
process.env.MIGRATIONS_DIR,
(0, path_1.join)(process.cwd(), "migrations"),
(0, path_1.join)(__dirname, "..", "..", "migrations"),
(0, path_1.join)(__dirname, "..", "..", "prisma", "migrations"),
(0, path_1.join)(process.cwd(), "prisma", "migrations")
].filter((p) => Boolean(p));
const found = candidates.find((dir) => (0, fs_1.existsSync)(dir));
if (!found) {
throw new Error(`\u067E\u0648\u0634\u0647 migrations \u067E\u06CC\u062F\u0627 \u0646\u0634\u062F. \u0645\u0633\u06CC\u0631\u0647\u0627\u06CC \u0628\u0631\u0631\u0633\u06CC\u200C\u0634\u062F\u0647: ${candidates.join(", ")}`);
}
return found;
}
function splitSqlStatements(sql) {
const statements = [];
let current = "";
let i = 0;
while (i < sql.length) {
const char = sql[i];
const rest = sql.slice(i);
if (rest.startsWith("--")) {
const lineEnd = sql.indexOf("\n", i);
i = lineEnd === -1 ? sql.length : lineEnd + 1;
continue;
}
if (rest.startsWith("/*")) {
const commentEnd = sql.indexOf("*/", i + 2);
i = commentEnd === -1 ? sql.length : commentEnd + 2;
continue;
}
if (char === "'" || char === '"') {
const quote = char;
current += char;
i += 1;
while (i < sql.length) {
current += sql[i];
if (sql[i] === quote) {
if (sql[i + 1] === quote) {
current += sql[i + 1];
i += 2;
continue;
}
i += 1;
break;
}
i += 1;
}
continue;
}
const dollarTag = /^\$[A-Za-z_]*\$/.exec(rest);
if (dollarTag) {
const tag = dollarTag[0];
const closing = sql.indexOf(tag, i + tag.length);
const end = closing === -1 ? sql.length : closing + tag.length;
current += sql.slice(i, end);
i = end;
continue;
}
if (char === ";") {
if (current.trim())
statements.push(current.trim());
current = "";
i += 1;
continue;
}
current += char;
i += 1;
}
if (current.trim())
statements.push(current.trim());
return statements;
}
async function runMigrations(prisma) {
const dir = resolveMigrationsDir();
const files = (0, fs_1.readdirSync)(dir).filter((name) => name.endsWith(".sql")).sort();
const trackingExisted = await prisma.$queryRawUnsafe(`SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = current_schema() AND table_name = $1
) AS "exists"`, TRACKING_TABLE);
const freshDatabase = !trackingExisted[0]?.exists;
await prisma.$executeRawUnsafe(`CREATE TABLE IF NOT EXISTS "${TRACKING_TABLE}" (
name TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
)`);
const alreadyApplied = await prisma.$queryRawUnsafe(`SELECT name FROM "${TRACKING_TABLE}"`);
const appliedNames = new Set(alreadyApplied.map((row) => row.name));
const applied = [];
for (const file2 of files) {
if (appliedNames.has(file2))
continue;
const statements = splitSqlStatements((0, fs_1.readFileSync)((0, path_1.join)(dir, file2), "utf8"));
await prisma.$transaction(
async (tx) => {
for (const statement of statements) {
await tx.$executeRawUnsafe(statement);
}
await tx.$executeRawUnsafe(`INSERT INTO "${TRACKING_TABLE}" (name) VALUES ($1)`, file2);
},
// The baseline migration is hundreds of DDL statements; the 5s default is
// not enough, especially against a remote database.
{ maxWait: 6e4, timeout: 6e5 }
);
applied.push(file2);
}
return { freshDatabase, applied };
}
async function runOnce(prisma, name, task) {
const rows = await prisma.$queryRawUnsafe(`SELECT name FROM "${TRACKING_TABLE}" WHERE name = $1`, name);
if (rows.length > 0)
return false;
await task();
await prisma.$executeRawUnsafe(`INSERT INTO "${TRACKING_TABLE}" (name) VALUES ($1)`, name);
return true;
}
}
});
// dist/prisma/bootstrap.js
var require_bootstrap = __commonJS({
"dist/prisma/bootstrap.js"(exports2) {
"use strict";
Object.defineProperty(exports2, "__esModule", { value: true });
exports2.prepareDatabase = prepareDatabase;
var child_process_1 = require("child_process");
var fs_1 = require("fs");
var path_1 = require("path");
var util_1 = require("util");
var common_12 = require_common();
var client_1 = require("@prisma/client");
var migrate_1 = require_migrate();
var execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
function resolveSeedScript() {
const candidates = [
process.env.SEED_SCRIPT,
(0, path_1.join)(process.cwd(), "seed.js"),
(0, path_1.join)(__dirname, "..", "..", "seed.js")
].filter((p) => Boolean(p));
return candidates.find((file2) => (0, fs_1.existsSync)(file2));
}
async function prepareDatabase() {
if (process.env.RUN_MIGRATIONS === "false")
return;
const logger = new common_12.Logger("DatabaseBootstrap");
const prisma = new client_1.PrismaClient();
try {
const { applied } = await (0, migrate_1.runMigrations)(prisma);
if (applied.length > 0) {
logger.log(`Applied migrations: ${applied.join(", ")}`);
} else {
logger.log("No pending migrations");
}
if (process.env.RUN_SEED === "false")
return;
const seedScript = resolveSeedScript();
if (!seedScript) {
logger.warn("No seed script found \u2014 skipping seed");
return;
}
const seeded = await (0, migrate_1.runOnce)(prisma, "seed:initial", async () => {
const { stdout } = await execFileAsync(process.execPath, [seedScript]);
if (stdout.trim())
logger.log(stdout.trim());
});
if (seeded)
logger.log("Seed data inserted");
} finally {
await prisma.$disconnect();
}
}
}
});
// dist/prisma/prisma-exception.filter.js
var require_prisma_exception_filter = __commonJS({
"dist/prisma/prisma-exception.filter.js"(exports2) {
@@ -77626,8 +77810,10 @@ require_Reflect();
var core_1 = require_core3();
var common_1 = require_common();
var app_module_1 = require_app_module();
var bootstrap_1 = require_bootstrap();
var prisma_exception_filter_1 = require_prisma_exception_filter();
async function bootstrap() {
await (0, bootstrap_1.prepareDatabase)();
const app = await core_1.NestFactory.create(app_module_1.AppModule);
app.setGlobalPrefix("api");
app.useGlobalPipes(new common_1.ValidationPipe({ whitelist: true, transform: true }));
+431
View File
@@ -0,0 +1,431 @@
-- CreateEnum
CREATE TYPE "AdminRole" AS ENUM ('SUPER_ADMIN', 'SYSTEM_APPROVER', 'SALES_EXPERT', 'WORKSHOP_SUPERVISOR', 'ACCOUNTANT');
-- CreateEnum
CREATE TYPE "VerificationStep" AS ENUM ('UNVERIFIED', 'STEP_ONE_SUBMITTED', 'STEP_TWO_SUBMITTED', 'VERIFIED');
-- CreateEnum
CREATE TYPE "OrderStatus" AS ENUM ('SUSPENDED', 'PLACED', 'ADMIN_APPROVED', 'SOURCING', 'PREPARING', 'DELIVERED_TO_AGENT', 'DELIVERED_TO_CUSTOMER', 'CANCELLED');
-- CreateEnum
CREATE TYPE "PreOrderStatus" AS ENUM ('PLACED', 'ADMIN_APPROVED', 'WORKSHOP_APPROVED', 'READY_TO_SHIP', 'DELIVERED', 'CANCELLED');
-- CreateEnum
CREATE TYPE "LedgerCurrency" AS ENUM ('GOLD_GRAM', 'RIAL');
-- CreateEnum
CREATE TYPE "LedgerDirection" AS ENUM ('DEBIT', 'CREDIT');
-- CreateEnum
CREATE TYPE "LedgerSourceType" AS ENUM ('ORDER_STOCK', 'ORDER_PRE_ORDER', 'REPAYMENT_MELTED_GOLD', 'REPAYMENT_TRANSFER', 'REPAYMENT_CHEQUE', 'REPAYMENT_CASH', 'WALLET_BONUS', 'ADJUSTMENT');
-- CreateTable
CREATE TABLE "City" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"province" TEXT,
CONSTRAINT "City_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AdminUser" (
"id" TEXT NOT NULL,
"fullName" TEXT NOT NULL,
"mobile" TEXT NOT NULL,
"passwordHash" TEXT NOT NULL,
"role" "AdminRole" NOT NULL,
"branchId" TEXT,
"wagePercent" DECIMAL(6,3),
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AdminUser_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Customer" (
"id" TEXT NOT NULL,
"fullName" TEXT NOT NULL,
"mobile" TEXT NOT NULL,
"nationalCode" TEXT NOT NULL,
"passwordHash" TEXT NOT NULL,
"cityId" TEXT,
"referredByAgentId" TEXT,
"goldCreditLimitGrams" DECIMAL(14,4) NOT NULL DEFAULT 0,
"goldDebtGrams" DECIMAL(14,4) NOT NULL DEFAULT 0,
"fiatDebtRial" DECIMAL(20,2) NOT NULL DEFAULT 0,
"verificationStep" "VerificationStep" NOT NULL DEFAULT 'UNVERIFIED',
"nationalIdImageUrl" TEXT,
"businessLicenseUrl" TEXT,
"shopImageUrl" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Customer_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CustomerAddress" (
"id" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"cityId" TEXT,
"addressLine" TEXT NOT NULL,
"isDefault" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "CustomerAddress_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Branch" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"cityId" TEXT,
CONSTRAINT "Branch_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Workshop" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "Workshop_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Category" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"parentId" TEXT,
CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Brand" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
CONSTRAINT "Brand_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Collection" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "Collection_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CollectionProduct" (
"collectionId" TEXT NOT NULL,
"productId" TEXT NOT NULL,
CONSTRAINT "CollectionProduct_pkey" PRIMARY KEY ("collectionId","productId")
);
-- CreateTable
CREATE TABLE "Product" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"categoryId" TEXT,
"brandId" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Product_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProductImage" (
"id" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"url" TEXT NOT NULL,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "ProductImage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProductVariant" (
"id" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"title" TEXT NOT NULL,
"sku" TEXT,
"rashWeightGrams" DECIMAL(14,4) NOT NULL,
"defaultWagePercent" DECIMAL(6,3) NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "ProductVariant_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Order" (
"id" TEXT NOT NULL,
"orderNumber" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"branchId" TEXT,
"status" "OrderStatus" NOT NULL DEFAULT 'PLACED',
"totalGoldWeightGrams" DECIMAL(14,4) NOT NULL,
"wageWeightGrams" DECIMAL(14,4) NOT NULL,
"flatRialCharge" DECIMAL(20,2) NOT NULL DEFAULT 0,
"isSuspendedOverCreditLimit" BOOLEAN NOT NULL DEFAULT false,
"deliveredAt" TIMESTAMP(3),
"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,
"productVariantId" TEXT NOT NULL,
"quantity" INTEGER NOT NULL,
"rashWeightGramsSnapshot" DECIMAL(14,4) NOT NULL,
"wagePercentSnapshot" DECIMAL(6,3) NOT NULL,
"discountPercentSnapshot" DECIMAL(6,3) NOT NULL DEFAULT 0,
"computedWeightGrams" DECIMAL(14,4) NOT NULL,
"flatRialCharge" DECIMAL(20,2) NOT NULL DEFAULT 0,
CONSTRAINT "OrderItem_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OrderStatusEvent" (
"id" TEXT NOT NULL,
"orderId" TEXT NOT NULL,
"fromStatus" "OrderStatus",
"toStatus" "OrderStatus" NOT NULL,
"note" TEXT,
"actorAdminUserId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "OrderStatusEvent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PreOrder" (
"id" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"cityId" TEXT,
"workshopId" TEXT,
"status" "PreOrderStatus" NOT NULL DEFAULT 'PLACED',
"designImageUrl" TEXT,
"colorName" TEXT,
"sizeSpec" TEXT,
"stoneColor" TEXT,
"minaColor" TEXT,
"estimatedWeightGrams" DECIMAL(14,4),
"estimatedPriceRial" DECIMAL(20,2),
"description" TEXT,
"convertedOrderId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "PreOrder_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PreOrderStatusEvent" (
"id" TEXT NOT NULL,
"preOrderId" TEXT NOT NULL,
"changes" JSONB NOT NULL,
"actorAdminUserId" TEXT,
"ipAddress" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PreOrderStatusEvent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "LedgerEntry" (
"id" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"currency" "LedgerCurrency" NOT NULL,
"direction" "LedgerDirection" NOT NULL,
"sourceType" "LedgerSourceType" NOT NULL,
"amount" DECIMAL(20,4) NOT NULL,
"karatPerMille" INTEGER,
"pricePerGramRialAtEntry" DECIMAL(20,2),
"orderId" TEXT,
"confirmed" BOOLEAN NOT NULL DEFAULT false,
"confirmedAt" TIMESTAMP(3),
"postedByAdminUserId" TEXT,
"note" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "LedgerEntry_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "GoldPriceQuote" (
"id" TEXT NOT NULL,
"karatPerMille" INTEGER NOT NULL,
"pricePerGramRial" DECIMAL(20,2) NOT NULL,
"source" TEXT NOT NULL,
"quotedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "GoldPriceQuote_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "WalletBonus" (
"id" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"orderId" TEXT,
"goldGrams" DECIMAL(14,4) NOT NULL,
"grantedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expiresAt" TIMESTAMP(3) NOT NULL,
"consumedAt" TIMESTAMP(3),
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "WalletBonus_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "AdminUser_mobile_key" ON "AdminUser"("mobile");
-- CreateIndex
CREATE INDEX "AdminUser_role_idx" ON "AdminUser"("role");
-- CreateIndex
CREATE UNIQUE INDEX "Customer_mobile_key" ON "Customer"("mobile");
-- CreateIndex
CREATE UNIQUE INDEX "Customer_nationalCode_key" ON "Customer"("nationalCode");
-- CreateIndex
CREATE INDEX "Customer_referredByAgentId_idx" ON "Customer"("referredByAgentId");
-- CreateIndex
CREATE UNIQUE INDEX "ProductVariant_sku_key" ON "ProductVariant"("sku");
-- CreateIndex
CREATE UNIQUE INDEX "Order_orderNumber_key" ON "Order"("orderNumber");
-- CreateIndex
CREATE INDEX "Order_customerId_idx" ON "Order"("customerId");
-- CreateIndex
CREATE INDEX "Order_status_idx" ON "Order"("status");
-- CreateIndex
CREATE INDEX "OrderStatusEvent_orderId_idx" ON "OrderStatusEvent"("orderId");
-- CreateIndex
CREATE UNIQUE INDEX "PreOrder_convertedOrderId_key" ON "PreOrder"("convertedOrderId");
-- CreateIndex
CREATE INDEX "PreOrder_customerId_idx" ON "PreOrder"("customerId");
-- CreateIndex
CREATE INDEX "PreOrderStatusEvent_preOrderId_idx" ON "PreOrderStatusEvent"("preOrderId");
-- CreateIndex
CREATE INDEX "LedgerEntry_customerId_currency_idx" ON "LedgerEntry"("customerId", "currency");
-- CreateIndex
CREATE INDEX "LedgerEntry_orderId_idx" ON "LedgerEntry"("orderId");
-- CreateIndex
CREATE INDEX "GoldPriceQuote_karatPerMille_quotedAt_idx" ON "GoldPriceQuote"("karatPerMille", "quotedAt");
-- CreateIndex
CREATE INDEX "WalletBonus_customerId_isActive_idx" ON "WalletBonus"("customerId", "isActive");
-- AddForeignKey
ALTER TABLE "AdminUser" ADD CONSTRAINT "AdminUser_branchId_fkey" FOREIGN KEY ("branchId") REFERENCES "Branch"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Customer" ADD CONSTRAINT "Customer_cityId_fkey" FOREIGN KEY ("cityId") REFERENCES "City"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Customer" ADD CONSTRAINT "Customer_referredByAgentId_fkey" FOREIGN KEY ("referredByAgentId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CustomerAddress" ADD CONSTRAINT "CustomerAddress_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CustomerAddress" ADD CONSTRAINT "CustomerAddress_cityId_fkey" FOREIGN KEY ("cityId") REFERENCES "City"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Branch" ADD CONSTRAINT "Branch_cityId_fkey" FOREIGN KEY ("cityId") REFERENCES "City"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Category" ADD CONSTRAINT "Category_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Category"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CollectionProduct" ADD CONSTRAINT "CollectionProduct_collectionId_fkey" FOREIGN KEY ("collectionId") REFERENCES "Collection"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CollectionProduct" ADD CONSTRAINT "CollectionProduct_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Product" ADD CONSTRAINT "Product_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Product" ADD CONSTRAINT "Product_brandId_fkey" FOREIGN KEY ("brandId") REFERENCES "Brand"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProductImage" ADD CONSTRAINT "ProductImage_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProductVariant" ADD CONSTRAINT "ProductVariant_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Order" ADD CONSTRAINT "Order_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Order" ADD CONSTRAINT "Order_branchId_fkey" FOREIGN KEY ("branchId") REFERENCES "Branch"("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_productVariantId_fkey" FOREIGN KEY ("productVariantId") REFERENCES "ProductVariant"("id") ON DELETE RESTRICT 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_actorAdminUserId_fkey" FOREIGN KEY ("actorAdminUserId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PreOrder" ADD CONSTRAINT "PreOrder_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PreOrder" ADD CONSTRAINT "PreOrder_cityId_fkey" FOREIGN KEY ("cityId") REFERENCES "City"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PreOrder" ADD CONSTRAINT "PreOrder_workshopId_fkey" FOREIGN KEY ("workshopId") REFERENCES "Workshop"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PreOrder" ADD CONSTRAINT "PreOrder_convertedOrderId_fkey" FOREIGN KEY ("convertedOrderId") REFERENCES "Order"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PreOrderStatusEvent" ADD CONSTRAINT "PreOrderStatusEvent_preOrderId_fkey" FOREIGN KEY ("preOrderId") REFERENCES "PreOrder"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PreOrderStatusEvent" ADD CONSTRAINT "PreOrderStatusEvent_actorAdminUserId_fkey" FOREIGN KEY ("actorAdminUserId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LedgerEntry" ADD CONSTRAINT "LedgerEntry_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LedgerEntry" ADD CONSTRAINT "LedgerEntry_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LedgerEntry" ADD CONSTRAINT "LedgerEntry_postedByAdminUserId_fkey" FOREIGN KEY ("postedByAdminUserId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WalletBonus" ADD CONSTRAINT "WalletBonus_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,430 @@
-- CreateEnum
CREATE TYPE "AdminRole" AS ENUM ('SUPER_ADMIN', 'SYSTEM_APPROVER', 'SALES_EXPERT', 'WORKSHOP_SUPERVISOR', 'ACCOUNTANT');
-- CreateEnum
CREATE TYPE "VerificationStep" AS ENUM ('UNVERIFIED', 'STEP_ONE_SUBMITTED', 'STEP_TWO_SUBMITTED', 'VERIFIED');
-- CreateEnum
CREATE TYPE "OrderStatus" AS ENUM ('SUSPENDED', 'PLACED', 'ADMIN_APPROVED', 'SOURCING', 'PREPARING', 'DELIVERED_TO_AGENT', 'DELIVERED_TO_CUSTOMER', 'CANCELLED');
-- CreateEnum
CREATE TYPE "PreOrderStatus" AS ENUM ('PLACED', 'ADMIN_APPROVED', 'WORKSHOP_APPROVED', 'READY_TO_SHIP', 'DELIVERED', 'CANCELLED');
-- CreateEnum
CREATE TYPE "LedgerCurrency" AS ENUM ('GOLD_GRAM', 'RIAL');
-- CreateEnum
CREATE TYPE "LedgerDirection" AS ENUM ('DEBIT', 'CREDIT');
-- CreateEnum
CREATE TYPE "LedgerSourceType" AS ENUM ('ORDER_STOCK', 'ORDER_PRE_ORDER', 'REPAYMENT_MELTED_GOLD', 'REPAYMENT_TRANSFER', 'REPAYMENT_CHEQUE', 'REPAYMENT_CASH', 'WALLET_BONUS', 'ADJUSTMENT');
-- CreateTable
CREATE TABLE "City" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"province" TEXT,
CONSTRAINT "City_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AdminUser" (
"id" TEXT NOT NULL,
"fullName" TEXT NOT NULL,
"mobile" TEXT NOT NULL,
"passwordHash" TEXT NOT NULL,
"role" "AdminRole" NOT NULL,
"branchId" TEXT,
"wagePercent" DECIMAL(6,3),
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AdminUser_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Customer" (
"id" TEXT NOT NULL,
"fullName" TEXT NOT NULL,
"mobile" TEXT NOT NULL,
"nationalCode" TEXT NOT NULL,
"passwordHash" TEXT NOT NULL,
"cityId" TEXT,
"referredByAgentId" TEXT,
"goldCreditLimitGrams" DECIMAL(14,4) NOT NULL DEFAULT 0,
"goldDebtGrams" DECIMAL(14,4) NOT NULL DEFAULT 0,
"fiatDebtRial" DECIMAL(20,2) NOT NULL DEFAULT 0,
"verificationStep" "VerificationStep" NOT NULL DEFAULT 'UNVERIFIED',
"nationalIdImageUrl" TEXT,
"businessLicenseUrl" TEXT,
"shopImageUrl" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Customer_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CustomerAddress" (
"id" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"cityId" TEXT,
"addressLine" TEXT NOT NULL,
"isDefault" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "CustomerAddress_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Branch" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"cityId" TEXT,
CONSTRAINT "Branch_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Workshop" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "Workshop_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Category" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"parentId" TEXT,
CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Brand" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
CONSTRAINT "Brand_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Collection" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "Collection_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CollectionProduct" (
"collectionId" TEXT NOT NULL,
"productId" TEXT NOT NULL,
CONSTRAINT "CollectionProduct_pkey" PRIMARY KEY ("collectionId","productId")
);
-- CreateTable
CREATE TABLE "Product" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"categoryId" TEXT,
"brandId" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Product_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProductImage" (
"id" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"url" TEXT NOT NULL,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "ProductImage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProductVariant" (
"id" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"title" TEXT NOT NULL,
"sku" TEXT,
"rashWeightGrams" DECIMAL(14,4) NOT NULL,
"defaultWagePercent" DECIMAL(6,3) NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "ProductVariant_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Order" (
"id" TEXT NOT NULL,
"orderNumber" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"branchId" TEXT,
"status" "OrderStatus" NOT NULL DEFAULT 'PLACED',
"totalGoldWeightGrams" DECIMAL(14,4) NOT NULL,
"wageWeightGrams" DECIMAL(14,4) NOT NULL,
"flatRialCharge" DECIMAL(20,2) NOT NULL DEFAULT 0,
"isSuspendedOverCreditLimit" BOOLEAN NOT NULL DEFAULT false,
"deliveredAt" TIMESTAMP(3),
"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,
"productVariantId" TEXT NOT NULL,
"quantity" INTEGER NOT NULL,
"rashWeightGramsSnapshot" DECIMAL(14,4) NOT NULL,
"wagePercentSnapshot" DECIMAL(6,3) NOT NULL,
"discountPercentSnapshot" DECIMAL(6,3) NOT NULL DEFAULT 0,
"computedWeightGrams" DECIMAL(14,4) NOT NULL,
"flatRialCharge" DECIMAL(20,2) NOT NULL DEFAULT 0,
CONSTRAINT "OrderItem_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OrderStatusEvent" (
"id" TEXT NOT NULL,
"orderId" TEXT NOT NULL,
"fromStatus" "OrderStatus",
"toStatus" "OrderStatus" NOT NULL,
"note" TEXT,
"actorAdminUserId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "OrderStatusEvent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PreOrder" (
"id" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"cityId" TEXT,
"workshopId" TEXT,
"status" "PreOrderStatus" NOT NULL DEFAULT 'PLACED',
"designImageUrl" TEXT,
"colorName" TEXT,
"sizeSpec" TEXT,
"stoneColor" TEXT,
"minaColor" TEXT,
"estimatedWeightGrams" DECIMAL(14,4),
"estimatedPriceRial" DECIMAL(20,2),
"description" TEXT,
"convertedOrderId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "PreOrder_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PreOrderStatusEvent" (
"id" TEXT NOT NULL,
"preOrderId" TEXT NOT NULL,
"changes" JSONB NOT NULL,
"actorAdminUserId" TEXT,
"ipAddress" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PreOrderStatusEvent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "LedgerEntry" (
"id" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"currency" "LedgerCurrency" NOT NULL,
"direction" "LedgerDirection" NOT NULL,
"sourceType" "LedgerSourceType" NOT NULL,
"amount" DECIMAL(20,4) NOT NULL,
"karatPerMille" INTEGER,
"pricePerGramRialAtEntry" DECIMAL(20,2),
"orderId" TEXT,
"confirmed" BOOLEAN NOT NULL DEFAULT false,
"confirmedAt" TIMESTAMP(3),
"postedByAdminUserId" TEXT,
"note" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "LedgerEntry_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "GoldPriceQuote" (
"id" TEXT NOT NULL,
"karatPerMille" INTEGER NOT NULL,
"pricePerGramRial" DECIMAL(20,2) NOT NULL,
"source" TEXT NOT NULL,
"quotedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "GoldPriceQuote_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "WalletBonus" (
"id" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"orderId" TEXT,
"goldGrams" DECIMAL(14,4) NOT NULL,
"grantedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expiresAt" TIMESTAMP(3) NOT NULL,
"consumedAt" TIMESTAMP(3),
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "WalletBonus_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "AdminUser_mobile_key" ON "AdminUser"("mobile");
-- CreateIndex
CREATE INDEX "AdminUser_role_idx" ON "AdminUser"("role");
-- CreateIndex
CREATE UNIQUE INDEX "Customer_mobile_key" ON "Customer"("mobile");
-- CreateIndex
CREATE UNIQUE INDEX "Customer_nationalCode_key" ON "Customer"("nationalCode");
-- CreateIndex
CREATE INDEX "Customer_referredByAgentId_idx" ON "Customer"("referredByAgentId");
-- CreateIndex
CREATE UNIQUE INDEX "ProductVariant_sku_key" ON "ProductVariant"("sku");
-- CreateIndex
CREATE UNIQUE INDEX "Order_orderNumber_key" ON "Order"("orderNumber");
-- CreateIndex
CREATE INDEX "Order_customerId_idx" ON "Order"("customerId");
-- CreateIndex
CREATE INDEX "Order_status_idx" ON "Order"("status");
-- CreateIndex
CREATE INDEX "OrderStatusEvent_orderId_idx" ON "OrderStatusEvent"("orderId");
-- CreateIndex
CREATE UNIQUE INDEX "PreOrder_convertedOrderId_key" ON "PreOrder"("convertedOrderId");
-- CreateIndex
CREATE INDEX "PreOrder_customerId_idx" ON "PreOrder"("customerId");
-- CreateIndex
CREATE INDEX "PreOrderStatusEvent_preOrderId_idx" ON "PreOrderStatusEvent"("preOrderId");
-- CreateIndex
CREATE INDEX "LedgerEntry_customerId_currency_idx" ON "LedgerEntry"("customerId", "currency");
-- CreateIndex
CREATE INDEX "LedgerEntry_orderId_idx" ON "LedgerEntry"("orderId");
-- CreateIndex
CREATE INDEX "GoldPriceQuote_karatPerMille_quotedAt_idx" ON "GoldPriceQuote"("karatPerMille", "quotedAt");
-- CreateIndex
CREATE INDEX "WalletBonus_customerId_isActive_idx" ON "WalletBonus"("customerId", "isActive");
-- AddForeignKey
ALTER TABLE "AdminUser" ADD CONSTRAINT "AdminUser_branchId_fkey" FOREIGN KEY ("branchId") REFERENCES "Branch"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Customer" ADD CONSTRAINT "Customer_cityId_fkey" FOREIGN KEY ("cityId") REFERENCES "City"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Customer" ADD CONSTRAINT "Customer_referredByAgentId_fkey" FOREIGN KEY ("referredByAgentId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CustomerAddress" ADD CONSTRAINT "CustomerAddress_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CustomerAddress" ADD CONSTRAINT "CustomerAddress_cityId_fkey" FOREIGN KEY ("cityId") REFERENCES "City"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Branch" ADD CONSTRAINT "Branch_cityId_fkey" FOREIGN KEY ("cityId") REFERENCES "City"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Category" ADD CONSTRAINT "Category_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Category"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CollectionProduct" ADD CONSTRAINT "CollectionProduct_collectionId_fkey" FOREIGN KEY ("collectionId") REFERENCES "Collection"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CollectionProduct" ADD CONSTRAINT "CollectionProduct_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Product" ADD CONSTRAINT "Product_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Product" ADD CONSTRAINT "Product_brandId_fkey" FOREIGN KEY ("brandId") REFERENCES "Brand"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProductImage" ADD CONSTRAINT "ProductImage_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProductVariant" ADD CONSTRAINT "ProductVariant_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Order" ADD CONSTRAINT "Order_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Order" ADD CONSTRAINT "Order_branchId_fkey" FOREIGN KEY ("branchId") REFERENCES "Branch"("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_productVariantId_fkey" FOREIGN KEY ("productVariantId") REFERENCES "ProductVariant"("id") ON DELETE RESTRICT 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_actorAdminUserId_fkey" FOREIGN KEY ("actorAdminUserId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PreOrder" ADD CONSTRAINT "PreOrder_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PreOrder" ADD CONSTRAINT "PreOrder_cityId_fkey" FOREIGN KEY ("cityId") REFERENCES "City"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PreOrder" ADD CONSTRAINT "PreOrder_workshopId_fkey" FOREIGN KEY ("workshopId") REFERENCES "Workshop"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PreOrder" ADD CONSTRAINT "PreOrder_convertedOrderId_fkey" FOREIGN KEY ("convertedOrderId") REFERENCES "Order"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PreOrderStatusEvent" ADD CONSTRAINT "PreOrderStatusEvent_preOrderId_fkey" FOREIGN KEY ("preOrderId") REFERENCES "PreOrder"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PreOrderStatusEvent" ADD CONSTRAINT "PreOrderStatusEvent_actorAdminUserId_fkey" FOREIGN KEY ("actorAdminUserId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LedgerEntry" ADD CONSTRAINT "LedgerEntry_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LedgerEntry" ADD CONSTRAINT "LedgerEntry_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LedgerEntry" ADD CONSTRAINT "LedgerEntry_postedByAdminUserId_fkey" FOREIGN KEY ("postedByAdminUserId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WalletBonus" ADD CONSTRAINT "WalletBonus_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
+402
View File
@@ -0,0 +1,402 @@
"use strict";
// prisma/seed.ts
var import_client = require("@prisma/client");
var prisma = new import_client.PrismaClient();
var PLACEHOLDER_PASSWORD_HASH = "placeholder-hash-not-for-production";
var PLACEHOLDER_PRODUCT_IMAGE = "/img/no-items.png";
async function main() {
const [tehran, mashhad, isfahan, tabriz] = await Promise.all([
prisma.city.upsert({ where: { id: "city-tehran" }, update: {}, create: { id: "city-tehran", name: "\u062A\u0647\u0631\u0627\u0646", province: "\u062A\u0647\u0631\u0627\u0646" } }),
prisma.city.upsert({ where: { id: "city-mashhad" }, update: {}, create: { id: "city-mashhad", name: "\u0645\u0634\u0647\u062F", province: "\u062E\u0631\u0627\u0633\u0627\u0646 \u0631\u0636\u0648\u06CC" } }),
prisma.city.upsert({ where: { id: "city-isfahan" }, update: {}, create: { id: "city-isfahan", name: "\u0627\u0635\u0641\u0647\u0627\u0646", province: "\u0627\u0635\u0641\u0647\u0627\u0646" } }),
prisma.city.upsert({ where: { id: "city-tabriz" }, update: {}, create: { id: "city-tabriz", name: "\u062A\u0628\u0631\u06CC\u0632", province: "\u0622\u0630\u0631\u0628\u0627\u06CC\u062C\u0627\u0646 \u0634\u0631\u0642\u06CC" } })
]);
const branchTehran = await prisma.branch.upsert({
where: { id: "branch-tehran-bazaar" },
update: {},
create: { id: "branch-tehran-bazaar", name: "\u0634\u0639\u0628\u0647 \u0628\u0627\u0632\u0627\u0631 \u0628\u0632\u0631\u06AF \u062A\u0647\u0631\u0627\u0646", cityId: tehran.id }
});
const branchMashhad = await prisma.branch.upsert({
where: { id: "branch-mashhad-main" },
update: {},
create: { id: "branch-mashhad-main", name: "\u0634\u0639\u0628\u0647 \u0645\u0631\u06A9\u0632\u06CC \u0645\u0634\u0647\u062F", cityId: mashhad.id }
});
await prisma.workshop.upsert({
where: { id: "workshop-central" },
update: {},
create: { id: "workshop-central", name: "\u06A9\u0627\u0631\u06AF\u0627\u0647 \u0645\u0631\u06A9\u0632\u06CC \u0633\u0627\u062E\u062A \u0637\u0644\u0627" }
});
await prisma.adminUser.upsert({
where: { mobile: "09120000001" },
update: {},
create: {
mobile: "09120000001",
fullName: "\u0645\u062F\u06CC\u0631 \u0627\u0631\u0634\u062F \u0633\u06CC\u0633\u062A\u0645",
passwordHash: PLACEHOLDER_PASSWORD_HASH,
role: import_client.AdminRole.SUPER_ADMIN,
branchId: branchTehran.id
}
});
const agentReza = await prisma.adminUser.upsert({
where: { mobile: "09121234567" },
update: {},
create: {
mobile: "09121234567",
fullName: "\u0631\u0636\u0627 \u0627\u062D\u0645\u062F\u06CC",
passwordHash: PLACEHOLDER_PASSWORD_HASH,
role: import_client.AdminRole.SALES_EXPERT,
branchId: branchTehran.id,
wagePercent: 1.5
}
});
const agentSara = await prisma.adminUser.upsert({
where: { mobile: "09129876543" },
update: {},
create: {
mobile: "09129876543",
fullName: "\u0633\u0627\u0631\u0627 \u06A9\u0631\u06CC\u0645\u06CC",
passwordHash: PLACEHOLDER_PASSWORD_HASH,
role: import_client.AdminRole.SALES_EXPERT,
branchId: branchMashhad.id,
wagePercent: 1.2
}
});
await prisma.adminUser.upsert({
where: { mobile: "09123334444" },
update: {},
create: {
mobile: "09123334444",
fullName: "\u0647\u0648\u0645\u0646 \u06CC\u0632\u062F\u0627\u0646\u06CC",
passwordHash: PLACEHOLDER_PASSWORD_HASH,
role: import_client.AdminRole.WORKSHOP_SUPERVISOR
}
});
await prisma.adminUser.upsert({
where: { mobile: "09125556666" },
update: {},
create: {
mobile: "09125556666",
fullName: "\u0641\u0631\u0632\u0627\u0646\u0647 \u0645\u062D\u0645\u0648\u062F\u06CC",
passwordHash: PLACEHOLDER_PASSWORD_HASH,
role: import_client.AdminRole.ACCOUNTANT,
branchId: branchTehran.id
}
});
const customerData = [
{
id: "customer-1",
fullName: "\u0639\u0644\u06CC \u0645\u062D\u0645\u062F\u06CC",
mobile: "09151112233",
nationalCode: "0012345678",
cityId: tehran.id,
referredByAgentId: agentReza.id,
goldCreditLimitGrams: "200",
verificationStep: import_client.VerificationStep.VERIFIED
},
{
id: "customer-2",
fullName: "\u0645\u0631\u06CC\u0645 \u062D\u0633\u06CC\u0646\u06CC",
mobile: "09151112234",
nationalCode: "0012345679",
cityId: tehran.id,
referredByAgentId: agentReza.id,
goldCreditLimitGrams: "100",
verificationStep: import_client.VerificationStep.VERIFIED
},
{
id: "customer-3",
fullName: "\u062C\u0648\u0627\u062F \u0646\u0648\u0631\u0648\u0632\u06CC",
mobile: "09151112235",
nationalCode: "0012345680",
cityId: mashhad.id,
referredByAgentId: agentSara.id,
goldCreditLimitGrams: "50",
verificationStep: import_client.VerificationStep.STEP_TWO_SUBMITTED
},
{
id: "customer-4",
fullName: "\u0646\u0631\u06AF\u0633 \u0627\u0628\u0631\u0627\u0647\u06CC\u0645\u06CC",
mobile: "09151112236",
nationalCode: "0012345681",
cityId: isfahan.id,
referredByAgentId: null,
goldCreditLimitGrams: "30",
verificationStep: import_client.VerificationStep.UNVERIFIED
},
{
id: "customer-5",
fullName: "\u06A9\u0627\u0645\u0631\u0627\u0646 \u0635\u0627\u062F\u0642\u06CC",
mobile: "09151112237",
nationalCode: "0012345682",
cityId: tehran.id,
referredByAgentId: agentReza.id,
goldCreditLimitGrams: "500",
verificationStep: import_client.VerificationStep.VERIFIED
},
{
id: "customer-6",
fullName: "\u0632\u0647\u0631\u0627 \u0642\u0627\u0633\u0645\u06CC",
mobile: "09151112238",
nationalCode: "0012345683",
cityId: tabriz.id,
referredByAgentId: agentSara.id,
goldCreditLimitGrams: "80",
verificationStep: import_client.VerificationStep.STEP_ONE_SUBMITTED
}
];
const customers = [];
for (const c of customerData) {
customers.push(
await prisma.customer.upsert({
where: { id: c.id },
update: {},
create: {
id: c.id,
fullName: c.fullName,
mobile: c.mobile,
nationalCode: c.nationalCode,
passwordHash: PLACEHOLDER_PASSWORD_HASH,
cityId: c.cityId,
referredByAgentId: c.referredByAgentId,
goldCreditLimitGrams: c.goldCreditLimitGrams,
verificationStep: c.verificationStep
}
})
);
}
const [customer1, customer2, , , customer5] = customers;
const [ringsCategory, braceletsCategory, necklacesCategory, earringsCategory, coinsCategory] = await Promise.all([
prisma.category.upsert({ where: { id: "cat-rings" }, update: {}, create: { id: "cat-rings", title: "\u0627\u0646\u06AF\u0634\u062A\u0631" } }),
prisma.category.upsert({ where: { id: "cat-bracelets" }, update: {}, create: { id: "cat-bracelets", title: "\u062F\u0633\u062A\u0628\u0646\u062F" } }),
prisma.category.upsert({ where: { id: "cat-necklaces" }, update: {}, create: { id: "cat-necklaces", title: "\u06AF\u0631\u062F\u0646\u0628\u0646\u062F" } }),
prisma.category.upsert({ where: { id: "cat-earrings" }, update: {}, create: { id: "cat-earrings", title: "\u06AF\u0648\u0634\u0648\u0627\u0631\u0647" } }),
prisma.category.upsert({ where: { id: "cat-coins" }, update: {}, create: { id: "cat-coins", title: "\u0633\u06A9\u0647 \u0648 \u0634\u0645\u0634" } })
]);
const brandHenza = await prisma.brand.upsert({
where: { id: "brand-henza" },
update: {},
create: { id: "brand-henza", title: "\u0647\u0646\u0632\u0627 \u0637\u0644\u0627" }
});
const brandArtisan = await prisma.brand.upsert({
where: { id: "brand-artisan" },
update: {},
create: { id: "brand-artisan", title: "\u0645\u062C\u0645\u0648\u0639\u0647 \u0635\u0646\u0639\u062A\u06AF\u0631" }
});
const productDefs = [
{
id: "product-classic-ring",
title: "\u0627\u0646\u06AF\u0634\u062A\u0631 \u062A\u06A9 \u0646\u06AF\u06CC\u0646 \u06A9\u0644\u0627\u0633\u06CC\u06A9",
description: "\u0627\u0646\u06AF\u0634\u062A\u0631 \u0637\u0644\u0627\u06CC \u06F1\u06F8 \u0639\u06CC\u0627\u0631 \u0628\u0627 \u0637\u0631\u0627\u062D\u06CC \u0633\u0627\u062F\u0647 \u0648 \u0645\u06CC\u0646\u06CC\u0645\u0627\u0644.",
categoryId: ringsCategory.id,
brandId: brandHenza.id,
variants: [
{ id: "variant-ring-size-6", title: "\u0633\u0627\u06CC\u0632 \u06F6", sku: "RING-CLASSIC-6", rashWeightGrams: "2.850", defaultWagePercent: "9.0" },
{ id: "variant-ring-size-7", title: "\u0633\u0627\u06CC\u0632 \u06F7", sku: "RING-CLASSIC-7", rashWeightGrams: "3.100", defaultWagePercent: "9.0" }
]
},
{
id: "product-woven-bracelet",
title: "\u062F\u0633\u062A\u0628\u0646\u062F \u0628\u0627\u0641\u062A \u0632\u0646\u062C\u06CC\u0631\u06CC",
description: "\u062F\u0633\u062A\u0628\u0646\u062F \u0628\u0627\u0641\u062A\u200C\u062F\u0627\u0631 \u062F\u0633\u062A\u200C\u0633\u0627\u0632 \u0627\u0632 \u0637\u0644\u0627\u06CC \u06F1\u06F8 \u0639\u06CC\u0627\u0631.",
categoryId: braceletsCategory.id,
brandId: brandArtisan.id,
variants: [
{ id: "variant-bracelet-standard", title: "\u0627\u0633\u062A\u0627\u0646\u062F\u0627\u0631\u062F \u06F1\u06F8 \u0633\u0627\u0646\u062A\u06CC", sku: "BRC-WOVEN-18", rashWeightGrams: "14.200", defaultWagePercent: "11.5" }
]
},
{
id: "product-pendant-necklace",
title: "\u06AF\u0631\u062F\u0646\u0628\u0646\u062F \u0622\u0648\u06CC\u0632 \u0627\u0634\u06A9\u06CC",
description: "\u06AF\u0631\u062F\u0646\u0628\u0646\u062F \u0638\u0631\u06CC\u0641 \u0628\u0627 \u0622\u0648\u06CC\u0632 \u0627\u0634\u06A9\u06CC \u0648 \u0632\u0646\u062C\u06CC\u0631 \u0646\u0627\u0632\u06A9.",
categoryId: necklacesCategory.id,
brandId: brandHenza.id,
variants: [
{ id: "variant-necklace-40cm", title: "\u0632\u0646\u062C\u06CC\u0631 \u06F4\u06F0 \u0633\u0627\u0646\u062A\u06CC", sku: "NCK-TEARDROP-40", rashWeightGrams: "5.600", defaultWagePercent: "10.0" },
{ id: "variant-necklace-45cm", title: "\u0632\u0646\u062C\u06CC\u0631 \u06F4\u06F5 \u0633\u0627\u0646\u062A\u06CC", sku: "NCK-TEARDROP-45", rashWeightGrams: "6.100", defaultWagePercent: "10.0" }
]
},
{
id: "product-hoop-earrings",
title: "\u06AF\u0648\u0634\u0648\u0627\u0631\u0647 \u062D\u0644\u0642\u0647\u200C\u0627\u06CC \u06A9\u0644\u0627\u0633\u06CC\u06A9",
description: "\u06AF\u0648\u0634\u0648\u0627\u0631\u0647 \u062D\u0644\u0642\u0647\u200C\u0627\u06CC \u0637\u0644\u0627\u06CC \u06F1\u06F8 \u0639\u06CC\u0627\u0631 \u0645\u0646\u0627\u0633\u0628 \u0627\u0633\u062A\u0641\u0627\u062F\u0647 \u0631\u0648\u0632\u0645\u0631\u0647.",
categoryId: earringsCategory.id,
brandId: brandArtisan.id,
variants: [
{ id: "variant-hoop-small", title: "\u06A9\u0648\u0686\u06A9", sku: "ERG-HOOP-S", rashWeightGrams: "1.900", defaultWagePercent: "8.5" },
{ id: "variant-hoop-large", title: "\u0628\u0632\u0631\u06AF", sku: "ERG-HOOP-L", rashWeightGrams: "3.400", defaultWagePercent: "8.5" }
]
},
{
id: "product-full-coin",
title: "\u0633\u06A9\u0647 \u062A\u0645\u0627\u0645 \u0628\u0647\u0627\u0631 \u0622\u0632\u0627\u062F\u06CC",
description: "\u0633\u06A9\u0647 \u0637\u0644\u0627\u06CC \u062A\u0645\u0627\u0645 \u0628\u0647\u0627\u0631 \u0622\u0632\u0627\u062F\u06CC\u060C \u0639\u06CC\u0627\u0631 \u0627\u0633\u062A\u0627\u0646\u062F\u0627\u0631\u062F.",
categoryId: coinsCategory.id,
brandId: brandHenza.id,
variants: [
{ id: "variant-coin-full", title: "\u062A\u0645\u0627\u0645 \u0633\u06A9\u0647", sku: "COIN-FULL", rashWeightGrams: "8.133", defaultWagePercent: "2.0" },
{ id: "variant-coin-half", title: "\u0646\u06CC\u0645 \u0633\u06A9\u0647", sku: "COIN-HALF", rashWeightGrams: "4.066", defaultWagePercent: "2.5" }
]
}
];
const createdVariantIds = {};
let imageCount = 0;
for (const p of productDefs) {
const product = await prisma.product.upsert({
where: { id: p.id },
update: {},
create: {
id: p.id,
title: p.title,
description: p.description,
categoryId: p.categoryId,
brandId: p.brandId
}
});
for (const v of p.variants) {
await prisma.productVariant.upsert({
where: { id: v.id },
update: {},
create: {
id: v.id,
productId: product.id,
title: v.title,
sku: v.sku,
rashWeightGrams: v.rashWeightGrams,
defaultWagePercent: v.defaultWagePercent
}
});
createdVariantIds[v.id] = v.id;
}
await prisma.productImage.upsert({
where: { id: `${p.id}-img-1` },
update: { url: PLACEHOLDER_PRODUCT_IMAGE },
create: {
id: `${p.id}-img-1`,
productId: product.id,
url: PLACEHOLDER_PRODUCT_IMAGE,
sortOrder: 0
}
});
imageCount += 1;
}
const featured = await prisma.collection.upsert({
where: { id: "collection-featured" },
update: {},
create: { id: "collection-featured", title: "\u067E\u0631\u0641\u0631\u0648\u0634\u200C\u0647\u0627\u06CC \u0627\u06CC\u0646 \u0647\u0641\u062A\u0647", description: "\u0645\u0646\u062A\u062E\u0628\u06CC \u0627\u0632 \u0645\u062D\u0628\u0648\u0628\u200C\u062A\u0631\u06CC\u0646 \u0645\u062D\u0635\u0648\u0644\u0627\u062A \u0641\u0631\u0648\u0634\u06AF\u0627\u0647." }
});
await prisma.collectionProduct.upsert({
where: { collectionId_productId: { collectionId: featured.id, productId: "product-classic-ring" } },
update: {},
create: { collectionId: featured.id, productId: "product-classic-ring" }
});
await prisma.collectionProduct.upsert({
where: { collectionId_productId: { collectionId: featured.id, productId: "product-pendant-necklace" } },
update: {},
create: { collectionId: featured.id, productId: "product-pendant-necklace" }
});
await prisma.collectionProduct.upsert({
where: { collectionId_productId: { collectionId: featured.id, productId: "product-full-coin" } },
update: {},
create: { collectionId: featured.id, productId: "product-full-coin" }
});
await prisma.goldPriceQuote.create({
data: { karatPerMille: 750, pricePerGramRial: "61500000", source: "seed-illustrative" }
});
const existingDemoOrder = await prisma.order.findUnique({ where: { orderNumber: "ORD-SEED-0001" } });
if (!existingDemoOrder) {
const rashWeight = "3.100";
const wagePercent = 9;
const discountPercent = 0;
const computedWeight = (parseFloat(rashWeight) * (1 + (wagePercent - discountPercent) / 100)).toFixed(4);
const quantity = 1;
const flatRialChargePerUnit = "500000";
const order = await prisma.order.create({
data: {
orderNumber: "ORD-SEED-0001",
customerId: customer1.id,
branchId: branchTehran.id,
status: "DELIVERED_TO_CUSTOMER",
totalGoldWeightGrams: computedWeight,
wageWeightGrams: (parseFloat(computedWeight) - parseFloat(rashWeight)).toFixed(4),
flatRialCharge: flatRialChargePerUnit,
deliveredAt: /* @__PURE__ */ new Date(),
items: {
create: [
{
productVariantId: "variant-ring-size-7",
quantity,
rashWeightGramsSnapshot: rashWeight,
wagePercentSnapshot: wagePercent.toString(),
discountPercentSnapshot: discountPercent.toString(),
computedWeightGrams: computedWeight,
flatRialCharge: flatRialChargePerUnit
}
]
},
statusEvents: {
create: [{ toStatus: "PLACED" }, { toStatus: "ADMIN_APPROVED" }, { toStatus: "DELIVERED_TO_CUSTOMER" }]
}
}
});
await prisma.ledgerEntry.create({
data: {
customerId: customer1.id,
currency: "GOLD_GRAM",
direction: "DEBIT",
sourceType: "ORDER_STOCK",
amount: computedWeight,
orderId: order.id,
confirmed: true,
confirmedAt: /* @__PURE__ */ new Date(),
note: "\u0628\u062F\u0647\u06CC \u0648\u0632\u0646\u06CC \u0637\u0644\u0627 \u0647\u0646\u06AF\u0627\u0645 \u062A\u062D\u0648\u06CC\u0644 (\u062F\u0627\u062F\u0647 \u0622\u0632\u0645\u0627\u06CC\u0634\u06CC)"
}
});
await prisma.ledgerEntry.create({
data: {
customerId: customer1.id,
currency: "RIAL",
direction: "DEBIT",
sourceType: "ORDER_STOCK",
amount: flatRialChargePerUnit,
orderId: order.id,
confirmed: true,
confirmedAt: /* @__PURE__ */ new Date(),
note: "\u0647\u0632\u06CC\u0646\u0647 \u062B\u0627\u0628\u062A \u0631\u06CC\u0627\u0644\u06CC \u0647\u0646\u06AF\u0627\u0645 \u062A\u062D\u0648\u06CC\u0644 (\u062F\u0627\u062F\u0647 \u0622\u0632\u0645\u0627\u06CC\u0634\u06CC)"
}
});
await prisma.customer.update({
where: { id: customer1.id },
data: { goldDebtGrams: { increment: computedWeight }, fiatDebtRial: { increment: flatRialChargePerUnit } }
});
await prisma.ledgerEntry.create({
data: {
customerId: customer1.id,
currency: "GOLD_GRAM",
direction: "CREDIT",
sourceType: "REPAYMENT_MELTED_GOLD",
amount: "1.5000",
karatPerMille: 750,
confirmed: false,
note: "\u062A\u0633\u0648\u06CC\u0647 \u0628\u0627 \u0637\u0644\u0627\u06CC \u0622\u0628\u200C\u0634\u062F\u0647\u060C \u062F\u0631 \u0627\u0646\u062A\u0638\u0627\u0631 \u062A\u0627\u06CC\u06CC\u062F (\u062F\u0627\u062F\u0647 \u0622\u0632\u0645\u0627\u06CC\u0634\u06CC)"
}
});
}
await prisma.customer.update({ where: { id: customer2.id }, data: { goldDebtGrams: "8.2500", fiatDebtRial: "2000000" } });
await prisma.customer.update({ where: { id: customer5.id }, data: { goldDebtGrams: "0", fiatDebtRial: "0" } });
console.log("Seed complete:", {
cities: 4,
branches: 2,
adminUsers: 5,
customers: customers.length,
products: productDefs.length,
variants: Object.keys(createdVariantIds).length,
productImages: imageCount
});
}
main().catch((e) => {
console.error(e);
process.exit(1);
}).finally(async () => {
await prisma.$disconnect();
});