Files
falah-mobile/prisma/schema.prisma
T
root cfff74e2db feat: Souq native integration + auth simplification + CE-wide enhancements
Souq Marketplace:
- Native souq pages: browse+filters, create listing, detail+seller profile, orders+chat
- New API: reviews (POST), sellers/[id], upload (multipart), polar-checkout webhook
- Listings API enhanced: minPrice, maxPrice, sortBy, deliveryDays filters
- Seller workflow: start order, mark delivered, upload files, confirm delivery
- Wallet: 5 Polar.sh FLH tiers, production checkout, mock fallback, success banner
- Fix: order redirect /souq/history → /souq/orders

Auth System:
- Unified auth page, removed OAuth provider routing (2 files deleted)
- Deleted oauth.ts (319 lines) — simplified auth chain
- Streamlined login/register API routes

Prisma Schema (+179 lines):
- New models: Listing, Purchase, Review, Group/GroupMember
- Gamification: DailyStreak, XpTransaction, Achievement, UserLevel
- Learning: LearnCourse/Module/Enrollment/Certificate
- Notifications, Referrals, Dhikr, PremiumBenefits

Deleted legacy code:
- src/app/api/marketplace/* (3 files) — replaced by /api/souq/*
- src/app/api/files/[listingId]/route.ts — replaced by /api/souq/upload
- src/app/api/seller/[sellerId]/route.ts — replaced by /api/souq/sellers/[id]

Infrastructure:
- Dockerfile: standalone output, Prisma runtime migration
- docker-compose.yml: Polar env vars, healthcheck fix
- docker-compose.staging.yml: staging on port 4014
- .gitea/workflows/ci.yml: CI pipeline
2026-06-24 07:02:03 +02:00

492 lines
16 KiB
Plaintext

generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "linux-musl", "debian-openssl-3.0.x", "linux-musl-openssl-3.0.x"]
}
datasource db {
provider = "sqlite"
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")