generator client { provider = "prisma-client-js" binaryTargets = ["rhel-openssl-3.0.x"] } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id String @id @default(cuid()) email String @unique name String passwordHash String? provider String @default("email") providerId String? avatar String? flhBalance Int @default(5000) isPro Boolean @default(false) isPremium Boolean @default(false) experienceLevel String? madhab String? coachPersona String? preferredName String? coachingGoals String? lastCoachedAt DateTime? dailyMsgCount Int @default(0) dailyMsgResetAt DateTime? stripeCustomerId String? stripeSubscriptionId String? trialEndsAt DateTime? createdAt DateTime @default(now()) @@index([provider, providerId]) listings Listing[] purchases Purchase[] @relation("BuyerPurchases") sales Purchase[] @relation("SellerPurchases") cashouts CashoutRequest[] halalBookmarks HalalBookmark[] halalUsage HalalUsage? userPlaces UserPlace[] ownedGroups Group[] @relation("GroupOwner") groupMemberships GroupMember[] forumThreads ForumThread[] forumPosts ForumPost[] chatHistory ChatHistory[] notifications Notification[] referredReferrals Referral[] @relation("Referrer") referrerReferral Referral? @relation("Referred") dailyStreak DailyStreak? xpTransactions XpTransaction[] achievements Achievement[] userLevel UserLevel? dhikrSessions DhikrSession[] dhikrDays DhikrDay[] reviewListings Review[] @relation("ListingReviews") } model DailyStreak { userId String @id user User @relation(fields: [userId], references: [id]) currentStreak Int @default(0) longestStreak Int @default(0) lastClaimDate DateTime? streakFreeze Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model XpTransaction { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id]) amount Int reason String // 'daily_login' | 'chat_message' | 'forum_post' | 'purchase' | 'referral' | 'achievement' createdAt DateTime @default(now()) @@index([userId]) } model Achievement { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id]) type String // 'first_chat' | 'streak_7' | 'streak_30' | 'level_5' | 'first_purchase' | 'first_post' title String description String? icon String? unlockedAt DateTime @default(now()) @@index([userId]) @@unique([userId, type]) } model UserLevel { userId String @id user User @relation(fields: [userId], references: [id]) level Int @default(1) xp Int @default(0) xpToNext Int @default(100) updatedAt DateTime @updatedAt } model Listing { id String @id @default(cuid()) title String description String category String subcategory String? priceFlh Int packages String? // JSON: [{name, description, price, deliveryDays, revisions}] tags String? // JSON: ["tag1", "tag2"] images String? // JSON: ["url1"] deliveryDays Int? rating Float @default(0) reviewCount Int @default(0) salesCount Int @default(0) sellerId String seller User @relation(fields: [sellerId], references: [id]) status String @default("active") featured Boolean @default(false) featuredUntil DateTime? fileType String? createdAt DateTime @default(now()) purchases Purchase[] reviews Review[] } model Purchase { id String @id @default(cuid()) listingId String listing Listing @relation(fields: [listingId], references: [id]) buyerId String buyer User @relation("BuyerPurchases", fields: [buyerId], references: [id]) sellerId String seller User @relation("SellerPurchases", fields: [sellerId], references: [id]) packageName String? amountFlh Int platformFee Int sellerPayout Int status String @default("pending") // pending, paid, in_progress, delivered, completed, disputed, cancelled messages String? // JSON: [{from, text, timestamp}] deliveryFiles String? // JSON: [{name, size, url}] autoConfirmAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt reviews Review[] } model Review { id String @id @default(cuid()) listingId String listing Listing @relation(fields: [listingId], references: [id]) purchaseId String purchase Purchase @relation(fields: [purchaseId], references: [id]) reviewerId String reviewer User @relation("ListingReviews", fields: [reviewerId], references: [id]) sellerId String rating Int title String? text String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([listingId]) @@index([sellerId]) } model CashoutRequest { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id]) amountFlh Int fiatAmount Float status String @default("pending") createdAt DateTime @default(now()) } model ForumCategory { id String @id @default(cuid()) name String @unique description String? icon String? order Int? createdAt DateTime @default(now()) threads ForumThread[] } model ForumThread { id String @id @default(cuid()) title String content String categoryId String category ForumCategory @relation(fields: [categoryId], references: [id]) authorId String author User @relation(fields: [authorId], references: [id]) pinned Boolean @default(false) shariahStatus String @default("pending") shariahFlags String? createdAt DateTime @default(now()) posts ForumPost[] group Group? @relation(fields: [groupId], references: [id]) groupId String? } model Group { id String @id @default(cuid()) name String description String? ownerId String owner User @relation("GroupOwner", fields: [ownerId], references: [id]) inviteCode String @unique createdAt DateTime @default(now()) members GroupMember[] threads ForumThread[] } model GroupMember { id String @id @default(cuid()) groupId String group Group @relation(fields: [groupId], references: [id]) userId String user User @relation(fields: [userId], references: [id]) role String @default("member") joinedAt DateTime @default(now()) @@unique([groupId, userId]) } model ForumPost { id String @id @default(cuid()) content String threadId String thread ForumThread @relation(fields: [threadId], references: [id]) authorId String author User @relation(fields: [authorId], references: [id]) shariahStatus String @default("pending") shariahFlags String? createdAt DateTime @default(now()) } model ChatHistory { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id]) role String content String metadata String? createdAt DateTime @default(now()) @@index([userId]) } model HalalBookmark { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id]) itemId String itemType String label String? createdAt DateTime @default(now()) @@index([userId]) } model HalalUsage { userId String @id user User @relation(fields: [userId], references: [id]) queriesUsed Int @default(0) queriesLimit Int @default(100) periodEnd DateTime? } model UserPlace { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id]) name String address String lat Float lng Float type String // "mosque" | "restaurant" | "cafe" notes String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([userId]) @@index([lat, lng]) } model Notification { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id]) type String // 'streak_reminder' | 'daily_verse' | 'premium_expiring' | 'new_listing' | 'forum_reply' | 'achievement' | 'referral_bonus' title String body String? data String? // JSON string with extra payload read Boolean @default(false) createdAt DateTime @default(now()) @@index([userId, read]) } model Referral { id String @id @default(cuid()) referrerId String referrer User @relation("Referrer", fields: [referrerId], references: [id]) referredId String @unique referred User @relation("Referred", fields: [referredId], references: [id]) status String @default("pending") // pending | joined | upgraded rewardFlh Int @default(0) createdAt DateTime @default(now()) upgradedAt DateTime? @@index([referrerId]) } model PremiumBenefit { id String @id @default(cuid()) tier String // 'premium' | 'pro' name String description String? icon String? active Boolean @default(true) createdAt DateTime @default(now()) } model HalalPlaceCache { id String @id @default(cuid()) cacheKey String @unique data String expiresAt DateTime } model DhikrSession { id String @id @default(cuid()) userId String user User @relation(fields: [userId], references: [id]) type String // 'subhanallah' | 'alhamdulillah' | 'allahuakbar' | 'custom' count Int @default(0) target Int @default(33) completed Boolean @default(true) createdAt DateTime @default(now()) @@index([userId, createdAt]) } model DhikrDay { userId String user User @relation(fields: [userId], references: [id]) date String // YYYY-MM-DD subhanallah Int @default(0) alhamdulillah Int @default(0) allahuakbar Int @default(0) custom Int @default(0) totalCount Int @default(0) sessionCount Int @default(0) updatedAt DateTime @updatedAt @@id([userId, date]) @@index([userId]) } // ────────────────────────────────────── // LEARN / MICRO LEARNING MODULE // ────────────────────────────────────── model LearnCourse { id String @id @default(cuid()) slug String @unique title String author String description String imageUrl String? moduleCount Int @default(0) totalMinutes Int @default(0) difficulty String @default("beginner") giteaPath String // path in Gitea courses repo priceFlh Int? // FLH one-time price priceUsd Float? // USD one-time price via Polar subscriptionOnly Boolean @default(false) published Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt modules LearnModule[] enrollments LearnEnrollment[] certificates LearnCertificate[] } model LearnModule { id String @id @default(cuid()) courseId String course LearnCourse @relation(fields: [courseId], references: [id], onDelete: Cascade) order Int title String slug String keyTakeaway String? duration Int @default(5) // minutes content String? // cached markdown body audioPath String? // cached TTS audio file path quizData String? // JSON: [{question, options[], correctIndex}] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt progress LearnModuleProgress[] @@unique([courseId, slug]) } model LearnEnrollment { id String @id @default(cuid()) userId String courseId String course LearnCourse @relation(fields: [courseId], references: [id]) purchaseType String @default("one_time") // "one_time" | "subscription" | "free" completed Boolean @default(false) progress Float @default(0) // 0.0 to 1.0 startedAt DateTime @default(now()) completedAt DateTime? updatedAt DateTime @updatedAt modules LearnModuleProgress[] @@unique([userId, courseId]) @@index([userId]) } model LearnModuleProgress { id String @id @default(cuid()) enrollmentId String enrollment LearnEnrollment @relation(fields: [enrollmentId], references: [id], onDelete: Cascade) moduleId String module LearnModule @relation(fields: [moduleId], references: [id], onDelete: Cascade) completed Boolean @default(false) score Float? // quiz score 0-100 listened Boolean @default(false) completedAt DateTime? updatedAt DateTime @updatedAt @@unique([enrollmentId, moduleId]) } model LearnSubscription { id String @id @default(cuid()) userId String status String @default("active") // active | cancelled | expired polarSubId String? @unique currentPeriodStart DateTime currentPeriodEnd DateTime cancelledAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([userId]) @@index([status]) } model LearnCertificate { id String @id @default(cuid()) serialNumber String @unique userId String userName String // cached at issue time courseId String course LearnCourse @relation(fields: [courseId], references: [id]) moduleCount Int @default(0) score Float? // aggregate quiz score pdfPath String? // /data/certificates/{serial}.pdf issuedAt DateTime @default(now()) viewedAt DateTime? revoked Boolean @default(false) revokedAt DateTime? hashOnChain String? // SHA256 for future blockchain anchoring metadata String? // JSON extra @@index([serialNumber]) @@index([userId]) @@index([courseId, userId]) } // Add relations to User model /// NOTE: The Notification and Referral relations are declared on the models above. /// User now implicitly has: notifications Notification[], referredReferrals Referral[] (via "Referrer"), referrerReferral Referral? (via "Referred")