api: fix migration runner, serve uploads correctly, add product photos

Three real bugs found and fixed while wiring up product images end-to-end:

1. src/prisma/migrate.ts only discovered flat prisma/migrations/<name>.sql
   files, never Prisma's own folder-per-migration layout
   (<timestamp>_<name>/migration.sql). Every migration created the normal
   way (via 'prisma migrate dev') was silently skipped; only a hand-added
   flat duplicate (prisma/migrations/0001_init.sql, now removed) ever ran.
   This meant schema.prisma had drifted well ahead of what any real deploy
   database actually had (missing columns on City, Customer, Branch,
   Product, several new tables). Fixed to discover both layouts; the
   drift is closed with a new 20260914165255_sync_schema_drift migration.

2. Neither nginx.conf (production) nor vite.config.ts (dev) proxied
   /uploads/ to the API — only /api/. UploadedFile/ProductImage URLs
   (/uploads/<file>, served by main.ts's useStaticAssets OUTSIDE the /api
   prefix) 404'd through both. Both now proxy /uploads/ the same way /api/
   already was.

3. docker-compose.yml's api service had no volume for storage/uploads, so
   every uploaded file — and by extension every ProductImage row pointing
   at one — was lost on the next container recreate/redeploy. Added a
   named volume.

Also: main.ts's static file serving now sets Content-Type: image/avif
explicitly (serve-static's bundled mime table predates .avif and served it
as application/octet-stream; browsers rendered it anyway by sniffing, but
a stricter client would not have).

