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>
708 lines
19 KiB
Plaintext
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])
|
|
}
|