build: refresh release artifacts after product design surveys and catalog polish

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
mahmood
2026-11-14 08:32:21 +03:30
co-authored by Cursor
parent 7dd74ff4f7
commit b71fb678be
259 changed files with 12252 additions and 493 deletions
+1695 -50
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,13 @@
-- Product detail opens in the customer app, for the admin «آمار» screen.
CREATE TABLE "ProductView" (
"id" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ProductView_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "ProductView_productId_createdAt_idx" ON "ProductView"("productId", "createdAt");
CREATE INDEX "ProductView_createdAt_idx" ON "ProductView"("createdAt");
ALTER TABLE "ProductView" ADD CONSTRAINT "ProductView_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,8 @@
-- Product labels from the «برچسب‌ها» list. IF NOT EXISTS: some databases
-- already carry this column from an earlier, unmerged migration.
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "tags" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[];
UPDATE "Product" SET "tags" = ARRAY[]::TEXT[] WHERE "tags" IS NULL;
ALTER TABLE "Product" ALTER COLUMN "tags" SET DEFAULT ARRAY[]::TEXT[];
ALTER TABLE "Product" ALTER COLUMN "tags" SET NOT NULL;
CREATE INDEX IF NOT EXISTS "Product_tags_idx" ON "Product" USING GIN ("tags");
@@ -0,0 +1,13 @@
-- Server-side customer cart for daily expiry + reminder SMS (cart.expiry).
CREATE TABLE "CustomerCart" (
"customerId" TEXT NOT NULL,
"items" JSONB NOT NULL DEFAULT '[]',
"reminderSentForClearAt" TIMESTAMP(3),
"clearedAt" TIMESTAMP(3),
"updatedAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CustomerCart_pkey" PRIMARY KEY ("customerId")
);
ALTER TABLE "CustomerCart" ADD CONSTRAINT "CustomerCart_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,12 @@
-- Deferred «خرید بعدی» list — separate from CustomerCart so daily expiry
-- and order-clear never touch it, and stock is never reserved.
CREATE TABLE "CustomerBuyLater" (
"customerId" TEXT NOT NULL,
"items" JSONB NOT NULL DEFAULT '[]',
"updatedAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CustomerBuyLater_pkey" PRIMARY KEY ("customerId")
);
ALTER TABLE "CustomerBuyLater" ADD CONSTRAINT "CustomerBuyLater_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,49 @@
-- Product design survey (like/dislike before manufacturing).
CREATE TYPE "ProductSurveyStatus" AS ENUM ('PENDING_REVIEW', 'OPEN', 'CLOSED', 'APPROVED', 'REJECTED');
CREATE TYPE "ProductSurveyVoteValue" AS ENUM ('LIKE', 'DISLIKE');
CREATE TABLE "ProductSurvey" (
"id" TEXT NOT NULL,
"title" TEXT,
"imageUrl" TEXT NOT NULL,
"suggestedWeightGrams" DECIMAL(14,4) NOT NULL,
"status" "ProductSurveyStatus" NOT NULL DEFAULT 'PENDING_REVIEW',
"createdByCustomerId" TEXT,
"createdByAdminId" TEXT,
"publishedAt" TIMESTAMP(3),
"decidedAt" TIMESTAMP(3),
"decidedByAdminId" TEXT,
"decisionNote" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProductSurvey_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "ProductSurveyVote" (
"id" TEXT NOT NULL,
"surveyId" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"value" "ProductSurveyVoteValue" NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ProductSurveyVote_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "ProductSurvey_status_idx" ON "ProductSurvey"("status");
CREATE INDEX "ProductSurvey_createdByCustomerId_idx" ON "ProductSurvey"("createdByCustomerId");
CREATE INDEX "ProductSurvey_createdAt_idx" ON "ProductSurvey"("createdAt");
CREATE INDEX "ProductSurveyVote_customerId_idx" ON "ProductSurveyVote"("customerId");
CREATE UNIQUE INDEX "ProductSurveyVote_surveyId_customerId_key" ON "ProductSurveyVote"("surveyId", "customerId");
ALTER TABLE "ProductSurvey" ADD CONSTRAINT "ProductSurvey_createdByCustomerId_fkey" FOREIGN KEY ("createdByCustomerId") REFERENCES "Customer"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "ProductSurvey" ADD CONSTRAINT "ProductSurvey_createdByAdminId_fkey" FOREIGN KEY ("createdByAdminId") REFERENCES "Admin"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "ProductSurvey" ADD CONSTRAINT "ProductSurvey_decidedByAdminId_fkey" FOREIGN KEY ("decidedByAdminId") REFERENCES "Admin"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "ProductSurveyVote" ADD CONSTRAINT "ProductSurveyVote_surveyId_fkey" FOREIGN KEY ("surveyId") REFERENCES "ProductSurvey"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ProductSurveyVote" ADD CONSTRAINT "ProductSurveyVote_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
INSERT INTO "permission" ("rid", "userTypeId")
SELECT 'product.survey', 85
WHERE EXISTS (SELECT 1 FROM "user_type" WHERE "id" = 85)
ON CONFLICT DO NOTHING;
+2
View File
@@ -1621,6 +1621,7 @@ var ALL_RIDS = [
"config.statics.tags.edit",
"config.statics.tags.remove",
"config.wallet",
"config.cart",
"custom.config.statics.slider",
"custom.config.statics.tags.banner.ids",
"dashboard.view",
@@ -1712,6 +1713,7 @@ var ALL_RIDS = [
"product.list.variants.remove",
"product.list.view",
"product.percent",
"product.survey",
"report.order.item",
"report.product",
"sms.create",
File diff suppressed because one or more lines are too long
+66
View File
@@ -248,6 +248,22 @@ exports.Prisma.CustomerScalarFieldEnum = {
updatedAt: 'updatedAt'
};
exports.Prisma.CustomerCartScalarFieldEnum = {
customerId: 'customerId',
items: 'items',
reminderSentForClearAt: 'reminderSentForClearAt',
clearedAt: 'clearedAt',
updatedAt: 'updatedAt',
createdAt: 'createdAt'
};
exports.Prisma.CustomerBuyLaterScalarFieldEnum = {
customerId: 'customerId',
items: 'items',
updatedAt: 'updatedAt',
createdAt: 'createdAt'
};
exports.Prisma.CustomerFavoriteScalarFieldEnum = {
customerId: 'customerId',
productId: 'productId',
@@ -318,11 +334,18 @@ exports.Prisma.ProductScalarFieldEnum = {
showType: 'showType',
isActive: 'isActive',
createdAt: 'createdAt',
tags: 'tags',
accountingStockWeight: 'accountingStockWeight',
accountingStockCount: 'accountingStockCount',
accountingStockAt: 'accountingStockAt'
};
exports.Prisma.ProductViewScalarFieldEnum = {
id: 'id',
productId: 'productId',
createdAt: 'createdAt'
};
exports.Prisma.ProductImageScalarFieldEnum = {
id: 'id',
productId: 'productId',
@@ -484,6 +507,31 @@ exports.Prisma.CommentScalarFieldEnum = {
updatedAt: 'updatedAt'
};
exports.Prisma.ProductSurveyScalarFieldEnum = {
id: 'id',
title: 'title',
imageUrl: 'imageUrl',
suggestedWeightGrams: 'suggestedWeightGrams',
status: 'status',
createdByCustomerId: 'createdByCustomerId',
createdByAdminId: 'createdByAdminId',
publishedAt: 'publishedAt',
decidedAt: 'decidedAt',
decidedByAdminId: 'decidedByAdminId',
decisionNote: 'decisionNote',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.ProductSurveyVoteScalarFieldEnum = {
id: 'id',
surveyId: 'surveyId',
customerId: 'customerId',
value: 'value',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.SettingScalarFieldEnum = {
key: 'key',
value: 'value',
@@ -753,6 +801,19 @@ exports.CommentStatus = exports.$Enums.CommentStatus = {
REJECTED: 'REJECTED'
};
exports.ProductSurveyStatus = exports.$Enums.ProductSurveyStatus = {
PENDING_REVIEW: 'PENDING_REVIEW',
OPEN: 'OPEN',
CLOSED: 'CLOSED',
APPROVED: 'APPROVED',
REJECTED: 'REJECTED'
};
exports.ProductSurveyVoteValue = exports.$Enums.ProductSurveyVoteValue = {
LIKE: 'LIKE',
DISLIKE: 'DISLIKE'
};
exports.AccountingEntityType = exports.$Enums.AccountingEntityType = {
CUSTOMER: 'CUSTOMER',
PRODUCT: 'PRODUCT'
@@ -803,6 +864,8 @@ exports.Prisma.ModelName = {
AdminSession: 'AdminSession',
SalesExpertProfile: 'SalesExpertProfile',
Customer: 'Customer',
CustomerCart: 'CustomerCart',
CustomerBuyLater: 'CustomerBuyLater',
CustomerFavorite: 'CustomerFavorite',
CustomerAddress: 'CustomerAddress',
Branch: 'Branch',
@@ -812,6 +875,7 @@ exports.Prisma.ModelName = {
Collection: 'Collection',
CollectionProduct: 'CollectionProduct',
Product: 'Product',
ProductView: 'ProductView',
ProductImage: 'ProductImage',
ProductVariant: 'ProductVariant',
Order: 'Order',
@@ -825,6 +889,8 @@ exports.Prisma.ModelName = {
UserType: 'UserType',
Permission: 'Permission',
Comment: 'Comment',
ProductSurvey: 'ProductSurvey',
ProductSurveyVote: 'ProductSurveyVote',
Setting: 'Setting',
AccountingLink: 'AccountingLink',
AccountingOutbox: 'AccountingOutbox',
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-116b04ea3171f5cde20e1cb240deadd7003ef9332c0f79ef1b0e851c763aeb43",
"name": "prisma-client-07ff23aaae8323751fefc58ab63b3494732076b6adca58b30cf4c84ef5f8c271",
"main": "index.js",
"types": "index.d.ts",
"browser": "index-browser.js",
+140 -19
View File
@@ -173,14 +173,16 @@ model Admin {
isActive Boolean @default(true)
createdAt DateTime @default(now())
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")
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")
createdProductSurveys ProductSurvey[] @relation("SurveyAdminCreator")
decidedProductSurveys ProductSurvey[] @relation("SurveyDecider")
/// Issued admin-panel tokens, recorded only while session tracking is on
/// (AUTH_SESSION_TRACKING, or implicitly whenever the IAM is enabled) so a
@@ -553,23 +555,62 @@ model Customer {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
addresses CustomerAddress[]
orders Order[]
preOrders PreOrder[]
ledgerEntries LedgerEntry[]
walletBonuses WalletBonus[]
comments Comment[]
promotions Promotion[]
notifications NotificationRecipient[]
activityLogs CustomerActivityLog[]
otpChallenges CustomerOtp[]
favorites CustomerFavorite[]
addresses CustomerAddress[]
orders Order[]
preOrders PreOrder[]
ledgerEntries LedgerEntry[]
walletBonuses WalletBonus[]
comments Comment[]
promotions Promotion[]
notifications NotificationRecipient[]
activityLogs CustomerActivityLog[]
otpChallenges CustomerOtp[]
favorites CustomerFavorite[]
productSurveys ProductSurvey[] @relation("SurveyCustomerCreator")
productSurveyVotes ProductSurveyVote[]
/// App shopping cart synced from user-app-v2 so daily expiry + reminder SMS
/// can run on the server (localStorage alone cannot).
cart CustomerCart?
/// Deferred «خرید بعدی» list — never reserved against stock and never
/// cleared by the daily cart-expiry worker.
buyLater CustomerBuyLater?
@@index([referredByAgentId])
@@index([firstName])
@@index([lastName])
}
/// Server-side mirror of the customer-app cart (`appCart` in localStorage).
/// Empty `items` means no basket; the daily expiry worker clears it and may
/// SMS a reminder before the configured clear time (Setting `cart.expiry`).
model CustomerCart {
customerId String @id
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
/// Cart lines as JSON — same shape the app stores (pid, vid, qty, …).
items Json @default("[]")
/// Which clear-deadline this cart already got a reminder SMS for (ISO of
/// that deadline). Null = never reminded for the current cycle.
reminderSentForClearAt DateTime?
clearedAt DateTime?
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
}
/// «سبد خرید بعدی» — wish/defer list. Same line shape as the cart, but:
/// never decrements stock, never participates in checkout, never cleared by
/// cart.expiry. Moving a line here only removes it from the active cart.
model CustomerBuyLater {
customerId String @id
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
items Json @default("[]")
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
}
/// Legacy `fav` (251 rows) — saved products. The target had no model for this,
/// so every customer's favourites would have been lost in the migration; the
/// owner confirmed on 2026-10-06 that they matter.
@@ -694,6 +735,9 @@ model Product {
showType Int @default(3)
isActive Boolean @default(true)
createdAt DateTime @default(now())
/// Labels picked from the «ثابت‌ها → برچسب‌ها» list (Setting `__list__tags`).
/// Shown on the app's product cards; tagged products get their own home section.
tags String[] @default([])
/// Stock of the linked کارساخته as last read from the accounting software
/// (D27). Weight for gram-type items, count for count-type ones — whichever
@@ -710,6 +754,7 @@ model Product {
promotions Promotion[]
allocationItems BranchAllocationItem[]
favoritedBy CustomerFavorite[]
views ProductView[]
// Storefront paging (owner: the real catalogue reaches ~16k products, so
// GET /catalog/products must page rather than return everything). This model
@@ -722,6 +767,19 @@ model Product {
@@index([isActive, createdAt])
@@index([categoryId])
@@index([brandId])
@@index([tags], type: Gin)
}
/// One opening of a product's detail screen in the customer app. Read by the
/// admin «آمار» screen for the most-viewed products and brands.
model ProductView {
id String @id @default(cuid())
productId String
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@index([productId, createdAt])
@@index([createdAt])
}
model ProductImage {
@@ -1097,6 +1155,69 @@ model Comment {
@@index([status])
}
// ─────────────────────────────────────────────────────────────────────────
// Product design survey — before manufacturing, a design (photo + suggested
// weight) is shown for like/dislike. Admin or customer can create; customer
// submissions need admin publish. Admin decides APPROVED/REJECTED by hand.
// Not linked to Product / PreOrder.
// ─────────────────────────────────────────────────────────────────────────
enum ProductSurveyStatus {
PENDING_REVIEW
OPEN
CLOSED
APPROVED
REJECTED
}
enum ProductSurveyVoteValue {
LIKE
DISLIKE
}
model ProductSurvey {
id String @id @default(cuid())
title String?
imageUrl String
suggestedWeightGrams Decimal @db.Decimal(14, 4)
status ProductSurveyStatus @default(PENDING_REVIEW)
createdByCustomerId String?
createdByCustomer Customer? @relation("SurveyCustomerCreator", fields: [createdByCustomerId], references: [id])
createdByAdminId String?
createdByAdmin Admin? @relation("SurveyAdminCreator", fields: [createdByAdminId], references: [id])
publishedAt DateTime?
decidedAt DateTime?
decidedByAdminId String?
decidedByAdmin Admin? @relation("SurveyDecider", fields: [decidedByAdminId], references: [id])
decisionNote String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
votes ProductSurveyVote[]
@@index([status])
@@index([createdByCustomerId])
@@index([createdAt])
}
model ProductSurveyVote {
id String @id @default(cuid())
surveyId String
survey ProductSurvey @relation(fields: [surveyId], references: [id], onDelete: Cascade)
customerId String
customer Customer @relation(fields: [customerId], references: [id])
value ProductSurveyVoteValue
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([surveyId, customerId])
@@index([customerId])
}
// ─────────────────────────────────────────────────────────────────────────
// Settings — replaces legacy `config_static`, a key/value table the admin
// panel read and wrote through endpoints like /wallet/config and
+66
View File
@@ -248,6 +248,22 @@ exports.Prisma.CustomerScalarFieldEnum = {
updatedAt: 'updatedAt'
};
exports.Prisma.CustomerCartScalarFieldEnum = {
customerId: 'customerId',
items: 'items',
reminderSentForClearAt: 'reminderSentForClearAt',
clearedAt: 'clearedAt',
updatedAt: 'updatedAt',
createdAt: 'createdAt'
};
exports.Prisma.CustomerBuyLaterScalarFieldEnum = {
customerId: 'customerId',
items: 'items',
updatedAt: 'updatedAt',
createdAt: 'createdAt'
};
exports.Prisma.CustomerFavoriteScalarFieldEnum = {
customerId: 'customerId',
productId: 'productId',
@@ -318,11 +334,18 @@ exports.Prisma.ProductScalarFieldEnum = {
showType: 'showType',
isActive: 'isActive',
createdAt: 'createdAt',
tags: 'tags',
accountingStockWeight: 'accountingStockWeight',
accountingStockCount: 'accountingStockCount',
accountingStockAt: 'accountingStockAt'
};
exports.Prisma.ProductViewScalarFieldEnum = {
id: 'id',
productId: 'productId',
createdAt: 'createdAt'
};
exports.Prisma.ProductImageScalarFieldEnum = {
id: 'id',
productId: 'productId',
@@ -484,6 +507,31 @@ exports.Prisma.CommentScalarFieldEnum = {
updatedAt: 'updatedAt'
};
exports.Prisma.ProductSurveyScalarFieldEnum = {
id: 'id',
title: 'title',
imageUrl: 'imageUrl',
suggestedWeightGrams: 'suggestedWeightGrams',
status: 'status',
createdByCustomerId: 'createdByCustomerId',
createdByAdminId: 'createdByAdminId',
publishedAt: 'publishedAt',
decidedAt: 'decidedAt',
decidedByAdminId: 'decidedByAdminId',
decisionNote: 'decisionNote',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.ProductSurveyVoteScalarFieldEnum = {
id: 'id',
surveyId: 'surveyId',
customerId: 'customerId',
value: 'value',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.SettingScalarFieldEnum = {
key: 'key',
value: 'value',
@@ -753,6 +801,19 @@ exports.CommentStatus = exports.$Enums.CommentStatus = {
REJECTED: 'REJECTED'
};
exports.ProductSurveyStatus = exports.$Enums.ProductSurveyStatus = {
PENDING_REVIEW: 'PENDING_REVIEW',
OPEN: 'OPEN',
CLOSED: 'CLOSED',
APPROVED: 'APPROVED',
REJECTED: 'REJECTED'
};
exports.ProductSurveyVoteValue = exports.$Enums.ProductSurveyVoteValue = {
LIKE: 'LIKE',
DISLIKE: 'DISLIKE'
};
exports.AccountingEntityType = exports.$Enums.AccountingEntityType = {
CUSTOMER: 'CUSTOMER',
PRODUCT: 'PRODUCT'
@@ -803,6 +864,8 @@ exports.Prisma.ModelName = {
AdminSession: 'AdminSession',
SalesExpertProfile: 'SalesExpertProfile',
Customer: 'Customer',
CustomerCart: 'CustomerCart',
CustomerBuyLater: 'CustomerBuyLater',
CustomerFavorite: 'CustomerFavorite',
CustomerAddress: 'CustomerAddress',
Branch: 'Branch',
@@ -812,6 +875,7 @@ exports.Prisma.ModelName = {
Collection: 'Collection',
CollectionProduct: 'CollectionProduct',
Product: 'Product',
ProductView: 'ProductView',
ProductImage: 'ProductImage',
ProductVariant: 'ProductVariant',
Order: 'Order',
@@ -825,6 +889,8 @@ exports.Prisma.ModelName = {
UserType: 'UserType',
Permission: 'Permission',
Comment: 'Comment',
ProductSurvey: 'ProductSurvey',
ProductSurveyVote: 'ProductSurveyVote',
Setting: 'Setting',
AccountingLink: 'AccountingLink',
AccountingOutbox: 'AccountingOutbox',