generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" } // ─────────────────────────────────────── // Auth.js required models // ─────────────────────────────────────── enum Role { ADMIN USER } model User { id String @id @default(cuid()) name String? email String? @unique emailVerified DateTime? image String? hashedPassword String? role Role @default(ADMIN) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt accounts Account[] sessions Session[] filaments Filament[] resins Resin[] paints Paint[] supplies Supply[] vendors Vendor[] locations Location[] usageLogs UsageLog[] tags Tag[] settings UserSettings? telegramLink TelegramLink? inviteCodes InviteCode[] } model Account { id String @id @default(cuid()) userId String type String provider String providerAccountId String refresh_token String? @db.Text access_token String? @db.Text expires_at Int? token_type String? scope String? id_token String? @db.Text session_state String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) } model Session { id String @id @default(cuid()) sessionToken String @unique userId String expires DateTime user User @relation(fields: [userId], references: [id], onDelete: Cascade) } model VerificationToken { identifier String token String expires DateTime @@unique([identifier, token]) } // ─────────────────────────────────────── // Domain models // ─────────────────────────────────────── model Vendor { id String @id @default(cuid()) name String @db.VarChar(64) website String? @db.VarChar(256) notes String? @db.Text archived Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) filaments Filament[] resins Resin[] paints Paint[] supplies Supply[] @@index([userId]) @@index([archived]) } model Location { id String @id @default(cuid()) name String @db.VarChar(64) description String? @db.VarChar(256) archived Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) filaments Filament[] resins Resin[] paints Paint[] supplies Supply[] @@index([userId]) @@index([archived]) } model Filament { id String @id @default(cuid()) name String @db.VarChar(128) brand String @db.VarChar(64) material String @db.VarChar(32) color String @db.VarChar(64) colorHex String @db.VarChar(7) diameter Float @default(1.75) spoolWeight Float usedWeight Float @default(0) emptySpoolWeight Float @default(0) purchaseDate DateTime? cost Float? notes String? @db.Text archived Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt userId String vendorId String? locationId String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) vendor Vendor? @relation(fields: [vendorId], references: [id], onDelete: SetNull) location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull) tags TagOnFilament[] usageLogs UsageLog[] @@index([userId]) @@index([vendorId]) @@index([locationId]) @@index([material]) @@index([archived]) @@index([brand]) } model Resin { id String @id @default(cuid()) name String @db.VarChar(128) brand String @db.VarChar(64) resinType String @db.VarChar(32) color String @db.VarChar(64) colorHex String @db.VarChar(7) bottleSize Float usedML Float @default(0) purchaseDate DateTime? cost Float? notes String? @db.Text archived Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt userId String vendorId String? locationId String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) vendor Vendor? @relation(fields: [vendorId], references: [id], onDelete: SetNull) location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull) tags TagOnResin[] usageLogs UsageLog[] @@index([userId]) @@index([vendorId]) @@index([locationId]) @@index([resinType]) @@index([archived]) @@index([brand]) } model Paint { id String @id @default(cuid()) name String @db.VarChar(128) brand String @db.VarChar(64) line String? @db.VarChar(64) color String @db.VarChar(64) colorHex String @db.VarChar(7) finish String @db.VarChar(32) volumeML Float usedML Float @default(0) purchaseDate DateTime? cost Float? notes String? @db.Text archived Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt userId String vendorId String? locationId String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) vendor Vendor? @relation(fields: [vendorId], references: [id], onDelete: SetNull) location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull) tags TagOnPaint[] usageLogs UsageLog[] @@index([userId]) @@index([vendorId]) @@index([locationId]) @@index([finish]) @@index([archived]) @@index([brand]) } model Tag { id String @id @default(cuid()) name String @db.VarChar(64) createdAt DateTime @default(now()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) filaments TagOnFilament[] resins TagOnResin[] paints TagOnPaint[] supplies TagOnSupply[] @@unique([name, userId]) @@index([userId]) } model TagOnFilament { filamentId String tagId String filament Filament @relation(fields: [filamentId], references: [id], onDelete: Cascade) tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) @@id([filamentId, tagId]) } model TagOnResin { resinId String tagId String resin Resin @relation(fields: [resinId], references: [id], onDelete: Cascade) tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) @@id([resinId, tagId]) } model TagOnPaint { paintId String tagId String paint Paint @relation(fields: [paintId], references: [id], onDelete: Cascade) tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) @@id([paintId, tagId]) } model Supply { id String @id @default(cuid()) name String @db.VarChar(128) brand String @db.VarChar(64) category String @db.VarChar(32) color String? @db.VarChar(64) colorHex String? @db.VarChar(7) totalAmount Float usedAmount Float @default(0) unit String @db.VarChar(16) purchaseDate DateTime? cost Float? notes String? @db.Text archived Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt userId String vendorId String? locationId String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) vendor Vendor? @relation(fields: [vendorId], references: [id], onDelete: SetNull) location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull) tags TagOnSupply[] usageLogs UsageLog[] @@index([userId]) @@index([vendorId]) @@index([locationId]) @@index([category]) @@index([archived]) @@index([brand]) } model TagOnSupply { supplyId String tagId String supply Supply @relation(fields: [supplyId], references: [id], onDelete: Cascade) tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) @@id([supplyId, tagId]) } model UsageLog { id String @id @default(cuid()) itemType String @db.VarChar(16) itemId String amount Float unit String @db.VarChar(16) notes String? @db.Text createdAt DateTime @default(now()) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) filament Filament? @relation(fields: [filamentId], references: [id], onDelete: Cascade) filamentId String? resin Resin? @relation(fields: [resinId], references: [id], onDelete: Cascade) resinId String? paint Paint? @relation(fields: [paintId], references: [id], onDelete: Cascade) paintId String? supply Supply? @relation(fields: [supplyId], references: [id], onDelete: Cascade) supplyId String? @@index([userId]) @@index([itemType, itemId]) @@index([createdAt]) } model UserSettings { id String @id @default(cuid()) userId String @unique lowStockThreshold Float @default(10) currency String @default("USD") @db.VarChar(3) theme String @default("dark") @db.VarChar(8) units String @default("metric") @db.VarChar(8) user User @relation(fields: [userId], references: [id], onDelete: Cascade) } // ─────────────────────────────────────── // Telegram ingestion models // ─────────────────────────────────────── enum AuthState { PENDING AWAITING_CODE AWAITING_PASSWORD AUTHENTICATED EXPIRED } enum ChannelType { SOURCE DESTINATION } enum ChannelRole { READER WRITER } enum ArchiveType { ZIP RAR } enum IngestionStatus { RUNNING COMPLETED FAILED CANCELLED } enum FetchStatus { PENDING IN_PROGRESS COMPLETED FAILED } model TelegramAccount { id String @id @default(cuid()) phone String @unique displayName String? isActive Boolean @default(true) authState AuthState @default(PENDING) authCode String? lastSeenAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt channelMaps AccountChannelMap[] ingestionRuns IngestionRun[] fetchRequests ChannelFetchRequest[] @@index([isActive]) @@map("telegram_accounts") } model TelegramChannel { id String @id @default(cuid()) telegramId BigInt @unique title String type ChannelType isForum Boolean @default(false) isActive Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt accountMaps AccountChannelMap[] packages Package[] @@index([type, isActive]) @@map("telegram_channels") } model AccountChannelMap { id String @id @default(cuid()) accountId String channelId String role ChannelRole @default(READER) lastProcessedMessageId BigInt? createdAt DateTime @default(now()) account TelegramAccount @relation(fields: [accountId], references: [id], onDelete: Cascade) channel TelegramChannel @relation(fields: [channelId], references: [id], onDelete: Cascade) topicProgress TopicProgress[] @@unique([accountId, channelId]) @@index([accountId]) @@index([channelId]) @@map("account_channel_map") } model Package { id String @id @default(cuid()) contentHash String @unique fileName String fileSize BigInt archiveType ArchiveType creator String? sourceChannelId String sourceMessageId BigInt sourceTopicId BigInt? destChannelId String? destMessageId BigInt? isMultipart Boolean @default(false) partCount Int @default(1) fileCount Int @default(0) previewData Bytes? // JPEG thumbnail from nearby Telegram photo (stored as raw bytes) previewMsgId BigInt? // Telegram message ID of the matched photo indexedAt DateTime @default(now()) createdAt DateTime @default(now()) sourceChannel TelegramChannel @relation(fields: [sourceChannelId], references: [id]) files PackageFile[] ingestionRun IngestionRun? @relation(fields: [ingestionRunId], references: [id]) ingestionRunId String? sendRequests BotSendRequest[] @@index([sourceChannelId]) @@index([destChannelId]) @@index([fileName]) @@index([indexedAt]) @@index([archiveType]) @@index([creator]) @@map("packages") } model PackageFile { id String @id @default(cuid()) packageId String path String fileName String extension String? compressedSize BigInt @default(0) uncompressedSize BigInt @default(0) crc32 String? package Package @relation(fields: [packageId], references: [id], onDelete: Cascade) @@index([packageId]) @@index([extension]) @@index([fileName]) @@map("package_files") } model IngestionRun { id String @id @default(cuid()) accountId String status IngestionStatus @default(RUNNING) startedAt DateTime @default(now()) finishedAt DateTime? messagesScanned Int @default(0) zipsFound Int @default(0) zipsDuplicate Int @default(0) zipsIngested Int @default(0) errorMessage String? // Live activity tracking — written by worker in real-time currentActivity String? // Human-readable: "Downloading pack.zip (part 2/5)" currentStep String? // Machine-readable step key currentChannel String? // Channel title being processed currentFile String? // File currently being processed currentFileNum Int? // Which archive set (1-indexed) totalFiles Int? // Total archive sets found downloadedBytes BigInt? // Current download progress in bytes totalBytes BigInt? // Total size of current download downloadPercent Int? // 0-100 lastActivityAt DateTime? // When activity was last updated account TelegramAccount @relation(fields: [accountId], references: [id]) packages Package[] @@index([accountId]) @@index([status]) @@index([startedAt]) @@map("ingestion_runs") } model TopicProgress { id String @id @default(cuid()) accountChannelMapId String topicId BigInt topicName String? lastProcessedMessageId BigInt? accountChannelMap AccountChannelMap @relation(fields: [accountChannelMapId], references: [id], onDelete: Cascade) @@unique([accountChannelMapId, topicId]) @@index([accountChannelMapId]) @@map("topic_progress") } model GlobalSetting { key String @id @db.VarChar(64) value String @db.Text updatedAt DateTime @updatedAt @@map("global_settings") } model InviteCode { id String @id @default(cuid()) code String @unique @db.VarChar(32) maxUses Int @default(1) uses Int @default(0) expiresAt DateTime? createdBy String createdAt DateTime @default(now()) creator User @relation(fields: [createdBy], references: [id], onDelete: Cascade) @@index([code]) @@map("invite_codes") } model ChannelFetchRequest { id String @id @default(cuid()) accountId String status FetchStatus @default(PENDING) resultJson String? @db.Text error String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt account TelegramAccount @relation(fields: [accountId], references: [id], onDelete: Cascade) @@index([accountId, status]) @@map("channel_fetch_requests") } // ─────────────────────────────────────── // Telegram Bot models // ─────────────────────────────────────── enum BotSendStatus { PENDING SENDING SENT FAILED } /// Links a NextAuth user to a Telegram user ID. /// Created when a user sends /link to the bot. model TelegramLink { id String @id @default(cuid()) userId String @unique telegramUserId BigInt @unique telegramName String? @db.VarChar(128) createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) sendRequests BotSendRequest[] @@map("telegram_links") } /// A queued request to send a package to a Telegram user via the bot. model BotSendRequest { id String @id @default(cuid()) packageId String telegramLinkId String requestedByUserId String status BotSendStatus @default(PENDING) error String? createdAt DateTime @default(now()) completedAt DateTime? package Package @relation(fields: [packageId], references: [id]) telegramLink TelegramLink @relation(fields: [telegramLinkId], references: [id], onDelete: Cascade) @@index([status]) @@index([telegramLinkId]) @@index([createdAt]) @@map("bot_send_requests") } /// Subscriptions for new-package notifications via the bot. model BotSubscription { id String @id @default(cuid()) telegramUserId BigInt pattern String @db.VarChar(256) createdAt DateTime @default(now()) @@unique([telegramUserId, pattern]) @@index([telegramUserId]) @@map("bot_subscriptions") }