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[] 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[] } 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") 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") 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[] 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[] 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) 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) attachments NoteAttachment[] aiSuggestion NoteAiSuggestion? 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([activityId]) @@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 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) createPerson Boolean @default(true) createDeal Boolean @default(true) pipelineId String? source String? thankYouMessage String? redirectUrl String? assigneeIds String[] @default([]) roundRobinIndex Int @default(0) submitLabel String @default("ارسال درخواست") submitColor String @default("#14b8a6") coverStorageKey String? 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? createdAt DateTime @default(now()) @@index([formId, createdAt]) } 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 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]) }