Files
marketplace/server/prisma/schema.prisma
delta-lynx-89e8 dbbbbd26f4 Add rental system: listings, bookings, payments, payouts, reviews
Full rental marketplace with 6 categories (apartment, house, car, motorcycle, bicycle, ebike).
Booking workflow: create → confirm → pay → active → complete → payout.
Landlord dashboard, admin moderation, availability calendar, Stripe Connect payouts.
14 QA bugs found and fixed including validator schemas, API response types, HTTP methods.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 15:33:29 -08:00

708 lines
19 KiB
Plaintext

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum Category {
ELECTRONICS
FURNITURE
CLOTHING
HOME_GARDEN
SPORTS
BOOKS
GAMES
VEHICLES
OTHER
}
enum ListingCondition {
NEW
LIKE_NEW
GENTLY_USED
USED
FAIR
}
enum ListingStatus {
DRAFT
PENDING_REVIEW
ACTIVE
SOLD
DELETED
}
enum OfferStatus {
PENDING
ACCEPTED
DECLINED
COUNTERED
CANCELLED
EXPIRED
}
enum NotificationType {
NEW_OFFER
OFFER_ACCEPTED
OFFER_DECLINED
ITEM_SOLD
NEW_MESSAGE
ITEM_FAVORITED
LISTING_APPROVED
LISTING_REJECTED
MODERATION_WARNING
ACCOUNT_BANNED
ACCOUNT_UNBANNED
REPORT_RESOLVED
BOOKING_REQUEST
BOOKING_CONFIRMED
BOOKING_REJECTED
BOOKING_CANCELLED
BOOKING_STARTED
BOOKING_COMPLETED
RENTAL_REVIEW
PAYOUT_SENT
PAYOUT_FAILED
}
enum PaymentStatus {
PENDING
COMPLETED
FAILED
REFUNDED
}
enum UserRole {
USER
MODERATOR
ADMIN
SUPER_ADMIN
}
enum ReportReason {
SPAM
INAPPROPRIATE
SCAM
COUNTERFEIT
PROHIBITED_ITEM
HARASSMENT
OTHER
}
enum ReportStatus {
OPEN
REVIEWING
RESOLVED
DISMISSED
}
enum ReportTargetType {
LISTING
USER
}
enum SubscriptionTier {
BASIC
PRO
BUSINESS
}
enum SubscriptionStatus {
ACTIVE
CANCELLED
EXPIRED
PAST_DUE
}
enum PaymentType {
LISTING_FEE
COMMISSION
PROMOTION
SUBSCRIPTION
RENTAL_BOOKING
RENTAL_COMMISSION
RENTAL_DEPOSIT
RENTAL_DEPOSIT_REFUND
RENTAL_PAYOUT
}
enum ModerationAction {
APPROVED
REJECTED
WARNING
BAN
UNBAN
LISTING_DELETED
LISTING_FEATURED
}
// ── Rental enums ──────────────────────────────────────────────────
enum RentalCategory {
APARTMENT
HOUSE
CAR
MOTORCYCLE
BICYCLE
EBIKE
}
enum RentalPeriodType {
DAILY
MONTHLY
}
enum RentalListingStatus {
DRAFT
PENDING_REVIEW
ACTIVE
PAUSED
DELETED
}
enum BookingStatus {
PENDING
CONFIRMED
ACTIVE
COMPLETED
CANCELLED_BY_TENANT
CANCELLED_BY_LANDLORD
REJECTED
EXPIRED
}
enum PayoutStatus {
PENDING
PROCESSING
COMPLETED
FAILED
}
enum CancellationPolicy {
FLEXIBLE
MODERATE
STRICT
}
// ── Models ────────────────────────────────────────────────────────
model User {
id String @id @default(cuid())
email String @unique
passwordHash String
fullName String
nickname String?
avatar String?
phone String?
location String?
bio String?
rating Float @default(0)
ratingCount Int @default(0)
role UserRole @default(USER)
isBanned Boolean @default(false)
banReason String?
bannedAt DateTime?
bannedBy String?
showEmail Boolean @default(false)
showPhone Boolean @default(true)
showLocation Boolean @default(true)
showOnline Boolean @default(true)
showRating Boolean @default(true)
twoFactorEnabled Boolean @default(false)
isActive Boolean @default(true)
notifNewOffer Boolean @default(true)
notifMessages Boolean @default(true)
notifItemSold Boolean @default(true)
notifFavorites Boolean @default(true)
notifEmail Boolean @default(true)
marketingEmail Boolean @default(false)
resetToken String? @unique
resetTokenExpiry DateTime?
isLandlord Boolean @default(false)
landlordVerified Boolean @default(false)
stripeAccountId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sessions Session[]
listings Listing[]
images ListingImage[]
sentOffers Offer[] @relation("BuyerOffers")
receivedOffers Offer[] @relation("SellerOffers")
conversations1 Conversation[] @relation("User1Conversations")
conversations2 Conversation[] @relation("User2Conversations")
messages Message[]
favorites Favorite[]
notifications Notification[]
payments Payment[]
blockedUsers BlockedUser[] @relation("Blocker")
blockedBy BlockedUser[] @relation("Blocked")
reports Report[]
subscription Subscription?
promotedListings PromotedListing[]
moderationLogs ModerationLog[]
// Rental relations
rentalListings RentalListing[]
tenantBookings Booking[] @relation("TenantBookings")
landlordBookings Booking[] @relation("LandlordBookings")
payouts Payout[]
rentalReviews RentalReview[] @relation("TenantReviews")
landlordReviews RentalReview[] @relation("LandlordReviews")
rentalFavorites RentalFavorite[]
promotedRentals PromotedRental[]
}
model Session {
id String @id @default(cuid())
userId String
refreshToken String @unique
userAgent String?
ipAddress String?
expiresAt DateTime
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
model Listing {
id String @id @default(cuid())
title String
description String
price Float
obo Boolean @default(false)
category Category
condition ListingCondition
status ListingStatus @default(DRAFT)
location String
viewCount Int @default(0)
isFeatured Boolean @default(false)
rejectionReason String?
reviewedBy String?
reviewedAt DateTime?
sellerId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
seller User @relation(fields: [sellerId], references: [id], onDelete: Cascade)
images ListingImage[]
offers Offer[]
conversations Conversation[]
favorites Favorite[]
payments Payment[]
promotedListing PromotedListing?
@@index([sellerId])
@@index([category])
@@index([status])
@@index([createdAt])
}
model ListingImage {
id String @id @default(cuid())
url String
order Int @default(0)
listingId String
uploadedBy String
listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade)
user User @relation(fields: [uploadedBy], references: [id], onDelete: Cascade)
@@index([listingId])
}
model Offer {
id String @id @default(cuid())
amount Float
message String?
status OfferStatus @default(PENDING)
counterAmount Float?
expiresAt DateTime?
buyerId String
sellerId String
listingId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
buyer User @relation("BuyerOffers", fields: [buyerId], references: [id], onDelete: Cascade)
seller User @relation("SellerOffers", fields: [sellerId], references: [id], onDelete: Cascade)
listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade)
@@index([buyerId])
@@index([sellerId])
@@index([listingId])
}
model Conversation {
id String @id @default(cuid())
user1Id String
user2Id String
listingId String?
rentalListingId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user1 User @relation("User1Conversations", fields: [user1Id], references: [id], onDelete: Cascade)
user2 User @relation("User2Conversations", fields: [user2Id], references: [id], onDelete: Cascade)
listing Listing? @relation(fields: [listingId], references: [id], onDelete: SetNull)
rentalListing RentalListing? @relation(fields: [rentalListingId], references: [id], onDelete: SetNull)
messages Message[]
@@unique([user1Id, user2Id, listingId])
@@index([user1Id])
@@index([user2Id])
}
model Message {
id String @id @default(cuid())
content String
senderId String
conversationId String
isRead Boolean @default(false)
offerAmount Float?
createdAt DateTime @default(now())
sender User @relation(fields: [senderId], references: [id], onDelete: Cascade)
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
@@index([conversationId])
@@index([senderId])
}
model Favorite {
id String @id @default(cuid())
userId String
listingId String
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade)
@@unique([userId, listingId])
}
model Notification {
id String @id @default(cuid())
userId String
type NotificationType
title String
body String
data Json?
isRead Boolean @default(false)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([createdAt])
}
model Payment {
id String @id @default(cuid())
userId String
listingId String
stripePaymentId String? @unique
amount Float
status PaymentStatus @default(PENDING)
type PaymentType @default(LISTING_FEE)
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([listingId])
}
model BlockedUser {
id String @id @default(cuid())
blockerId String
blockedId String
createdAt DateTime @default(now())
blocker User @relation("Blocker", fields: [blockerId], references: [id], onDelete: Cascade)
blocked User @relation("Blocked", fields: [blockedId], references: [id], onDelete: Cascade)
@@unique([blockerId, blockedId])
}
model Report {
id String @id @default(cuid())
reporterId String
targetType ReportTargetType
targetId String
reason ReportReason
description String?
status ReportStatus @default(OPEN)
resolvedBy String?
resolution String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
reporter User @relation(fields: [reporterId], references: [id], onDelete: Cascade)
@@index([status])
@@index([targetType, targetId])
}
model PlatformConfig {
id String @id @default(cuid())
listingFee Float @default(5.00)
commissionPercent Float @default(5.0)
autoApprove Boolean @default(true)
maxImagesPerListing Int @default(6)
maxListingsFreeTier Int @default(5)
proPrice Float @default(9.99)
businessPrice Float @default(29.99)
promotionDayPrice Float @default(2.99)
blockedKeywords String[] @default([])
rentalCommissionPercent Float @default(10.0)
rentalAutoApprove Boolean @default(false)
maxRentalImagesPerListing Int @default(10)
bookingExpiryHours Int @default(48)
rentalPromotionDayPrice Float @default(3.99)
updatedAt DateTime @updatedAt
}
model Subscription {
id String @id @default(cuid())
userId String @unique
tier SubscriptionTier @default(BASIC)
status SubscriptionStatus @default(ACTIVE)
stripeSubscriptionId String? @unique
currentPeriodEnd DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model PromotedListing {
id String @id @default(cuid())
listingId String @unique
userId String
startDate DateTime @default(now())
endDate DateTime
amountPaid Float
isActive Boolean @default(true)
createdAt DateTime @default(now())
listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([isActive, endDate])
}
model ModerationLog {
id String @id @default(cuid())
moderatorId String
targetUserId String?
targetListingId String?
action ModerationAction
reason String?
details Json?
createdAt DateTime @default(now())
moderator User @relation(fields: [moderatorId], references: [id], onDelete: Cascade)
@@index([moderatorId])
@@index([createdAt])
}
// ── Rental Models ─────────────────────────────────────────────────
model RentalListing {
id String @id @default(cuid())
title String
description String
category RentalCategory
location String
dailyPrice Float?
monthlyPrice Float?
depositAmount Float?
details Json?
amenities String[] @default([])
rules String[] @default([])
cancellationPolicy CancellationPolicy @default(FLEXIBLE)
minDays Int?
maxDays Int?
minMonths Int?
maxMonths Int?
status RentalListingStatus @default(DRAFT)
viewCount Int @default(0)
isFeatured Boolean @default(false)
isVerified Boolean @default(false)
rejectionReason String?
reviewedBy String?
reviewedAt DateTime?
landlordId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
landlord User @relation(fields: [landlordId], references: [id], onDelete: Cascade)
images RentalImage[]
availabilityBlocks AvailabilityBlock[]
bookings Booking[]
reviews RentalReview[]
favorites RentalFavorite[]
promotedRental PromotedRental?
conversations Conversation[]
@@index([landlordId])
@@index([category])
@@index([status])
@@index([createdAt])
}
model RentalImage {
id String @id @default(cuid())
url String
order Int @default(0)
rentalListingId String
rentalListing RentalListing @relation(fields: [rentalListingId], references: [id], onDelete: Cascade)
@@index([rentalListingId])
}
model AvailabilityBlock {
id String @id @default(cuid())
rentalListingId String
startDate DateTime
endDate DateTime
isBlocked Boolean @default(true)
reason String?
createdAt DateTime @default(now())
rentalListing RentalListing @relation(fields: [rentalListingId], references: [id], onDelete: Cascade)
@@index([rentalListingId])
@@index([startDate, endDate])
}
model Booking {
id String @id @default(cuid())
rentalListingId String
tenantId String
landlordId String
periodType RentalPeriodType
startDate DateTime
endDate DateTime
pricePerPeriod Float
totalPeriods Int
subtotal Float
commissionRate Float @default(10.0)
commissionAmount Float
depositAmount Float @default(0)
totalAmount Float
status BookingStatus @default(PENDING)
message String?
rejectionReason String?
cancellationReason String?
stripePaymentIntentId String?
expiresAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
rentalListing RentalListing @relation(fields: [rentalListingId], references: [id], onDelete: Cascade)
tenant User @relation("TenantBookings", fields: [tenantId], references: [id], onDelete: Cascade)
landlord User @relation("LandlordBookings", fields: [landlordId], references: [id], onDelete: Cascade)
payments BookingPayment[]
payout Payout?
review RentalReview?
@@index([rentalListingId])
@@index([tenantId])
@@index([landlordId])
@@index([status])
}
model BookingPayment {
id String @id @default(cuid())
bookingId String
stripePaymentId String? @unique
amount Float
type PaymentType
status PaymentStatus @default(PENDING)
createdAt DateTime @default(now())
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
@@index([bookingId])
}
model Payout {
id String @id @default(cuid())
bookingId String @unique
landlordId String
grossAmount Float
commissionAmount Float
netAmount Float
status PayoutStatus @default(PENDING)
stripeTransferId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
landlord User @relation(fields: [landlordId], references: [id], onDelete: Cascade)
@@index([landlordId])
@@index([status])
}
model RentalReview {
id String @id @default(cuid())
bookingId String @unique
rentalListingId String
tenantId String
landlordId String
rating Int
comment String?
landlordResponse String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
rentalListing RentalListing @relation(fields: [rentalListingId], references: [id], onDelete: Cascade)
tenant User @relation("TenantReviews", fields: [tenantId], references: [id], onDelete: Cascade)
landlord User @relation("LandlordReviews", fields: [landlordId], references: [id], onDelete: Cascade)
@@index([rentalListingId])
@@index([landlordId])
}
model RentalFavorite {
id String @id @default(cuid())
userId String
rentalListingId String
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
rentalListing RentalListing @relation(fields: [rentalListingId], references: [id], onDelete: Cascade)
@@unique([userId, rentalListingId])
}
model PromotedRental {
id String @id @default(cuid())
rentalListingId String @unique
userId String
startDate DateTime @default(now())
endDate DateTime
amountPaid Float
isActive Boolean @default(true)
createdAt DateTime @default(now())
rentalListing RentalListing @relation(fields: [rentalListingId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([isActive, endDate])
}