Files
crm/apps/api/prisma/schema.prisma
T
MahdiandCursor c2bfa7749d Add project board, loyalty club, and if/then automations.
Separate modules for post-sales kanban, person points/tiers, and deal-won/ticket-resolved rules without overlapping the sales pipeline.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 09:42:07 +03:30

855 lines
28 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 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 {
deal_won
ticket_resolved
}
enum AutomationAction {
create_activity
create_notification
add_loyalty_points
}
model Organization {
id String @id @default(cuid())
name String
slug String @unique
baseCurrency String @default("IRR")
enabledCurrencies String[] @default(["IRR", "USD", "EUR"])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
users User[]
companies Company[]
people Person[]
pipelines Pipeline[]
deals Deal[]
activities Activity[]
notes Note[]
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[]
automationRules AutomationRule[]
}
model User {
id String @id @default(cuid())
email String
passwordHash String
fullName String
role UserRole @default(sales)
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
ownedDeals Deal[] @relation("DealOwner")
ownedQuotes Quote[] @relation("QuoteOwner")
ownedInvoices Invoice[] @relation("InvoiceOwner")
ownedPeople Person[] @relation("PersonOwner")
ownedCompanies Company[] @relation("CompanyOwner")
ownedProjectCards ProjectCard[] @relation("ProjectCardOwner")
activities Activity[] @relation("ActivityAssignee")
notes Note[] @relation("NoteAuthor")
noteAttachments NoteAttachment[] @relation("NoteAttachmentUploader")
notifications Notification[]
salesGoals SalesGoal[] @relation("SalesGoalUser")
assignedTickets Ticket[] @relation("TicketAssignee")
reportedTickets Ticket[] @relation("TicketReporter")
ticketMessages TicketMessage[] @relation("TicketMessageAuthor")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([organizationId, email])
@@index([organizationId])
}
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?
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?
notes String?
customFields Json @default("{}")
loyaltyPoints Int @default(0)
loyaltyTierId String?
loyaltyTier LoyaltyTier? @relation(fields: [loyaltyTierId], references: [id], onDelete: SetNull)
deals Deal[]
activities Activity[]
smartNotes Note[]
quotes Quote[]
invoices Invoice[]
ownerId String?
owner User? @relation("PersonOwner", fields: [ownerId], references: [id], onDelete: SetNull)
tickets Ticket[]
projectCards ProjectCard[]
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)
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)
title String
description String?
dueAt DateTime?
completedAt 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)
assigneeId String?
assignee User? @relation("ActivityAssignee", fields: [assigneeId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, status, dueAt])
@@index([organizationId, assigneeId])
@@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?
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)
authorId String?
author User? @relation("NoteAuthor", fields: [authorId], references: [id], onDelete: SetNull)
attachments NoteAttachment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, pinned, updatedAt])
@@index([organizationId, noteType, updatedAt])
@@index([organizationId, reminderAt])
@@index([personId])
@@index([companyId])
@@index([dealId])
@@index([authorId])
}
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 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)
name String
sku String?
description String?
unitPrice Decimal? @db.Decimal(18, 2)
currency String @default("IRR")
isActive Boolean @default(true)
lineItems DealLineItem[]
quoteLineItems QuoteLineItem[]
invoiceLineItems InvoiceLineItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, name])
@@index([organizationId, isActive])
}
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
publicToken String @unique @default(cuid())
isActive Boolean @default(true)
createPerson Boolean @default(true)
createDeal Boolean @default(true)
pipelineId String?
source String?
thankYouMessage String?
redirectUrl String?
assigneeIds String[] @default([])
roundRobinIndex Int @default(0)
fields LeadFormField[]
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)
@@unique([formId, key])
@@index([formId, position])
}
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?
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)
currency String @default("IRR")
notes String?
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[]
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)
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
periodStart DateTime
periodEnd DateTime
targetAmount Decimal @db.Decimal(18, 2)
currency String @default("IRR")
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, periodStart, periodEnd])
@@index([userId])
}
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?
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)
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 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])
}