Files
fitcrm/apps/api/prisma/schema.prisma
root fb414d4a57
Some checks failed
CI / Lint & Format (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Build All Apps (push) Has been cancelled
CI / E2E Tests (Playwright) (push) Has been cancelled
CI / Deploy to Production (push) Has been cancelled
feat: CRM-модуль — Entity Factory Pattern, сделки, воронки, таймлайн, вебхуки
Sprint 9: Полная реализация CRM-модуля на базе Entity Factory Pattern.

Backend (45 файлов):
- Entity Factory Core: EntityManager<T> с lifecycle-хуками, событиями, правами
- Pipelines & Stages: CRUD, дефолтные B2B/B2C воронки с 7-8 стадиями
- Deals: создание, перемещение по стадиям, win/lose, cursor-пагинация, kanban view
- Timeline: лента событий (комментарии, звонки, стадии, формы), pin/unpin
- Activities: дела с планированием, завершением, просроченные через BullMQ scheduler
- Custom Fields: 8 типов (STRING/INTEGER/FLOAT/BOOLEAN/DATE/TIME/EMAIL/PHONE), CRUD
- Webhooks: антифрод (honeypot/timing/disposable/phone/fingerprint/IP), Smart Field Detection
- Trainings: entity manager с timeline-интеграцией
- CRM Scheduler: BullMQ processor (overdue activities, stale deals, unprocessed leads)

Frontend — Platform Admin + Club Admin:
- Kanban-доска с HTML5 drag-and-drop между стадиями
- Табличный вид со всеми фильтрами (pipeline, source, search)
- Карточка сделки: контакт, реквизиты, таймлайн, дела, тренировки
- Настройки CRM: 4 вкладки (воронки, кастомные поля, вебхуки, причины проигрыша)
- Форма лендинга: honeypot, timing, UTM, POST на /crm/deals/from-form

E2E тесты: Pipelines, Deals CRUD, Timeline, Activities, Form spam, RBAC, Lost Reasons, Custom Fields

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 07:04:59 +00:00

1531 lines
42 KiB
Plaintext

// MyFitCRM - Prisma Schema
// Multi-tenant CRM for fitness clubs
// Every table has club_id (except Club itself)
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
// ---------------------------------------------------------------------------
// Enums
// ---------------------------------------------------------------------------
enum UserRole {
TRAINER
COORDINATOR
MANAGER
RECEPTIONIST
CLUB_ADMIN
SUPER_ADMIN
}
enum Gender {
MALE
FEMALE
}
enum TrainingStatus {
PLANNED
CONFIRMED
COMPLETED
PAID
NOT_PAID
NO_SHOW
CANCELLED
}
enum CancelReason {
CLIENT_CANCEL
NO_DEDUCTION
SALE_RETURN
SICK
}
enum FunnelStage {
NEW
ASSIGNED
APPOINTMENT_SET
COMPLETED
REGULAR
TRANSFERRED
REFUSED
}
enum SaleStatus {
PENDING
PAID
CANCELLED
REFUNDED
}
enum CallDirection {
OUTBOUND
}
enum CallStatus {
INITIATED
RINGING
ANSWERED
COMPLETED
FAILED
NO_ANSWER
BUSY
}
enum NotificationType {
CLIENT_ASSIGNED
SERVICE_BOOKED
SERVICE_PAID
PACKAGE_TOPUP
BOOKING_CANCELLED
TRAINING_DEDUCTED
TRAININGS_ENDING
CLIENT_RETURNED
CLIENT_BIRTHDAY
}
enum LicenseType {
STARTER
STANDARD
PREMIUM
}
enum ReportStatus {
PENDING
GENERATING
COMPLETED
FAILED
}
enum SipTransport {
UDP
TCP
TLS
}
enum Platform {
IOS
ANDROID
}
enum EmailType {
WELCOME
TRAINING_REMINDER
MEMBERSHIP_EXPIRING
PROMO
BIRTHDAY
}
enum EmailStatus {
PENDING
SENT
DELIVERED
OPENED
CLICKED
BOUNCED
FAILED
}
enum ClientServiceStatus {
ACTIVE
EXPIRED
CANCELLED
FROZEN
}
// CRM Enums
enum CrmDealSource {
FORM
MANUAL
IMPORT
API
WEBHOOK
}
enum CrmStageType {
OPEN
WON
LOST
}
enum CrmActivityType {
CALL
MEETING
EMAIL
MESSAGE
CUSTOM
}
enum CrmTimelineType {
FORM_SUBMISSION
CALL_INCOMING
CALL_OUTGOING
COMMENT
MESSAGE_TG
MESSAGE_WA
MESSAGE_VK
MESSAGE_EMAIL
STAGE_CHANGE
ACTIVITY_CREATED
ACTIVITY_DONE
TRAINING_CREATED
SYSTEM
}
enum CrmTrainingType {
PERSONAL
GROUP
TRIAL
SPLIT
}
enum CrmTrainingStatus {
CRM_PLANNED
CRM_COMPLETED
CRM_CANCELLED
CRM_NO_SHOW
}
enum CrmUserFieldType {
STRING
INTEGER
FLOAT
LIST
BOOLEAN
DATE
DATETIME
TIME
}
enum CrmActivityPriority {
LOW
MEDIUM
HIGH
URGENT
}
enum CrmMessageChannel {
TG
WHATSAPP
VK
EMAIL
}
// ---------------------------------------------------------------------------
// 1. Club
// ---------------------------------------------------------------------------
model Club {
id String @id @default(uuid())
name String
slug String @unique
address String?
phone String?
email String?
logoUrl String?
timezone String @default("Europe/Moscow")
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
users User[]
apiKeys ApiKey[]
departments Department[]
rooms Room[]
services Service[]
serviceCategories ServiceCategory[]
packages Package[]
clients Client[]
clientMemberships ClientMembership[]
clientGoals ClientGoal[]
clientInjuries ClientInjury[]
clientServices ClientService[]
trainings Training[]
funnelEntries FunnelEntry[]
sales Sale[]
workSchedules WorkSchedule[]
sipCalls SipCall[]
sipConfig SipConfig?
sipAccounts SipAccount[]
notifications Notification[]
pushTokens PushToken[]
webhookSubscriptions WebhookSubscription[]
webhookLogs WebhookLog[]
clubModules ClubModule[]
license License?
usageRecords UsageRecord[]
auditLogs AuditLog[]
integrationConfig IntegrationConfig?
syncLogs SyncLog[]
notificationSettings NotificationSetting[]
reports Report[]
ratingPeriods RatingPeriod[]
emailTemplates EmailTemplate[]
emailLogs EmailLog[]
// CRM relations
crmPipelines CrmPipeline[]
crmDeals CrmDeal[]
crmTrainings CrmTraining[]
crmLostReasons CrmLostReason[]
crmUserFields CrmUserField[]
crmWebhookEndpoints CrmWebhookEndpoint[]
@@map("clubs")
}
// ---------------------------------------------------------------------------
// 2. User (all 6 roles)
// ---------------------------------------------------------------------------
model User {
id String @id @default(uuid())
clubId String
phone String
passwordHash String
firstName String
lastName String
patronymic String?
role UserRole
departmentId String?
avatarUrl String?
isActive Boolean @default(true)
totpSecret String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
department Department? @relation(fields: [departmentId], references: [id], onDelete: SetNull)
refreshTokens RefreshToken[]
trainingsAsTrainer Training[] @relation("TrainerTrainings")
assignedClients Client[] @relation("AssignedTrainer")
funnelEntries FunnelEntry[] @relation("FunnelTrainer")
sales Sale[]
workSchedules WorkSchedule[]
sipCalls SipCall[]
sipAccount SipAccount?
notifications Notification[]
pushTokens PushToken[]
auditLogs AuditLog[]
reports Report[]
ratingPeriods RatingPeriod[]
// CRM relations
crmDealsAssigned CrmDeal[] @relation("CrmDealAssignee")
crmDealsCreated CrmDeal[] @relation("CrmDealCreator")
crmTrainingsAsTrainer CrmTraining[] @relation("CrmTrainingTrainer")
crmTimelineEntries CrmTimeline[] @relation("CrmTimelineAuthor")
crmActivitiesAssigned CrmActivity[] @relation("CrmActivityAssignee")
crmActivitiesCompleted CrmActivity[] @relation("CrmActivityCompleter")
crmStageHistoryMoves CrmStageHistory[] @relation("CrmStageHistoryMover")
@@unique([clubId, phone])
@@index([clubId])
@@index([departmentId])
@@map("users")
}
// ---------------------------------------------------------------------------
// 3. RefreshToken
// ---------------------------------------------------------------------------
model RefreshToken {
id String @id @default(uuid())
userId String
token String @unique
expiresAt DateTime
createdAt DateTime @default(now())
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@map("refresh_tokens")
}
// ---------------------------------------------------------------------------
// 4. ApiKey
// ---------------------------------------------------------------------------
model ApiKey {
id String @id @default(uuid())
clubId String
key String @unique
name String
scopes String[]
isActive Boolean @default(true)
lastUsedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
@@index([clubId])
@@map("api_keys")
}
// ---------------------------------------------------------------------------
// 5. Department
// ---------------------------------------------------------------------------
model Department {
id String @id @default(uuid())
clubId String
name String
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
users User[]
@@index([clubId])
@@map("departments")
}
// ---------------------------------------------------------------------------
// 6. Room
// ---------------------------------------------------------------------------
model Room {
id String @id @default(uuid())
clubId String
name String
capacity Int
description String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
trainings Training[]
@@index([clubId])
@@map("rooms")
}
// ---------------------------------------------------------------------------
// 7. Service
// ---------------------------------------------------------------------------
model Service {
id String @id @default(uuid())
clubId String
name String
description String?
price Decimal @db.Decimal(12, 2)
duration Int
categoryId String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
category ServiceCategory? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
packages Package[]
trainings Training[]
sales Sale[]
clientServices ClientService[]
@@index([clubId])
@@index([categoryId])
@@map("services")
}
// ---------------------------------------------------------------------------
// 8. ServiceCategory
// ---------------------------------------------------------------------------
model ServiceCategory {
id String @id @default(uuid())
clubId String
name String
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
services Service[]
@@index([clubId])
@@map("service_categories")
}
// ---------------------------------------------------------------------------
// 9. Package
// ---------------------------------------------------------------------------
model Package {
id String @id @default(uuid())
clubId String
name String
description String?
price Decimal @db.Decimal(12, 2)
totalSessions Int
validDays Int
serviceId String
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade)
sales Sale[]
@@index([clubId])
@@index([serviceId])
@@map("packages")
}
// ---------------------------------------------------------------------------
// 10. Client
// ---------------------------------------------------------------------------
model Client {
id String @id @default(uuid())
clubId String
externalId String?
firstName String
lastName String
patronymic String?
phone String
email String?
birthDate DateTime?
gender Gender?
avatarUrl String?
notes String?
source String?
assignedTrainerId String?
lastActivityAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
assignedTrainer User? @relation("AssignedTrainer", fields: [assignedTrainerId], references: [id], onDelete: SetNull)
memberships ClientMembership[]
goals ClientGoal[]
injuries ClientInjury[]
clientServices ClientService[]
trainings Training[]
funnelEntries FunnelEntry[]
sales Sale[]
sipCalls SipCall[]
emailLogs EmailLog[]
@@index([clubId])
@@index([assignedTrainerId])
@@index([clubId, phone])
@@index([clubId, externalId])
@@map("clients")
}
// ---------------------------------------------------------------------------
// 11. ClientMembership
// ---------------------------------------------------------------------------
model ClientMembership {
id String @id @default(uuid())
clubId String
clientId String
name String
startDate DateTime
endDate DateTime
isActive Boolean @default(true)
externalId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
@@index([clubId])
@@index([clientId])
@@map("client_memberships")
}
// ---------------------------------------------------------------------------
// 12. Training
// ---------------------------------------------------------------------------
model Training {
id String @id @default(uuid())
clubId String
trainerId String
clientId String
serviceId String?
roomId String?
scheduledAt DateTime
duration Int
status TrainingStatus @default(PLANNED)
cancelReason CancelReason?
cancelComment String?
isRepeating Boolean @default(false)
repeatParentId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
trainer User @relation("TrainerTrainings", fields: [trainerId], references: [id], onDelete: Cascade)
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
service Service? @relation(fields: [serviceId], references: [id], onDelete: SetNull)
room Room? @relation(fields: [roomId], references: [id], onDelete: SetNull)
repeatParent Training? @relation("TrainingRepeat", fields: [repeatParentId], references: [id], onDelete: SetNull)
repeatChildren Training[] @relation("TrainingRepeat")
@@index([clubId])
@@index([trainerId])
@@index([clientId])
@@index([status])
@@index([clubId, scheduledAt])
@@index([clubId, status, scheduledAt])
@@map("trainings")
}
// ---------------------------------------------------------------------------
// 13. FunnelEntry
// ---------------------------------------------------------------------------
model FunnelEntry {
id String @id @default(uuid())
clubId String
clientId String
trainerId String?
stage FunnelStage @default(NEW)
previousStage FunnelStage?
comment String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
trainer User? @relation("FunnelTrainer", fields: [trainerId], references: [id], onDelete: SetNull)
@@index([clubId])
@@index([clientId])
@@index([trainerId])
@@index([clubId, stage])
@@map("funnel_entries")
}
// ---------------------------------------------------------------------------
// 14. Sale
// ---------------------------------------------------------------------------
model Sale {
id String @id @default(uuid())
clubId String
trainerId String
clientId String
packageId String?
serviceId String?
amount Decimal @db.Decimal(12, 2)
description String?
status SaleStatus @default(PENDING)
paidAt DateTime?
externalId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
trainer User @relation(fields: [trainerId], references: [id], onDelete: Cascade)
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
package Package? @relation(fields: [packageId], references: [id], onDelete: SetNull)
service Service? @relation(fields: [serviceId], references: [id], onDelete: SetNull)
@@index([clubId])
@@index([trainerId])
@@index([clientId])
@@index([clubId, status])
@@index([clubId, createdAt])
@@map("sales")
}
// ---------------------------------------------------------------------------
// 15. WorkSchedule
// ---------------------------------------------------------------------------
model WorkSchedule {
id String @id @default(uuid())
clubId String
trainerId String
date DateTime
startTime String
endTime String
isAvailable Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
trainer User @relation(fields: [trainerId], references: [id], onDelete: Cascade)
@@index([clubId])
@@index([trainerId])
@@index([clubId, trainerId, date])
@@map("work_schedules")
}
// ---------------------------------------------------------------------------
// 16. SipCall
// ---------------------------------------------------------------------------
model SipCall {
id String @id @default(uuid())
clubId String
callerId String
clientId String?
callerNumber String
calleeNumber String
direction CallDirection @default(OUTBOUND)
status CallStatus @default(INITIATED)
startedAt DateTime
answeredAt DateTime?
endedAt DateTime?
duration Int?
createdAt DateTime @default(now())
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
caller User @relation(fields: [callerId], references: [id], onDelete: Cascade)
client Client? @relation(fields: [clientId], references: [id], onDelete: SetNull)
@@index([clubId])
@@index([callerId])
@@index([clientId])
@@index([clubId, startedAt])
@@map("sip_calls")
}
// ---------------------------------------------------------------------------
// 17. Notification
// ---------------------------------------------------------------------------
model Notification {
id String @id @default(uuid())
clubId String
userId String
type NotificationType
title String
body String
data Json?
isRead Boolean @default(false)
sentAt DateTime
createdAt DateTime @default(now())
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([clubId])
@@index([userId])
@@index([userId, isRead])
@@index([userId, createdAt])
@@map("notifications")
}
// ---------------------------------------------------------------------------
// 18. WebhookSubscription
// ---------------------------------------------------------------------------
model WebhookSubscription {
id String @id @default(uuid())
clubId String
url String
secret String
events String[]
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
logs WebhookLog[]
@@index([clubId])
@@map("webhook_subscriptions")
}
// ---------------------------------------------------------------------------
// 19. WebhookLog
// ---------------------------------------------------------------------------
model WebhookLog {
id String @id @default(uuid())
clubId String
subscriptionId String
event String
payload Json
statusCode Int?
response String?
attempt Int @default(1)
deliveredAt DateTime?
createdAt DateTime @default(now())
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
subscription WebhookSubscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
@@index([clubId])
@@index([subscriptionId])
@@map("webhook_logs")
}
// ---------------------------------------------------------------------------
// 20. ClubModule
// ---------------------------------------------------------------------------
model ClubModule {
id String @id @default(uuid())
clubId String
moduleId String
enabled Boolean @default(false)
limitsJson Json?
usageJson Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
@@unique([clubId, moduleId])
@@index([clubId])
@@map("club_modules")
}
// ---------------------------------------------------------------------------
// 21. License
// ---------------------------------------------------------------------------
model License {
id String @id @default(uuid())
clubId String @unique
type LicenseType @default(STARTER)
startDate DateTime
endDate DateTime
isActive Boolean @default(true)
gracePeriodEnd DateTime?
maxUsers Int
maxClients Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
@@map("licenses")
}
// ---------------------------------------------------------------------------
// 22. UsageRecord
// ---------------------------------------------------------------------------
model UsageRecord {
id String @id @default(uuid())
clubId String
period DateTime
activeUsers Int @default(0)
clients Int @default(0)
callStorageGb Decimal @default(0) @db.Decimal(10, 4)
webhooksSent Int @default(0)
pushSent Int @default(0)
apiRequests Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
@@unique([clubId, period])
@@index([clubId])
@@map("usage_records")
}
// ---------------------------------------------------------------------------
// 23. AuditLog
// ---------------------------------------------------------------------------
model AuditLog {
id String @id @default(uuid())
clubId String?
userId String?
action String
entity String
entityId String?
oldData Json?
newData Json?
ipAddress String?
createdAt DateTime @default(now())
// Relations
club Club? @relation(fields: [clubId], references: [id], onDelete: SetNull)
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
@@index([clubId])
@@index([userId])
@@index([entity, entityId])
@@index([createdAt])
@@map("audit_logs")
}
// ---------------------------------------------------------------------------
// 24. IntegrationConfig
// ---------------------------------------------------------------------------
model IntegrationConfig {
id String @id @default(uuid())
clubId String @unique
provider String @default("1c")
apiUrl String?
apiKey String?
syncEnabled Boolean @default(false)
lastSyncAt DateTime?
settings Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
syncLogs SyncLog[]
@@map("integration_configs")
}
// ---------------------------------------------------------------------------
// 25. NotificationSetting
// ---------------------------------------------------------------------------
model NotificationSetting {
id String @id @default(uuid())
clubId String
notificationType NotificationType
enabled Boolean @default(true)
customText String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
@@unique([clubId, notificationType])
@@index([clubId])
@@map("notification_settings")
}
// ---------------------------------------------------------------------------
// 26. ClientGoal
// ---------------------------------------------------------------------------
model ClientGoal {
id String @id @default(uuid())
clubId String
clientId String
title String
isAchieved Boolean @default(false)
createdAt DateTime @default(now())
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
@@index([clubId])
@@index([clientId])
@@map("client_goals")
}
// ---------------------------------------------------------------------------
// 27. ClientInjury
// ---------------------------------------------------------------------------
model ClientInjury {
id String @id @default(uuid())
clubId String
clientId String
description String
severity String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
@@index([clubId])
@@index([clientId])
@@map("client_injuries")
}
// ---------------------------------------------------------------------------
// 28. ClientService (purchased services/packages)
// ---------------------------------------------------------------------------
model ClientService {
id String @id @default(uuid())
clubId String
clientId String
serviceId String
sessionsTotal Int?
sessionsUsed Int @default(0)
startsAt DateTime?
expiresAt DateTime?
status ClientServiceStatus @default(ACTIVE)
externalId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade)
@@index([clubId])
@@index([clientId])
@@index([serviceId])
@@index([expiresAt])
@@index([clientId, status])
@@map("client_services")
}
// ---------------------------------------------------------------------------
// 29. SipConfig (per-club SIP settings)
// ---------------------------------------------------------------------------
model SipConfig {
id String @id @default(uuid())
clubId String @unique
sipServer String
sipPort Int @default(5060)
transport SipTransport @default(TLS)
realm String?
stunServer String?
turnServer String?
turnUsername String?
turnPassword String?
callerId String
codecs String[] @default(["opus", "g711_alaw", "g711_ulaw"])
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
@@map("sip_configs")
}
// ---------------------------------------------------------------------------
// 30. SipAccount (per-user SIP credentials)
// ---------------------------------------------------------------------------
model SipAccount {
id String @id @default(uuid())
clubId String
userId String @unique
sipUsername String
sipPassword String
displayName String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([clubId])
@@map("sip_accounts")
}
// ---------------------------------------------------------------------------
// 31. PushToken (device push tokens)
// ---------------------------------------------------------------------------
model PushToken {
id String @id @default(uuid())
clubId String
userId String
token String
platform Platform
deviceId String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([clubId])
@@index([userId])
@@map("push_tokens")
}
// ---------------------------------------------------------------------------
// 32. SyncLog (1C sync history)
// ---------------------------------------------------------------------------
model SyncLog {
id String @id @default(uuid())
clubId String
integrationId String
direction String
entityType String
recordsProcessed Int @default(0)
recordsFailed Int @default(0)
errorDetails String?
startedAt DateTime
completedAt DateTime?
createdAt DateTime @default(now())
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
integration IntegrationConfig @relation(fields: [integrationId], references: [id], onDelete: Cascade)
@@index([clubId])
@@index([integrationId])
@@map("sync_logs")
}
// ---------------------------------------------------------------------------
// 33. Report (generated PDF reports)
// ---------------------------------------------------------------------------
model Report {
id String @id @default(uuid())
clubId String
generatedById String
type String
title String
parametersJson Json?
fileUrl String?
status ReportStatus @default(PENDING)
generatedAt DateTime?
createdAt DateTime @default(now())
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
generatedBy User @relation(fields: [generatedById], references: [id], onDelete: Cascade)
@@index([clubId])
@@index([generatedById])
@@map("reports")
}
// ---------------------------------------------------------------------------
// 34. RatingPeriod (trainer rating snapshots)
// ---------------------------------------------------------------------------
model RatingPeriod {
id String @id @default(uuid())
clubId String
userId String
period DateTime
conversionNewToAssigned Decimal? @db.Decimal(5, 2)
conversionAssignedToCompleted Decimal? @db.Decimal(5, 2)
conversionCompletedToRegular Decimal? @db.Decimal(5, 2)
totalTrainings Int @default(0)
totalSalesAmount Decimal @default(0) @db.Decimal(12, 2)
totalClients Int @default(0)
newClients Int @default(0)
rank Int?
score Decimal? @db.Decimal(5, 2)
createdAt DateTime @default(now())
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([clubId, userId, period])
@@index([clubId])
@@index([userId])
@@map("rating_periods")
}
// ---------------------------------------------------------------------------
// 35. EmailTemplate
// ---------------------------------------------------------------------------
model EmailTemplate {
id String @id @default(uuid())
clubId String
type EmailType
subject String
bodyHtml String
bodyText String?
variables String[] @default([])
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
emailLogs EmailLog[]
@@unique([clubId, type])
@@index([clubId])
@@map("email_templates")
}
// ---------------------------------------------------------------------------
// 36. EmailLog
// ---------------------------------------------------------------------------
model EmailLog {
id String @id @default(uuid())
clubId String
templateId String?
recipientEmail String
recipientClientId String?
subject String
type EmailType
status EmailStatus @default(PENDING)
providerMessageId String?
errorMessage String?
sentAt DateTime?
deliveredAt DateTime?
openedAt DateTime?
clickedAt DateTime?
createdAt DateTime @default(now())
// Relations
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
template EmailTemplate? @relation(fields: [templateId], references: [id], onDelete: SetNull)
client Client? @relation(fields: [recipientClientId], references: [id], onDelete: SetNull)
@@index([clubId])
@@index([templateId])
@@index([recipientClientId])
@@index([clubId, type])
@@index([clubId, status])
@@map("email_logs")
}
// ---------------------------------------------------------------------------
// 37. PlatformTheme (platform-level active theme — single row, no clubId)
// ---------------------------------------------------------------------------
model PlatformTheme {
id String @id @default(uuid())
themeId String
setById String?
createdAt DateTime @default(now())
@@index([createdAt])
@@map("platform_themes")
}
// ---------------------------------------------------------------------------
// CRM Module
// ---------------------------------------------------------------------------
// 38. CrmPipeline
model CrmPipeline {
id String @id @default(uuid())
clubId String?
name String
isDefault Boolean @default(false)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
club Club? @relation(fields: [clubId], references: [id], onDelete: Cascade)
stages CrmStage[]
deals CrmDeal[]
@@index([clubId])
@@map("crm_pipelines")
}
// 39. CrmStage
model CrmStage {
id String @id @default(uuid())
pipelineId String
name String
position Int
color String @default("#3B82F6")
type CrmStageType @default(OPEN)
autoActivityType CrmActivityType?
autoActivityDays Int?
staleDays Int?
isDefault Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
pipeline CrmPipeline @relation(fields: [pipelineId], references: [id], onDelete: Cascade)
deals CrmDeal[]
stageHistoryFrom CrmStageHistory[] @relation("StageHistoryFrom")
stageHistoryTo CrmStageHistory[] @relation("StageHistoryTo")
@@index([pipelineId])
@@index([pipelineId, position])
@@map("crm_stages")
}
// 40. CrmDeal
model CrmDeal {
id String @id @default(uuid())
clubId String?
pipelineId String
stageId String
assigneeId String?
title String
amount Decimal? @db.Decimal(12, 2)
currency String @default("RUB")
// Contact info
contactName String
contactPhone String?
contactEmail String?
contactTelegram String?
contactWhatsapp String?
contactVk String?
// Company requisites
companyName String?
companyInn String?
companyKpp String?
companyOgrn String?
companyLegalAddress String?
companyBankAccount String?
companyBik String?
companyBankName String?
// Meta
source CrmDealSource @default(MANUAL)
probability Int?
expectedCloseDate DateTime?
closedAt DateTime?
lostReasonId String?
lostComment String?
metadata Json?
deletedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdById String?
// Relations
club Club? @relation(fields: [clubId], references: [id], onDelete: Cascade)
pipeline CrmPipeline @relation(fields: [pipelineId], references: [id], onDelete: Cascade)
stage CrmStage @relation(fields: [stageId], references: [id], onDelete: Cascade)
assignee User? @relation("CrmDealAssignee", fields: [assigneeId], references: [id], onDelete: SetNull)
createdBy User? @relation("CrmDealCreator", fields: [createdById], references: [id], onDelete: SetNull)
lostReason CrmLostReason? @relation(fields: [lostReasonId], references: [id], onDelete: SetNull)
timeline CrmTimeline[]
activities CrmActivity[]
trainings CrmTraining[]
stageHistory CrmStageHistory[]
@@index([clubId])
@@index([pipelineId])
@@index([stageId])
@@index([assigneeId])
@@index([clubId, stageId])
@@index([clubId, createdAt])
@@index([clubId, pipelineId, stageId])
@@map("crm_deals")
}
// 41. CrmTraining
model CrmTraining {
id String @id @default(uuid())
clubId String?
dealId String
trainerId String?
clientName String
type CrmTrainingType @default(PERSONAL)
status CrmTrainingStatus @default(CRM_PLANNED)
scheduledAt DateTime
duration Int @default(60)
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
club Club? @relation(fields: [clubId], references: [id], onDelete: Cascade)
deal CrmDeal @relation(fields: [dealId], references: [id], onDelete: Cascade)
trainer User? @relation("CrmTrainingTrainer", fields: [trainerId], references: [id], onDelete: SetNull)
@@index([clubId])
@@index([dealId])
@@index([trainerId])
@@map("crm_trainings")
}
// 42. CrmTimeline
model CrmTimeline {
id String @id @default(uuid())
dealId String
userId String?
type CrmTimelineType
subject String?
content String?
metadata Json?
pinnedAt DateTime?
createdAt DateTime @default(now())
deal CrmDeal @relation(fields: [dealId], references: [id], onDelete: Cascade)
user User? @relation("CrmTimelineAuthor", fields: [userId], references: [id], onDelete: SetNull)
@@index([dealId])
@@index([dealId, createdAt])
@@map("crm_timeline")
}
// 43. CrmActivity
model CrmActivity {
id String @id @default(uuid())
dealId String?
assigneeId String
type CrmActivityType @default(CALL)
subject String
description String?
scheduledAt DateTime
deadline DateTime?
priority CrmActivityPriority @default(MEDIUM)
completedAt DateTime?
completedById String?
result String?
channel CrmMessageChannel?
isOverdue Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deal CrmDeal? @relation(fields: [dealId], references: [id], onDelete: Cascade)
assignee User @relation("CrmActivityAssignee", fields: [assigneeId], references: [id], onDelete: Cascade)
completedBy User? @relation("CrmActivityCompleter", fields: [completedById], references: [id], onDelete: SetNull)
@@index([dealId])
@@index([assigneeId])
@@index([assigneeId, scheduledAt])
@@index([assigneeId, completedAt])
@@map("crm_activities")
}
// 44. CrmStageHistory
model CrmStageHistory {
id String @id @default(uuid())
dealId String
fromStageId String?
toStageId String
movedById String?
duration Int?
movedAt DateTime @default(now())
deal CrmDeal @relation(fields: [dealId], references: [id], onDelete: Cascade)
fromStage CrmStage? @relation("StageHistoryFrom", fields: [fromStageId], references: [id], onDelete: SetNull)
toStage CrmStage @relation("StageHistoryTo", fields: [toStageId], references: [id], onDelete: Cascade)
movedBy User? @relation("CrmStageHistoryMover", fields: [movedById], references: [id], onDelete: SetNull)
@@index([dealId])
@@index([dealId, movedAt])
@@map("crm_stage_history")
}
// 45. CrmLostReason
model CrmLostReason {
id String @id @default(uuid())
clubId String?
name String
position Int @default(0)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
club Club? @relation(fields: [clubId], references: [id], onDelete: Cascade)
deals CrmDeal[]
@@index([clubId])
@@map("crm_lost_reasons")
}
// 46. CrmUserField
model CrmUserField {
id String @id @default(uuid())
clubId String?
entityType String
name String
fieldName String
type CrmUserFieldType @default(STRING)
listOptions Json?
isRequired Boolean @default(false)
isMultiple Boolean @default(false)
showToTrainer Boolean @default(false)
position Int @default(0)
defaultValue String?
description String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
club Club? @relation(fields: [clubId], references: [id], onDelete: Cascade)
values CrmUserFieldValue[]
@@unique([clubId, entityType, fieldName])
@@index([clubId, entityType])
@@map("crm_user_fields")
}
// 47. CrmUserFieldValue
model CrmUserFieldValue {
id String @id @default(uuid())
fieldId String
entityId String
entityType String
value String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
field CrmUserField @relation(fields: [fieldId], references: [id], onDelete: Cascade)
@@unique([fieldId, entityId])
@@index([fieldId])
@@index([entityId, entityType])
@@map("crm_user_field_values")
}
// 48. CrmWebhookEndpoint
model CrmWebhookEndpoint {
id String @id @default(uuid())
clubId String?
pipelineId String?
stageId String?
token String @unique
name String
isActive Boolean @default(true)
fieldMappings Json?
listMappings Json?
defaultAssigneeId String?
antifraudEnabled Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
club Club? @relation(fields: [clubId], references: [id], onDelete: Cascade)
@@index([clubId])
@@map("crm_webhook_endpoints")
}