generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } enum SubscriptionStatus { pending_activation active past_due canceled expired } enum PaymentInvoiceStatus { pending paid expired canceled } enum GenerationMode { text_to_image image_to_image } enum GenerationRequestStatus { queued running succeeded failed canceled } enum GenerationAttemptStatus { started succeeded failed } enum ProviderFailureCategory { transport timeout provider_5xx provider_4xx_user insufficient_funds unknown } enum ProviderKeyState { active cooldown out_of_funds manual_review disabled } enum UsageLedgerEntryType { cycle_reset generation_success manual_adjustment refund } enum TelegramPairingStatus { pending completed expired revoked } enum AdminActorType { system web_admin telegram_admin cli_operator } model User { id String @id @default(cuid()) email String @unique passwordHash String passwordResetVersion Int @default(0) isAdmin Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt subscriptions Subscription[] invoices PaymentInvoice[] generationRequests GenerationRequest[] usageLedgerEntries UsageLedgerEntry[] sessions UserSession[] passwordResetTokens PasswordResetToken[] } model UserSession { id String @id @default(cuid()) userId String tokenHash String @unique expiresAt DateTime revokedAt DateTime? lastSeenAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, createdAt]) @@index([expiresAt, revokedAt]) } model PasswordResetToken { id String @id @default(cuid()) userId String tokenHash String @unique expiresAt DateTime consumedAt DateTime? createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, createdAt]) @@index([expiresAt, consumedAt]) } model SubscriptionPlan { id String @id @default(cuid()) code String @unique displayName String monthlyRequestLimit Int monthlyPriceUsd Decimal @db.Decimal(12, 2) billingCurrency String isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt subscriptions Subscription[] } model Subscription { id String @id @default(cuid()) userId String planId String status SubscriptionStatus renewsManually Boolean @default(true) activatedAt DateTime? currentPeriodStart DateTime? currentPeriodEnd DateTime? canceledAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) plan SubscriptionPlan @relation(fields: [planId], references: [id], onDelete: Restrict) invoices PaymentInvoice[] @@index([userId, status]) } model PaymentInvoice { id String @id @default(cuid()) userId String subscriptionId String? provider String providerInvoiceId String? @unique status PaymentInvoiceStatus currency String amountCrypto Decimal @db.Decimal(20, 8) amountUsd Decimal? @db.Decimal(12, 2) paymentAddress String? expiresAt DateTime? paidAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) subscription Subscription? @relation(fields: [subscriptionId], references: [id], onDelete: SetNull) @@index([userId, status]) } model GenerationRequest { id String @id @default(cuid()) userId String mode GenerationMode status GenerationRequestStatus @default(queued) providerModel String prompt String sourceImageKey String? resolutionPreset String batchSize Int imageStrength Decimal? @db.Decimal(4, 3) idempotencyKey String? @unique terminalErrorCode String? terminalErrorText String? requestedAt DateTime @default(now()) startedAt DateTime? completedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) attempts GenerationAttempt[] assets GeneratedAsset[] usageLedgerEntry UsageLedgerEntry? @@index([userId, status, requestedAt]) } model GenerationAttempt { id String @id @default(cuid()) generationRequestId String providerKeyId String attemptIndex Int status GenerationAttemptStatus @default(started) usedProxy Boolean @default(false) directFallbackUsed Boolean @default(false) failureCategory ProviderFailureCategory? providerHttpStatus Int? providerErrorCode String? providerErrorText String? startedAt DateTime @default(now()) completedAt DateTime? createdAt DateTime @default(now()) generationRequest GenerationRequest @relation(fields: [generationRequestId], references: [id], onDelete: Cascade) providerKey ProviderKey @relation(fields: [providerKeyId], references: [id], onDelete: Restrict) @@unique([generationRequestId, attemptIndex]) @@index([providerKeyId, startedAt]) } model GeneratedAsset { id String @id @default(cuid()) generationRequestId String objectKey String @unique mimeType String width Int? height Int? bytes Int? createdAt DateTime @default(now()) generationRequest GenerationRequest @relation(fields: [generationRequestId], references: [id], onDelete: Cascade) @@index([generationRequestId]) } model UsageLedgerEntry { id String @id @default(cuid()) userId String generationRequestId String? @unique entryType UsageLedgerEntryType deltaRequests Int cycleStartedAt DateTime? cycleEndsAt DateTime? note String? createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) generationRequest GenerationRequest? @relation(fields: [generationRequestId], references: [id], onDelete: SetNull) @@index([userId, createdAt]) } model ProviderProxy { id String @id @default(cuid()) label String @unique baseUrl String isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt providerKeys ProviderKey[] } model ProviderKey { id String @id @default(cuid()) providerCode String label String @unique apiKeyCiphertext String apiKeyLastFour String state ProviderKeyState @default(active) roundRobinOrder Int consecutiveRetryableFailures Int @default(0) cooldownUntil DateTime? lastErrorCategory ProviderFailureCategory? lastErrorCode String? lastErrorAt DateTime? balanceMinorUnits BigInt? balanceCurrency String? balanceRefreshedAt DateTime? proxyId String? disabledAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt proxy ProviderProxy? @relation(fields: [proxyId], references: [id], onDelete: SetNull) attempts GenerationAttempt[] statusEvents ProviderKeyStatusEvent[] @@index([providerCode, state, roundRobinOrder]) } model ProviderKeyStatusEvent { id String @id @default(cuid()) providerKeyId String fromState ProviderKeyState? toState ProviderKeyState reason String errorCategory ProviderFailureCategory? errorCode String? actorType AdminActorType actorRef String? metadata Json? createdAt DateTime @default(now()) providerKey ProviderKey @relation(fields: [providerKeyId], references: [id], onDelete: Cascade) @@index([providerKeyId, createdAt]) } model TelegramPairing { id String @id @default(cuid()) telegramUserId String telegramUsername String? displayNameSnapshot String codeHash String expiresAt DateTime status TelegramPairingStatus @default(pending) completedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([telegramUserId, status]) @@index([expiresAt, status]) } model TelegramAdminAllowlistEntry { telegramUserId String @id telegramUsername String? displayNameSnapshot String pairedAt DateTime @default(now()) revokedAt DateTime? isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model AdminAuditLog { id String @id @default(cuid()) actorType AdminActorType actorRef String? action String targetType String targetId String? metadata Json? createdAt DateTime @default(now()) @@index([targetType, targetId, createdAt]) @@index([actorType, createdAt]) }