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>
This commit is contained in:
@@ -29,6 +29,7 @@ enum ListingCondition {
|
||||
|
||||
enum ListingStatus {
|
||||
DRAFT
|
||||
PENDING_REVIEW
|
||||
ACTIVE
|
||||
SOLD
|
||||
DELETED
|
||||
@@ -50,6 +51,21 @@ enum NotificationType {
|
||||
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 {
|
||||
@@ -59,6 +75,120 @@ enum PaymentStatus {
|
||||
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
|
||||
@@ -71,6 +201,11 @@ model User {
|
||||
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)
|
||||
@@ -86,6 +221,9 @@ model User {
|
||||
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
|
||||
|
||||
@@ -102,6 +240,20 @@ model User {
|
||||
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 {
|
||||
@@ -119,19 +271,23 @@ model Session {
|
||||
}
|
||||
|
||||
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)
|
||||
sellerId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
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[]
|
||||
@@ -139,6 +295,7 @@ model Listing {
|
||||
conversations Conversation[]
|
||||
favorites Favorite[]
|
||||
payments Payment[]
|
||||
promotedListing PromotedListing?
|
||||
|
||||
@@index([sellerId])
|
||||
@@index([category])
|
||||
@@ -182,17 +339,19 @@ model Offer {
|
||||
}
|
||||
|
||||
model Conversation {
|
||||
id String @id @default(cuid())
|
||||
user1Id String
|
||||
user2Id String
|
||||
listingId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
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)
|
||||
messages Message[]
|
||||
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])
|
||||
@@ -250,6 +409,8 @@ model Payment {
|
||||
stripePaymentId String? @unique
|
||||
amount Float
|
||||
status PaymentStatus @default(PENDING)
|
||||
type PaymentType @default(LISTING_FEE)
|
||||
description String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -271,3 +432,276 @@ model BlockedUser {
|
||||
|
||||
@@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])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user