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:
delta-lynx-89e8
2026-02-22 15:33:29 -08:00
parent 8961fa701a
commit dbbbbd26f4
90 changed files with 11052 additions and 124 deletions

View File

@@ -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])
}