build: rebuild API release bundle for the admin/expert table split

app.js, seed.js, the Prisma client in release/vendor and the new migration
20261026120000_split_admin_table, built with `pnpm build:release` (API only;
the admin panel and user apps are unchanged by this work).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-10-25 23:55:07 +03:30
co-authored by Claude Sonnet 5.5
parent 06c8a5f434
commit e2ec30f241
10 changed files with 5403 additions and 3445 deletions
+92 -48
View File
@@ -140,6 +140,61 @@ model City {
@@index([parentId])
}
/// A staff account for the ADMIN PANEL — and nothing else.
///
/// Split out of AdminUser (which now holds sales experts only) so that:
/// * the same mobile can be an admin AND a sales expert — they are two
/// separate roles, each unique only within its own table;
/// * the expert table (production: ~12,000 rows) is never written by panel
/// staff management.
/// Migration 20261026120000_split_admin_table copies the existing non-expert
/// AdminUser rows here WITH THEIR IDS, so issued panel tokens and every
/// "who did this" column keep resolving. The old rows are left in AdminUser,
/// dormant; no code reads them as admins any more.
///
/// ⚠️ fullName/firstName/lastName are kept in sync by the DB trigger
/// `admin_sync_full_name_trg` (it shares the function admin_user_sync_full_name) — same semantics as AdminUser: the parts win.
model Admin {
id String @id @default(cuid())
fullName String
firstName String?
lastName String?
mobile String @unique
passwordHash String
/// Never SALES_EXPERT — that role lives in AdminUser. Enforced in
/// AdminUsersService, not in the database, so the enum stays shared.
role AdminRole
userTypeId Int?
userType UserType? @relation(fields: [userTypeId], references: [id])
branchId String?
branch Branch? @relation(fields: [branchId], references: [id])
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")
/// Issued admin-panel tokens, recorded only while session tracking is on
/// (AUTH_SESSION_TRACKING, or implicitly whenever the IAM is enabled) so a
/// token can be revoked before it expires. See AdminSession.
sessions AdminSession[]
@@index([role])
@@index([userTypeId])
@@index([firstName])
@@index([lastName])
}
/// A SALES EXPERT (role SALES_EXPERT). Panel staff live in `Admin`; rows here with
/// any other role are pre-split leftovers and are ignored by the code.
model AdminUser {
id String @id @default(cuid())
@@ -185,26 +240,13 @@ model AdminUser {
pasazhName String? // legacy pasazh_name
memberCode String? // legacy mcode
referredCustomers Customer[] @relation("ReferralAgent")
referredCustomers Customer[] @relation("ReferralAgent")
/// Orders credited to this sales expert — see Order.salesAgentId.
creditedOrders Order[] @relation("OrderSalesAgent")
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")
creditedOrders Order[] @relation("OrderSalesAgent")
/// Only ever set for role = SALES_EXPERT (D17).
salesExpertProfile SalesExpertProfile?
/// Issued admin-panel tokens, recorded only while session tracking is on
/// (AUTH_SESSION_TRACKING, or implicitly whenever the IAM is enabled) so a
/// token can be revoked before it expires. See AdminSession.
sessions AdminSession[]
@@index([role])
@@index([userTypeId])
@@index([avatarFileId])
@@ -274,8 +316,8 @@ model AdminSession {
/// The token's `jti` claim. Unique: one row per issued token.
jti String @unique
adminUserId String
adminUser AdminUser @relation(fields: [adminUserId], references: [id], onDelete: Cascade)
adminId String @map("adminUserId")
admin Admin @relation(fields: [adminId], references: [id], onDelete: Cascade)
/// Set when the login was verified by the IAM: its session id (`sid`) and the
/// stable user id (`sub`) there. Back-channel logout arrives keyed by these —
@@ -294,7 +336,7 @@ model AdminSession {
revokedAt DateTime?
revokedReason String?
@@index([adminUserId])
@@index([adminId], map: "AdminSession_adminUserId_idx")
@@index([iamSessionId])
@@index([iamSub])
@@index([expiresAt])
@@ -571,6 +613,7 @@ model Branch {
cityId String?
city City? @relation(fields: [cityId], references: [id])
adminUsers AdminUser[]
admins Admin[]
orders Order[]
allocations BranchAllocation[]
}
@@ -583,7 +626,7 @@ model Workshop {
phone String?
/// The supervisor created alongside the workshop by the legacy Add form.
supervisorId String?
supervisor AdminUser? @relation("WorkshopSupervisor", fields: [supervisorId], references: [id])
supervisor Admin? @relation("WorkshopSupervisor", fields: [supervisorId], references: [id])
isActive Boolean @default(true)
preOrders PreOrder[]
}
@@ -826,15 +869,15 @@ model OrderItem {
}
model OrderStatusEvent {
id String @id @default(cuid())
orderId String
order Order @relation(fields: [orderId], references: [id])
fromStatus OrderStatus?
toStatus OrderStatus
note String?
actorAdminUserId String?
actorAdminUser AdminUser? @relation(fields: [actorAdminUserId], references: [id])
createdAt DateTime @default(now())
id String @id @default(cuid())
orderId String
order Order @relation(fields: [orderId], references: [id])
fromStatus OrderStatus?
toStatus OrderStatus
note String?
actorAdminId String? @map("actorAdminUserId")
actorAdmin Admin? @relation(fields: [actorAdminId], references: [id])
createdAt DateTime @default(now())
@@index([orderId])
}
@@ -875,15 +918,15 @@ model PreOrder {
}
model PreOrderStatusEvent {
id String @id @default(cuid())
preOrderId String
preOrder PreOrder @relation(fields: [preOrderId], references: [id])
id String @id @default(cuid())
preOrderId String
preOrder PreOrder @relation(fields: [preOrderId], references: [id])
/// Field-level diff, e.g. {"status": {"old": "PLACED", "new": "ADMIN_APPROVED"}}
changes Json
actorAdminUserId String?
actorAdminUser AdminUser? @relation(fields: [actorAdminUserId], references: [id])
ipAddress String?
createdAt DateTime @default(now())
changes Json
actorAdminId String? @map("actorAdminUserId")
actorAdmin Admin? @relation(fields: [actorAdminId], references: [id])
ipAddress String?
createdAt DateTime @default(now())
@@index([preOrderId])
}
@@ -912,10 +955,10 @@ model LedgerEntry {
order Order? @relation(fields: [orderId], references: [id])
/// Pending entries don't count toward the cached customer balance until confirmed.
confirmed Boolean @default(false)
confirmedAt DateTime?
postedByAdminUserId String?
postedByAdminUser AdminUser? @relation(fields: [postedByAdminUserId], references: [id])
confirmed Boolean @default(false)
confirmedAt DateTime?
postedByAdminId String? @map("postedByAdminUserId")
postedByAdmin Admin? @relation(fields: [postedByAdminId], 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
@@ -997,6 +1040,7 @@ model UserType {
permissions Permission[]
adminUsers AdminUser[]
admins Admin[]
@@map("user_type")
}
@@ -1251,8 +1295,8 @@ model BranchAllocation {
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])
allocatorAdminId String? @map("allocatorAdminUserId")
allocatorAdmin Admin? @relation("AllocationAuthor", fields: [allocatorAdminId], references: [id])
note String?
createdAt DateTime @default(now())
@@ -1299,8 +1343,8 @@ model SmsMessage {
/// How many numbers the send actually resolved to.
sentCount Int @default(0)
sentByAdminUserId String?
sentByAdminUser AdminUser? @relation("SmsAuthor", fields: [sentByAdminUserId], references: [id])
sentByAdminId String? @map("sentByAdminUserId")
sentByAdmin Admin? @relation("SmsAuthor", fields: [sentByAdminId], references: [id])
createdAt DateTime @default(now())
@@ -1313,8 +1357,8 @@ model Notification {
body String
audience MessageAudience @default(ALL_CUSTOMERS)
sentByAdminUserId String?
sentByAdminUser AdminUser? @relation("NotificationAuthor", fields: [sentByAdminUserId], references: [id])
sentByAdminId String? @map("sentByAdminUserId")
sentByAdmin Admin? @relation("NotificationAuthor", fields: [sentByAdminId], references: [id])
createdAt DateTime @default(now())
@@ -1379,8 +1423,8 @@ model UploadedFile {
/// 'customer.nationalId'. Kept loose so a new form needs no schema change.
purpose String?
uploadedByAdminUserId String?
uploadedByAdminUser AdminUser? @relation("FileUploader", fields: [uploadedByAdminUserId], references: [id])
uploadedByAdminId String? @map("uploadedByAdminUserId")
uploadedByAdmin Admin? @relation("FileUploader", fields: [uploadedByAdminId], references: [id])
createdAt DateTime @default(now())
customerAvatars Customer[] @relation("CustomerAvatar")