Files
CRM_Montzar/apps/api/prisma/schema.prisma
T
Mahdi ade43fea0f feat(tahesab): sales report snapshot with org-module gate
Sync linked asnad into CRM, filter by group/kind/metal, and require the tahesab superadmin module plus reports role access.
2026-10-22 18:20:46 +03:30

3649 lines
140 KiB
Plaintext

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum UserRole {
owner
admin
sales
support
}
enum DealStatus {
open
won
lost
}
enum ActivityType {
call
meeting
task
note
}
enum ActivityStatus {
open
done
canceled
}
enum ActivityPriority {
low
medium
high
very_low
very_high
}
enum PersonMood {
newcomer
happy
unhappy
special
}
enum CustomFieldEntity {
person
company
deal
}
enum CustomFieldType {
text
number
date
boolean
select
}
enum QuoteStatus {
draft
sent
accepted
rejected
expired
}
enum InvoiceStatus {
draft
issued
partially_paid
paid
canceled
}
enum PaymentMethod {
cash
transfer
card
check
other
}
enum InstallmentStatus {
pending
paid
canceled
}
enum TicketStatus {
open
in_progress
waiting
resolved
closed
}
enum TicketPriority {
low
normal
high
urgent
}
enum AutomationTrigger {
person_created
lead_submitted
deal_created
deal_stage_changed
deal_won
deal_lost
ticket_created
ticket_resolved
invoice_issued
invoice_paid
activity_done
sms_link_clicked
}
enum AutomationAction {
create_activity
create_notification
add_loyalty_points
}
enum NoteAiSuggestionStatus {
pending
applied
dismissed
failed
}
enum MemorySourceType {
note
manual
}
model Organization {
id String @id @default(cuid())
name String
slug String @unique
/// زمینه فعالیت سازمان — فهرست هم‌تراز صنایع دیدار
businessField String?
baseCurrency String @default("IRR")
enabledCurrencies String[] @default(["IRR", "USD", "EUR"])
/// ماژول‌های فعال منو برای این محیط
enabledModules String[] @default([])
/// تاریخ قطع بایگانی اطلاعات قبل (شروع روز انتخاب‌شده)؛ null = بدون فیلتر
priorArchiveEnabledAt DateTime?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
users User[]
memberships Membership[]
orgRoles OrgRole[]
teams Team[]
companies Company[]
customerGroups CustomerGroup[]
people Person[]
attendanceEvents AttendanceEvent[]
discAssessments DiscAssessment[]
salesCopilotSessions SalesCopilotSession[]
pipelines Pipeline[]
deals Deal[]
activities Activity[]
notes Note[]
noteRevisions NoteRevision[]
customFields CustomFieldDefinition[]
products Product[]
notifications Notification[]
noteAttachments NoteAttachment[]
leadForms LeadForm[]
quotes Quote[]
invoices Invoice[]
salesGoals SalesGoal[]
exchangeRates ExchangeRate[]
tickets Ticket[]
faqArticles FaqArticle[]
portalOtps PortalOtp[]
projectBoards ProjectBoard[]
projectCards ProjectCard[]
loyaltyTiers LoyaltyTier[]
loyaltySettings LoyaltySettings?
loyaltyLedgers LoyaltyLedger[]
loyaltyRewards LoyaltyReward[]
loyaltyRedemptions LoyaltyRedemption[]
automationRules AutomationRule[]
messageAutomationRules MessageAutomationRule[]
messageAutomationJobs MessageAutomationJob[]
memoryItems MemoryItem[]
noteAiSuggestions NoteAiSuggestion[]
accountingConnections AccountingConnection[]
accountingSyncJobs AccountingSyncJob[]
accountingAsnadLines AccountingAsnadLine[]
warehouses Warehouse[]
stockMovements StockMovement[]
partyOpenings PartyOpeningBalance[]
commissionEntries CommissionEntry[]
cheques Cheque[]
auditLogs AuditLog[]
savedViews SavedView[]
apiKeys OrganizationApiKey[]
webhookEndpoints WebhookEndpoint[]
smsCampaigns SmsCampaign[]
outboundSmsLogs OutboundSmsLog[]
smsAccount SmsAccount?
financialAccounts FinancialAccount[]
financialCategories FinancialCategory[]
financialDocuments FinancialDocument[]
financialMovements FinancialMovement[]
financialReceivables FinancialReceivable[]
financialPayrolls FinancialPayroll[]
financialRecurringItems FinancialRecurringItem[]
financialReconciliations FinancialReconciliation[]
financialAuditLogs FinancialAuditLog[]
financialAttachments FinancialAttachment[]
wooCommerceConnections WooCommerceConnection[]
montifyConnections MontifyConnection[]
voipConnections VoipConnection[]
reservationServices ReservationService[]
reservationAppointments ReservationAppointment[]
leads Lead[]
leadStages LeadStage[]
activityBoards ActivityBoard[]
activityStages ActivityStage[]
activityTypeDefs ActivityTypeDef[]
leadEvents LeadEvent[]
leadImportJobs LeadImportJob[]
conversationSettings ConversationSettings?
conversationRooms ConversationRoom[]
conversationHiddenMessages ConversationHiddenMessage[]
conversationPinnedMessages ConversationPinnedMessage[]
conversationRoomUserPins ConversationRoomUserPin[]
conversationRoomMembers ConversationRoomMember[]
conversationRoomUserMutes ConversationRoomUserMute[]
conversationMessageReactions ConversationMessageReaction[]
trackingLinks TrackingLink[]
trackingEvents TrackingEvent[]
routineCategories RoutineCategory[]
routines Routine[]
routineTasks RoutineTask[]
routineTaskEvents RoutineTaskEvent[]
routineUserSettings RoutineUserSettings[]
gamificationSettings GamificationSettings?
gamificationRules GamificationRule[]
gamificationTransactions GamificationTransaction[]
gamificationWallets GamificationWallet[]
gamificationRewards GamificationReward[]
gamificationRedemptions GamificationRedemption[]
gamificationBadges GamificationBadge[]
gamificationUserBadges GamificationUserBadge[]
gamificationBonuses GamificationBonus[]
/// month | two_months | three_months | six_months | year | never
deletionRetention String @default("never")
deletionArchives DeletionArchive[]
}
model User {
id String @id @default(cuid())
email String
/// موبایل نرمال‌شده (مثلاً 9121234567) — برای ورود با موبایل
mobile String?
passwordHash String
fullName String
role UserRole @default(sales)
/// مسیر فایل آواتار در فضای آپلود
avatarStorageKey String?
isActive Boolean @default(true)
lastLoginAt DateTime?
orgRoleId String?
orgRole OrgRole? @relation(fields: [orgRoleId], references: [id], onDelete: SetNull)
emailVerifiedAt DateTime?
mobileVerifiedAt DateTime?
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
ownedDeals Deal[] @relation("DealOwner")
assignedDeals Deal[] @relation("DealAssignees")
createdDeals Deal[] @relation("DealCreator")
ownedQuotes Quote[] @relation("QuoteOwner")
ownedInvoices Invoice[] @relation("InvoiceOwner")
ownedPeople Person[] @relation("PersonOwner")
ownedCompanies Company[] @relation("CompanyOwner")
ownedProjectCards ProjectCard[] @relation("ProjectCardOwner")
activities Activity[] @relation("ActivityAssignee")
assignedActivities Activity[] @relation("ActivityAssignees")
createdActivities Activity[] @relation("ActivityCreator")
notes Note[] @relation("NoteAuthor")
noteRevisions NoteRevision[] @relation("NoteRevisionEditor")
noteAttachments NoteAttachment[] @relation("NoteAttachmentUploader")
notifications Notification[]
salesGoals SalesGoal[] @relation("SalesGoalUser")
commissionPercent Decimal @default(0) @db.Decimal(8, 2)
commissionEntries CommissionEntry[]
receivedCheques Cheque[] @relation("ChequeOwner")
assignedTickets Ticket[] @relation("TicketAssignee")
reportedTickets Ticket[] @relation("TicketReporter")
ticketMessages TicketMessage[] @relation("TicketMessageAuthor")
confirmedMemories MemoryItem[] @relation("MemoryConfirmedBy")
memberships Membership[]
billingAccount BillingAccount?
auditLogs AuditLog[] @relation("AuditLogActor")
savedViews SavedView[] @relation("SavedViewOwner")
tablePreferences UserTablePreference[]
assignedProjectCards ProjectCard[] @relation("ProjectCardAssignees")
teamMemberships TeamMember[]
reservationAssignments ReservationAppointment[] @relation("ReservationAssignee")
assignedLeads Lead[] @relation("LeadAssignee")
createdLeads Lead[] @relation("LeadCreator")
assignedRoutines Routine[] @relation("RoutineAssignee")
createdRoutines Routine[] @relation("RoutineCreator")
routineTasks RoutineTask[] @relation("RoutineTaskAssignee")
createdRoutineTasks RoutineTask[] @relation("RoutineTaskCreator")
routineTaskEvents RoutineTaskEvent[]
routineSettings RoutineUserSettings[]
attendanceEvents AttendanceEvent[]
deletionArchives DeletionArchive[] @relation("DeletionActor")
discAssessments DiscAssessment[] @relation("DiscAssessor")
salesCopilotSessions SalesCopilotSession[] @relation("CopilotAuthor")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, email])
@@unique([mobile])
@@index([organizationId])
@@index([email])
@@index([orgRoleId])
}
model OrgRole {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
description String @default("")
color String @default("teal")
permissions String[] @default([])
/// عمق داده به‌ازای بخش، مثلاً {"customers":{"mode":"rules","rules":[{"owners":["self"]}]}}
dataAccessPolicy Json @default("{}")
/// owner | admin | sales | support — نقش‌های سیستمی قابل حذف نیستند
systemKey String?
/// نقش والد برای سلسله‌مراتب زیرنقش‌ها
parentId String?
parent OrgRole? @relation("OrgRoleTree", fields: [parentId], references: [id], onDelete: Restrict)
children OrgRole[] @relation("OrgRoleTree")
users User[]
memberships Membership[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, name])
@@index([organizationId])
@@index([parentId])
@@index([organizationId, parentId])
}
model Membership {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
role UserRole @default(sales)
orgRoleId String?
orgRole OrgRole? @relation(fields: [orgRoleId], references: [id], onDelete: SetNull)
isActive Boolean @default(true)
commissionPercent Decimal @default(0) @db.Decimal(8, 2)
/// own | team | org — سطح دید داده‌ها برای نقش‌های غیرمالک/مدیر
dataVisibility String @default("own")
/// سطح دید تفکیکی هر بخش، مثلاً {"contacts":"org","deals":"own"}؛ بخش‌های بدون مقدار = dataVisibility
dataVisibilityScopes Json @default("{}")
/// Override عمق داده نسبت به نقش؛ کلید غایب = ارث از OrgRole.dataAccessPolicy
dataAccessOverride Json @default("{}")
/// سوئیچ «بایگانی اطلاعات قبل» برای این عضو؛ فقط وقتی ماژول محیط روشن است اثر دارد
priorArchiveEnabled Boolean @default(false)
/// current | archive
priorArchiveLens String @default("current")
/// ساعت خروج روزانه به وقت تهران، مثل 18:00؛ خالی یعنی بدون محدودیت
logoutTime String?
/// بعد از ساعت خروج تا پایان روز (تهران) اجازهٔ ورود دوباره به این محیط نیست
blockLoginAfterLogout Boolean @default(false)
/// ورود به این محیط فقط با OTP؛ رمز عبور برای این عضویت کار نمی‌کند
loginOtpOnly Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, organizationId])
@@index([organizationId, role])
@@index([userId])
@@index([orgRoleId])
}
/// گروه کاری داخل سازمان برای دیدپذیری مشترک
model Team {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
description String @default("")
members TeamMember[]
routines Routine[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, name])
@@index([organizationId])
}
model TeamMember {
id String @id @default(cuid())
teamId String
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
/// member | manager
role String @default("member")
createdAt DateTime @default(now())
@@unique([teamId, userId])
@@index([userId])
@@index([teamId])
}
/// حساب سوپرادمین پلتفرم — جدا از User سازمانی
model PlatformAdmin {
id String @id @default(cuid())
/// ورود اصلی با موبایل نرمال‌شده (مثلاً 0912…)
mobile String? @unique
email String? @unique
passwordHash String
fullName String
isActive Boolean @default(true)
lastLoginAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
/// اشتراک SaaS روی مالک حساب (نه روی هر عضویت)
model BillingAccount {
id String @id @default(cuid())
ownerUserId String @unique
owner User @relation(fields: [ownerUserId], references: [id], onDelete: Cascade)
/// trial | monthly | quarterly | semiannual | yearly | lifetime | custom
planCode String @default("trial")
/// active | expired | suspended
status String @default("active")
startsAt DateTime @default(now())
expiresAt DateTime
maxOrganizations Int @default(1)
/// رزرو برای درگاه/لندینگ بعدی
externalPaymentRef String?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([status])
@@index([expiresAt])
}
model Company {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
/// کد داخلی شرکت (مثل «کد شرکت» در دیدار)
code String?
/// منبع ورود: manual | didar | tahesab | excel | other
importSource String @default("manual")
/// شناسه در مبدأ (مثلاً کد دیدار شرکت)
sourceRef String?
nationalId String?
economicCode String?
registrationNumber String?
phone String?
otherPhones String?
fax String?
email String?
otherEmails String?
website String?
province String?
city String?
postalCode String?
address String?
/// قدیمی؛ از نسخهٔ گروه‌بندی، گروه‌ها در CompanyGroupLink هستند
groupName String?
notes String?
registeredAt DateTime?
tags String[] @default([])
customFields Json @default("{}")
groupLinks CompanyGroupLink[]
people Person[]
deals Deal[]
activities Activity[]
smartNotes Note[]
quotes Quote[]
invoices Invoice[]
ownerId String?
owner User? @relation("CompanyOwner", fields: [ownerId], references: [id], onDelete: SetNull)
tickets Ticket[]
projectCards ProjectCard[]
routineTasks RoutineTask[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId])
@@index([organizationId, name])
@@index([organizationId, email])
@@index([organizationId, phone])
@@index([organizationId, sourceRef])
@@index([organizationId, code])
@@index([ownerId])
}
/// گروه مشتری (مشترک بین اشخاص و شرکت‌ها؛ هر مشتری چند گروه)
model CustomerGroup {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
color String @default("teal")
position Int @default(0)
/// گروه متناظر در ته‌حساب (GID) روی همان اتصال
tahesabConnectionId String?
tahesabGid String?
people PersonGroupLink[]
companies CompanyGroupLink[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, name])
@@index([organizationId, position])
@@index([tahesabConnectionId, tahesabGid])
}
model PersonGroupLink {
personId String
person Person @relation(fields: [personId], references: [id], onDelete: Cascade)
groupId String
group CustomerGroup @relation(fields: [groupId], references: [id], onDelete: Cascade)
/// manual | tahesab — گروهِ ته‌حسابی با همگام‌سازی جایگزین می‌شود؛ دستی دست نمی‌خورد
source String @default("manual")
createdAt DateTime @default(now())
@@id([personId, groupId])
@@index([groupId])
}
model CompanyGroupLink {
companyId String
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
groupId String
group CustomerGroup @relation(fields: [groupId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@id([companyId, groupId])
@@index([groupId])
}
model Person {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
companyId String?
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
firstName String
lastName String
/// نام قبل از همگام‌سازی مبدأ (مثلاً ته‌حساب)
previousFirstName String?
previousLastName String?
/// منبع ورود: manual | tahesab | didar | excel | phone | lead_form | other
importSource String @default("manual")
/// شناسه در مبدأ (کد دیدار، کد مشتری ته‌حساب، …)
sourceRef String?
/// منبع و کد قبلی وقتی شخص از منبع دیگری دوباره وارد شده (مثلاً دیدار قبل از ته‌حساب)
previousImportSource String?
previousSourceRef String?
email String?
phone String?
mobile String?
position String?
/// آدرس وب‌سایت
website String?
/// پیج اینستاگرام
instagram String?
/// تاریخ تولد
birthDate DateTime?
/// نام معرف
referrer String?
/// لیست آدرس‌ها (JSON array of strings)
addresses Json @default("[]")
notes String?
mood PersonMood @default(newcomer)
tags String[] @default([])
customFields Json @default("{}")
groupLinks PersonGroupLink[]
loyaltyPoints Int @default(0)
loyaltyTierId String?
loyaltyTier LoyaltyTier? @relation(fields: [loyaltyTierId], references: [id], onDelete: SetNull)
loyaltyLedgers LoyaltyLedger[]
loyaltyRedemptions LoyaltyRedemption[]
deals Deal[]
activities Activity[]
smartNotes Note[]
quotes Quote[]
invoices Invoice[]
reservationAppointments ReservationAppointment[]
ownerId String?
owner User? @relation("PersonOwner", fields: [ownerId], references: [id], onDelete: SetNull)
tickets Ticket[]
projectCards ProjectCard[]
memoryItems MemoryItem[]
accountingLinks PersonAccountingLink[]
accountingDocuments AccountingDocumentLog[]
asnadLines AccountingAsnadLine[]
openingBalances PartyOpeningBalance[]
cheques Cheque[]
trackingLinks TrackingLink[]
trackingEvents TrackingEvent[]
routineTasks RoutineTask[]
outboundSmsLogs OutboundSmsLog[]
financialDocuments FinancialDocument[]
discLabel String?
discReliable Boolean @default(false)
discAssessedAt DateTime?
discAssessments DiscAssessment[]
salesCopilotSessions SalesCopilotSession[]
financialReceivables FinancialReceivable[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId])
@@index([organizationId, email])
@@index([organizationId, mobile])
@@index([organizationId, lastName, firstName])
@@index([organizationId, importSource])
@@index([organizationId, sourceRef])
@@index([organizationId, previousSourceRef])
@@index([organizationId, loyaltyPoints])
@@index([companyId])
@@index([ownerId])
@@index([loyaltyTierId])
@@index([organizationId, discLabel])
}
model Pipeline {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
isDefault Boolean @default(false)
stages PipelineStage[]
deals Deal[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId])
@@index([organizationId, isDefault])
}
model PipelineStage {
id String @id @default(cuid())
pipelineId String
pipeline Pipeline @relation(fields: [pipelineId], references: [id], onDelete: Cascade)
name String
position Int
isWon Boolean @default(false)
isLost Boolean @default(false)
deals Deal[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([pipelineId, position])
}
model Deal {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
pipelineId String
pipeline Pipeline @relation(fields: [pipelineId], references: [id], onDelete: Cascade)
stageId String
stage PipelineStage @relation(fields: [stageId], references: [id], onDelete: Restrict)
title String
/// کد دستی معامله
code String?
/// منبع ورود: manual | didar | excel
importSource String @default("manual")
/// اگر پر باشد، رکورد از بایگانی به کار جاری منتقل شده (فقط برچسب؛ بقیه فیلدها دست‌نخورده)
promotedFromPriorAt DateTime?
/// شناسه در مبدأ (کد دیدار معامله)
sourceRef String?
amount Decimal? @db.Decimal(18, 2)
currency String @default("IRR")
status DealStatus @default(open)
/// احتمال موفقیت (۰ تا ۱۰۰)
probability Int?
/// شیوه آشنایی
source String?
/// شیوه آشنایی دلخواه
sourceDetail String?
referrer String?
lostReason String?
lostNote String?
/// تاریخ انجام (موفق/ناموفق شدن)
closedAt DateTime?
notes String?
tags String[] @default([])
isStale Boolean @default(false)
isRotten Boolean @default(false)
customFields Json @default("{}")
expectedCloseAt DateTime?
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
salesCopilotSessions SalesCopilotSession[]
companyId String?
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
ownerId String?
owner User? @relation("DealOwner", fields: [ownerId], references: [id], onDelete: SetNull)
createdById String?
createdBy User? @relation("DealCreator", fields: [createdById], references: [id], onDelete: SetNull)
assignees User[] @relation("DealAssignees")
position Int @default(0)
activities Activity[]
smartNotes Note[]
lineItems DealLineItem[]
quotes Quote[]
invoices Invoice[]
tickets Ticket[]
projectCards ProjectCard[]
routineTasks RoutineTask[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId])
@@index([pipelineId, stageId])
@@index([stageId, position])
@@index([personId])
@@index([companyId])
@@index([ownerId])
@@index([organizationId, sourceRef])
@@index([organizationId, createdAt])
}
/// مراحل/ستون‌های فعالیت (تب و کانبان) — kind وضعیت فعالیت داخل این مرحله را تعیین می‌کند
/// گروه فعالیت (مثل مسیر فروش برای معاملات): هر گروه ستون‌های خودش را دارد
model ActivityBoard {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
isDefault Boolean @default(false)
position Int @default(0)
stages ActivityStage[]
activities Activity[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, name])
@@index([organizationId, position])
}
model ActivityStage {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
boardId String
board ActivityBoard @relation(fields: [boardId], references: [id], onDelete: Cascade)
name String
position Int @default(0)
color String @default("green")
kind ActivityStatus @default(open)
activities Activity[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([boardId, name])
@@index([organizationId, position])
@@index([boardId, position])
}
/// نوع فعالیت قابل تعریف توسط سازمان با فیلدهای اختصاصی
model ActivityTypeDef {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
/// نوع پایه — مبنای گزارش‌ها، یادآورها و سازگاری
baseType ActivityType @default(task)
icon String @default("check")
color String @default("green")
/// تعریف فیلدها: [{ key, label, type, options?, required?, placeholder? }]
fields Json @default("[]")
position Int @default(0)
isActive Boolean @default(true)
/// نوع اصلیِ هر baseType — حذف‌شدنی نیست
isSystem Boolean @default(false)
activities Activity[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, name])
@@index([organizationId, position])
}
model Activity {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
type ActivityType @default(task)
status ActivityStatus @default(open)
priority ActivityPriority @default(medium)
title String
/// manual | didar — سابقهٔ منتقل‌شده از اطلاعات قبل
importSource String @default("manual")
/// اگر پر باشد، رکورد از بایگانی به کار جاری منتقل شده (فقط برچسب؛ بقیه فیلدها دست‌نخورده)
promotedFromPriorAt DateTime?
description String?
dueAt DateTime?
durationMinutes Int?
reminderAt DateTime?
/// اعلان داخل پنل هنگام رسیدن یادآور
reminderNotifyApp Boolean @default(true)
/// پیامک به موبایل مسئول هنگام رسیدن یادآور
reminderNotifySms Boolean @default(false)
/// زمان ارسال یادآور؛ با تغییر reminderAt پاک می‌شود
reminderSentAt DateTime?
completedAt DateTime?
tags String[] @default([])
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
companyId String?
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
dealId String?
deal Deal? @relation(fields: [dealId], references: [id], onDelete: SetNull)
assigneeId String?
assignee User? @relation("ActivityAssignee", fields: [assigneeId], references: [id], onDelete: SetNull)
/// مسئولان مشترک (علاوه بر assigneeId به‌عنوان مسئول اصلی)
assignees User[] @relation("ActivityAssignees")
createdById String?
createdBy User? @relation("ActivityCreator", fields: [createdById], references: [id], onDelete: SetNull)
notes Note[]
routineTasks RoutineTask[]
/// گروه فعالیت؛ null = گروه پیش‌فرض محیط
boardId String?
board ActivityBoard? @relation(fields: [boardId], references: [id], onDelete: SetNull)
/// مرحله (ستون کانبان)؛ null یا مرحلهٔ ناهم‌نوع با status → اولین مرحلهٔ هم‌نوع
stageId String?
stage ActivityStage? @relation(fields: [stageId], references: [id], onDelete: SetNull)
/// نوع سفارشی؛ null → نوع اصلیِ همان type
typeDefId String?
typeDef ActivityTypeDef? @relation(fields: [typeDefId], references: [id], onDelete: SetNull)
/// مقادیر فیلدهای اختصاصی نوع (کلید فیلد → مقدار)
fields Json?
/// کاربران شرکت‌کننده؛ فعالیت در لیست آن‌ها هم دیده می‌شود
participantIds String[] @default([])
/// لید مرتبط (مثلاً فعالیت ساخته‌شده از ارسال فرم)
leadId String?
lead Lead? @relation(fields: [leadId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([stageId])
@@index([boardId])
@@index([typeDefId])
@@index([leadId])
@@index([organizationId, status, dueAt])
@@index([organizationId, importSource])
@@index([organizationId, createdAt])
@@index([organizationId, assigneeId])
@@index([organizationId, createdById])
@@index([reminderAt, reminderSentAt])
@@index([personId])
@@index([companyId])
@@index([dealId])
}
model Note {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
title String?
body String
noteType String @default("general")
pinned Boolean @default(false)
color String? @default("default")
tags String[] @default([])
reminderAt DateTime?
/// [{ id: string, text: string, done: boolean }]
checklist Json?
locationText String?
analyzeEnabled Boolean @default(true)
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
companyId String?
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
dealId String?
deal Deal? @relation(fields: [dealId], references: [id], onDelete: SetNull)
activityId String?
activity Activity? @relation(fields: [activityId], references: [id], onDelete: SetNull)
authorId String?
author User? @relation("NoteAuthor", fields: [authorId], references: [id], onDelete: SetNull)
/// team = قابل‌مشاهده برای تیم/سازمان در محدوده پرونده | personal = فقط نویسنده
visibility String @default("team")
attachments NoteAttachment[]
aiSuggestion NoteAiSuggestion?
revisions NoteRevision[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, pinned, updatedAt])
@@index([organizationId, noteType, updatedAt])
@@index([organizationId, reminderAt])
@@index([organizationId, visibility])
@@index([personId])
@@index([companyId])
@@index([dealId])
@@index([activityId])
@@index([authorId])
}
/// اسنپ‌شات متن قبل از هر ویرایش معنادار یادداشت
model NoteRevision {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
noteId String
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
editorId String?
editor User? @relation("NoteRevisionEditor", fields: [editorId], references: [id], onDelete: SetNull)
title String?
body String
noteType String
visibility String @default("team")
tags String[] @default([])
checklist Json?
createdAt DateTime @default(now())
@@index([noteId, createdAt])
@@index([organizationId])
@@index([editorId])
}
model NoteAttachment {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
noteId String
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
fileName String
mimeType String
sizeBytes Int
storageKey String
uploadedById String?
uploadedBy User? @relation("NoteAttachmentUploader", fields: [uploadedById], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
@@index([noteId, createdAt])
@@index([organizationId])
@@index([uploadedById])
}
model NoteAiSuggestion {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
noteId String @unique
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
status NoteAiSuggestionStatus @default(pending)
payload Json?
errorMessage String?
analyzedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, status])
}
model MemoryItem {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
personId String
person Person @relation(fields: [personId], references: [id], onDelete: Cascade)
content String
sourceType MemorySourceType @default(manual)
sourceId String?
confirmedById String?
confirmedBy User? @relation("MemoryConfirmedBy", fields: [confirmedById], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, personId, updatedAt])
@@index([personId])
@@index([confirmedById])
}
model CustomFieldDefinition {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
entityType CustomFieldEntity
key String
label String
fieldType CustomFieldType @default(text)
options String[] @default([])
required Boolean @default(false)
position Int @default(0)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, entityType, key])
@@index([organizationId, entityType, position])
}
model Product {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
/// product | service
kind String @default("product")
name String
sku String?
description String?
category String?
unit String? @default("عدد")
stockQty Int?
brand String?
model String?
country String?
tags String[] @default([])
unitPrice Decimal? @db.Decimal(18, 2)
currency String @default("IRR")
isActive Boolean @default(true)
lineItems DealLineItem[]
quoteLineItems QuoteLineItem[]
invoiceLineItems InvoiceLineItem[]
stockMovements StockMovement[]
accountingLinks ProductAccountingLink[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, name])
@@index([organizationId, isActive])
@@index([organizationId, category])
}
model DealLineItem {
id String @id @default(cuid())
dealId String
deal Deal @relation(fields: [dealId], references: [id], onDelete: Cascade)
productId String?
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
name String
quantity Decimal @default(1) @db.Decimal(18, 2)
unitPrice Decimal @db.Decimal(18, 2)
currency String @default("IRR")
position Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([dealId, position])
@@index([productId])
}
model Notification {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
type String
title String
body String?
link String?
entityType String?
entityId String?
readAt DateTime?
createdAt DateTime @default(now())
@@index([userId, readAt, createdAt])
@@index([organizationId, createdAt])
@@index([userId, type, entityId])
}
model LeadForm {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
description String?
publicToken String @unique @default(cuid())
isActive Boolean @default(true)
/// ثبت به‌عنوان Lead در CRM (مسیر اصلی انتشار فرم)
createLead Boolean @default(true)
createPerson Boolean @default(false)
createDeal Boolean @default(false)
pipelineId String?
/// مرحله اولیه لید (اختیاری؛ وگرنه اولین مرحله)
leadStageId String?
/// skip | update | activity
duplicateMode String @default("update")
source String?
thankYouMessage String?
redirectUrl String?
assigneeIds String[] @default([])
roundRobinIndex Int @default(0)
submitLabel String @default("ارسال درخواست")
submitColor String @default("#14b8a6")
coverStorageKey String?
viewCount Int @default(0)
startCount Int @default(0)
submitCount Int @default(0)
leadCreateCount Int @default(0)
duplicateCount Int @default(0)
fields LeadFormField[]
submissions LeadFormSubmission[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, updatedAt])
}
model LeadFormField {
id String @id @default(cuid())
formId String
form LeadForm @relation(fields: [formId], references: [id], onDelete: Cascade)
key String
label String
fieldType String @default("text")
required Boolean @default(false)
mapTo String
position Int @default(0)
placeholder String?
options Json?
@@unique([formId, key])
@@index([formId, position])
}
model LeadFormSubmission {
id String @id @default(cuid())
formId String
form LeadForm @relation(fields: [formId], references: [id], onDelete: Cascade)
values Json
personId String?
leadId String?
/// created | updated | duplicate_skipped | error
status String @default("created")
utm Json?
meta Json?
createdAt DateTime @default(now())
@@index([formId, createdAt])
@@index([leadId])
}
model Quote {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
number String
title String
status QuoteStatus @default(draft)
currency String @default("IRR")
notes String?
discountAmount Decimal @default(0) @db.Decimal(18, 2)
taxPercent Decimal @default(0) @db.Decimal(8, 2)
issuedAt DateTime?
validUntil DateTime?
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
companyId String?
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
dealId String?
deal Deal? @relation(fields: [dealId], references: [id], onDelete: SetNull)
ownerId String?
owner User? @relation("QuoteOwner", fields: [ownerId], references: [id], onDelete: SetNull)
lineItems QuoteLineItem[]
invoices Invoice[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, number])
@@index([organizationId, status, updatedAt])
@@index([personId])
@@index([companyId])
@@index([dealId])
@@index([ownerId])
}
model QuoteLineItem {
id String @id @default(cuid())
quoteId String
quote Quote @relation(fields: [quoteId], references: [id], onDelete: Cascade)
productId String?
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
name String
quantity Decimal @default(1) @db.Decimal(18, 2)
unitPrice Decimal @db.Decimal(18, 2)
currency String @default("IRR")
position Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([quoteId, position])
@@index([productId])
}
model Invoice {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
number String
title String
status InvoiceStatus @default(draft)
kind String @default("invoice") // invoice | credit
currency String @default("IRR")
notes String?
discountAmount Decimal @default(0) @db.Decimal(18, 2)
taxPercent Decimal @default(0) @db.Decimal(8, 2)
issuedAt DateTime?
dueDate DateTime?
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
companyId String?
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
dealId String?
deal Deal? @relation(fields: [dealId], references: [id], onDelete: SetNull)
quoteId String?
quote Quote? @relation(fields: [quoteId], references: [id], onDelete: SetNull)
ownerId String?
owner User? @relation("InvoiceOwner", fields: [ownerId], references: [id], onDelete: SetNull)
lineItems InvoiceLineItem[]
payments Payment[]
installments InvoiceInstallment[]
stockMovements StockMovement[]
commissionPercent Decimal? @db.Decimal(8, 2)
commissionFixedAmount Decimal? @db.Decimal(18, 2)
commissionEntry CommissionEntry?
cheques Cheque[]
routineTasks RoutineTask[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, number])
@@index([organizationId, status, updatedAt])
@@index([personId])
@@index([companyId])
@@index([dealId])
@@index([quoteId])
@@index([ownerId])
}
model InvoiceLineItem {
id String @id @default(cuid())
invoiceId String
invoice Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
productId String?
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
name String
quantity Decimal @default(1) @db.Decimal(18, 2)
unitPrice Decimal @db.Decimal(18, 2)
currency String @default("IRR")
position Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([invoiceId, position])
@@index([productId])
}
model Payment {
id String @id @default(cuid())
invoiceId String
invoice Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
amount Decimal @db.Decimal(18, 2)
currency String @default("IRR")
method PaymentMethod @default(transfer)
paidAt DateTime @default(now())
notes String?
/// اگر پر باشد، این پرداخت یک‌بار در صندوق نشسته است
financialAccountId String?
installmentId String? @unique
installment InvoiceInstallment? @relation(fields: [installmentId], references: [id], onDelete: SetNull)
cheque Cheque?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([invoiceId, paidAt])
}
model InvoiceInstallment {
id String @id @default(cuid())
invoiceId String
invoice Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
position Int @default(0)
dueDate DateTime
amount Decimal @db.Decimal(18, 2)
currency String @default("IRR")
status InstallmentStatus @default(pending)
paidAt DateTime?
payment Payment?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([invoiceId, position])
@@index([invoiceId, dueDate])
@@index([status, dueDate])
}
model SalesGoal {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
/// null = هدف سازمانی / تیمی
userId String?
user User? @relation("SalesGoalUser", fields: [userId], references: [id], onDelete: Cascade)
title String
/// sales|customer|revenue|deal|activity|quality
type String @default("sales")
periodStart DateTime
periodEnd DateTime
targetAmount Decimal @db.Decimal(18, 2)
forecastAmount Decimal? @db.Decimal(18, 2)
/// 0-100
probability Decimal? @db.Decimal(5, 2)
currency String @default("IRR")
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, periodStart, periodEnd])
@@index([userId])
@@index([organizationId, type])
}
model ExchangeRate {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
/// ارز مبدأ؛ ۱ واحد این ارز = rateToBase واحد ارز پایه
currency String
rateToBase Decimal @db.Decimal(24, 6)
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
@@unique([organizationId, currency])
@@index([organizationId])
}
model Ticket {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
number String
subject String
description String?
status TicketStatus @default(open)
priority TicketPriority @default(normal)
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
companyId String?
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
dealId String?
deal Deal? @relation(fields: [dealId], references: [id], onDelete: SetNull)
assigneeId String?
assignee User? @relation("TicketAssignee", fields: [assigneeId], references: [id], onDelete: SetNull)
reporterId String?
reporter User? @relation("TicketReporter", fields: [reporterId], references: [id], onDelete: SetNull)
resolvedAt DateTime?
/// مهلت پاسخ اول بر اساس اولویت
slaDueAt DateTime?
firstRespondedAt DateTime?
csatScore Int?
csatComment String?
csatAt DateTime?
messages TicketMessage[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, number])
@@index([organizationId, status, updatedAt])
@@index([organizationId, slaDueAt])
@@index([assigneeId])
@@index([reporterId])
@@index([personId])
@@index([companyId])
@@index([dealId])
}
model TicketMessage {
id String @id @default(cuid())
ticketId String
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
authorId String?
author User? @relation("TicketMessageAuthor", fields: [authorId], references: [id], onDelete: SetNull)
body String
isInternal Boolean @default(false)
createdAt DateTime @default(now())
@@index([ticketId, createdAt])
@@index([authorId])
}
model FaqArticle {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
question String
answer String
category String?
isPublished Boolean @default(true)
position Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, isPublished, position])
}
/// کد یک‌بارمصرف ورود پرتال مشتری (موبایل)
model PortalOtp {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
mobile String
code String
expiresAt DateTime
consumedAt DateTime?
createdAt DateTime @default(now())
@@index([organizationId, mobile, createdAt])
@@index([expiresAt])
}
/// برد کانبان برای کارهای داخلی / پس‌ازفروش (جدا از مسیر فروش)
model ProjectBoard {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
isDefault Boolean @default(false)
stages ProjectBoardStage[]
cards ProjectCard[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId])
@@index([organizationId, isDefault])
}
model ProjectBoardStage {
id String @id @default(cuid())
boardId String
board ProjectBoard @relation(fields: [boardId], references: [id], onDelete: Cascade)
name String
position Int
cards ProjectCard[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([boardId, position])
}
model ProjectCard {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
boardId String
board ProjectBoard @relation(fields: [boardId], references: [id], onDelete: Cascade)
stageId String
stage ProjectBoardStage @relation(fields: [stageId], references: [id], onDelete: Restrict)
title String
notes String?
tags String[] @default([])
position Int @default(0)
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
companyId String?
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
dealId String?
deal Deal? @relation(fields: [dealId], references: [id], onDelete: SetNull)
ownerId String?
owner User? @relation("ProjectCardOwner", fields: [ownerId], references: [id], onDelete: SetNull)
assignees User[] @relation("ProjectCardAssignees")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId])
@@index([boardId, stageId])
@@index([stageId, position])
@@index([personId])
@@index([companyId])
@@index([dealId])
@@index([ownerId])
}
model LoyaltyTier {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
minPoints Int @default(0)
position Int @default(0)
color String?
people Person[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, position])
@@index([organizationId, minPoints])
}
model LoyaltySettings {
id String @id @default(cuid())
organizationId String @unique
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
enabled Boolean @default(true)
welcomePoints Int @default(0)
notifyOnAdjust Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model LoyaltyLedger {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
personId String
person Person @relation(fields: [personId], references: [id], onDelete: Cascade)
delta Int
reason String?
source String @default("manual")
createdAt DateTime @default(now())
@@index([organizationId, createdAt])
@@index([personId, createdAt])
}
model LoyaltyReward {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
description String?
pointsCost Int
isActive Boolean @default(true)
position Int @default(0)
color String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
redemptions LoyaltyRedemption[]
@@index([organizationId, position])
}
model LoyaltyRedemption {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
personId String
person Person @relation(fields: [personId], references: [id], onDelete: Cascade)
rewardId String
reward LoyaltyReward @relation(fields: [rewardId], references: [id], onDelete: Restrict)
pointsSpent Int
createdAt DateTime @default(now())
@@index([organizationId, createdAt])
@@index([personId])
@@index([rewardId])
}
model AutomationRule {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
isActive Boolean @default(true)
trigger AutomationTrigger
action AutomationAction
/// عنوان فعالیت/اعلان، assigneeId، notifyUserId، points، activityType
config Json @default("{}")
cursors AutomationCursor[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, isActive, trigger])
}
/// شمارندهٔ تقسیم نوبتی برای هر گام عمل در یک قانون اتوماسیون
model AutomationCursor {
id String @id @default(cuid())
ruleId String
rule AutomationRule @relation(fields: [ruleId], references: [id], onDelete: Cascade)
stepId String
counter Int @default(0)
updatedAt DateTime @updatedAt
@@unique([ruleId, stepId])
}
enum MessageAutomationTrigger {
person_created
lead_submitted
deal_created
deal_stage_changed
deal_won
deal_lost
ticket_created
ticket_resolved
invoice_issued
invoice_paid
activity_created
activity_done
sms_link_clicked
}
enum MessageAutomationDelayUnit {
minutes
hours
days
weeks
months
}
enum MessageAutomationChannel {
app
sms
both
}
enum MessageAutomationTarget {
staff
person
both
}
enum MessageAutomationJobStatus {
pending
sent
cancelled
failed
}
model MessageAutomationRule {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
isActive Boolean @default(true)
trigger MessageAutomationTrigger
startDelayValue Int @default(0)
startDelayUnit MessageAutomationDelayUnit @default(minutes)
repeatCount Int @default(1)
intervalValue Int @default(1)
intervalUnit MessageAutomationDelayUnit @default(days)
notifyTarget MessageAutomationTarget @default(person)
channel MessageAutomationChannel @default(sms)
title String
description String?
body String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
jobs MessageAutomationJob[]
@@index([organizationId, isActive, trigger])
}
model MessageAutomationJob {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
ruleId String
rule MessageAutomationRule @relation(fields: [ruleId], references: [id], onDelete: Cascade)
trigger MessageAutomationTrigger
entityType String
entityId String
entityTitle String
personId String?
ownerId String?
assigneeId String?
attemptIndex Int
sendAt DateTime
status MessageAutomationJobStatus @default(pending)
createdAt DateTime @default(now())
sentAt DateTime?
@@unique([ruleId, entityId, attemptIndex])
@@index([status, sendAt])
@@index([organizationId, entityType, entityId, status])
}
/// چالش OTP برای ثبت‌نام / ورود پنل
model AuthChallenge {
id String @id @default(cuid())
/// register | login
kind String
/// برای login: email | mobile
channel String?
email String?
mobile String?
passwordHash String?
/// دادهٔ موقت ثبت‌نام (سازمان، نام، زمینه، کد معرف، …)
payload Json @default("{}")
emailCode String?
mobileCode String?
emailVerified Boolean @default(false)
mobileVerified Boolean @default(false)
expiresAt DateTime
consumedAt DateTime?
createdAt DateTime @default(now())
@@index([kind, createdAt])
@@index([expiresAt])
@@index([email])
@@index([mobile])
}
/// اتصال به نرم‌افزار حسابداری خارجی (ته‌حساب) — چند اتصال/DB به‌ازای هر سازمان
model AccountingConnection {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
/// فعلاً فقط tahesab
provider String @default("tahesab")
label String
baseUrl String
dbName String
/// AES-256-GCM ciphertext (base64)
tokenCiphertext String
tokenIv String
tokenTag String
/// چهار کاراکتر آخر توکن برای نمایش ماسک‌شده
tokenLast4 String @default("")
enabled Boolean @default(true)
isDefault Boolean @default(false)
/// اگر true باشد verify SSL نادیده گرفته می‌شود (فقط برای نصب‌های داخلی با گواهی خودامضا)
allowInsecureTls Boolean @default(false)
lastHealthAt DateTime?
lastHealthOk Boolean?
lastError String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
personLinks PersonAccountingLink[]
productLinks ProductAccountingLink[]
documentLogs AccountingDocumentLog[]
balanceCaches AccountingBalanceCache[]
asnadLines AccountingAsnadLine[]
@@unique([organizationId, baseUrl, dbName])
@@index([organizationId, enabled])
@@index([organizationId, isDefault])
}
model PersonAccountingLink {
id String @id @default(cuid())
personId String
person Person @relation(fields: [personId], references: [id], onDelete: Cascade)
connectionId String
connection AccountingConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
/// کد مشتری در ته‌حساب (Moshtari_Code)
moshtariCode Int
/// manual | mobile | name | created
matchMethod String
linkedAt DateTime @default(now())
lastBalanceSyncedAt DateTime?
balanceCache AccountingBalanceCache?
asnadLines AccountingAsnadLine[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([personId, connectionId])
@@unique([connectionId, moshtariCode])
@@index([personId])
@@index([connectionId])
}
/// لینک محصول CRM ↔ نام کارساخته (یا معادل) در ته‌حساب
model ProductAccountingLink {
id String @id @default(cuid())
productId String
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
connectionId String
connection AccountingConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
/// نام کارساخته در ته‌حساب (DoListNameKarsakhte.Name)
externalName String
ayar Int?
vaznSabet Float?
/// import | manual
matchMethod String @default("import")
linkedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([productId, connectionId])
@@unique([connectionId, externalName])
@@index([productId])
@@index([connectionId])
}
model AccountingDocumentLog {
id String @id @default(cuid())
connectionId String
connection AccountingConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
/// مثلاً DoNewSanadBuySaleGOLD
docType String
/// کد یکتای ≥۲۰ کاراکتر — idempotency
factorCode String
requestPayload Json
responsePayload Json?
/// pending | success | failed
status String @default("pending")
tahesabOk String?
tahesabFactorId Int?
errorMessage String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([connectionId, factorCode])
@@index([connectionId, status])
@@index([personId])
@@index([docType])
}
model AccountingBalanceCache {
id String @id @default(cuid())
linkId String @unique
link PersonAccountingLink @relation(fields: [linkId], references: [id], onDelete: Cascade)
connectionId String
connection AccountingConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
goldBalance Float?
rialBalance Float?
currencyBalance Float?
/// مانده هم‌ارز طلا (MandeyeKolBeTALA) در صورت وجود
totalGoldEquiv Float?
/// جزئیات گروه‌بندی‌شده برای UI
mandehDetail Json @default("[]")
rawResponse Json?
fetchedAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([connectionId])
}
/// اسنپ‌شات ردیف‌های DoListAsnad برای گزارش فروش گروهی
model AccountingAsnadLine {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
connectionId String
connection AccountingConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
linkId String?
link PersonAccountingLink? @relation(fields: [linkId], references: [id], onDelete: SetNull)
moshtariCode Int
/// کلید پایدار برای upsert: ID ته‌حساب یا f:factor|r:radif|t:tarikh|n:no
lineKey String
tahesabId Int?
factorCode String?
shFactor Int?
radif Int?
/// شمسی YYYY-MM-DD
tarikh String
noRaw String @default("")
/// sale | return | purchase | other
kind String @default("other")
jensFelez Int?
noKar String?
vazn Float?
tabdilVazn Float?
mali Float?
count Float?
ayar Float?
mazaneh Float?
fi Float?
sharh1 String?
syncedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([connectionId, lineKey])
@@index([organizationId, connectionId, tarikh])
@@index([organizationId, kind])
@@index([personId])
@@index([linkId])
@@index([moshtariCode])
}
model AccountingSyncJob {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
connectionId String?
/// search | import | match
kind String
status String @default("pending")
request Json @default("{}")
result Json?
errorMessage String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, createdAt])
}
/// انبار داخلی — وقتی ته‌حساب وصل نیست
model Warehouse {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
isDefault Boolean @default(true)
movements StockMovement[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, isDefault])
}
/// گردش کالا؛ مقدار مثبت ورود، منفی خروج
model StockMovement {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
warehouseId String
warehouse Warehouse @relation(fields: [warehouseId], references: [id], onDelete: Cascade)
productId String
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
/// opening | purchase | sale | credit | adjustment
type String
quantity Decimal @db.Decimal(18, 2)
note String?
invoiceId String?
invoice Invoice? @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
occurredAt DateTime @default(now())
createdAt DateTime @default(now())
@@index([organizationId, productId, occurredAt])
@@index([invoiceId])
@@index([warehouseId])
}
/// پورسانت کارشناس بعد از صدور فاکتور
model CommissionEntry {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
invoiceId String @unique
invoice Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
percent Decimal @db.Decimal(8, 2)
amount Decimal @db.Decimal(18, 2)
currency String @default("IRR")
/// posted | void
status String @default("posted")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, userId, createdAt])
@@index([status])
}
/// چک دریافتی؛ مستقل یا وصل به پرداخت فاکتور
model Cheque {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
paymentId String? @unique
payment Payment? @relation(fields: [paymentId], references: [id], onDelete: Cascade)
invoiceId String?
invoice Invoice? @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
ownerId String?
owner User? @relation("ChequeOwner", fields: [ownerId], references: [id], onDelete: SetNull)
amount Decimal @db.Decimal(18, 2)
currency String @default("IRR")
number String
bank String
dueDate DateTime
/// pending | cleared | bounced
status String @default("pending")
/// in = دریافتی، out = پرداختی
direction String @default("in")
reminderNotifiedAt DateTime?
/// حساب صندوق برای لحظهٔ وصول؛ تا قبل از وصول موجودی را عوض نمی‌کند
financialAccountId String?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, status, dueDate])
@@index([organizationId, direction, dueDate])
@@index([invoiceId])
@@index([personId])
@@index([ownerId])
}
/// مانده اول دوره طرف‌حساب (مثبت = بدهکار ما)
model PartyOpeningBalance {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
personId String
person Person @relation(fields: [personId], references: [id], onDelete: Cascade)
amount Decimal @db.Decimal(18, 2)
currency String @default("IRR")
note String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, personId])
@@index([personId])
}
/// لاگ فعالیت کاربران روی منابع سازمان
model AuditLog {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
actorUserId String?
actor User? @relation("AuditLogActor", fields: [actorUserId], references: [id], onDelete: SetNull)
action String
method String
path String
entityType String?
entityId String?
summary String
meta Json?
ip String?
createdAt DateTime @default(now())
@@index([organizationId, createdAt])
@@index([organizationId, actorUserId, createdAt])
@@index([organizationId, entityType, createdAt])
}
/// نسخهٔ قابل بازگشتِ رکوردهای حذف‌شده
model DeletionArchive {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
/// نام مدل پریزما
model String
entityId String
label String
snapshot Json
deletedById String?
deletedBy User? @relation("DeletionActor", fields: [deletedById], references: [id], onDelete: SetNull)
deletedAt DateTime @default(now())
@@index([organizationId, deletedAt])
@@index([organizationId, model])
}
/// فیلترهای ذخیره‌شده برای لیست‌ها و مسیرهای فروش
/// ستون‌های مخفی هر جدول برای هر کاربر (به‌ازای هر سازمان)
model UserTablePreference {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
organizationId String
tableKey String
/// ستون جدیدی که بعداً اضافه شود به‌صورت پیش‌فرض نمایش داده می‌شود
hiddenColumns String[] @default([])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, organizationId, tableKey])
@@index([userId, organizationId])
}
model SavedView {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
ownerUserId String?
owner User? @relation("SavedViewOwner", fields: [ownerUserId], references: [id], onDelete: SetNull)
name String
/// deals | people | companies | activities | project_cards | reports
scope String
filters Json @default("{}")
isShared Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, scope])
@@index([organizationId, ownerUserId])
}
/// کلید API عمومی برای یکپارچه‌سازی خارجی
model OrganizationApiKey {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
/// پیشوند قابل‌نمایش (مثلاً mk_ab12)
prefix String
/// SHA-256 هش کلید کامل
keyHash String
lastUsedAt DateTime?
revokedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([keyHash])
@@index([organizationId, revokedAt])
@@index([prefix])
}
/// وب‌هوک خروجی سازمان
model WebhookEndpoint {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
url String
secret String
events String[] @default([])
isActive Boolean @default(true)
deliveries WebhookDelivery[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, isActive])
}
model WebhookDelivery {
id String @id @default(cuid())
organizationId String
endpointId String
endpoint WebhookEndpoint @relation(fields: [endpointId], references: [id], onDelete: Cascade)
event String
payload Json
status String @default("pending")
statusCode Int?
attempts Int @default(0)
lastError String?
nextAttemptAt DateTime @default(now())
deliveredAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, createdAt])
@@index([status, nextAttemptAt])
@@index([endpointId, createdAt])
}
/// دفتر مالی داخلی: حساب صندوق/بانک/تنخواه
model FinancialAccount {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
/// bank | cash | petty
type String
bankName String?
accountRef String?
responsibleName String?
openingBalance Decimal @default(0) @db.Decimal(18, 2)
openingDate DateTime
notes String?
isActive Boolean @default(true)
currency String @default("IRR")
createdById String?
movements FinancialMovement[]
documents FinancialDocument[] @relation("DocumentAccount")
reconciliations FinancialReconciliation[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, isActive])
}
model FinancialCategory {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
/// income | expense
kind String
isActive Boolean @default(true)
documents FinancialDocument[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, kind, name])
@@index([organizationId, kind])
}
/// درآمد یا هزینهٔ ثبت‌شده (تعهدی). موجودی حساب را خودش عوض نمی‌کند.
model FinancialDocument {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
/// income | expense
kind String
title String
amount Decimal @db.Decimal(18, 2)
bookedAt DateTime
dueAt DateTime?
categoryId String?
category FinancialCategory? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
partyName String?
accountId String?
account FinancialAccount? @relation("DocumentAccount", fields: [accountId], references: [id], onDelete: SetNull)
/// cash | card_to_card | pos | transfer | other
method String?
/// open | partial | settled | void
status String @default("open")
reference String?
notes String?
createdById String?
voidedAt DateTime?
voidReason String?
movements FinancialMovement[]
attachments FinancialAttachment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, kind, bookedAt])
@@index([organizationId, status])
@@index([personId])
}
/// گردش قطعی حساب. افتتاحیه، دریافت، پرداخت، انتقال.
model FinancialMovement {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
accountId String
account FinancialAccount @relation(fields: [accountId], references: [id], onDelete: Cascade)
documentId String?
document FinancialDocument? @relation(fields: [documentId], references: [id], onDelete: SetNull)
receivableId String?
receivable FinancialReceivable? @relation(fields: [receivableId], references: [id], onDelete: SetNull)
installmentId String?
installment FinancialInstallment? @relation(fields: [installmentId], references: [id], onDelete: SetNull)
payrollId String?
payroll FinancialPayroll? @relation(fields: [payrollId], references: [id], onDelete: SetNull)
paymentId String?
chequeId String?
/// in | out
direction String
amount Decimal @db.Decimal(18, 2)
/// opening | receipt | payment | transfer_in | transfer_out
kind String
transferGroupId String?
occurredAt DateTime
memo String?
reference String?
createdById String?
voidedAt DateTime?
createdAt DateTime @default(now())
@@index([organizationId, accountId, occurredAt])
@@index([transferGroupId])
@@index([documentId])
@@index([receivableId])
@@index([installmentId])
@@index([payrollId])
@@index([paymentId])
@@index([chequeId])
}
model FinancialAuditLog {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
entityType String
entityId String
action String
before Json?
after Json?
actorUserId String?
createdAt DateTime @default(now())
@@index([organizationId, entityType, entityId, createdAt])
}
model FinancialAttachment {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
documentId String
document FinancialDocument @relation(fields: [documentId], references: [id], onDelete: Cascade)
fileName String
mimeType String
storageKey String
createdById String?
createdAt DateTime @default(now())
@@index([documentId])
}
model FinancialPayroll {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
userId String
period String
baseAmount Decimal @db.Decimal(18, 2)
benefits Decimal @default(0) @db.Decimal(18, 2)
bonus Decimal @default(0) @db.Decimal(18, 2)
deductions Decimal @default(0) @db.Decimal(18, 2)
payable Decimal @db.Decimal(18, 2)
dueDate DateTime
notes String?
/// open | partial | settled | void
status String @default("open")
createdById String?
voidedAt DateTime?
movements FinancialMovement[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, userId, period])
@@index([organizationId, status])
}
model FinancialRecurringItem {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
title String
/// income | expense
kind String
amount Decimal @db.Decimal(18, 2)
/// weekly | monthly | yearly
interval String
startDate DateTime
endDate DateTime?
nextDue DateTime
accountId String?
categoryId String?
isActive Boolean @default(true)
createdById String?
occurrences FinancialRecurringOccurrence[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, isActive, nextDue])
}
model FinancialRecurringOccurrence {
id String @id @default(cuid())
itemId String
item FinancialRecurringItem @relation(fields: [itemId], references: [id], onDelete: Cascade)
periodKey String
dueDate DateTime
documentId String?
/// pending | confirmed
status String @default("pending")
createdAt DateTime @default(now())
@@unique([itemId, periodKey])
}
model FinancialReconciliation {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
accountId String
account FinancialAccount @relation(fields: [accountId], references: [id], onDelete: Cascade)
reconciledAt DateTime
statedBalance Decimal @db.Decimal(18, 2)
systemBalance Decimal @db.Decimal(18, 2)
difference Decimal @db.Decimal(18, 2)
notes String?
createdById String?
createdAt DateTime @default(now())
@@index([organizationId, accountId, reconciledAt])
}
/// قرارداد یا طلب اقساطی دفتر داخلی
model FinancialReceivable {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
title String
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
partyName String?
totalAmount Decimal @db.Decimal(18, 2)
notes String?
/// open | settled | void
status String @default("open")
createdById String?
voidedAt DateTime?
installments FinancialInstallment[]
movements FinancialMovement[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, status])
@@index([personId])
}
model FinancialInstallment {
id String @id @default(cuid())
receivableId String
receivable FinancialReceivable @relation(fields: [receivableId], references: [id], onDelete: Cascade)
position Int
dueDate DateTime
amount Decimal @db.Decimal(18, 2)
reminderNotifiedAt DateTime?
movements FinancialMovement[]
createdAt DateTime @default(now())
@@unique([receivableId, position])
@@index([dueDate])
}
/// کیف‌پول و تنظیمات پیامک هر محیط (الگوی همکار / سوپرادمین آبشُده)
model SmsAccount {
id String @id @default(cuid())
organizationId String @unique
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
balanceRials Int @default(0)
pricePerPartRials Int @default(2500)
isBlocked Boolean @default(false)
/// اگر ست باشد: وقتی موجودی < این مقدار، ارسال قطع می‌شود (می‌تواند منفی باشد). null = فقط کلید دستی
autoBlockBelowRials Int?
alertCriticalRials Int @default(10000)
alertWarningRials Int @default(100000)
/// platform = سامانهٔ ما | own = پنل اختصاصی مشتری (بدون کسر/لاگ کیف)
panelMode String @default("platform")
/// وقتی panelMode=own: 0098 | asanak | trez
ownPanelType String @default("0098")
ownPanelUsername String?
ownPanelPassword String?
ownPanelFrom String?
chargeCard String?
chargeIban String?
chargeHolder String?
chargeInstruction String?
alertDaysSent Int @default(0)
lastAlertDate DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ledgers SmsLedger[]
sendLogs SmsBillingSendLog[]
@@index([isBlocked])
@@index([balanceRials])
}
model SmsLedger {
id String @id @default(cuid())
smsAccountId String
smsAccount SmsAccount @relation(fields: [smsAccountId], references: [id], onDelete: Cascade)
/// charge | debit | alert_debit
type String
amountRials Int
balanceAfterRials Int
parts Int?
pricePerPartRials Int?
note String?
smsSendLogId String?
createdById String?
createdAt DateTime @default(now())
@@index([smsAccountId, createdAt])
@@index([type])
}
/// لاگ ارسال صورتحساب‌شده (همهٔ مسیرها وقتی panelMode=platform)
model SmsBillingSendLog {
id String @id @default(cuid())
smsAccountId String
smsAccount SmsAccount @relation(fields: [smsAccountId], references: [id], onDelete: Cascade)
receiver String
body String
parts Int
pricePerPartRials Int
totalAmountRials Int
provider String?
source String?
isAlert Boolean @default(false)
createdAt DateTime @default(now())
@@index([smsAccountId, createdAt])
@@index([source])
}
/// لاگ یکپارچهٔ پیامک خروجی (دستی / کمپین / اتوماسیون)
model OutboundSmsLog {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
leadId String?
lead Lead? @relation(fields: [leadId], references: [id], onDelete: SetNull)
mobile String
body String
/// manual | campaign | automation
source String
sourceId String?
/// sent | failed
status String
error String?
actorUserId String?
createdAt DateTime @default(now())
@@index([organizationId, personId, createdAt])
@@index([organizationId, leadId, createdAt])
@@index([organizationId, createdAt])
}
/// کمپین پیامک گروهی
model SmsCampaign {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
body String
/// draft | queued | sending | done | cancelled
status String @default("draft")
scheduledAt DateTime?
totalCount Int @default(0)
sentCount Int @default(0)
failedCount Int @default(0)
skippedCount Int @default(0)
createdById String?
recipients SmsCampaignRecipient[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, status, scheduledAt])
}
model SmsCampaignRecipient {
id String @id @default(cuid())
campaignId String
campaign SmsCampaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
personId String?
/// برای پشتیبانی آینده از Lead (فعلاً کمپین روی Person است)
leadId String?
mobile String?
/// pending | sent | failed | skipped
status String @default("pending")
error String?
sentAt DateTime?
createdAt DateTime @default(now())
trackingLinks TrackingLink[]
@@index([campaignId, status])
@@index([personId])
@@index([leadId])
}
/// لینک ردیابی یکتا — کانال‌خنثی (SMS / Email / …)
model TrackingLink {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
/// توکن امن و غیرقابل‌حدس (بدون PII)
token String @unique
originalUrl String
/// sms | email | whatsapp | notification | other
channel String @default("sms")
/// sms_campaign | …
sourceType String
sourceId String
/// sms_campaign_recipient | …
recipientType String?
recipientId String?
smsRecipientId String?
smsRecipient SmsCampaignRecipient? @relation(fields: [smsRecipientId], references: [id], onDelete: SetNull)
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
leadId String?
lead Lead? @relation(fields: [leadId], references: [id], onDelete: SetNull)
clickCount Int @default(0)
firstClickedAt DateTime?
lastClickedAt DateTime?
createdAt DateTime @default(now())
events TrackingEvent[]
@@index([organizationId, sourceType, sourceId])
@@index([organizationId, personId])
@@index([organizationId, leadId])
@@index([smsRecipientId])
@@index([token])
}
/// رویداد رفتاری عمومی — آمادهٔ Automation
model TrackingEvent {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
trackingLinkId String?
trackingLink TrackingLink? @relation(fields: [trackingLinkId], references: [id], onDelete: SetNull)
/// مثلاً sms_link_clicked | sms_delivered | sms_failed | …
eventType String
channel String @default("sms")
sourceType String?
sourceId String?
recipientType String?
recipientId String?
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
leadId String?
lead Lead? @relation(fields: [leadId], references: [id], onDelete: SetNull)
occurredAt DateTime @default(now())
/// ua / referrer / ipHash (اختیاری، بدون PII خام)
metadata Json?
createdAt DateTime @default(now())
@@index([organizationId, eventType, occurredAt])
@@index([organizationId, sourceType, sourceId, eventType])
@@index([organizationId, personId, eventType])
@@index([organizationId, leadId, eventType])
@@index([trackingLinkId, occurredAt])
}
/// اتصال فروشگاه ووکامرس
model WooCommerceConnection {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
label String
storeUrl String
consumerKeyCiphertext String
consumerKeyIv String
consumerKeyTag String
consumerKeyLast4 String @default("")
consumerSecretCiphertext String
consumerSecretIv String
consumerSecretTag String
webhookSecretCiphertext String?
webhookSecretIv String?
webhookSecretTag String?
enabled Boolean @default(true)
lastSyncAt DateTime?
lastHealthAt DateTime?
lastHealthOk Boolean?
lastError String?
orders WooCommerceOrderLink[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, enabled])
}
model WooCommerceOrderLink {
id String @id @default(cuid())
organizationId String
connectionId String
connection WooCommerceConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
wooOrderId String
wooOrderNumber String?
invoiceId String
status String @default("")
total Decimal @default(0) @db.Decimal(18, 2)
currency String @default("IRR")
rawPayload Json?
syncedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([connectionId, wooOrderId])
@@index([organizationId, syncedAt])
@@index([invoiceId])
}
/// اتصال فروشگاه Montify (هر فروشگاه: Base URL + توکن اختصاصی)
model MontifyConnection {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
label String
baseUrl String
storeId String @default("")
tokenCiphertext String
tokenIv String
tokenTag String
tokenLast4 String @default("")
enabled Boolean @default(true)
webhookRemoteId String?
webhookSecretCiphertext String?
webhookSecretIv String?
webhookSecretTag String?
/// بیشترین updated_at مشتریان دریافت‌شده (برای updated_after)
customersCursor DateTime?
/// idle | running | ok | error
syncStatus String @default("idle")
syncStartedAt DateTime?
lastSyncAt DateTime?
lastSyncResult Json?
lastHealthAt DateTime?
lastHealthOk Boolean?
lastError String?
lastWebhookAt DateTime?
links MontifyLink[]
orders MontifyOrder[]
transactions MontifyTransaction[]
deliveries MontifyWebhookDelivery[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, enabled])
}
/// پیوند مشتری فروشگاه با شخص CRM (entityType = customer، entityId = Person.id)
model MontifyLink {
id String @id @default(cuid())
organizationId String
connectionId String
connection MontifyConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
entityType String
externalId String
entityId String
externalUpdatedAt DateTime?
/// آخرین زمان دریافت سفارش‌های این مشتری (updated_after سفارش‌ها)
ordersSyncedAt DateTime?
/// پروفایل فروشگاهی مشتری: wallet_balance، gold_wallet_balance، summary، engagement، …
data Json?
syncedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([connectionId, entityType, externalId])
@@index([organizationId, entityType, entityId])
}
/// سفارش فروشگاه (فقط‌خواندنی، نگاشت مستند: code → order_reference، sts → order_status)
model MontifyOrder {
id String @id @default(cuid())
organizationId String
connectionId String
connection MontifyConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
externalId String
customerExternalId String?
personId String?
orderReference String?
orderStatus String?
paymentStatus String?
paymentType String?
totalPrice Decimal? @db.Decimal(20, 2)
discountTotalPrice Decimal? @db.Decimal(20, 2)
totalDiscounts Decimal? @db.Decimal(20, 2)
totalTax Decimal? @db.Decimal(20, 2)
totalQty Decimal? @db.Decimal(14, 3)
profit Decimal? @db.Decimal(20, 2)
percentProfit Decimal? @db.Decimal(10, 3)
sumGold Decimal? @db.Decimal(14, 3)
sumSilver Decimal? @db.Decimal(14, 3)
address Json?
note String?
cancelReason String?
cancelledAt DateTime?
closedAt DateTime?
codeDiscount String?
items Json @default("[]")
externalCreatedAt DateTime?
externalUpdatedAt DateTime?
deletedAt DateTime?
raw Json?
syncedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([connectionId, externalId])
@@index([organizationId, personId])
@@index([organizationId, externalCreatedAt])
}
/// تراکنش مالی مشتری در فروشگاه (واریز/برداشت ریالی یا گرم طلا)
model MontifyTransaction {
id String @id @default(cuid())
organizationId String
connectionId String
connection MontifyConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
externalId String
customerExternalId String?
personId String?
type String?
amount Decimal? @db.Decimal(24, 4)
currency String?
status String?
referenceCode String?
externalCreatedAt DateTime?
raw Json?
syncedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([connectionId, externalId])
@@index([organizationId, personId])
}
model MontifyWebhookDelivery {
id String @id @default(cuid())
connectionId String
connection MontifyConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
deliveryId String
event String
/// received | processed | failed
status String @default("received")
error String?
createdAt DateTime @default(now())
@@unique([connectionId, deliveryId])
@@index([connectionId, createdAt])
}
/// اتصال تلفن سازمانی / سانترال (وب‌هوک عمومی)
model VoipConnection {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
label String
enabled Boolean @default(true)
webhookSecretCiphertext String?
webhookSecretIv String?
webhookSecretTag String?
/// قالب URL شروع تماس؛ جای {number} شماره می‌نشیند
clickToCallUrl String?
lastEventAt DateTime?
lastError String?
calls VoipCallLog[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, enabled])
}
model VoipCallLog {
id String @id @default(cuid())
organizationId String
connectionId String
connection VoipConnection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
externalCallId String
/// inbound | outbound
direction String @default("inbound")
/// ringing | answered | missed | completed | failed
status String @default("completed")
fromNumber String?
toNumber String?
personId String?
activityId String?
durationSec Int?
startedAt DateTime @default(now())
endedAt DateTime?
rawPayload Json?
/// لینک فایل ضبط از سانترال
recordingUrl String?
/// متن خام (از وب‌هوک یا STT)
transcript String?
/// خلاصه فارسی تولیدشده
summary String?
/// none | pending | processing | done | failed | skipped
summaryStatus String @default("none")
summaryNoteId String?
summaryError String?
summaryAttempts Int @default(0)
summaryNextAttemptAt DateTime?
syncedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([connectionId, externalCallId])
@@index([organizationId, syncedAt])
@@index([personId])
@@index([activityId])
@@index([summaryStatus, summaryNextAttemptAt])
}
/// خدمت قابل رزرو (مثلاً مشاوره ۳۰ دقیقه‌ای)
model ReservationService {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
description String @default("")
durationMinutes Int @default(30)
/// روزهای کار ۰=یکشنبه … ۶=شنبه
workDays Int[] @default([0, 1, 2, 3, 4, 5, 6])
dayStartHour Int @default(9)
dayEndHour Int @default(17)
isActive Boolean @default(true)
appointments ReservationAppointment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, isActive])
}
model ReservationAppointment {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
serviceId String
service ReservationService @relation(fields: [serviceId], references: [id], onDelete: Restrict)
personId String
person Person @relation(fields: [personId], references: [id], onDelete: Cascade)
assigneeId String?
assignee User? @relation("ReservationAssignee", fields: [assigneeId], references: [id], onDelete: SetNull)
startsAt DateTime
endsAt DateTime
/// pending | confirmed | canceled | done
status String @default("pending")
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, startsAt])
@@index([organizationId, status])
@@index([serviceId, startsAt])
@@index([personId])
@@index([assigneeId, startsAt])
}
/// مرحله/وضعیت لید — هم لیست وضعیت و هم ستون Kanban
model LeadStage {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
position Int @default(0)
color String @default("teal")
/// نمایش در تخته Kanban
showOnBoard Boolean @default(true)
isConverted Boolean @default(false)
isLost Boolean @default(false)
isUnqualified Boolean @default(false)
leads Lead[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, name])
@@index([organizationId, position])
}
/// مخاطب بالقوه — جدا از Person/مشتری
model Lead {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
/// نام فروشگاه / شخص / عنوان نمایشی
title String
/// نام مالک یا مسئول
contactName String?
mobile String
phone String?
city String?
region String?
/// پیج اینستاگرام
instagram String?
/// تاریخ تولد
birthDate DateTime?
/// لیست آدرس‌ها (JSON array of strings)
addresses Json @default("[]")
/// صنف
trade String?
source String?
notes String?
/// لوگوی فروشگاه / شرکت
logoStorageKey String?
estimatedValue Decimal? @db.Decimal(18, 2)
stageId String
stage LeadStage @relation(fields: [stageId], references: [id], onDelete: Restrict)
position Int @default(0)
assigneeId String?
assignee User? @relation("LeadAssignee", fields: [assigneeId], references: [id], onDelete: SetNull)
createdById String?
createdBy User? @relation("LeadCreator", fields: [createdById], references: [id], onDelete: SetNull)
lastContactAt DateTime?
nextFollowUpAt DateTime?
/// شخص مشتری پس از تبدیل
convertedPersonId String?
convertedAt DateTime?
archivedAt DateTime?
events LeadEvent[]
salesCopilotSessions SalesCopilotSession[]
trackingLinks TrackingLink[]
trackingEvents TrackingEvent[]
routineTasks RoutineTask[]
activities Activity[]
outboundSmsLogs OutboundSmsLog[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, mobile])
@@index([organizationId, stageId, position])
@@index([organizationId, assigneeId])
@@index([organizationId, nextFollowUpAt])
@@index([organizationId, lastContactAt])
@@index([organizationId, archivedAt])
@@index([organizationId, createdAt])
@@index([organizationId, city])
@@index([organizationId, trade])
@@index([organizationId, source])
}
model LeadEvent {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
leadId String
lead Lead @relation(fields: [leadId], references: [id], onDelete: Cascade)
/// created | assigned | stage_changed | call | note | task | follow_up | converted | archived | import
type String
title String
body String?
meta Json?
actorId String?
createdAt DateTime @default(now())
@@index([leadId, createdAt])
@@index([organizationId, createdAt])
}
/// ایمپورت پس‌زمینه فایل اکسل لید
model LeadImportJob {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
createdById String?
fileName String?
/// pending | processing | done | failed
status String @default("pending")
totalCount Int @default(0)
processedCount Int @default(0)
successCount Int @default(0)
duplicateCount Int @default(0)
errorCount Int @default(0)
/// ردیف‌های خام [{...}]
rows Json @default("[]")
/// خلاصه خطاها
errorSamples Json @default("[]")
startedAt DateTime?
finishedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, status, createdAt])
@@index([status, createdAt])
}
/// تنظیمات اتصال به سرویس گفتگوی خارجی (هر محیط CRM)
model ConversationSettings {
organizationId String @id
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
chatHost String
apiKeyCipher String
apiKeyIv String
apiKeyTag String
uploadUrl String @default("")
downloadUrl String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
/// نگاشت roomهای سرویس چت (DM dedup و گروه‌های ساخته‌شده از CRM)
model ConversationRoom {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
externalRoomId String
kind String
name String?
dmUserMinId String?
dmUserMaxId String?
createdByUserId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, externalRoomId])
@@unique([organizationId, dmUserMinId, dmUserMaxId])
@@index([organizationId, kind])
}
/// پیام‌های حذف‌شده در CRM (سرویس چت خارجی حذف پایدار ندارد)
/// scope=all → برای همه؛ scope=self → فقط برای hiddenByUserId
model ConversationHiddenMessage {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
externalRoomId String
messageId String
hiddenByUserId String
/// all = حذف برای همه | self = حذف فقط برای کاربر
scope String @default("all")
createdAt DateTime @default(now())
@@unique([organizationId, externalRoomId, messageId, scope, hiddenByUserId])
@@index([organizationId, externalRoomId])
}
/// یک پیام سنجاق‌شده در هر اتاق (برای همه اعضا)
model ConversationPinnedMessage {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
externalRoomId String
messageId String
preview String @default("")
pinnedByUserId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, externalRoomId])
@@index([organizationId, externalRoomId])
}
/// سنجاق گفتگو در لیست — فقط برای همان کاربر
model ConversationRoomUserPin {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
userId String
externalRoomId String
createdAt DateTime @default(now())
@@unique([organizationId, userId, externalRoomId])
@@index([organizationId, userId])
}
/// اعضای گروه/کانال در CRM (نقش ادمین برای ارسال در کانال و مدیریت اعضا)
model ConversationRoomMember {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
externalRoomId String
userId String
/// admin | member
role String @default("member")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, externalRoomId, userId])
@@index([organizationId, externalRoomId])
}
/// بی‌صدا کردن گفتگو برای کاربر — با استثنای منشن
model ConversationRoomUserMute {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
userId String
externalRoomId String
/// اگر true باشد منشن همچنان اعلان/خوانده‌نشده می‌سازد
allowMentions Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, userId, externalRoomId])
@@index([organizationId, userId])
}
/// واکنش ایموجی روی پیام
model ConversationMessageReaction {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
externalRoomId String
messageId String
emoji String
userId String
createdAt DateTime @default(now())
@@unique([organizationId, externalRoomId, messageId, userId, emoji])
@@index([organizationId, externalRoomId])
}
enum RoutineTaskStatus {
todo
in_progress
completed
overdue
skipped
cancelled
}
enum RoutineRecurrence {
daily
weekdays
weekly
biweekly
monthly
every_n_days
specific_dates
}
enum RoutineTaskSource {
routine
manual
follow_up
lead
customer
deal
}
/// دسته‌بندی کارهای روتین (فروش، پیگیری، لید، …)
model RoutineCategory {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
color String @default("#14b8a6")
sortOrder Int @default(0)
routines Routine[]
tasks RoutineTask[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, name])
@@index([organizationId, sortOrder])
}
/// الگوی کار تکرارشونده؛ هر روز Task مستقل (RoutineTask) تولید می‌کند
model Routine {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
title String
description String?
assigneeId String?
assignee User? @relation("RoutineAssignee", fields: [assigneeId], references: [id], onDelete: SetNull)
/// اگر ست باشد برای هر عضو تیم یک Task جدا ساخته می‌شود
teamId String?
team Team? @relation(fields: [teamId], references: [id], onDelete: SetNull)
categoryId String?
category RoutineCategory? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
priority ActivityPriority @default(medium)
/// HH:mm به وقت تهران
timeOfDay String @default("09:00")
durationMinutes Int?
startDate DateTime @db.Date
endDate DateTime? @db.Date
recurrence RoutineRecurrence @default(daily)
/// ۰ = شنبه … ۶ = جمعه
weekdays Int[] @default([])
intervalDays Int?
monthDay Int?
specificDates DateTime[] @db.Date
isMandatory Boolean @default(true)
allowSkip Boolean @default(false)
skipReasonRequired Boolean @default(true)
reportRequired Boolean @default(false)
allowAfterDeadline Boolean @default(true)
carryOverOverdue Boolean @default(false)
/// none | optional | lead | customer
entityLink String @default("none")
reminderMinutesBefore Int?
isActive Boolean @default(true)
createdById String?
createdBy User? @relation("RoutineCreator", fields: [createdById], references: [id], onDelete: SetNull)
deletedAt DateTime?
tasks RoutineTask[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, isActive, deletedAt])
@@index([assigneeId])
@@index([teamId])
}
/// نمونه روزانه یک کار (از روتین، دستی، پیگیری، لید یا مشتری)
model RoutineTask {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
routineId String?
routine Routine? @relation(fields: [routineId], references: [id], onDelete: SetNull)
assigneeId String
assignee User @relation("RoutineTaskAssignee", fields: [assigneeId], references: [id], onDelete: Cascade)
title String
description String?
categoryId String?
category RoutineCategory? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
priority ActivityPriority @default(medium)
status RoutineTaskStatus @default(todo)
/// روز برنامه به وقت تهران
scheduledFor DateTime @db.Date
dueAt DateTime?
startedAt DateTime?
completedAt DateTime?
resultNote String?
skipReason String?
source RoutineTaskSource @default(manual)
isMandatory Boolean @default(false)
leadId String?
lead Lead? @relation(fields: [leadId], references: [id], onDelete: SetNull)
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull)
companyId String?
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
dealId String?
deal Deal? @relation(fields: [dealId], references: [id], onDelete: SetNull)
invoiceId String?
invoice Invoice? @relation(fields: [invoiceId], references: [id], onDelete: SetNull)
activityId String?
activity Activity? @relation(fields: [activityId], references: [id], onDelete: SetNull)
reminderAt DateTime?
reminderSentAt DateTime?
overdueNotifiedAt DateTime?
createdById String?
createdBy User? @relation("RoutineTaskCreator", fields: [createdById], references: [id], onDelete: SetNull)
events RoutineTaskEvent[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([routineId, assigneeId, scheduledFor])
@@index([organizationId, assigneeId, scheduledFor])
@@index([organizationId, status, dueAt])
@@index([reminderAt, reminderSentAt])
@@index([leadId])
@@index([personId])
@@index([companyId])
@@index([dealId])
@@index([activityId])
}
/// تاریخچه تغییرات هر Task
model RoutineTaskEvent {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
taskId String
task RoutineTask @relation(fields: [taskId], references: [id], onDelete: Cascade)
actorId String?
actor User? @relation(fields: [actorId], references: [id], onDelete: SetNull)
/// created | status_changed | rescheduled | note | reassigned | updated
type String
fromStatus RoutineTaskStatus?
toStatus RoutineTaskStatus?
note String?
meta Json @default("{}")
createdAt DateTime @default(now())
@@index([taskId, createdAt])
@@index([organizationId, createdAt])
}
/// تنظیمات اعلان کارهای روتین هر کاربر
model RoutineUserSettings {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
remindBeforeMinutes Int? @default(30)
endOfDaySummary Boolean @default(true)
overdueAlert Boolean @default(true)
summaryHour Int @default(16)
lastSummaryOn DateTime? @db.Date
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, userId])
}
/// ───────── باشگاه امتیازات کارکنان (جدا از باشگاه مشتریان) ─────────
model GamificationSettings {
id String @id @default(cuid())
organizationId String @unique
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
enabled Boolean @default(true)
/// [{ name, minScore }]
levels Json @default("[]")
monthlyGoal Int @default(1500)
showFullLeaderboard Boolean @default(true)
/// شروع استفاده — مبنای گزارش ROI
enabledAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model GamificationRule {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
description String?
event String
score Int @default(0)
coins Int @default(0)
/// [{ field, op, value }]
conditions Json @default("[]")
isActive Boolean @default(true)
maxPerDay Int?
maxCoinsPerDay Int?
maxPerUser Int?
cooldownMinutes Int?
oncePerEntity Boolean @default(false)
firstTimeOnly Boolean @default(false)
requiresApproval Boolean @default(false)
roles String[] @default([])
teamIds String[] @default([])
position Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, event, isActive])
}
/// دفتر کل امتیاز و سکه — هر تغییر کیف پول یک ردیف دارد
model GamificationTransaction {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
userId String
/// rule | bonus | achievement | adjust | redeem | refund
kind String
event String?
ruleId String?
bonusId String?
badgeId String?
score Int @default(0)
coins Int @default(0)
/// approved | pending | rejected
status String @default("approved")
title String
entityType String?
entityId String?
entityTitle String?
/// کلید یکتا برای جلوگیری از امتیاز تکراری
idempotencyKey String?
reason String?
createdById String?
decidedById String?
decidedAt DateTime?
createdAt DateTime @default(now())
@@unique([organizationId, idempotencyKey])
@@index([organizationId, createdAt])
@@index([organizationId, userId, createdAt])
@@index([organizationId, ruleId, userId, createdAt])
@@index([organizationId, status])
}
/// موجودی تجمیعی هر کاربر — برای خواندن سریع
model GamificationWallet {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
userId String
score Int @default(0)
coins Int @default(0)
coinsEarned Int @default(0)
bestMonthScore Int @default(0)
updatedAt DateTime @updatedAt
@@unique([organizationId, userId])
@@index([organizationId, score])
}
model GamificationReward {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
description String?
cost Int
icon String @default("gift")
color String @default("#14b8a6")
stock Int?
isActive Boolean @default(true)
requiresApproval Boolean @default(true)
position Int @default(0)
redemptions GamificationRedemption[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, isActive])
}
model GamificationRedemption {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
userId String
rewardId String
reward GamificationReward @relation(fields: [rewardId], references: [id], onDelete: Cascade)
cost Int
/// pending | approved | rejected | delivered
status String @default("pending")
note String?
decisionNote String?
decidedById String?
decidedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, status])
@@index([organizationId, userId])
}
model GamificationBadge {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
description String?
icon String @default("medal")
color String @default("#f59e0b")
/// event_count | score_total | routine_streak | single_deal_amount
metric String
event String?
threshold Int
coins Int @default(0)
isActive Boolean @default(true)
position Int @default(0)
earned GamificationUserBadge[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, isActive])
}
model GamificationUserBadge {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
badgeId String
badge GamificationBadge @relation(fields: [badgeId], references: [id], onDelete: Cascade)
userId String
earnedAt DateTime @default(now())
@@unique([badgeId, userId])
@@index([organizationId, userId])
}
model GamificationBonus {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
description String?
/// event_count_period | routine_streak | top_of_month
kind String
event String?
threshold Int @default(1)
/// day | week | month
period String @default("month")
score Int @default(0)
coins Int @default(0)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, isActive])
}
/// ارزیابی رفتاری DISC؛ هر بار یک رکورد تازه است و تاریخچه پاک نمی‌شود.
model DiscAssessment {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
personId String
person Person @relation(fields: [personId], references: [id], onDelete: Cascade)
assessorId String
assessor User @relation("DiscAssessor", fields: [assessorId], references: [id], onDelete: Restrict)
answers Json
scoreD Int
scoreI Int
scoreS Int
scoreC Int
percentD Int
percentI Int
percentS Int
percentC Int
validCount Int
primaryTypes String[] @default([])
secondaryTypes String[] @default([])
label String
secondaryLabel String?
reliable Boolean
confidence String
note String?
status String @default("confirmed")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, personId, createdAt])
@@index([personId])
}
/// نتیجهٔ تأییدشدهٔ دستیار فروش. پیشنهاد خام بدون تأیید کارشناس اینجا ذخیره نمی‌شود.
model SalesCopilotSession {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
personId String?
person Person? @relation(fields: [personId], references: [id], onDelete: Cascade)
leadId String?
lead Lead? @relation(fields: [leadId], references: [id], onDelete: SetNull)
dealId String?
deal Deal? @relation(fields: [dealId], references: [id], onDelete: SetNull)
userId String
user User @relation("CopilotAuthor", fields: [userId], references: [id], onDelete: Restrict)
topic String
goal String
customerStatement String
behaviorNotes String?
analysis Json
confirmedResponse String?
nextAction String?
followUpAt DateTime?
callOutcome String?
agentNote String?
status String
messageDraft String?
activityId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, personId, createdAt])
@@index([organizationId, leadId])
@@index([dealId])
}
/// ورود و خروج کارمندان به سامانه
model AttendanceEvent {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
/// login | logout | forced_logout
kind String
at DateTime @default(now())
/// ورود بعد از ساعت خروج تعیین‌شده
afterHours Boolean @default(false)
/// شروع نشست؛ برای اینکه خروج اجباری هر نشست فقط یک بار ثبت شود
sessionStartedAt DateTime?
createdAt DateTime @default(now())
@@index([organizationId, at])
@@index([organizationId, userId, at])
@@unique([organizationId, userId, kind, sessionStartedAt])
}