// ===================================================================== // МГТУ «СТАНКИН» — международная деятельность / личный кабинет // Prisma-схема (PostgreSQL). Выведена из проверенного schema.sql. // // ВАЖНО — что Prisma НЕ описывает и что остаётся в raw-SQL миграции // (применять файл schema.sql поверх после `prisma migrate`): // • Row-Level Security (политики доступа) // • триггеры (updated_at, аудит) // • роли БД и GRANT/REVOKE (наименьшие привилегии, неизменяемость аудита) // • шифрование полей (pgp_sym_encrypt) — приложение шифрует bytea-поля // • частичные индексы (WHERE ...) и CHECK-ограничения // // Поля externalId / sourceSystem / syncedAt / provider* — задел под // интеграции (1С, ЕСИА, эквайринг); сейчас не используются (NULL). // ===================================================================== generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ---------- Перечисления (маппятся на существующие enum-типы БД) ---------- enum UserRole { applicant student staff_dms admin @@map("user_role") } enum AccountStatus { pending active blocked @@map("account_status") } enum LocaleCode { ru en zh @@map("locale_code") } enum ApplicationStatus { new review docs_requested approved rejected @@map("application_status") } enum StudyBasis { quota contract @@map("study_basis") } enum DocumentKind { passport education transcript medical photo visa migration_card other @@map("document_kind") } enum DocumentStatus { uploaded under_review accepted rejected @@map("document_status") } enum PaymentStatus { pending paid failed refunded @@map("payment_status") } enum DormStatus { submitted review assigned declined @@map("dorm_status") } enum TicketStatus { open in_progress done @@map("ticket_status") } enum ConsentType { pdn_processing pdn_cross_border marketing pdn_transfer @@map("consent_type") } enum ContentType { news program partner page @@map("content_type") } enum ContentStatus { draft published archived @@map("content_status") } enum AccountTokenPurpose { email_verify password_reset @@map("account_token_purpose") } // ---------- Справочники ---------- model Country { code String @id @db.Char(2) nameRu String @map("name_ru") nameEn String @map("name_en") nameZh String @map("name_zh") persons Person[] @@map("country") } model Program { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid code String @unique level String faculty String? titleRu String @map("title_ru") titleEn String? @map("title_en") titleZh String? @map("title_zh") descriptionRu String? @map("description_ru") descriptionEn String? @map("description_en") descriptionZh String? @map("description_zh") language String? // коды языков обучения через запятую: ru,en,zh tuitionPerYear Int? @map("tuition_per_year") // годовая стоимость, ₽ currency String @default("RUB") @db.Char(3) durationSemesters Int? @map("duration_semesters") studyForm String? @map("study_form") isActive Boolean @default(true) @map("is_active") externalId String? @map("external_id") sourceSystem String? @map("source_system") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) applications Application[] students Student[] @@map("program") } // ---------- Учётные записи и доступ ---------- model Account { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid email String? @unique @db.Citext passwordHash String? @map("password_hash") role UserRole @default(applicant) locale LocaleCode @default(ru) status AccountStatus @default(pending) emailVerified Boolean @default(false) @map("email_verified") failedLogins Int @default(0) @map("failed_logins") lockedUntil DateTime? @map("locked_until") @db.Timestamptz(6) lastLoginAt DateTime? @map("last_login_at") @db.Timestamptz(6) externalId String? @map("external_id") sourceSystem String? @map("source_system") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) person Person? sessions AuthSession[] logins LoginAudit[] applicationsOwned Application[] @relation("ApplicationOwner") applicationsAssigned Application[] @relation("ApplicationAssignee") statusChanges ApplicationStatusHistory[] documentReviews DocumentReview[] ticketMessages TicketMessage[] notifications Notification[] tokens AccountToken[] photo AccountPhoto? @@map("account") } model AccountPhoto { accountId String @id @map("account_id") @db.Uuid account Account @relation(fields: [accountId], references: [id], onDelete: Cascade) fileKey String @map("file_key") mime String sizeBytes BigInt @map("size_bytes") updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) @@map("account_photo") } model AuthSession { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid accountId String @map("account_id") @db.Uuid account Account @relation(fields: [accountId], references: [id], onDelete: Cascade) tokenHash String @map("token_hash") ip String? @db.Inet userAgent String? @map("user_agent") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) expiresAt DateTime @map("expires_at") @db.Timestamptz(6) revokedAt DateTime? @map("revoked_at") @db.Timestamptz(6) @@index([accountId]) @@map("auth_session") } model LoginAudit { id BigInt @id @default(autoincrement()) accountId String? @map("account_id") @db.Uuid account Account? @relation(fields: [accountId], references: [id], onDelete: SetNull) emailTried String? @map("email_tried") @db.Citext success Boolean ip String? @db.Inet userAgent String? @map("user_agent") occurredAt DateTime @default(now()) @map("occurred_at") @db.Timestamptz(6) @@index([occurredAt]) @@map("login_audit") } // ---------- Персональные данные ---------- model Person { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid accountId String? @unique @map("account_id") @db.Uuid account Account? @relation(fields: [accountId], references: [id], onDelete: SetNull) surname String givenName String @map("given_name") patronymic String? sex String? @db.Char(1) birthDate DateTime? @map("birth_date") @db.Date citizenship String? @db.Char(2) country Country? @relation(fields: [citizenship], references: [code]) phoneEnc Bytes? @map("phone_enc") contactEmailEnc Bytes? @map("contact_email_enc") messengerEnc Bytes? @map("messenger_enc") snilsHash Bytes? @map("snils_hash") snilsEnc Bytes? @map("snils_enc") isAnonymized Boolean @default(false) @map("is_anonymized") retentionUntil DateTime? @map("retention_until") @db.Date createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) identityDocs PersonDocument[] consents Consent[] applications Application[] documents Document[] payments Payment[] dormApplications DormApplication[] tickets Ticket[] student Student? visaRecords VisaRecord[] @@map("person") } model PersonDocument { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid personId String @map("person_id") @db.Uuid person Person @relation(fields: [personId], references: [id], onDelete: Cascade) kind DocumentKind numberEnc Bytes? @map("number_enc") issuedBy String? @map("issued_by") issuedAt DateTime? @map("issued_at") @db.Date expiresAt DateTime? @map("expires_at") @db.Date createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) @@index([personId]) @@map("person_document") } model Consent { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid personId String @map("person_id") @db.Uuid person Person @relation(fields: [personId], references: [id], onDelete: Cascade) type ConsentType policyVersion String @map("policy_version") givenAt DateTime @default(now()) @map("given_at") @db.Timestamptz(6) withdrawnAt DateTime? @map("withdrawn_at") @db.Timestamptz(6) ip String? @db.Inet channel String? @@index([personId, type]) @@map("consent") } // ---------- Заявки ---------- model Application { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid personId String @map("person_id") @db.Uuid person Person @relation(fields: [personId], references: [id], onDelete: Cascade) accountId String? @map("account_id") @db.Uuid account Account? @relation("ApplicationOwner", fields: [accountId], references: [id], onDelete: SetNull) programId String? @map("program_id") @db.Uuid program Program? @relation(fields: [programId], references: [id]) programText String? @map("program_text") level String? studyLanguage LocaleCode? @map("study_language") basis StudyBasis? educationDoc String? @map("education_doc") comment String? status ApplicationStatus @default(new) assignedTo String? @map("assigned_to") @db.Uuid assignee Account? @relation("ApplicationAssignee", fields: [assignedTo], references: [id]) submittedAt DateTime @default(now()) @map("submitted_at") @db.Timestamptz(6) source String @default("web") externalId String? @map("external_id") sourceSystem String? @map("source_system") syncedAt DateTime? @map("synced_at") @db.Timestamptz(6) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) statusHistory ApplicationStatusHistory[] documents Document[] payments Payment[] @@index([status]) @@index([assignedTo]) @@index([personId]) @@map("application") } model ApplicationStatusHistory { id BigInt @id @default(autoincrement()) applicationId String @map("application_id") @db.Uuid application Application @relation(fields: [applicationId], references: [id], onDelete: Cascade) fromStatus ApplicationStatus? @map("from_status") toStatus ApplicationStatus @map("to_status") changedBy String? @map("changed_by") @db.Uuid changedByAccount Account? @relation(fields: [changedBy], references: [id]) comment String? changedAt DateTime @default(now()) @map("changed_at") @db.Timestamptz(6) @@index([applicationId]) @@map("application_status_history") } // ---------- Документы ---------- model Document { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid personId String @map("person_id") @db.Uuid person Person @relation(fields: [personId], references: [id], onDelete: Cascade) applicationId String? @map("application_id") @db.Uuid application Application? @relation(fields: [applicationId], references: [id], onDelete: SetNull) kind DocumentKind fileKey String @map("file_key") fileName String @map("file_name") mime String? sizeBytes BigInt? @map("size_bytes") checksum String? avScanned Boolean @default(false) @map("av_scanned") avClean Boolean? @map("av_clean") status DocumentStatus @default(uploaded) uploadedAt DateTime @default(now()) @map("uploaded_at") @db.Timestamptz(6) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) reviews DocumentReview[] @@index([personId]) @@index([applicationId]) @@map("document") } model DocumentReview { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid documentId String @map("document_id") @db.Uuid document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) reviewerId String @map("reviewer_id") @db.Uuid reviewer Account @relation(fields: [reviewerId], references: [id]) decision DocumentStatus comment String? decidedAt DateTime @default(now()) @map("decided_at") @db.Timestamptz(6) @@index([documentId]) @@map("document_review") } // ---------- Платежи ---------- model Payment { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid personId String @map("person_id") @db.Uuid person Person @relation(fields: [personId], references: [id], onDelete: Cascade) applicationId String? @map("application_id") @db.Uuid application Application? @relation(fields: [applicationId], references: [id], onDelete: SetNull) purpose String amountMinor BigInt @map("amount_minor") currency String @default("RUB") @db.Char(3) method String? status PaymentStatus @default(pending) provider String? providerTxnId String? @map("provider_txn_id") receiptUrl String? @map("receipt_url") paidAt DateTime? @map("paid_at") @db.Timestamptz(6) externalId String? @map("external_id") sourceSystem String? @map("source_system") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) @@index([personId]) @@index([status]) @@map("payment") } // ---------- Общежитие ---------- model DormApplication { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid personId String @map("person_id") @db.Uuid person Person @relation(fields: [personId], references: [id], onDelete: Cascade) roomType String? @map("room_type") moveInDate DateTime? @map("move_in_date") @db.Date preferences String? status DormStatus @default(submitted) assignedRoom String? @map("assigned_room") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) @@index([personId]) @@map("dorm_application") } // ---------- Обращения ---------- model Ticket { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid personId String @map("person_id") @db.Uuid person Person @relation(fields: [personId], references: [id], onDelete: Cascade) category String? subject String? status TicketStatus @default(open) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) messages TicketMessage[] @@index([personId]) @@map("ticket") } model TicketMessage { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid ticketId String @map("ticket_id") @db.Uuid ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) authorId String? @map("author_id") @db.Uuid author Account? @relation(fields: [authorId], references: [id], onDelete: SetNull) body String createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) @@index([ticketId]) @@map("ticket_message") } // ---------- Студент / виза ---------- model Student { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid personId String @unique @map("person_id") @db.Uuid person Person @relation(fields: [personId], references: [id], onDelete: Cascade) studentCardNo String? @map("student_card_no") programId String? @map("program_id") @db.Uuid program Program? @relation(fields: [programId], references: [id]) groupCode String? @map("group_code") yearOfStudy Int? @map("year_of_study") studyForm String? @map("study_form") contractNo String? @map("contract_no") contractStatus String? @map("contract_status") contractFrom DateTime? @map("contract_from") @db.Date contractTo DateTime? @map("contract_to") @db.Date status String? externalId String? @map("external_id") sourceSystem String? @map("source_system") syncedAt DateTime? @map("synced_at") @db.Timestamptz(6) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) @@map("student") } model VisaRecord { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid personId String @map("person_id") @db.Uuid person Person @relation(fields: [personId], references: [id], onDelete: Cascade) visaType String? @map("visa_type") validFrom DateTime? @map("valid_from") @db.Date validUntil DateTime? @map("valid_until") @db.Date registrationAddress String? @map("registration_address") migrationStatus String? @map("migration_status") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) @@index([personId]) @@map("visa_record") } // ---------- Уведомления ---------- model Notification { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid accountId String @map("account_id") @db.Uuid account Account @relation(fields: [accountId], references: [id], onDelete: Cascade) kind String title String? body String? payload Json? @db.JsonB readAt DateTime? @map("read_at") @db.Timestamptz(6) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) @@index([accountId]) @@map("notification") } // ---------- Контент (i18n) ---------- model ContentItem { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid type ContentType slug String status ContentStatus @default(draft) publishedAt DateTime? @map("published_at") @db.Timestamptz(6) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) translations ContentTranslation[] @@unique([type, slug]) @@map("content_item") } model ContentTranslation { contentId String @map("content_id") @db.Uuid content ContentItem @relation(fields: [contentId], references: [id], onDelete: Cascade) locale LocaleCode title String? body String? meta Json? @db.JsonB @@id([contentId, locale]) @@map("content_translation") } // ---------- Аудит (append-only; защита через GRANT в raw-SQL) ---------- model AuditLog { id BigInt @id @default(autoincrement()) occurredAt DateTime @default(now()) @map("occurred_at") @db.Timestamptz(6) actorAccountId String? @map("actor_account_id") @db.Uuid action String entityType String @map("entity_type") entityId String? @map("entity_id") @db.Uuid ip String? @db.Inet before Json? @db.JsonB after Json? @db.JsonB @@index([entityType, entityId]) @@index([occurredAt]) @@map("audit_log") } // ---------- Токены регистрации (подтверждение email / сброс пароля) ---------- model AccountToken { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid accountId String @map("account_id") @db.Uuid account Account @relation(fields: [accountId], references: [id], onDelete: Cascade) purpose AccountTokenPurpose tokenHash Bytes @map("token_hash") expiresAt DateTime @map("expires_at") @db.Timestamptz(6) usedAt DateTime? @map("used_at") @db.Timestamptz(6) ip String? @db.Inet createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) @@index([tokenHash]) @@index([accountId, purpose]) @@map("account_token") } // Редактор контента сайта: правки текстов страниц (отсутствие строки = исходный текст сборки) model SiteContent { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid page String key String lang LocaleCode value String updatedBy String? @map("updated_by") @db.Uuid updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz(6) @@unique([page, key, lang], map: "uq_site_content") @@index([page], map: "ix_site_content_page") @@map("site_content") } model SiteContentHistory { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid page String key String lang LocaleCode action String oldValue String? @map("old_value") newValue String? @map("new_value") changedBy String? @map("changed_by") @db.Uuid changedAt DateTime @default(now()) @map("changed_at") @db.Timestamptz(6) @@index([page, key, changedAt(sort: Desc)], map: "ix_site_content_history_key") @@map("site_content_history") } model SiteCollection { name String @id items Json updatedBy String? @map("updated_by") @db.Uuid updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz(6) @@map("site_collection") } model SiteCollectionHistory { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid name String action String oldItems Json? @map("old_items") newItems Json? @map("new_items") changedBy String? @map("changed_by") @db.Uuid changedAt DateTime @default(now()) @map("changed_at") @db.Timestamptz(6) @@index([name, changedAt(sort: Desc)], map: "ix_site_collection_history") @@map("site_collection_history") } model SiteMedia { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid fileKey String @unique @map("file_key") mime String sizeBytes BigInt @map("size_bytes") originalName String? @map("original_name") uploadedBy String? @map("uploaded_by") @db.Uuid createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) @@map("site_media") }