Files
CRM_Montzar/apps/api/prisma/schema.prisma
T

2098 lines
73 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
}
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([])
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
users User[]
memberships Membership[]
orgRoles OrgRole[]
teams Team[]
companies Company[]
people Person[]
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[]
warehouses Warehouse[]
stockMovements StockMovement[]
partyOpenings PartyOpeningBalance[]
commissionEntries CommissionEntry[]
cheques Cheque[]
auditLogs AuditLog[]
savedViews SavedView[]
apiKeys OrganizationApiKey[]
webhookEndpoints WebhookEndpoint[]
smsCampaigns SmsCampaign[]
wooCommerceConnections WooCommerceConnection[]
voipConnections VoipConnection[]
reservationServices ReservationService[]
reservationAppointments ReservationAppointment[]
leads Lead[]
leadStages LeadStage[]
leadEvents LeadEvent[]
leadImportJobs LeadImportJob[]
conversationSettings ConversationSettings?
conversationRooms ConversationRoom[]
}
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")
ownedQuotes Quote[] @relation("QuoteOwner")
ownedInvoices Invoice[] @relation("InvoiceOwner")
ownedPeople Person[] @relation("PersonOwner")
ownedCompanies Company[] @relation("CompanyOwner")
ownedProjectCards ProjectCard[] @relation("ProjectCardOwner")
activities Activity[] @relation("ActivityAssignee")
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")
assignedProjectCards ProjectCard[] @relation("ProjectCardAssignees")
teamMemberships TeamMember[]
reservationAssignments ReservationAppointment[] @relation("ReservationAssignee")
assignedLeads Lead[] @relation("LeadAssignee")
createdLeads Lead[] @relation("LeadCreator")
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([])
/// owner | admin | sales | support — نقش‌های سیستمی قابل حذف نیستند
systemKey String?
users User[]
memberships Membership[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, name])
@@index([organizationId])
}
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")
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[]
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 | 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
nationalId String?
phone String?
email String?
website String?
address String?
notes String?
tags String[] @default([])
customFields Json @default("{}")
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[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId])
@@index([organizationId, name])
@@index([organizationId, email])
@@index([organizationId, phone])
@@index([ownerId])
}
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
email String?
phone String?
mobile String?
position String?
/// آدرس وب‌سایت
website String?
/// نام معرف
referrer String?
notes String?
mood PersonMood @default(newcomer)
tags String[] @default([])
customFields Json @default("{}")
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[]
openingBalances PartyOpeningBalance[]
cheques Cheque[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId])
@@index([organizationId, email])
@@index([organizationId, mobile])
@@index([organizationId, lastName, firstName])
@@index([organizationId, loyaltyPoints])
@@index([companyId])
@@index([ownerId])
@@index([loyaltyTierId])
}
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
amount Decimal? @db.Decimal(18, 2)
currency String @default("IRR")
status DealStatus @default(open)
source String?
lostReason String?
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)
companyId String?
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
ownerId String?
owner User? @relation("DealOwner", fields: [ownerId], 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[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId])
@@index([pipelineId, stageId])
@@index([stageId, position])
@@index([personId])
@@index([companyId])
@@index([ownerId])
}
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
description String?
dueAt DateTime?
durationMinutes Int?
reminderAt DateTime?
/// اعلان داخل پنل هنگام رسیدن یادآور
reminderNotifyApp Boolean @default(true)
/// پیامک به موبایل مسئول هنگام رسیدن یادآور
reminderNotifySms Boolean @default(false)
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)
createdById String?
createdBy User? @relation("ActivityCreator", fields: [createdById], references: [id], onDelete: SetNull)
notes Note[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, status, dueAt])
@@index([organizationId, assigneeId])
@@index([organizationId, createdById])
@@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[]
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?
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("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, isActive, trigger])
}
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
}
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[]
@@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?
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])
}
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?
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 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 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?
mobile String?
/// pending | sent | failed | skipped
status String @default("pending")
error String?
sentAt DateTime?
createdAt DateTime @default(now())
@@index([campaignId, status])
}
/// اتصال فروشگاه ووکامرس
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])
}
/// اتصال تلفن سازمانی / سانترال (وب‌هوک عمومی)
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
@@index([organizationId, position])
@@unique([organizationId, name])
}
/// مخاطب بالقوه — جدا از 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?
/// صنف
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[]
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])
}