Product photos: copied 15 real jewelry photos into the uploads volume,
registered them as UploadedFile rows, and assigned each of the 5 existing
Product rows a randomly picked one (duplicates allowed, per request),
replacing their /img/no-items.png placeholder ProductImage row. Verified
end-to-end in a real browser against the real containerized API+DB: logged
in, opened the admin product list, confirmed all 5 rows render their photo
(naturalWidth/Height populated, network 200s, no console errors).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-09-14 20:56:17 +03:30
co-authored by Claude Sonnet 5
parent a2ab9ba192
commit 2b6795d007
18 changed files with 46398 additions and 1509 deletions
+14
View File
@@ -20,6 +20,20 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
}
# Uploaded files (product images, category icons, ...) are served by the
# API's own static handler (main.ts useStaticAssets), not stored here —
# without this the ProductImage.url values the admin panel renders
# (/uploads/<file>) 404 through this nginx instead of reaching the API.
location /uploads/ {
set $api_upstream http://goldbank-api:3000;
proxy_pass $api_upstream$request_uri;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
try_files $uri $uri/ /index.html;
}
+4
View File
@@ -45,6 +45,10 @@ export default defineConfig(({ mode }) => {
port: 5174,
proxy: {
'/api': 'http://localhost:3000',
// Uploaded files (ProductImage.url etc.) are served by the API's own
// static handler, not this dev server — without this they 404 in dev
// even though production nginx (nginx.conf) already proxies them.
'/uploads': 'http://localhost:3000',
},
},
};
-431
View File
@@ -1,431 +0,0 @@
-- 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,362 @@
-- CreateEnum
CREATE TYPE "CustomerKind" AS ENUM ('SALES_AGENT', 'APP_SHOPKEEPER', 'WALK_IN_SHOPKEEPER', 'BANKER', 'WORKSHOP_SUPERVISOR');
-- CreateEnum
CREATE TYPE "OrderChannel" AS ENUM ('RETAIL', 'WHOLESALE');
-- CreateEnum
CREATE TYPE "CommentStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED');
-- CreateEnum
CREATE TYPE "PromotionScope" AS ENUM ('PRODUCT', 'CATEGORY', 'CUSTOMER', 'BRAND');
-- CreateEnum
CREATE TYPE "PromotionUsage" AS ENUM ('ONCE', 'UNTIL_END_DATE');
-- CreateEnum
CREATE TYPE "MessageAudience" AS ENUM ('SINGLE_NUMBER', 'MULTIPLE_NUMBERS', 'ALL_CUSTOMERS', 'WHOLESALE_CUSTOMERS', 'RETAIL_CUSTOMERS', 'SALES_AGENTS');
-- AlterTable
ALTER TABLE "AdminUser" ADD COLUMN "userTypeId" INTEGER;
-- AlterTable
ALTER TABLE "Branch" ADD COLUMN "address" TEXT,
ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "phone" TEXT;
-- AlterTable
ALTER TABLE "Category" ADD COLUMN "description" TEXT,
ADD COLUMN "iconUrl" TEXT,
ADD COLUMN "imageUrl" TEXT,
ADD COLUMN "metaTag" TEXT,
ADD COLUMN "type" INTEGER NOT NULL DEFAULT 1;
-- AlterTable
ALTER TABLE "City" ADD COLUMN "percent" DECIMAL(6,3),
ADD COLUMN "percentAux" DECIMAL(6,3);
-- AlterTable
ALTER TABLE "Customer" ADD COLUMN "address" TEXT,
ADD COLUMN "avatarFileId" TEXT,
ADD COLUMN "canPlaceOrders" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "isArchived" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "kind" "CustomerKind" NOT NULL DEFAULT 'APP_SHOPKEEPER',
ADD COLUMN "memberCode" TEXT,
ADD COLUMN "pasazhName" TEXT,
ADD COLUMN "phone" TEXT,
ADD COLUMN "storeName" TEXT,
ADD COLUMN "zone" TEXT;
-- AlterTable
ALTER TABLE "LedgerEntry" ADD COLUMN "accountName" TEXT,
ADD COLUMN "bankName" TEXT,
ADD COLUMN "hallmark" TEXT,
ADD COLUMN "labName" TEXT,
ADD COLUMN "originalValue" DECIMAL(14,4),
ADD COLUMN "priceRial" DECIMAL(20,2),
ADD COLUMN "receivedAt" TIMESTAMP(3),
ADD COLUMN "transferTo" TEXT;
-- AlterTable
ALTER TABLE "Order" ADD COLUMN "channel" "OrderChannel" NOT NULL DEFAULT 'RETAIL';
-- AlterTable
ALTER TABLE "Product" ADD COLUMN "productCode" TEXT,
ADD COLUMN "showType" INTEGER NOT NULL DEFAULT 3;
-- AlterTable
ALTER TABLE "ProductVariant" ADD COLUMN "isPreOrder" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "qty" INTEGER NOT NULL DEFAULT 0;
-- AlterTable
ALTER TABLE "Workshop" ADD COLUMN "address" TEXT,
ADD COLUMN "phone" TEXT,
ADD COLUMN "supervisorId" TEXT;
-- DropTable
DROP TABLE "_goldbank_migrations";
-- CreateTable
CREATE TABLE "user_type" (
"id" INTEGER NOT NULL,
"title" TEXT NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "user_type_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "permission" (
"rid" TEXT NOT NULL,
"userTypeId" INTEGER NOT NULL,
CONSTRAINT "permission_pkey" PRIMARY KEY ("rid","userTypeId")
);
-- CreateTable
CREATE TABLE "Comment" (
"id" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"body" TEXT NOT NULL,
"status" "CommentStatus" NOT NULL DEFAULT 'PENDING',
"replyToId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Comment_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Setting" (
"key" TEXT NOT NULL,
"value" JSONB NOT NULL,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Setting_pkey" PRIMARY KEY ("key")
);
-- CreateTable
CREATE TABLE "Promotion" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"scope" "PromotionScope" NOT NULL DEFAULT 'PRODUCT',
"usage" "PromotionUsage" NOT NULL DEFAULT 'ONCE',
"discountPercent" DECIMAL(6,3) NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"platform" INTEGER NOT NULL DEFAULT 3,
"startsAt" TIMESTAMP(3),
"endsAt" TIMESTAMP(3),
"productId" TEXT,
"categoryId" TEXT,
"customerId" TEXT,
"brandId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Promotion_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PromotionExclusion" (
"id" TEXT NOT NULL,
"type" INTEGER NOT NULL,
"itemId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PromotionExclusion_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BranchAllocation" (
"id" TEXT NOT NULL,
"branchId" TEXT NOT NULL,
"allocatorAdminUserId" TEXT,
"note" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "BranchAllocation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BranchAllocationItem" (
"id" TEXT NOT NULL,
"allocationId" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"weightGrams" DECIMAL(14,4),
CONSTRAINT "BranchAllocationItem_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SmsMessage" (
"id" TEXT NOT NULL,
"audience" "MessageAudience" NOT NULL,
"recipients" TEXT,
"body" TEXT NOT NULL,
"sentCount" INTEGER NOT NULL DEFAULT 0,
"sentByAdminUserId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SmsMessage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Notification" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"body" TEXT NOT NULL,
"audience" "MessageAudience" NOT NULL DEFAULT 'ALL_CUSTOMERS',
"sentByAdminUserId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Notification_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "NotificationRecipient" (
"id" TEXT NOT NULL,
"notificationId" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"readAt" TIMESTAMP(3),
CONSTRAINT "NotificationRecipient_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "NewsletterSubscriber" (
"id" TEXT NOT NULL,
"email" TEXT,
"mobile" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "NewsletterSubscriber_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CustomerActivityLog" (
"id" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"action" TEXT NOT NULL,
"detail" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CustomerActivityLog_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "UploadedFile" (
"id" TEXT NOT NULL,
"url" TEXT NOT NULL,
"originalName" TEXT NOT NULL,
"mimeType" TEXT NOT NULL,
"sizeBytes" INTEGER NOT NULL,
"purpose" TEXT,
"uploadedByAdminUserId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "UploadedFile_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "permission_userTypeId_idx" ON "permission"("userTypeId");
-- CreateIndex
CREATE INDEX "Comment_productId_idx" ON "Comment"("productId");
-- CreateIndex
CREATE INDEX "Comment_customerId_idx" ON "Comment"("customerId");
-- CreateIndex
CREATE INDEX "Comment_status_idx" ON "Comment"("status");
-- CreateIndex
CREATE INDEX "Promotion_scope_isActive_idx" ON "Promotion"("scope", "isActive");
-- CreateIndex
CREATE INDEX "PromotionExclusion_type_idx" ON "PromotionExclusion"("type");
-- CreateIndex
CREATE UNIQUE INDEX "PromotionExclusion_type_itemId_key" ON "PromotionExclusion"("type", "itemId");
-- CreateIndex
CREATE INDEX "BranchAllocation_branchId_idx" ON "BranchAllocation"("branchId");
-- CreateIndex
CREATE UNIQUE INDEX "BranchAllocationItem_allocationId_productId_key" ON "BranchAllocationItem"("allocationId", "productId");
-- CreateIndex
CREATE INDEX "SmsMessage_createdAt_idx" ON "SmsMessage"("createdAt");
-- CreateIndex
CREATE INDEX "Notification_createdAt_idx" ON "Notification"("createdAt");
-- CreateIndex
CREATE INDEX "NotificationRecipient_customerId_readAt_idx" ON "NotificationRecipient"("customerId", "readAt");
-- CreateIndex
CREATE UNIQUE INDEX "NotificationRecipient_notificationId_customerId_key" ON "NotificationRecipient"("notificationId", "customerId");
-- CreateIndex
CREATE UNIQUE INDEX "NewsletterSubscriber_email_key" ON "NewsletterSubscriber"("email");
-- CreateIndex
CREATE UNIQUE INDEX "NewsletterSubscriber_mobile_key" ON "NewsletterSubscriber"("mobile");
-- CreateIndex
CREATE INDEX "CustomerActivityLog_customerId_createdAt_idx" ON "CustomerActivityLog"("customerId", "createdAt");
-- CreateIndex
CREATE INDEX "UploadedFile_purpose_idx" ON "UploadedFile"("purpose");
-- CreateIndex
CREATE INDEX "AdminUser_userTypeId_idx" ON "AdminUser"("userTypeId");
-- CreateIndex
CREATE UNIQUE INDEX "Product_productCode_key" ON "Product"("productCode");
-- AddForeignKey
ALTER TABLE "AdminUser" ADD CONSTRAINT "AdminUser_userTypeId_fkey" FOREIGN KEY ("userTypeId") REFERENCES "user_type"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Customer" ADD CONSTRAINT "Customer_avatarFileId_fkey" FOREIGN KEY ("avatarFileId") REFERENCES "UploadedFile"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Workshop" ADD CONSTRAINT "Workshop_supervisorId_fkey" FOREIGN KEY ("supervisorId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "permission" ADD CONSTRAINT "permission_userTypeId_fkey" FOREIGN KEY ("userTypeId") REFERENCES "user_type"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_replyToId_fkey" FOREIGN KEY ("replyToId") REFERENCES "Comment"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Promotion" ADD CONSTRAINT "Promotion_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Promotion" ADD CONSTRAINT "Promotion_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Promotion" ADD CONSTRAINT "Promotion_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Promotion" ADD CONSTRAINT "Promotion_brandId_fkey" FOREIGN KEY ("brandId") REFERENCES "Brand"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BranchAllocation" ADD CONSTRAINT "BranchAllocation_branchId_fkey" FOREIGN KEY ("branchId") REFERENCES "Branch"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BranchAllocation" ADD CONSTRAINT "BranchAllocation_allocatorAdminUserId_fkey" FOREIGN KEY ("allocatorAdminUserId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BranchAllocationItem" ADD CONSTRAINT "BranchAllocationItem_allocationId_fkey" FOREIGN KEY ("allocationId") REFERENCES "BranchAllocation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BranchAllocationItem" ADD CONSTRAINT "BranchAllocationItem_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SmsMessage" ADD CONSTRAINT "SmsMessage_sentByAdminUserId_fkey" FOREIGN KEY ("sentByAdminUserId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_sentByAdminUserId_fkey" FOREIGN KEY ("sentByAdminUserId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "NotificationRecipient" ADD CONSTRAINT "NotificationRecipient_notificationId_fkey" FOREIGN KEY ("notificationId") REFERENCES "Notification"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "NotificationRecipient" ADD CONSTRAINT "NotificationRecipient_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CustomerActivityLog" ADD CONSTRAINT "CustomerActivityLog_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UploadedFile" ADD CONSTRAINT "UploadedFile_uploadedByAdminUserId_fkey" FOREIGN KEY ("uploadedByAdminUserId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+15834 -412
View File
File diff suppressed because it is too large Load Diff
-431
View File
@@ -1,431 +0,0 @@
-- 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,362 @@
-- CreateEnum
CREATE TYPE "CustomerKind" AS ENUM ('SALES_AGENT', 'APP_SHOPKEEPER', 'WALK_IN_SHOPKEEPER', 'BANKER', 'WORKSHOP_SUPERVISOR');
-- CreateEnum
CREATE TYPE "OrderChannel" AS ENUM ('RETAIL', 'WHOLESALE');
-- CreateEnum
CREATE TYPE "CommentStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED');
-- CreateEnum
CREATE TYPE "PromotionScope" AS ENUM ('PRODUCT', 'CATEGORY', 'CUSTOMER', 'BRAND');
-- CreateEnum
CREATE TYPE "PromotionUsage" AS ENUM ('ONCE', 'UNTIL_END_DATE');
-- CreateEnum
CREATE TYPE "MessageAudience" AS ENUM ('SINGLE_NUMBER', 'MULTIPLE_NUMBERS', 'ALL_CUSTOMERS', 'WHOLESALE_CUSTOMERS', 'RETAIL_CUSTOMERS', 'SALES_AGENTS');
-- AlterTable
ALTER TABLE "AdminUser" ADD COLUMN "userTypeId" INTEGER;
-- AlterTable
ALTER TABLE "Branch" ADD COLUMN "address" TEXT,
ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "phone" TEXT;
-- AlterTable
ALTER TABLE "Category" ADD COLUMN "description" TEXT,
ADD COLUMN "iconUrl" TEXT,
ADD COLUMN "imageUrl" TEXT,
ADD COLUMN "metaTag" TEXT,
ADD COLUMN "type" INTEGER NOT NULL DEFAULT 1;
-- AlterTable
ALTER TABLE "City" ADD COLUMN "percent" DECIMAL(6,3),
ADD COLUMN "percentAux" DECIMAL(6,3);
-- AlterTable
ALTER TABLE "Customer" ADD COLUMN "address" TEXT,
ADD COLUMN "avatarFileId" TEXT,
ADD COLUMN "canPlaceOrders" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "isArchived" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "kind" "CustomerKind" NOT NULL DEFAULT 'APP_SHOPKEEPER',
ADD COLUMN "memberCode" TEXT,
ADD COLUMN "pasazhName" TEXT,
ADD COLUMN "phone" TEXT,
ADD COLUMN "storeName" TEXT,
ADD COLUMN "zone" TEXT;
-- AlterTable
ALTER TABLE "LedgerEntry" ADD COLUMN "accountName" TEXT,
ADD COLUMN "bankName" TEXT,
ADD COLUMN "hallmark" TEXT,
ADD COLUMN "labName" TEXT,
ADD COLUMN "originalValue" DECIMAL(14,4),
ADD COLUMN "priceRial" DECIMAL(20,2),
ADD COLUMN "receivedAt" TIMESTAMP(3),
ADD COLUMN "transferTo" TEXT;
-- AlterTable
ALTER TABLE "Order" ADD COLUMN "channel" "OrderChannel" NOT NULL DEFAULT 'RETAIL';
-- AlterTable
ALTER TABLE "Product" ADD COLUMN "productCode" TEXT,
ADD COLUMN "showType" INTEGER NOT NULL DEFAULT 3;
-- AlterTable
ALTER TABLE "ProductVariant" ADD COLUMN "isPreOrder" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "qty" INTEGER NOT NULL DEFAULT 0;
-- AlterTable
ALTER TABLE "Workshop" ADD COLUMN "address" TEXT,
ADD COLUMN "phone" TEXT,
ADD COLUMN "supervisorId" TEXT;
-- DropTable
DROP TABLE "_goldbank_migrations";
-- CreateTable
CREATE TABLE "user_type" (
"id" INTEGER NOT NULL,
"title" TEXT NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "user_type_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "permission" (
"rid" TEXT NOT NULL,
"userTypeId" INTEGER NOT NULL,
CONSTRAINT "permission_pkey" PRIMARY KEY ("rid","userTypeId")
);
-- CreateTable
CREATE TABLE "Comment" (
"id" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"body" TEXT NOT NULL,
"status" "CommentStatus" NOT NULL DEFAULT 'PENDING',
"replyToId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Comment_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Setting" (
"key" TEXT NOT NULL,
"value" JSONB NOT NULL,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Setting_pkey" PRIMARY KEY ("key")
);
-- CreateTable
CREATE TABLE "Promotion" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"scope" "PromotionScope" NOT NULL DEFAULT 'PRODUCT',
"usage" "PromotionUsage" NOT NULL DEFAULT 'ONCE',
"discountPercent" DECIMAL(6,3) NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"platform" INTEGER NOT NULL DEFAULT 3,
"startsAt" TIMESTAMP(3),
"endsAt" TIMESTAMP(3),
"productId" TEXT,
"categoryId" TEXT,
"customerId" TEXT,
"brandId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Promotion_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PromotionExclusion" (
"id" TEXT NOT NULL,
"type" INTEGER NOT NULL,
"itemId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PromotionExclusion_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BranchAllocation" (
"id" TEXT NOT NULL,
"branchId" TEXT NOT NULL,
"allocatorAdminUserId" TEXT,
"note" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "BranchAllocation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BranchAllocationItem" (
"id" TEXT NOT NULL,
"allocationId" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"weightGrams" DECIMAL(14,4),
CONSTRAINT "BranchAllocationItem_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SmsMessage" (
"id" TEXT NOT NULL,
"audience" "MessageAudience" NOT NULL,
"recipients" TEXT,
"body" TEXT NOT NULL,
"sentCount" INTEGER NOT NULL DEFAULT 0,
"sentByAdminUserId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SmsMessage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Notification" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"body" TEXT NOT NULL,
"audience" "MessageAudience" NOT NULL DEFAULT 'ALL_CUSTOMERS',
"sentByAdminUserId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Notification_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "NotificationRecipient" (
"id" TEXT NOT NULL,
"notificationId" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"readAt" TIMESTAMP(3),
CONSTRAINT "NotificationRecipient_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "NewsletterSubscriber" (
"id" TEXT NOT NULL,
"email" TEXT,
"mobile" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "NewsletterSubscriber_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CustomerActivityLog" (
"id" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"action" TEXT NOT NULL,
"detail" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CustomerActivityLog_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "UploadedFile" (
"id" TEXT NOT NULL,
"url" TEXT NOT NULL,
"originalName" TEXT NOT NULL,
"mimeType" TEXT NOT NULL,
"sizeBytes" INTEGER NOT NULL,
"purpose" TEXT,
"uploadedByAdminUserId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "UploadedFile_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "permission_userTypeId_idx" ON "permission"("userTypeId");
-- CreateIndex
CREATE INDEX "Comment_productId_idx" ON "Comment"("productId");
-- CreateIndex
CREATE INDEX "Comment_customerId_idx" ON "Comment"("customerId");
-- CreateIndex
CREATE INDEX "Comment_status_idx" ON "Comment"("status");
-- CreateIndex
CREATE INDEX "Promotion_scope_isActive_idx" ON "Promotion"("scope", "isActive");
-- CreateIndex
CREATE INDEX "PromotionExclusion_type_idx" ON "PromotionExclusion"("type");
-- CreateIndex
CREATE UNIQUE INDEX "PromotionExclusion_type_itemId_key" ON "PromotionExclusion"("type", "itemId");
-- CreateIndex
CREATE INDEX "BranchAllocation_branchId_idx" ON "BranchAllocation"("branchId");
-- CreateIndex
CREATE UNIQUE INDEX "BranchAllocationItem_allocationId_productId_key" ON "BranchAllocationItem"("allocationId", "productId");
-- CreateIndex
CREATE INDEX "SmsMessage_createdAt_idx" ON "SmsMessage"("createdAt");
-- CreateIndex
CREATE INDEX "Notification_createdAt_idx" ON "Notification"("createdAt");
-- CreateIndex
CREATE INDEX "NotificationRecipient_customerId_readAt_idx" ON "NotificationRecipient"("customerId", "readAt");
-- CreateIndex
CREATE UNIQUE INDEX "NotificationRecipient_notificationId_customerId_key" ON "NotificationRecipient"("notificationId", "customerId");
-- CreateIndex
CREATE UNIQUE INDEX "NewsletterSubscriber_email_key" ON "NewsletterSubscriber"("email");
-- CreateIndex
CREATE UNIQUE INDEX "NewsletterSubscriber_mobile_key" ON "NewsletterSubscriber"("mobile");
-- CreateIndex
CREATE INDEX "CustomerActivityLog_customerId_createdAt_idx" ON "CustomerActivityLog"("customerId", "createdAt");
-- CreateIndex
CREATE INDEX "UploadedFile_purpose_idx" ON "UploadedFile"("purpose");
-- CreateIndex
CREATE INDEX "AdminUser_userTypeId_idx" ON "AdminUser"("userTypeId");
-- CreateIndex
CREATE UNIQUE INDEX "Product_productCode_key" ON "Product"("productCode");
-- AddForeignKey
ALTER TABLE "AdminUser" ADD CONSTRAINT "AdminUser_userTypeId_fkey" FOREIGN KEY ("userTypeId") REFERENCES "user_type"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Customer" ADD CONSTRAINT "Customer_avatarFileId_fkey" FOREIGN KEY ("avatarFileId") REFERENCES "UploadedFile"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Workshop" ADD CONSTRAINT "Workshop_supervisorId_fkey" FOREIGN KEY ("supervisorId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "permission" ADD CONSTRAINT "permission_userTypeId_fkey" FOREIGN KEY ("userTypeId") REFERENCES "user_type"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_replyToId_fkey" FOREIGN KEY ("replyToId") REFERENCES "Comment"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Promotion" ADD CONSTRAINT "Promotion_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Promotion" ADD CONSTRAINT "Promotion_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Promotion" ADD CONSTRAINT "Promotion_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Promotion" ADD CONSTRAINT "Promotion_brandId_fkey" FOREIGN KEY ("brandId") REFERENCES "Brand"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BranchAllocation" ADD CONSTRAINT "BranchAllocation_branchId_fkey" FOREIGN KEY ("branchId") REFERENCES "Branch"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BranchAllocation" ADD CONSTRAINT "BranchAllocation_allocatorAdminUserId_fkey" FOREIGN KEY ("allocatorAdminUserId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BranchAllocationItem" ADD CONSTRAINT "BranchAllocationItem_allocationId_fkey" FOREIGN KEY ("allocationId") REFERENCES "BranchAllocation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BranchAllocationItem" ADD CONSTRAINT "BranchAllocationItem_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SmsMessage" ADD CONSTRAINT "SmsMessage_sentByAdminUserId_fkey" FOREIGN KEY ("sentByAdminUserId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_sentByAdminUserId_fkey" FOREIGN KEY ("sentByAdminUserId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "NotificationRecipient" ADD CONSTRAINT "NotificationRecipient_notificationId_fkey" FOREIGN KEY ("notificationId") REFERENCES "Notification"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "NotificationRecipient" ADD CONSTRAINT "NotificationRecipient_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CustomerActivityLog" ADD CONSTRAINT "CustomerActivityLog_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UploadedFile" ADD CONSTRAINT "UploadedFile_uploadedByAdminUserId_fkey" FOREIGN KEY ("uploadedByAdminUserId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+2022 -13
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+215 -2
View File
@@ -125,7 +125,9 @@ exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
exports.Prisma.CityScalarFieldEnum = {
id: 'id',
name: 'name',
province: 'province'
province: 'province',
percent: 'percent',
percentAux: 'percentAux'
};
exports.Prisma.AdminUserScalarFieldEnum = {
@@ -134,6 +136,7 @@ exports.Prisma.AdminUserScalarFieldEnum = {
mobile: 'mobile',
passwordHash: 'passwordHash',
role: 'role',
userTypeId: 'userTypeId',
branchId: 'branchId',
wagePercent: 'wagePercent',
isActive: 'isActive',
@@ -147,6 +150,13 @@ exports.Prisma.CustomerScalarFieldEnum = {
nationalCode: 'nationalCode',
passwordHash: 'passwordHash',
cityId: 'cityId',
kind: 'kind',
phone: 'phone',
address: 'address',
storeName: 'storeName',
pasazhName: 'pasazhName',
zone: 'zone',
memberCode: 'memberCode',
referredByAgentId: 'referredByAgentId',
goldCreditLimitGrams: 'goldCreditLimitGrams',
goldDebtGrams: 'goldDebtGrams',
@@ -155,7 +165,10 @@ exports.Prisma.CustomerScalarFieldEnum = {
nationalIdImageUrl: 'nationalIdImageUrl',
businessLicenseUrl: 'businessLicenseUrl',
shopImageUrl: 'shopImageUrl',
avatarFileId: 'avatarFileId',
canPlaceOrders: 'canPlaceOrders',
isActive: 'isActive',
isArchived: 'isArchived',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
@@ -171,18 +184,29 @@ exports.Prisma.CustomerAddressScalarFieldEnum = {
exports.Prisma.BranchScalarFieldEnum = {
id: 'id',
name: 'name',
address: 'address',
phone: 'phone',
isActive: 'isActive',
cityId: 'cityId'
};
exports.Prisma.WorkshopScalarFieldEnum = {
id: 'id',
name: 'name',
address: 'address',
phone: 'phone',
supervisorId: 'supervisorId',
isActive: 'isActive'
};
exports.Prisma.CategoryScalarFieldEnum = {
id: 'id',
title: 'title',
type: 'type',
imageUrl: 'imageUrl',
iconUrl: 'iconUrl',
metaTag: 'metaTag',
description: 'description',
parentId: 'parentId'
};
@@ -209,6 +233,8 @@ exports.Prisma.ProductScalarFieldEnum = {
description: 'description',
categoryId: 'categoryId',
brandId: 'brandId',
productCode: 'productCode',
showType: 'showType',
isActive: 'isActive',
createdAt: 'createdAt'
};
@@ -227,6 +253,8 @@ exports.Prisma.ProductVariantScalarFieldEnum = {
sku: 'sku',
rashWeightGrams: 'rashWeightGrams',
defaultWagePercent: 'defaultWagePercent',
qty: 'qty',
isPreOrder: 'isPreOrder',
isActive: 'isActive'
};
@@ -235,6 +263,7 @@ exports.Prisma.OrderScalarFieldEnum = {
orderNumber: 'orderNumber',
customerId: 'customerId',
branchId: 'branchId',
channel: 'channel',
status: 'status',
totalGoldWeightGrams: 'totalGoldWeightGrams',
wageWeightGrams: 'wageWeightGrams',
@@ -308,6 +337,14 @@ exports.Prisma.LedgerEntryScalarFieldEnum = {
confirmed: 'confirmed',
confirmedAt: 'confirmedAt',
postedByAdminUserId: 'postedByAdminUserId',
labName: 'labName',
hallmark: 'hallmark',
accountName: 'accountName',
bankName: 'bankName',
transferTo: 'transferTo',
originalValue: 'originalValue',
priceRial: 'priceRial',
receivedAt: 'receivedAt',
note: 'note',
createdAt: 'createdAt'
};
@@ -331,6 +368,128 @@ exports.Prisma.WalletBonusScalarFieldEnum = {
isActive: 'isActive'
};
exports.Prisma.UserTypeScalarFieldEnum = {
id: 'id',
title: 'title',
isActive: 'isActive',
createdAt: 'createdAt'
};
exports.Prisma.PermissionScalarFieldEnum = {
rid: 'rid',
userTypeId: 'userTypeId'
};
exports.Prisma.CommentScalarFieldEnum = {
id: 'id',
productId: 'productId',
customerId: 'customerId',
body: 'body',
status: 'status',
replyToId: 'replyToId',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.SettingScalarFieldEnum = {
key: 'key',
value: 'value',
updatedAt: 'updatedAt'
};
exports.Prisma.PromotionScalarFieldEnum = {
id: 'id',
title: 'title',
description: 'description',
scope: 'scope',
usage: 'usage',
discountPercent: 'discountPercent',
isActive: 'isActive',
platform: 'platform',
startsAt: 'startsAt',
endsAt: 'endsAt',
productId: 'productId',
categoryId: 'categoryId',
customerId: 'customerId',
brandId: 'brandId',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.PromotionExclusionScalarFieldEnum = {
id: 'id',
type: 'type',
itemId: 'itemId',
createdAt: 'createdAt'
};
exports.Prisma.BranchAllocationScalarFieldEnum = {
id: 'id',
branchId: 'branchId',
allocatorAdminUserId: 'allocatorAdminUserId',
note: 'note',
createdAt: 'createdAt'
};
exports.Prisma.BranchAllocationItemScalarFieldEnum = {
id: 'id',
allocationId: 'allocationId',
productId: 'productId',
weightGrams: 'weightGrams'
};
exports.Prisma.SmsMessageScalarFieldEnum = {
id: 'id',
audience: 'audience',
recipients: 'recipients',
body: 'body',
sentCount: 'sentCount',
sentByAdminUserId: 'sentByAdminUserId',
createdAt: 'createdAt'
};
exports.Prisma.NotificationScalarFieldEnum = {
id: 'id',
title: 'title',
body: 'body',
audience: 'audience',
sentByAdminUserId: 'sentByAdminUserId',
createdAt: 'createdAt'
};
exports.Prisma.NotificationRecipientScalarFieldEnum = {
id: 'id',
notificationId: 'notificationId',
customerId: 'customerId',
readAt: 'readAt'
};
exports.Prisma.NewsletterSubscriberScalarFieldEnum = {
id: 'id',
email: 'email',
mobile: 'mobile',
createdAt: 'createdAt'
};
exports.Prisma.CustomerActivityLogScalarFieldEnum = {
id: 'id',
customerId: 'customerId',
action: 'action',
detail: 'detail',
createdAt: 'createdAt'
};
exports.Prisma.UploadedFileScalarFieldEnum = {
id: 'id',
url: 'url',
originalName: 'originalName',
mimeType: 'mimeType',
sizeBytes: 'sizeBytes',
purpose: 'purpose',
uploadedByAdminUserId: 'uploadedByAdminUserId',
createdAt: 'createdAt'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
@@ -363,6 +522,14 @@ exports.AdminRole = exports.$Enums.AdminRole = {
ACCOUNTANT: 'ACCOUNTANT'
};
exports.CustomerKind = exports.$Enums.CustomerKind = {
SALES_AGENT: 'SALES_AGENT',
APP_SHOPKEEPER: 'APP_SHOPKEEPER',
WALK_IN_SHOPKEEPER: 'WALK_IN_SHOPKEEPER',
BANKER: 'BANKER',
WORKSHOP_SUPERVISOR: 'WORKSHOP_SUPERVISOR'
};
exports.VerificationStep = exports.$Enums.VerificationStep = {
UNVERIFIED: 'UNVERIFIED',
STEP_ONE_SUBMITTED: 'STEP_ONE_SUBMITTED',
@@ -370,6 +537,11 @@ exports.VerificationStep = exports.$Enums.VerificationStep = {
VERIFIED: 'VERIFIED'
};
exports.OrderChannel = exports.$Enums.OrderChannel = {
RETAIL: 'RETAIL',
WHOLESALE: 'WHOLESALE'
};
exports.OrderStatus = exports.$Enums.OrderStatus = {
SUSPENDED: 'SUSPENDED',
PLACED: 'PLACED',
@@ -411,6 +583,33 @@ exports.LedgerSourceType = exports.$Enums.LedgerSourceType = {
ADJUSTMENT: 'ADJUSTMENT'
};
exports.CommentStatus = exports.$Enums.CommentStatus = {
PENDING: 'PENDING',
APPROVED: 'APPROVED',
REJECTED: 'REJECTED'
};
exports.PromotionScope = exports.$Enums.PromotionScope = {
PRODUCT: 'PRODUCT',
CATEGORY: 'CATEGORY',
CUSTOMER: 'CUSTOMER',
BRAND: 'BRAND'
};
exports.PromotionUsage = exports.$Enums.PromotionUsage = {
ONCE: 'ONCE',
UNTIL_END_DATE: 'UNTIL_END_DATE'
};
exports.MessageAudience = exports.$Enums.MessageAudience = {
SINGLE_NUMBER: 'SINGLE_NUMBER',
MULTIPLE_NUMBERS: 'MULTIPLE_NUMBERS',
ALL_CUSTOMERS: 'ALL_CUSTOMERS',
WHOLESALE_CUSTOMERS: 'WHOLESALE_CUSTOMERS',
RETAIL_CUSTOMERS: 'RETAIL_CUSTOMERS',
SALES_AGENTS: 'SALES_AGENTS'
};
exports.Prisma.ModelName = {
City: 'City',
AdminUser: 'AdminUser',
@@ -432,7 +631,21 @@ exports.Prisma.ModelName = {
PreOrderStatusEvent: 'PreOrderStatusEvent',
LedgerEntry: 'LedgerEntry',
GoldPriceQuote: 'GoldPriceQuote',
WalletBonus: 'WalletBonus'
WalletBonus: 'WalletBonus',
UserType: 'UserType',
Permission: 'Permission',
Comment: 'Comment',
Setting: 'Setting',
Promotion: 'Promotion',
PromotionExclusion: 'PromotionExclusion',
BranchAllocation: 'BranchAllocation',
BranchAllocationItem: 'BranchAllocationItem',
SmsMessage: 'SmsMessage',
Notification: 'Notification',
NotificationRecipient: 'NotificationRecipient',
NewsletterSubscriber: 'NewsletterSubscriber',
CustomerActivityLog: 'CustomerActivityLog',
UploadedFile: 'UploadedFile'
};
/**
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "prisma-client-d0d5db76aa59566edce744ecac2768fcb398c1a9f370089a45b9416272282fab",
"name": "prisma-client-9a5464add1bac751da0fd2476fcc0a15f434a77547c687bcb5959b29d451c62b",
"main": "index.js",
"types": "index.d.ts",
"browser": "index-browser.js",
+432
View File
@@ -26,6 +26,16 @@ enum AdminRole {
ACCOUNTANT
}
/// Legacy Customer.type — what kind of wholesale customer this is. Values and
/// labels taken verbatim from bulkCustomer.vue's userTypes list.
enum CustomerKind {
SALES_AGENT // 1 کارشناس فروش
APP_SHOPKEEPER // 2 مغازه دار اپلیکیشن
WALK_IN_SHOPKEEPER // 3 مغازه دار حضوری
BANKER // 4 بنکدار
WORKSHOP_SUPERVISOR // 5 سرپرست کارگاه
}
enum VerificationStep {
UNVERIFIED
STEP_ONE_SUBMITTED
@@ -33,6 +43,14 @@ enum VerificationStep {
VERIFIED
}
/// Legacy kept retail and wholesale orders in two SEPARATE tables
/// (orders_orders vs orders). The redesigned schema has one Order model, so the
/// distinction lives here instead.
enum OrderChannel {
RETAIL // تک فروشی
WHOLESALE // عمده فروشی
}
enum OrderStatus {
SUSPENDED // credit limit (goldCreditLimitGrams) exceeded at placement
PLACED
@@ -82,6 +100,10 @@ model City {
id String @id @default(cuid())
name String
province String?
/// Legacy cities.percent / percent_aux — per-city wage surcharges the city
/// screen edits through GET cities/get-percent/{id} and get-percent-aux/{id}.
percent Decimal? @db.Decimal(6, 3)
percentAux Decimal? @db.Decimal(6, 3)
customers Customer[]
addresses CustomerAddress[]
branches Branch[]
@@ -94,6 +116,11 @@ model AdminUser {
mobile String @unique
passwordHash String
role AdminRole
/// Legacy `users.ut`. Granular ACL lives here (Permission rows hang off UserType);
/// the coarse `role` enum above is retained for the business rules that already
/// switch on it. Nullable so existing seeded admins remain valid.
userTypeId Int?
userType UserType? @relation(fields: [userTypeId], references: [id])
branchId String?
branch Branch? @relation(fields: [branchId], references: [id])
wagePercent Decimal? @db.Decimal(6, 3) // legacy Customer.percent, commission on referred orders
@@ -101,11 +128,17 @@ model AdminUser {
createdAt DateTime @default(now())
referredCustomers Customer[] @relation("ReferralAgent")
supervisedWorkshops Workshop[] @relation("WorkshopSupervisor")
branchAllocations BranchAllocation[] @relation("AllocationAuthor")
postedLedgerEntries LedgerEntry[]
orderStatusEvents OrderStatusEvent[]
preOrderStatusEvents PreOrderStatusEvent[]
sentSmsMessages SmsMessage[] @relation("SmsAuthor")
sentNotifications Notification[] @relation("NotificationAuthor")
uploadedFiles UploadedFile[] @relation("FileUploader")
@@index([role])
@@index([userTypeId])
}
model Customer {
@@ -117,6 +150,20 @@ model Customer {
cityId String?
city City? @relation(fields: [cityId], references: [id])
/// Legacy Customer.type — distinguishes بنکدار from مغازه دار, which the
/// BankerList / ShopkeeperList screens filter on.
kind CustomerKind @default(APP_SHOPKEEPER)
/// Legacy contact + shop columns, shown on the wholesale customer form and as
/// (hidden-by-default) columns in its grid.
phone String?
address String?
storeName String?
pasazhName String?
zone String?
/// Legacy mcode — the operator-assigned customer code.
memberCode String?
referredByAgentId String?
referredByAgent AdminUser? @relation("ReferralAgent", fields: [referredByAgentId], references: [id])
@@ -131,7 +178,21 @@ model Customer {
businessLicenseUrl String?
shopImageUrl String?
/// Legacy `image_id` — the profile photo (عکس پروفایل) on the sales-agent
/// form. Legacy stored an FK to its Image table and read it back as a nested
/// `image: { id, image }`, so this is a relation, not a bare URL string.
avatarFileId String?
avatarFile UploadedFile? @relation("CustomerAvatar", fields: [avatarFileId], references: [id])
/// Legacy had TWO independent switches on a customer:
/// sts -> may they place orders (this field)
/// active -> is the account enabled (isActive, below)
/// The port previously drove both from isActive; they are separate again.
canPlaceOrders Boolean @default(true)
isActive Boolean @default(true)
/// Legacy archive flag — the archived list filtered on this, not on isActive.
isArchived Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -140,6 +201,10 @@ model Customer {
preOrders PreOrder[]
ledgerEntries LedgerEntry[]
walletBonuses WalletBonus[]
comments Comment[]
promotions Promotion[]
notifications NotificationRecipient[]
activityLogs CustomerActivityLog[]
@@index([referredByAgentId])
}
@@ -157,15 +222,27 @@ model CustomerAddress {
model Branch {
id String @id @default(cuid())
name String
/// Legacy Branch.address / phone — both are columns in the branch grid.
address String?
phone String?
/// Legacy GET branch/active/{id} toggles this.
isActive Boolean @default(true)
cityId String?
city City? @relation(fields: [cityId], references: [id])
adminUsers AdminUser[]
orders Order[]
allocations BranchAllocation[]
}
model Workshop {
id String @id @default(cuid())
name String
/// Legacy Workshop.address / phone — both are columns in the workshop grid.
address String?
phone String?
/// The supervisor created alongside the workshop by the legacy Add form.
supervisorId String?
supervisor AdminUser? @relation("WorkshopSupervisor", fields: [supervisorId], references: [id])
isActive Boolean @default(true)
preOrders PreOrder[]
}
@@ -177,16 +254,27 @@ model Workshop {
model Category {
id String @id @default(cuid())
title String
/// Legacy Category.type — which storefront the category belongs to:
/// 1 سایت | 2 اپلیکشن موبایل. Drives the "type_str" grid column.
type Int @default(1)
/// Legacy Category.image / icon — the grid renders the image, the storefront the icon.
imageUrl String?
iconUrl String?
/// Legacy meta tag + description, used for SEO on the site categories.
metaTag String?
description String?
parentId String?
parent Category? @relation("CategoryTree", fields: [parentId], references: [id])
children Category[] @relation("CategoryTree")
products Product[]
promotions Promotion[]
}
model Brand {
id String @id @default(cuid())
title String
products Product[]
promotions Promotion[]
}
/// Curated product groupings — replaces legacy "Ciny/CinyProduct" (a tagging
@@ -216,12 +304,19 @@ model Product {
category Category? @relation(fields: [categoryId], references: [id])
brandId String?
brand Brand? @relation(fields: [brandId], references: [id])
/// Legacy Product.product_code — the human-facing SKU shown in the grid.
productCode String? @unique
/// Legacy Product.show_type: 1 site, 2 mobile, 3 both.
showType Int @default(3)
isActive Boolean @default(true)
createdAt DateTime @default(now())
variants ProductVariant[]
images ProductImage[]
collections CollectionProduct[]
comments Comment[]
promotions Promotion[]
allocationItems BranchAllocationItem[]
}
model ProductImage {
@@ -242,6 +337,12 @@ model ProductVariant {
rashWeightGrams Decimal @db.Decimal(14, 4)
/// Default making-charge percentage applied on top of rash weight.
defaultWagePercent Decimal @db.Decimal(6, 3)
/// Legacy ProductVariant.qty — on-hand stock. The cart refuses a variant with
/// qty 0 unless it is flagged pre-order, so both fields are required for that
/// guard to work (see apps/admin-panel/src/stores/cart.ts).
qty Int @default(0)
/// Legacy is_pre_order: orderable even at zero stock.
isPreOrder Boolean @default(false)
isActive Boolean @default(true)
orderItems OrderItem[]
@@ -258,6 +359,8 @@ model Order {
customer Customer @relation(fields: [customerId], references: [id])
branchId String?
branch Branch? @relation(fields: [branchId], references: [id])
/// Which sales channel placed this order — see OrderChannel.
channel OrderChannel @default(RETAIL)
status OrderStatus @default(PLACED)
/// Total gold weight incl. wage across all items ("allGram" / order.w).
@@ -391,6 +494,22 @@ model LedgerEntry {
postedByAdminUserId String?
postedByAdminUser AdminUser? @relation(fields: [postedByAdminUserId], references: [id])
/// Instrument metadata from the legacy "cash" document form. Which fields are
/// filled depends on sourceType: a melted-gold receipt carries lab/hallmark and
/// the pre-normalization weight, a cheque carries the account and bank, and a
/// transfer carries its destination.
/// Legacy columns: lab, ang, account_name, bank_name, to, original_value, recive_date.
labName String?
hallmark String?
accountName String?
bankName String?
transferTo String?
/// Weight as handed over, BEFORE karat normalization (legacy original_value).
originalValue Decimal? @db.Decimal(14, 4)
/// Rial amount for cash/cheque documents (legacy price).
priceRial Decimal? @db.Decimal(20, 2)
receivedAt DateTime?
note String?
createdAt DateTime @default(now())
@@ -423,3 +542,316 @@ model WalletBonus {
@@index([customerId, isActive])
}
// ─────────────────────────────────────────────────────────────────────────
// Admin ACL — faithful port of the legacy granular permission model.
//
// Legacy (henza_server):
// Schema::create('permission', function (Blueprint $table) {
// $table->string('rid');
// $table->integer('ut');
// $table->primary(['rid' , 'ut']);
// });
//
// Permissions attach to the USER TYPE (`ut`), never to an individual user, so
// every admin of a given type shares one permission set. `ut` pointed at a row
// in the legacy `config_static` lookup table; UserType is that table, narrowed
// to the admin-user-type rows it was actually used for.
//
// A `rid` is a dotted permission id such as "persons.list.client.omde". The
// frontend expands each stored rid into its ancestors at runtime via
// checkParent() — see apps/admin-panel/src/stores/users.ts. Only the 58 leaf
// rids the routers actually gate are seeded; ancestors are derived, not stored.
// ─────────────────────────────────────────────────────────────────────────
model UserType {
/// Legacy `permission.ut` / `users.ut` — an integer, kept as the PK so seeded
/// rows keep their legacy identity rather than getting new cuids.
id Int @id
title String
isActive Boolean @default(true)
createdAt DateTime @default(now())
permissions Permission[]
adminUsers AdminUser[]
@@map("user_type")
}
model Permission {
/// Dotted permission id, e.g. "order.list", "persons.list.client.omde".
rid String
userTypeId Int
userType UserType @relation(fields: [userTypeId], references: [id], onDelete: Cascade)
/// Legacy's composite primary key (rid, ut), preserved exactly.
@@id([rid, userTypeId])
@@index([userTypeId])
@@map("permission")
}
// ─────────────────────────────────────────────────────────────────────────
// Product comments / reviews — legacy `comment` table (App\Models\Comment),
// moderated from views/customer/Comments/Comments.vue.
//
// Legacy fillable: product_id, comment, reply_to, sts, customer_id
// Legacy Comment::$STS = [1 => 'در انتظار تایید', 2 => 'تایید شده', 3 => 'رد شده']
// which is represented here as an enum rather than a magic integer.
// ─────────────────────────────────────────────────────────────────────────
enum CommentStatus {
PENDING // 1 — در انتظار تایید
APPROVED // 2 — تایید شده
REJECTED // 3 — رد شده
}
model Comment {
id String @id @default(cuid())
productId String
product Product @relation(fields: [productId], references: [id])
customerId String
customer Customer @relation(fields: [customerId], references: [id])
body String
status CommentStatus @default(PENDING)
/// Legacy `reply_to`: a staff reply threads under the comment it answers.
replyToId String?
replyTo Comment? @relation("CommentReplies", fields: [replyToId], references: [id])
replies Comment[] @relation("CommentReplies")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([productId])
@@index([customerId])
@@index([status])
}
// ─────────────────────────────────────────────────────────────────────────
// Settings — replaces legacy `config_static`, a key/value table the admin
// panel read and wrote through endpoints like /wallet/config and
// /const/__list__ut. Values are stored as JSON so a setting can hold a scalar,
// an object, or the dynamic option lists legacy kept under `__list__*` keys.
// ─────────────────────────────────────────────────────────────────────────
model Setting {
key String @id
value Json
updatedAt DateTime @updatedAt
}
// ─────────────────────────────────────────────────────────────────────────
// Promotions / discounts — legacy views/promotion/*, served by
// PromotionController (`product/variant/promotion`). The redesigned schema had
// no promotion concept at all; this models what the legacy forms actually
// collect.
// ─────────────────────────────────────────────────────────────────────────
/// Legacy Promotion.type — what the discount is scoped to.
enum PromotionScope {
PRODUCT // 1 محصولات
CATEGORY // 2 دسته بندی
CUSTOMER // 3 مشتری
BRAND // 4 برند
}
/// Legacy Promotion.use_only — how often a customer may redeem it.
enum PromotionUsage {
ONCE // 1 فقط یکبار
UNTIL_END_DATE // 2 تا تاریخ پایان
}
model Promotion {
id String @id @default(cuid())
title String
description String?
scope PromotionScope @default(PRODUCT)
usage PromotionUsage @default(ONCE)
/// Percentage off, 0-100 (legacy discount_percent).
discountPercent Decimal @db.Decimal(6, 3)
isActive Boolean @default(true)
/// Which storefront it shows on, mirroring Product.showType (legacy platform).
platform Int @default(3)
startsAt DateTime?
endsAt DateTime?
/// Exactly one of these is populated, per `scope`.
productId String?
product Product? @relation(fields: [productId], references: [id])
categoryId String?
category Category? @relation(fields: [categoryId], references: [id])
customerId String?
customer Customer? @relation(fields: [customerId], references: [id])
brandId String?
brand Brand? @relation(fields: [brandId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([scope, isActive])
}
/// Legacy "ignore" table (views/offersIgnore) — specific products, categories,
/// customers or brands excluded from the automatic discount engine.
/// Legacy posts { type, item_id[] }: "type" reuses the promotion scope codes
/// (1 محصولات | 2 دسته بندی | 3 مشتری | 4 برند) and "itemId" is the excluded row.
model PromotionExclusion {
id String @id @default(cuid())
type Int
itemId String
createdAt DateTime @default(now())
@@unique([type, itemId])
@@index([type])
}
// ─────────────────────────────────────────────────────────────────────────
// Branch stock allocation — legacy views/invoice/* ("فاکتور انتقال"), served by
// BranchController as `branch/product`. Despite the screen being called an
// invoice, it is not a sales document: it records which products head office
// allocated to a branch, and who allocated them.
// ─────────────────────────────────────────────────────────────────────────
model BranchAllocation {
id String @id @default(cuid())
branchId String
branch Branch @relation(fields: [branchId], references: [id])
/// Legacy `allocator_user` — the admin who filed the allocation.
allocatorAdminUserId String?
allocatorAdminUser AdminUser? @relation("AllocationAuthor", fields: [allocatorAdminUserId], references: [id])
note String?
createdAt DateTime @default(now())
items BranchAllocationItem[]
@@index([branchId])
}
model BranchAllocationItem {
id String @id @default(cuid())
allocationId String
allocation BranchAllocation @relation(fields: [allocationId], references: [id], onDelete: Cascade)
productId String
product Product @relation(fields: [productId], references: [id])
/// Weight snapshot at allocation time, so later catalog edits do not rewrite history.
weightGrams Decimal? @db.Decimal(14, 4)
@@unique([allocationId, productId])
}
// ─────────────────────────────────────────────────────────────────────────
// Messaging + audit — legacy views/sms, views/notify, views/newsletters and
// views/statics. None of these had a model in the redesigned schema.
// ─────────────────────────────────────────────────────────────────────────
/// Who an SMS or notification was aimed at. Legacy's sms/create offered these
/// audiences, each gated by its own rid (sms.create.all, .omde, .single, ...).
enum MessageAudience {
SINGLE_NUMBER // یک شماره
MULTIPLE_NUMBERS // چند شماره
ALL_CUSTOMERS // همه
WHOLESALE_CUSTOMERS // عمده
RETAIL_CUSTOMERS // تک
SALES_AGENTS // کارشناسان فروش
}
model SmsMessage {
id String @id @default(cuid())
audience MessageAudience
/// Comma-separated recipients for the SINGLE/MULTIPLE audiences.
recipients String?
body String
/// How many numbers the send actually resolved to.
sentCount Int @default(0)
sentByAdminUserId String?
sentByAdminUser AdminUser? @relation("SmsAuthor", fields: [sentByAdminUserId], references: [id])
createdAt DateTime @default(now())
@@index([createdAt])
}
model Notification {
id String @id @default(cuid())
title String
body String
audience MessageAudience @default(ALL_CUSTOMERS)
sentByAdminUserId String?
sentByAdminUser AdminUser? @relation("NotificationAuthor", fields: [sentByAdminUserId], references: [id])
createdAt DateTime @default(now())
recipients NotificationRecipient[]
@@index([createdAt])
}
/// Per-customer delivery + read state — legacy `GET notify/read/{id}`.
model NotificationRecipient {
id String @id @default(cuid())
notificationId String
notification Notification @relation(fields: [notificationId], references: [id], onDelete: Cascade)
customerId String
customer Customer @relation(fields: [customerId], references: [id])
readAt DateTime?
@@unique([notificationId, customerId])
@@index([customerId, readAt])
}
/// Legacy views/newsletters — the mailing-list signups collected by the site.
model NewsletterSubscriber {
id String @id @default(cuid())
email String?
mobile String?
createdAt DateTime @default(now())
@@unique([email])
@@unique([mobile])
}
/// Legacy views/statics — the customer activity log the "آمار" screen reads
/// (`log/list/customer`, `log/ago/{customerId}`).
model CustomerActivityLog {
id String @id @default(cuid())
customerId String
customer Customer @relation(fields: [customerId], references: [id])
/// Free-form action key, e.g. 'login', 'order.placed', 'profile.updated'.
action String
detail String?
createdAt DateTime @default(now())
@@index([customerId, createdAt])
}
// ─────────────────────────────────────────────────────────────────────────
// Uploads — replaces the legacy MyUpload / SingleUploadInput widgets, which
// POSTed to a file endpoint and stored the returned URL on the owning record.
// Ports of those forms currently take a URL by hand; this gives them a real
// destination to upload to.
// ─────────────────────────────────────────────────────────────────────────
model UploadedFile {
id String @id @default(cuid())
/// Public URL the stored file is served from.
url String
originalName String
mimeType String
sizeBytes Int
/// Free-form tag for what the file belongs to, e.g. 'product', 'category',
/// 'customer.nationalId'. Kept loose so a new form needs no schema change.
purpose String?
uploadedByAdminUserId String?
uploadedByAdminUser AdminUser? @relation("FileUploader", fields: [uploadedByAdminUserId], references: [id])
createdAt DateTime @default(now())
customerAvatars Customer[] @relation("CustomerAvatar")
@@index([purpose])
}
+215 -2
View File
@@ -125,7 +125,9 @@ exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
exports.Prisma.CityScalarFieldEnum = {
id: 'id',
name: 'name',
province: 'province'
province: 'province',
percent: 'percent',
percentAux: 'percentAux'
};
exports.Prisma.AdminUserScalarFieldEnum = {
@@ -134,6 +136,7 @@ exports.Prisma.AdminUserScalarFieldEnum = {
mobile: 'mobile',
passwordHash: 'passwordHash',
role: 'role',
userTypeId: 'userTypeId',
branchId: 'branchId',
wagePercent: 'wagePercent',
isActive: 'isActive',
@@ -147,6 +150,13 @@ exports.Prisma.CustomerScalarFieldEnum = {
nationalCode: 'nationalCode',
passwordHash: 'passwordHash',
cityId: 'cityId',
kind: 'kind',
phone: 'phone',
address: 'address',
storeName: 'storeName',
pasazhName: 'pasazhName',
zone: 'zone',
memberCode: 'memberCode',
referredByAgentId: 'referredByAgentId',
goldCreditLimitGrams: 'goldCreditLimitGrams',
goldDebtGrams: 'goldDebtGrams',
@@ -155,7 +165,10 @@ exports.Prisma.CustomerScalarFieldEnum = {
nationalIdImageUrl: 'nationalIdImageUrl',
businessLicenseUrl: 'businessLicenseUrl',
shopImageUrl: 'shopImageUrl',
avatarFileId: 'avatarFileId',
canPlaceOrders: 'canPlaceOrders',
isActive: 'isActive',
isArchived: 'isArchived',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
@@ -171,18 +184,29 @@ exports.Prisma.CustomerAddressScalarFieldEnum = {
exports.Prisma.BranchScalarFieldEnum = {
id: 'id',
name: 'name',
address: 'address',
phone: 'phone',
isActive: 'isActive',
cityId: 'cityId'
};
exports.Prisma.WorkshopScalarFieldEnum = {
id: 'id',
name: 'name',
address: 'address',
phone: 'phone',
supervisorId: 'supervisorId',
isActive: 'isActive'
};
exports.Prisma.CategoryScalarFieldEnum = {
id: 'id',
title: 'title',
type: 'type',
imageUrl: 'imageUrl',
iconUrl: 'iconUrl',
metaTag: 'metaTag',
description: 'description',
parentId: 'parentId'
};
@@ -209,6 +233,8 @@ exports.Prisma.ProductScalarFieldEnum = {
description: 'description',
categoryId: 'categoryId',
brandId: 'brandId',
productCode: 'productCode',
showType: 'showType',
isActive: 'isActive',
createdAt: 'createdAt'
};
@@ -227,6 +253,8 @@ exports.Prisma.ProductVariantScalarFieldEnum = {
sku: 'sku',
rashWeightGrams: 'rashWeightGrams',
defaultWagePercent: 'defaultWagePercent',
qty: 'qty',
isPreOrder: 'isPreOrder',
isActive: 'isActive'
};
@@ -235,6 +263,7 @@ exports.Prisma.OrderScalarFieldEnum = {
orderNumber: 'orderNumber',
customerId: 'customerId',
branchId: 'branchId',
channel: 'channel',
status: 'status',
totalGoldWeightGrams: 'totalGoldWeightGrams',
wageWeightGrams: 'wageWeightGrams',
@@ -308,6 +337,14 @@ exports.Prisma.LedgerEntryScalarFieldEnum = {
confirmed: 'confirmed',
confirmedAt: 'confirmedAt',
postedByAdminUserId: 'postedByAdminUserId',
labName: 'labName',
hallmark: 'hallmark',
accountName: 'accountName',
bankName: 'bankName',
transferTo: 'transferTo',
originalValue: 'originalValue',
priceRial: 'priceRial',
receivedAt: 'receivedAt',
note: 'note',
createdAt: 'createdAt'
};
@@ -331,6 +368,128 @@ exports.Prisma.WalletBonusScalarFieldEnum = {
isActive: 'isActive'
};
exports.Prisma.UserTypeScalarFieldEnum = {
id: 'id',
title: 'title',
isActive: 'isActive',
createdAt: 'createdAt'
};
exports.Prisma.PermissionScalarFieldEnum = {
rid: 'rid',
userTypeId: 'userTypeId'
};
exports.Prisma.CommentScalarFieldEnum = {
id: 'id',
productId: 'productId',
customerId: 'customerId',
body: 'body',
status: 'status',
replyToId: 'replyToId',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.SettingScalarFieldEnum = {
key: 'key',
value: 'value',
updatedAt: 'updatedAt'
};
exports.Prisma.PromotionScalarFieldEnum = {
id: 'id',
title: 'title',
description: 'description',
scope: 'scope',
usage: 'usage',
discountPercent: 'discountPercent',
isActive: 'isActive',
platform: 'platform',
startsAt: 'startsAt',
endsAt: 'endsAt',
productId: 'productId',
categoryId: 'categoryId',
customerId: 'customerId',
brandId: 'brandId',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.PromotionExclusionScalarFieldEnum = {
id: 'id',
type: 'type',
itemId: 'itemId',
createdAt: 'createdAt'
};
exports.Prisma.BranchAllocationScalarFieldEnum = {
id: 'id',
branchId: 'branchId',
allocatorAdminUserId: 'allocatorAdminUserId',
note: 'note',
createdAt: 'createdAt'
};
exports.Prisma.BranchAllocationItemScalarFieldEnum = {
id: 'id',
allocationId: 'allocationId',
productId: 'productId',
weightGrams: 'weightGrams'
};
exports.Prisma.SmsMessageScalarFieldEnum = {
id: 'id',
audience: 'audience',
recipients: 'recipients',
body: 'body',
sentCount: 'sentCount',
sentByAdminUserId: 'sentByAdminUserId',
createdAt: 'createdAt'
};
exports.Prisma.NotificationScalarFieldEnum = {
id: 'id',
title: 'title',
body: 'body',
audience: 'audience',
sentByAdminUserId: 'sentByAdminUserId',
createdAt: 'createdAt'
};
exports.Prisma.NotificationRecipientScalarFieldEnum = {
id: 'id',
notificationId: 'notificationId',
customerId: 'customerId',
readAt: 'readAt'
};
exports.Prisma.NewsletterSubscriberScalarFieldEnum = {
id: 'id',
email: 'email',
mobile: 'mobile',
createdAt: 'createdAt'
};
exports.Prisma.CustomerActivityLogScalarFieldEnum = {
id: 'id',
customerId: 'customerId',
action: 'action',
detail: 'detail',
createdAt: 'createdAt'
};
exports.Prisma.UploadedFileScalarFieldEnum = {
id: 'id',
url: 'url',
originalName: 'originalName',
mimeType: 'mimeType',
sizeBytes: 'sizeBytes',
purpose: 'purpose',
uploadedByAdminUserId: 'uploadedByAdminUserId',
createdAt: 'createdAt'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
@@ -363,6 +522,14 @@ exports.AdminRole = exports.$Enums.AdminRole = {
ACCOUNTANT: 'ACCOUNTANT'
};
exports.CustomerKind = exports.$Enums.CustomerKind = {
SALES_AGENT: 'SALES_AGENT',
APP_SHOPKEEPER: 'APP_SHOPKEEPER',
WALK_IN_SHOPKEEPER: 'WALK_IN_SHOPKEEPER',
BANKER: 'BANKER',
WORKSHOP_SUPERVISOR: 'WORKSHOP_SUPERVISOR'
};
exports.VerificationStep = exports.$Enums.VerificationStep = {
UNVERIFIED: 'UNVERIFIED',
STEP_ONE_SUBMITTED: 'STEP_ONE_SUBMITTED',
@@ -370,6 +537,11 @@ exports.VerificationStep = exports.$Enums.VerificationStep = {
VERIFIED: 'VERIFIED'
};
exports.OrderChannel = exports.$Enums.OrderChannel = {
RETAIL: 'RETAIL',
WHOLESALE: 'WHOLESALE'
};
exports.OrderStatus = exports.$Enums.OrderStatus = {
SUSPENDED: 'SUSPENDED',
PLACED: 'PLACED',
@@ -411,6 +583,33 @@ exports.LedgerSourceType = exports.$Enums.LedgerSourceType = {
ADJUSTMENT: 'ADJUSTMENT'
};
exports.CommentStatus = exports.$Enums.CommentStatus = {
PENDING: 'PENDING',
APPROVED: 'APPROVED',
REJECTED: 'REJECTED'
};
exports.PromotionScope = exports.$Enums.PromotionScope = {
PRODUCT: 'PRODUCT',
CATEGORY: 'CATEGORY',
CUSTOMER: 'CUSTOMER',
BRAND: 'BRAND'
};
exports.PromotionUsage = exports.$Enums.PromotionUsage = {
ONCE: 'ONCE',
UNTIL_END_DATE: 'UNTIL_END_DATE'
};
exports.MessageAudience = exports.$Enums.MessageAudience = {
SINGLE_NUMBER: 'SINGLE_NUMBER',
MULTIPLE_NUMBERS: 'MULTIPLE_NUMBERS',
ALL_CUSTOMERS: 'ALL_CUSTOMERS',
WHOLESALE_CUSTOMERS: 'WHOLESALE_CUSTOMERS',
RETAIL_CUSTOMERS: 'RETAIL_CUSTOMERS',
SALES_AGENTS: 'SALES_AGENTS'
};
exports.Prisma.ModelName = {
City: 'City',
AdminUser: 'AdminUser',
@@ -432,7 +631,21 @@ exports.Prisma.ModelName = {
PreOrderStatusEvent: 'PreOrderStatusEvent',
LedgerEntry: 'LedgerEntry',
GoldPriceQuote: 'GoldPriceQuote',
WalletBonus: 'WalletBonus'
WalletBonus: 'WalletBonus',
UserType: 'UserType',
Permission: 'Permission',
Comment: 'Comment',
Setting: 'Setting',
Promotion: 'Promotion',
PromotionExclusion: 'PromotionExclusion',
BranchAllocation: 'BranchAllocation',
BranchAllocationItem: 'BranchAllocationItem',
SmsMessage: 'SmsMessage',
Notification: 'Notification',
NotificationRecipient: 'NotificationRecipient',
NewsletterSubscriber: 'NewsletterSubscriber',
CustomerActivityLog: 'CustomerActivityLog',
UploadedFile: 'UploadedFile'
};
/**
+13 -1
View File
@@ -2,11 +2,19 @@ import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'node:path';
import { extname, join } from 'node:path';
import type { Response } from 'express';
import { AppModule } from './app.module';
import { prepareDatabase } from './prisma/bootstrap';
import { PrismaExceptionFilter } from './prisma/prisma-exception.filter';
// serve-static's bundled `mime` version predates .avif, so it falls back to
// application/octet-stream — browsers still render the image (they sniff
// content), but a strict client or an <img> under some CSPs would not.
const EXTRA_MIME_TYPES: Record<string, string> = {
'.avif': 'image/avif',
};
async function bootstrap() {
await prepareDatabase();
@@ -21,6 +29,10 @@ async function bootstrap() {
prefix: '/uploads/',
index: false,
dotfiles: 'deny',
setHeaders: (res: Response, path: string) => {
const mime = EXTRA_MIME_TYPES[extname(path).toLowerCase()];
if (mime) res.setHeader('Content-Type', mime);
},
});
// Bind all interfaces explicitly so the container is reachable from nginx.
await app.listen(process.env.PORT ?? 3000, '0.0.0.0');
+45 -8
View File
@@ -1,4 +1,4 @@
import { existsSync, readdirSync, readFileSync } from 'fs';
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
import { join } from 'path';
import { PrismaClient } from '@prisma/client';
@@ -105,11 +105,48 @@ export function splitSqlStatements(sql: string): string[] {
return statements;
}
/**
* Bug fixed here (was silently a no-op): this used to only look for flat
* `<name>.sql` files directly under prisma/migrations. Prisma's own CLI
* (`prisma migrate dev`) creates a FOLDER per migration instead
* (`<timestamp>_<name>/migration.sql`), which `readdirSync(...).filter(name
* => name.endsWith('.sql'))` never matches — a directory name doesn't end in
* ".sql". Every migration created the normal way was silently skipped; only
* a hand-added flat duplicate (prisma/migrations/0001_init.sql, now removed)
* ever ran. Both layouts are now discovered, sorted together by name so
* later migrations still apply after earlier ones regardless of shape.
*/
type MigrationFile = { name: string; path: string };
function discoverMigrationFiles(dir: string): MigrationFile[] {
const entries = readdirSync(dir);
const files: MigrationFile[] = [];
for (const entry of entries) {
const full = join(dir, entry);
const stat = statSync(full);
if (stat.isFile() && entry.endsWith('.sql')) {
files.push({ name: entry, path: full });
continue;
}
if (stat.isDirectory()) {
const sqlPath = join(full, 'migration.sql');
if (existsSync(sqlPath)) {
// Tracked by the folder name (e.g. "20260910113720_first"), matching
// how Prisma's own _prisma_migrations table identifies a migration.
files.push({ name: entry, path: sqlPath });
}
}
}
return files.sort((a, b) => a.name.localeCompare(b.name));
}
export async function runMigrations(prisma: PrismaClient): Promise<MigrationResult> {
const dir = resolveMigrationsDir();
const files = readdirSync(dir)
.filter((name) => name.endsWith('.sql'))
.sort();
const files = discoverMigrationFiles(dir);
const trackingExisted = await prisma.$queryRawUnsafe<{ exists: boolean }[]>(
`SELECT EXISTS (
@@ -134,9 +171,9 @@ export async function runMigrations(prisma: PrismaClient): Promise<MigrationResu
const applied: string[] = [];
for (const file of files) {
if (appliedNames.has(file)) continue;
if (appliedNames.has(file.name)) continue;
const statements = splitSqlStatements(readFileSync(join(dir, file), 'utf8'));
const statements = splitSqlStatements(readFileSync(file.path, 'utf8'));
await prisma.$transaction(
async (tx) => {
for (const statement of statements) {
@@ -144,14 +181,14 @@ export async function runMigrations(prisma: PrismaClient): Promise<MigrationResu
}
await tx.$executeRawUnsafe(
`INSERT INTO "${TRACKING_TABLE}" (name) VALUES ($1)`,
file,
file.name,
);
},
// The baseline migration is hundreds of DDL statements; the 5s default is
// not enough, especially against a remote database.
{ maxWait: 60_000, timeout: 600_000 },
);
applied.push(file);
applied.push(file.name);
}
return { freshDatabase, applied };
+7
View File
@@ -34,6 +34,12 @@ services:
# No host ports — Dokploy/Traefik routes by domain to container port 3000
expose:
- "3000"
# Uploaded files (product images, etc.) land in storage/uploads inside the
# container. Without a volume they vanish on every recreate/redeploy, and
# every UploadedFile/ProductImage row created before that would then point
# at a URL that 404s.
volumes:
- api_uploads:/app/storage/uploads
restart: unless-stopped
# The bare name "api" is ambiguous on Dokploy's shared network (other stacks
# register it too), which made nginx dial the wrong containers. Use a unique alias.
@@ -68,3 +74,4 @@ services:
volumes:
postgres_data:
api_uploads: