819 lines
33 KiB
Plaintext
819 lines
33 KiB
Plaintext
// ═══════════════════════════════════════════════════════════════════════════
|
|
// PRISMA SCHEMA - Tools Platform
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Feature: Database & Authentication Foundation
|
|
// Branch: 001-database-auth-foundation
|
|
//
|
|
// This schema defines the complete database structure for the tools platform,
|
|
// including user management, subscriptions, payments, tools, jobs, and analytics.
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// USER MODEL
|
|
// Synced with Keycloak - keycloakId is the link
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
model User {
|
|
id String @id @default(uuid())
|
|
keycloakId String @unique // Links to Keycloak user ID
|
|
email String @unique
|
|
name String?
|
|
|
|
// Tier (derived from subscription, but cached for performance)
|
|
tier UserTier @default(FREE)
|
|
|
|
// Auth Wrapper - Account Status
|
|
emailVerified Boolean @default(false)
|
|
accountStatus AccountStatus @default(ACTIVE)
|
|
|
|
// i18n - User's preferred locale (Feature 009)
|
|
preferredLocale String? @default("en") // User's language preference (en, fr, ar)
|
|
|
|
// Timestamps
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
lastLoginAt DateTime?
|
|
|
|
// Monetization (014): Day Pass expiry; if > NOW() user has DAY_PASS tier
|
|
dayPassExpiresAt DateTime?
|
|
|
|
// Relations
|
|
subscription Subscription?
|
|
payments Payment[]
|
|
jobs Job[]
|
|
usageLogs UsageLog[]
|
|
batches Batch[]
|
|
sessions Session[] // Auth Wrapper - Active sessions
|
|
authEvents AuthEvent[] // Auth Wrapper - Event log
|
|
emailTokens EmailToken[] // Feature 008 - Email tokens
|
|
emailLogs EmailLog[] // Feature 008 - Email delivery logs
|
|
adminNotes UserAdminNote[] // Step 01 - Admin notes on this user
|
|
|
|
@@index([keycloakId])
|
|
@@index([email])
|
|
@@index([accountStatus]) // Auth Wrapper - Filter by status
|
|
@@index([preferredLocale]) // Feature 009 - i18n analytics
|
|
}
|
|
|
|
enum UserTier {
|
|
FREE
|
|
PREMIUM
|
|
}
|
|
|
|
enum AccountStatus {
|
|
ACTIVE
|
|
LOCKED
|
|
DISABLED
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// SUBSCRIPTION MODEL
|
|
// Tracks active subscriptions (Stripe/PayPal)
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
model Subscription {
|
|
id String @id @default(uuid())
|
|
userId String @unique
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
// Plan
|
|
plan SubscriptionPlan
|
|
|
|
// Status
|
|
status SubscriptionStatus
|
|
|
|
// Payment Provider
|
|
provider PaymentProvider
|
|
providerSubscriptionId String? // Stripe/PayPal subscription ID
|
|
providerCustomerId String? // Stripe customer ID / PayPal payer ID
|
|
|
|
// Billing Period
|
|
currentPeriodStart DateTime?
|
|
currentPeriodEnd DateTime?
|
|
|
|
// Cancellation
|
|
cancelledAt DateTime?
|
|
cancelAtPeriodEnd Boolean @default(false)
|
|
|
|
// Timestamps
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([providerSubscriptionId])
|
|
@@index([status])
|
|
@@index([status, currentPeriodEnd]) // Expiring subscriptions query
|
|
}
|
|
|
|
enum SubscriptionPlan {
|
|
PREMIUM_MONTHLY
|
|
PREMIUM_YEARLY
|
|
}
|
|
|
|
enum SubscriptionStatus {
|
|
ACTIVE
|
|
CANCELLED
|
|
PAST_DUE
|
|
EXPIRED
|
|
TRIALING
|
|
}
|
|
|
|
enum PaymentProvider {
|
|
STRIPE
|
|
PAYPAL
|
|
PADDLE
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// PAYMENT MODEL
|
|
// Tracks payment history
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
model Payment {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
// Amount
|
|
amount Decimal @db.Decimal(10, 2)
|
|
currency String @default("USD")
|
|
|
|
// Payment Details
|
|
provider PaymentProvider
|
|
providerPaymentId String? // Stripe PaymentIntent / PayPal Order ID
|
|
|
|
// Status
|
|
status PaymentStatus
|
|
|
|
// Type
|
|
type PaymentType
|
|
|
|
// Timestamps
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([userId])
|
|
@@index([providerPaymentId])
|
|
@@index([createdAt])
|
|
}
|
|
|
|
enum PaymentStatus {
|
|
PENDING
|
|
COMPLETED
|
|
FAILED
|
|
REFUNDED
|
|
}
|
|
|
|
enum PaymentType {
|
|
SUBSCRIPTION_INITIAL
|
|
SUBSCRIPTION_RENEWAL
|
|
SUBSCRIPTION_UPGRADE
|
|
DAY_PASS_PURCHASE
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// TOOL MODEL
|
|
// Defines available tools and their configuration
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
model Tool {
|
|
id String @id @default(uuid())
|
|
slug String @unique // 'pdf-merge', 'image-remove-bg'
|
|
category String // 'pdf', 'image', 'utilities'
|
|
name String // 'Merge PDF'
|
|
description String?
|
|
|
|
// Monetization (014): minimum tier to access; GUEST=anyone, FREE=registered+, PREMIUM=Day Pass/Pro
|
|
accessLevel AccessLevel @default(FREE)
|
|
// When false, using this tool does not count toward ops limit (e.g. QR code, frontend-only)
|
|
countsAsOperation Boolean @default(true)
|
|
|
|
// Processing
|
|
dockerService String? // 'stirling-pdf', 'rembg', etc.
|
|
processingType ProcessingType @default(API) // API or CLI
|
|
|
|
// Status
|
|
isActive Boolean @default(true)
|
|
|
|
// SEO
|
|
metaTitle String?
|
|
metaDescription String?
|
|
|
|
// Localized content (optional JSON: locale -> string) - Feature 001-localise-tools-errors
|
|
nameLocalized Json? // e.g. {"fr": "Fusionner PDF", "ar": "دمج PDF"}
|
|
descriptionLocalized Json?
|
|
metaTitleLocalized Json?
|
|
metaDescriptionLocalized Json?
|
|
|
|
// Timestamps
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
jobs Job[]
|
|
usageLogs UsageLog[]
|
|
|
|
@@index([slug])
|
|
@@index([category])
|
|
@@index([accessLevel])
|
|
@@index([countsAsOperation])
|
|
}
|
|
|
|
enum AccessLevel {
|
|
GUEST // Anyone can use (no account required)
|
|
FREE // Registered free users and above
|
|
PREMIUM // Day Pass and Pro users only
|
|
}
|
|
|
|
enum ProcessingType {
|
|
API
|
|
CLI
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// JOB MODEL
|
|
// Tracks processing jobs
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
model Job {
|
|
id String @id @default(uuid())
|
|
|
|
// User (optional for anonymous)
|
|
userId String?
|
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
|
|
// Tool
|
|
toolId String
|
|
tool Tool @relation(fields: [toolId], references: [id])
|
|
|
|
// Batch (optional - for batch processing)
|
|
batchId String?
|
|
batch Batch? @relation("BatchJobs", fields: [batchId], references: [id], onDelete: SetNull)
|
|
|
|
// Status
|
|
status JobStatus @default(QUEUED)
|
|
progress Int @default(0) // 0-100
|
|
|
|
// Files
|
|
inputFileIds String[] // MinIO file IDs
|
|
outputFileId String? // MinIO file ID
|
|
|
|
// Processing
|
|
processingTimeMs Int?
|
|
errorMessage String?
|
|
|
|
// Metadata
|
|
metadata Json? // Tool-specific options
|
|
|
|
// Anonymous tracking
|
|
ipHash String? // Hashed IP for anonymous users
|
|
|
|
// Email notification tracking (Feature 008)
|
|
emailNotificationSentAt DateTime? // When job failure email was sent
|
|
emailNotificationCount Int @default(0) // Number of notifications sent
|
|
|
|
// Timestamps
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
completedAt DateTime?
|
|
|
|
// Auto-delete after 24 hours
|
|
expiresAt DateTime @default(dbgenerated("NOW() + INTERVAL '24 hours'"))
|
|
|
|
@@index([userId])
|
|
@@index([batchId])
|
|
@@index([status])
|
|
@@index([createdAt])
|
|
@@index([expiresAt])
|
|
@@index([userId, createdAt(sort: Desc)]) // User's recent jobs
|
|
@@index([expiresAt, status]) // Cleanup queries
|
|
@@index([status, createdAt]) // Monitoring queries
|
|
}
|
|
|
|
enum JobStatus {
|
|
QUEUED
|
|
PROCESSING
|
|
COMPLETED
|
|
FAILED
|
|
CANCELLED
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// BATCH MODEL
|
|
// Tracks batch processing operations for PREMIUM users
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
model Batch {
|
|
id String @id @default(uuid())
|
|
|
|
// User
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
// Status
|
|
status BatchStatus @default(PENDING)
|
|
|
|
// Progress tracking
|
|
totalJobs Int
|
|
completedJobs Int @default(0)
|
|
failedJobs Int @default(0)
|
|
|
|
// Timestamps
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Auto-cleanup after 24 hours
|
|
expiresAt DateTime?
|
|
|
|
// Relations
|
|
jobs Job[] @relation("BatchJobs")
|
|
|
|
@@index([userId])
|
|
@@index([status])
|
|
@@index([expiresAt])
|
|
@@index([userId, createdAt(sort: Desc)])
|
|
}
|
|
|
|
enum BatchStatus {
|
|
PENDING
|
|
PROCESSING
|
|
COMPLETED
|
|
FAILED
|
|
PARTIAL
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// USAGE LOG MODEL
|
|
// Analytics and usage tracking
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
model UsageLog {
|
|
id String @id @default(uuid())
|
|
|
|
// User (optional for anonymous)
|
|
userId String?
|
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
|
|
// Tool
|
|
toolId String
|
|
tool Tool @relation(fields: [toolId], references: [id])
|
|
|
|
// Request Info
|
|
fileSizeMb Decimal? @db.Decimal(10, 2)
|
|
processingTimeMs Int?
|
|
status String // 'success', 'failed'
|
|
|
|
// Anonymous tracking
|
|
ipHash String?
|
|
userAgent String?
|
|
country String?
|
|
|
|
// Timestamps
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([userId])
|
|
@@index([toolId])
|
|
@@index([createdAt])
|
|
@@index([toolId, createdAt]) // Tool analytics by date
|
|
@@index([userId, createdAt(sort: Desc)]) // User usage history
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// SESSION MODEL - Auth Wrapper
|
|
// Tracks active user sessions for listing and revocation
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
model Session {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
// Keycloak Integration
|
|
keycloakSessionId String @unique // Links to Keycloak session
|
|
|
|
// Device Information
|
|
deviceInfo Json // { type, os, browser, location }
|
|
ipAddress String
|
|
userAgent String
|
|
|
|
// Session Lifecycle
|
|
createdAt DateTime @default(now())
|
|
lastActivityAt DateTime @updatedAt
|
|
expiresAt DateTime
|
|
|
|
@@index([userId, createdAt(sort: Desc)]) // User's sessions
|
|
@@index([expiresAt]) // Cleanup query
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// AUTH EVENT MODEL - Auth Wrapper
|
|
// Audit log for authentication events
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
model AuthEvent {
|
|
id String @id @default(uuid())
|
|
|
|
// User (nullable for failed login attempts)
|
|
userId String?
|
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
|
|
// Event Details
|
|
eventType AuthEventType
|
|
outcome AuthEventOutcome
|
|
|
|
// Request Information
|
|
ipAddress String
|
|
userAgent String
|
|
deviceInfo Json // Parsed device info
|
|
|
|
// Error Information
|
|
failureReason String? // Error code for failures
|
|
metadata Json? // Additional context
|
|
|
|
// Timestamp
|
|
timestamp DateTime @default(now())
|
|
|
|
@@index([userId, timestamp(sort: Desc)]) // User's event history
|
|
@@index([eventType]) // Filter by type
|
|
@@index([outcome, timestamp(sort: Desc)]) // Failure analysis
|
|
@@index([timestamp]) // Cleanup query
|
|
}
|
|
|
|
enum AuthEventType {
|
|
LOGIN
|
|
LOGIN_FAILED
|
|
LOGOUT
|
|
REGISTRATION
|
|
TOKEN_REFRESH
|
|
TOKEN_REFRESH_FAILED
|
|
PASSWORD_CHANGE
|
|
PASSWORD_RESET_REQUEST
|
|
PASSWORD_RESET_COMPLETE
|
|
PROFILE_UPDATE
|
|
SESSION_REVOKED
|
|
ACCOUNT_LOCKED
|
|
ACCOUNT_UNLOCKED
|
|
SOCIAL_LOGIN
|
|
SOCIAL_LOGIN_FAILED
|
|
IDENTITY_LINKED
|
|
IDENTITY_UNLINKED
|
|
}
|
|
|
|
enum AuthEventOutcome {
|
|
SUCCESS
|
|
FAILURE
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// FEATURE FLAG MODEL (Optional - for complex flags)
|
|
// Simple flags use ENV, complex flags use this table
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
model FeatureFlag {
|
|
id String @id @default(uuid())
|
|
name String @unique // 'beta_feature_x'
|
|
description String?
|
|
enabled Boolean @default(false)
|
|
|
|
// Targeting (optional)
|
|
userIds String[] // Specific users
|
|
userTiers UserTier[] // Specific tiers
|
|
rolloutPercent Int @default(0) // 0-100
|
|
|
|
// Timestamps
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// PENDING REGISTRATION - User created in our DB only after email verification
|
|
// Stores Keycloak user + token until user clicks verification link
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
model PendingRegistration {
|
|
id String @id @default(uuid())
|
|
keycloakId String // Keycloak user ID (user exists in Keycloak, not yet in our DB)
|
|
email String
|
|
name String?
|
|
tokenHash String @unique
|
|
expiresAt DateTime
|
|
usedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([tokenHash])
|
|
@@index([email])
|
|
@@index([expiresAt])
|
|
}
|
|
|
|
// EMAIL TOKEN MODEL - Feature 008
|
|
// Stores secure tokens for email verification (password reset, job retry still use this)
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
model EmailToken {
|
|
id String @id @default(uuid())
|
|
|
|
// User Association
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
// Token Data
|
|
tokenHash String @unique // SHA-256 hash of the token
|
|
tokenType EmailTokenType
|
|
|
|
// Lifecycle
|
|
createdAt DateTime @default(now())
|
|
expiresAt DateTime // Automatically expires
|
|
usedAt DateTime? // Single-use enforcement
|
|
|
|
// Optional Context
|
|
metadata Json? // Additional data (e.g., email address, job ID)
|
|
|
|
@@index([userId, tokenType]) // Find user's tokens by type
|
|
@@index([tokenHash]) // Fast token lookup
|
|
@@index([expiresAt]) // Cleanup expired tokens
|
|
@@index([tokenType, createdAt(sort: Desc)]) // Recent tokens by type
|
|
}
|
|
|
|
enum EmailTokenType {
|
|
VERIFICATION // Email address verification (24h expiry)
|
|
PASSWORD_RESET // Password reset flow (1h expiry)
|
|
JOB_RETRY // Job retry link (7d expiry)
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// EMAIL LOG MODEL - Feature 008
|
|
// Tracks all email sending attempts for monitoring and debugging
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
model EmailLog {
|
|
id String @id @default(uuid())
|
|
|
|
// User Association (nullable for anonymous recipients)
|
|
userId String?
|
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
|
|
// Email Details
|
|
recipientEmail String
|
|
recipientName String?
|
|
emailType EmailType
|
|
subject String
|
|
|
|
// Delivery Status
|
|
status EmailStatus @default(PENDING)
|
|
resendMessageId String? // Resend's message ID for tracking
|
|
|
|
// Error Tracking
|
|
errorMessage String?
|
|
errorCode String?
|
|
retryCount Int @default(0)
|
|
|
|
// Timestamps
|
|
sentAt DateTime @default(now())
|
|
deliveredAt DateTime? // Updated via webhook (future)
|
|
bouncedAt DateTime? // Updated via webhook (future)
|
|
|
|
// Metadata
|
|
metadata Json? // Additional context (e.g., template variables)
|
|
|
|
@@index([userId, sentAt(sort: Desc)]) // User's email history
|
|
@@index([emailType, sentAt(sort: Desc)]) // Emails by type
|
|
@@index([status, sentAt(sort: Desc)]) // Failed emails
|
|
@@index([recipientEmail]) // Find emails to specific address
|
|
@@index([sentAt]) // Time-based queries
|
|
@@index([resendMessageId]) // Lookup by Resend ID
|
|
}
|
|
|
|
enum EmailType {
|
|
VERIFICATION // Email address verification
|
|
PASSWORD_RESET // Password reset request
|
|
PASSWORD_CHANGED // Password changed confirmation
|
|
WELCOME // Welcome email after verification
|
|
CONTACT_AUTO_REPLY // Contact form auto-reply
|
|
MISSED_JOB // Job failure notification (legacy)
|
|
JOB_COMPLETED // Job completed with download link
|
|
JOB_FAILED // Job failure notification
|
|
SUBSCRIPTION_CONFIRMED // Pro subscription created
|
|
SUBSCRIPTION_CANCELLED // Subscription cancelled
|
|
DAY_PASS_PURCHASED // Day pass purchase confirmation
|
|
DAY_PASS_EXPIRING_SOON // Day pass expiring in 2-4h
|
|
DAY_PASS_EXPIRED // Day pass expired
|
|
SUBSCRIPTION_EXPIRING_SOON // Subscription renewal in 7d/1d
|
|
PAYMENT_FAILED // Subscription payment failed
|
|
USAGE_LIMIT_WARNING // Free tier usage threshold
|
|
PROMO_UPGRADE // Campaign: promo upgrade
|
|
FEATURE_ANNOUNCEMENT // Campaign: feature announcement
|
|
ADMIN_CUSTOM // Admin composer: custom subject/body (Step 06)
|
|
}
|
|
|
|
enum EmailStatus {
|
|
PENDING // Queued but not yet sent
|
|
SENT // Successfully sent to Resend
|
|
DELIVERED // Delivered to recipient inbox (webhook)
|
|
FAILED // Sending failed
|
|
BOUNCED // Bounced by recipient server (webhook)
|
|
COMPLAINED // Marked as spam by recipient (webhook)
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// DELETED EMAIL - Account deletion abuse prevention
|
|
// One row per deletion; 3+ deletions in 30 days for same email blocks registration
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
model DeletedEmail {
|
|
id String @id @default(uuid())
|
|
email String
|
|
deletedAt DateTime @default(now())
|
|
@@index([email])
|
|
@@index([email, deletedAt])
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// ADMIN AUDIT LOG (002-admin-dashboard-polish)
|
|
// One row per admin action (tool update, user update)
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
model AdminAuditLog {
|
|
id String @id @default(uuid())
|
|
adminUserId String // Keycloak sub or User.id
|
|
adminUserEmail String?
|
|
action String // e.g. tool.update, user.update, admin.login, config.change
|
|
entityType String // tool, user, config, payment, etc.
|
|
entityId String
|
|
changes Json? // optional summary of what changed (details)
|
|
ipAddress String? // Admin IP for audit (Step 01)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([entityType, entityId])
|
|
@@index([createdAt(sort: Desc)])
|
|
@@index([adminUserId])
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// ADMIN TASKS - Step 01 (Admin Panel)
|
|
// Tasks & reminders for admins
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
model AdminTask {
|
|
id String @id @default(uuid())
|
|
title String
|
|
description String?
|
|
category String // daily, weekly, monthly, quarterly
|
|
dueDate DateTime? @map("due_date")
|
|
recurring String? // daily, weekly, monthly or null
|
|
status AdminTaskStatus @default(PENDING)
|
|
completedAt DateTime? @map("completed_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@index([category])
|
|
@@index([status])
|
|
@@index([dueDate])
|
|
@@index([createdAt(sort: Desc)])
|
|
}
|
|
|
|
enum AdminTaskStatus {
|
|
PENDING
|
|
IN_PROGRESS
|
|
COMPLETED
|
|
CANCELLED
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// EMAIL CAMPAIGN - Step 01 (Admin Panel)
|
|
// Batch email campaign records (optional persistence)
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
model EmailCampaign {
|
|
id String @id @default(uuid())
|
|
name String
|
|
subject String
|
|
content String? @db.Text
|
|
recipientsFilter Json? // segment, limit, etc.
|
|
status EmailCampaignStatus @default(DRAFT)
|
|
sentCount Int @default(0) @map("sent_count")
|
|
failedCount Int @default(0) @map("failed_count")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
sentAt DateTime? @map("sent_at")
|
|
|
|
@@index([status])
|
|
@@index([createdAt(sort: Desc)])
|
|
}
|
|
|
|
enum EmailCampaignStatus {
|
|
DRAFT
|
|
SCHEDULED
|
|
SENDING
|
|
COMPLETED
|
|
CANCELLED
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// COUPON - Step 01 (Admin Panel)
|
|
// Coupon codes for promotions
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
model Coupon {
|
|
id String @id @default(uuid())
|
|
code String @unique
|
|
discountType CouponDiscountType @map("discount_type")
|
|
discountValue Decimal @db.Decimal(10, 2) @map("discount_value")
|
|
validFrom DateTime @map("valid_from")
|
|
validUntil DateTime @map("valid_until")
|
|
usageLimit Int? @map("usage_limit") // total uses, null = unlimited
|
|
usedCount Int @default(0) @map("used_count")
|
|
tierRestrict String[] @default([]) @map("tier_restrict") // empty = all tiers
|
|
countryRestrict String[] @default([]) @map("country_restrict") // empty = all countries
|
|
perUserLimit Int? @map("per_user_limit") // max uses per user, null = unlimited
|
|
isActive Boolean @default(true) @map("is_active")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@index([code])
|
|
@@index([validFrom, validUntil])
|
|
@@index([isActive])
|
|
}
|
|
|
|
enum CouponDiscountType {
|
|
PERCENT
|
|
FIXED
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// SEO SUBMISSION - Step 01 (Admin Panel)
|
|
// Sitemap/URL submission history to search engines
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
model SeoSubmission {
|
|
id String @id @default(uuid())
|
|
url String
|
|
platform String // google, bing, etc.
|
|
status String // submitted, success, error
|
|
submittedAt DateTime @default(now()) @map("submitted_at")
|
|
response Json? // API response or error
|
|
|
|
@@index([platform])
|
|
@@index([submittedAt(sort: Desc)])
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// USER ADMIN NOTE - Step 01 (Admin Panel)
|
|
// Admin notes on users (support, internal notes)
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
model UserAdminNote {
|
|
id String @id @default(uuid())
|
|
userId String @map("user_id")
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
adminId String @map("admin_id") // User.id of admin who wrote the note
|
|
note String @db.Text
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
@@index([userId])
|
|
@@index([createdAt(sort: Desc)])
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// APP CONFIG - Runtime configuration (022-runtime-config, Tier 2)
|
|
// Editable from Admin; cached in Redis.
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
model AppConfig {
|
|
id String @id @default(uuid())
|
|
key String @unique
|
|
value Json
|
|
valueType String @map("value_type") // string, number, boolean, json
|
|
category String // features, limits, pricing, ui, seo, admin
|
|
description String?
|
|
isSensitive Boolean @default(false) @map("is_sensitive")
|
|
isPublic Boolean @default(false) @map("is_public")
|
|
updatedBy String? @map("updated_by")
|
|
updatedAt DateTime @updatedAt
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([key])
|
|
@@index([category])
|
|
@@index([isPublic])
|
|
}
|
|
|
|
model AppConfigAudit {
|
|
id String @id @default(uuid())
|
|
configKey String @map("config_key")
|
|
oldValue Json? @map("old_value")
|
|
newValue Json? @map("new_value")
|
|
changedBy String? @map("changed_by")
|
|
changeReason String? @map("change_reason")
|
|
ipAddress String? @map("ip_address")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
@@index([configKey])
|
|
@@index([createdAt(sort: Desc)])
|
|
}
|