feat: product design surveys, cart expiry/buy-later, and catalog polish

Add like/dislike surveys with majority alerts for manufacturing decisions, clearer accounting mapping copy, and related cart/catalog admin-app improvements.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
mahmood
2026-11-12 17:55:52 +03:30
co-authored by Cursor
parent 032fa800d9
commit 7dd74ff4f7
74 changed files with 6351 additions and 893 deletions
+121
View File
@@ -181,6 +181,8 @@ model Admin {
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
@@ -564,12 +566,51 @@ model Customer {
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