fix update build

This commit is contained in:
Montech
2026-09-15 15:41:47 +03:30
parent 8fac6a3f36
commit a7e13fe64e
9 changed files with 45561 additions and 637 deletions
+15802 -407
View File
File diff suppressed because it is too large Load Diff
+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",
+469 -37
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
@@ -79,13 +97,17 @@ enum LedgerSourceType {
// ─────────────────────────────────────────────────────────────────────────
model City {
id String @id @default(cuid())
name String
province String?
customers Customer[]
addresses CustomerAddress[]
branches Branch[]
preOrders PreOrder[]
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[]
preOrders PreOrder[]
}
model AdminUser {
@@ -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,15 +178,33 @@ model Customer {
businessLicenseUrl String?
shopImageUrl String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
/// 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
addresses CustomerAddress[]
orders Order[]
preOrders PreOrder[]
ledgerEntries LedgerEntry[]
walletBonuses WalletBonus[]
comments Comment[]
promotions Promotion[]
notifications NotificationRecipient[]
activityLogs CustomerActivityLog[]
@@index([referredByAgentId])
}
@@ -155,19 +220,31 @@ model CustomerAddress {
}
model Branch {
id String @id @default(cuid())
name String
cityId String?
city City? @relation(fields: [cityId], references: [id])
adminUsers AdminUser[]
orders Order[]
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
isActive Boolean @default(true)
preOrders PreOrder[]
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[]
}
// ─────────────────────────────────────────────────────────────────────────
@@ -175,18 +252,29 @@ model Workshop {
// ─────────────────────────────────────────────────────────────────────────
model Category {
id String @id @default(cuid())
title String
parentId String?
parent Category? @relation("CategoryTree", fields: [parentId], references: [id])
children Category[] @relation("CategoryTree")
products Product[]
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[]
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[]
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[]
@@ -252,13 +353,15 @@ model ProductVariant {
// ─────────────────────────────────────────────────────────────────────────
model Order {
id String @id @default(cuid())
orderNumber String @unique
id String @id @default(cuid())
orderNumber String @unique
customerId String
customer Customer @relation(fields: [customerId], references: [id])
customer Customer @relation(fields: [customerId], references: [id])
branchId String?
branch Branch? @relation(fields: [branchId], references: [id])
status OrderStatus @default(PLACED)
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).
totalGoldWeightGrams Decimal @db.Decimal(14, 4)
@@ -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'
};
/**