generator client { provider = "prisma-client-js" previewFeatures = ["postgresqlExtensions"] } datasource db { provider = "postgresql" url = env("DATABASE_URL") extensions = [vector, uuidOssp(map: "uuid-ossp")] } // ========================================================================= // AUTH / RBAC // ========================================================================= enum Role { MANAGER SENIOR_ADMIN SECURITY SYSADMIN } model User { id String @id @default(uuid()) @db.Uuid email String @unique fullName String @map("full_name") passwordHash String @map("password_hash") role Role isActive Boolean @default(true) @map("is_active") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") refreshTokens RefreshToken[] consentActions PatientConsent[] biometryAccessLog BiometryAccessLog[] @@map("users") } model RefreshToken { id String @id @default(uuid()) @db.Uuid userId String @map("user_id") @db.Uuid tokenHash String @unique @map("token_hash") expiresAt DateTime @map("expires_at") revokedAt DateTime? @map("revoked_at") createdAt DateTime @default(now()) @map("created_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@map("refresh_tokens") } // ========================================================================= // DOMAIN: PATIENT / CONSENT // ========================================================================= model Patient { id String @id @default(uuid()) @db.Uuid polimedPatientId String? @unique @map("polimed_patient_id") fullName String? @map("full_name") consentReceivedAt DateTime? @map("consent_received_at") consentRevokedAt DateTime? @map("consent_revoked_at") pendingDeletionAt DateTime? @map("pending_deletion_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") consents PatientConsent[] tracks Track[] visits Visit[] consentRevocationJobs ConsentRevocationJob[] @@map("patients") } enum ConsentAction { GRANTED REVOKED } model PatientConsent { id String @id @default(uuid()) @db.Uuid patientId String @map("patient_id") @db.Uuid action ConsentAction paperRef String? @map("paper_ref") actorUserId String @map("actor_user_id") @db.Uuid occurredAt DateTime @default(now()) @map("occurred_at") patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade) actor User @relation(fields: [actorUserId], references: [id]) @@index([patientId]) @@map("patient_consents") } // ========================================================================= // CAMERAS / ZONES // ========================================================================= enum ZoneCode { A B C D E } model Zone { id String @id @default(uuid()) @db.Uuid code ZoneCode @unique name String cameras Camera[] trackEvents TrackEvent[] @@map("zones") } model Camera { id String @id @default(uuid()) @db.Uuid name String @unique rtspUrl String? @map("rtsp_url") zoneId String @map("zone_id") @db.Uuid zone Zone @relation(fields: [zoneId], references: [id]) trackEvents TrackEvent[] faceEmbeddings FaceEmbedding[] @@map("cameras") } // ========================================================================= // TRACKS / EVENTS / EMBEDDINGS // ========================================================================= enum TrackStatus { UNMATCHED MATCHED ANONYMIZED } model Track { id String @id @default(uuid()) @db.Uuid patientId String? @map("patient_id") @db.Uuid status TrackStatus @default(UNMATCHED) firstSeenAt DateTime @map("first_seen_at") lastSeenAt DateTime @map("last_seen_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") patient Patient? @relation(fields: [patientId], references: [id]) events TrackEvent[] faceEmbeddings FaceEmbedding[] @@index([status, firstSeenAt]) @@index([patientId]) @@map("tracks") } enum TrackEventType { arrived waiting service_started service_ended left_without_service } model TrackEvent { id String @id @default(uuid()) @db.Uuid trackId String @map("track_id") @db.Uuid type TrackEventType cameraId String @map("camera_id") @db.Uuid zoneId String @map("zone_id") @db.Uuid occurredAt DateTime @map("occurred_at") evidenceKey String? @map("evidence_key") // [x1, y1, x2, y2] нормализовано 0..1 — bbox распознанного лица на кадре. faceBbox Json? @map("face_bbox") @db.JsonB createdAt DateTime @default(now()) @map("created_at") track Track @relation(fields: [trackId], references: [id], onDelete: Cascade) camera Camera @relation(fields: [cameraId], references: [id]) zone Zone @relation(fields: [zoneId], references: [id]) @@index([trackId, occurredAt]) @@map("track_events") } // 512-d face embedding (InsightFace buffalo_l). // face-service пишет и читает напрямую через psycopg2 + pgvector. model FaceEmbedding { id String @id @default(uuid()) @db.Uuid embedding Unsupported("vector(512)") patientId String? @map("patient_id") @db.Uuid trackId String? @map("track_id") @db.Uuid cameraId String @map("camera_id") @db.Uuid quality Float @default(0) capturedAt DateTime @map("captured_at") createdAt DateTime @default(now()) @map("created_at") track Track? @relation(fields: [trackId], references: [id], onDelete: SetNull) camera Camera @relation(fields: [cameraId], references: [id]) @@index([trackId]) @@index([patientId]) @@index([capturedAt]) @@map("face_embeddings") } // ========================================================================= // VISITS // ========================================================================= model Visit { id String @id @default(uuid()) @db.Uuid patientId String @map("patient_id") @db.Uuid polimedAppointmentId String? @map("polimed_appointment_id") arrivedAt DateTime @map("arrived_at") serviceStartedAt DateTime? @map("service_started_at") serviceEndedAt DateTime? @map("service_ended_at") leftWithoutService Boolean @default(false) @map("left_without_service") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade) @@index([patientId, arrivedAt]) @@map("visits") } // ========================================================================= // AUDIT // ========================================================================= model BiometryAccessLog { id String @id @default(uuid()) @db.Uuid actorUserId String? @map("actor_user_id") @db.Uuid subjectPatientId String? @map("subject_patient_id") @db.Uuid action String requestPath String? @map("request_path") occurredAt DateTime @default(now()) @map("occurred_at") actor User? @relation(fields: [actorUserId], references: [id]) @@index([occurredAt]) @@index([subjectPatientId]) @@map("biometry_access_log") } // ========================================================================= // CONSENT REVOCATION JOBS (24h delete) // ========================================================================= enum ConsentRevocationStatus { PENDING DONE } model ConsentRevocationJob { id String @id @default(uuid()) @db.Uuid patientId String @map("patient_id") @db.Uuid revokedAt DateTime @map("revoked_at") scheduledFor DateTime @map("scheduled_for") status ConsentRevocationStatus @default(PENDING) completedAt DateTime? @map("completed_at") createdAt DateTime @default(now()) @map("created_at") patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade) @@index([status, scheduledFor]) @@map("consent_revocation_jobs") }