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

@@ -0,0 +1,173 @@
-- CreateEnum
CREATE TYPE "UserRole" AS ENUM ('USER', 'MODERATOR', 'ADMIN', 'SUPER_ADMIN');
-- CreateEnum
CREATE TYPE "ReportReason" AS ENUM ('SPAM', 'INAPPROPRIATE', 'SCAM', 'COUNTERFEIT', 'PROHIBITED_ITEM', 'HARASSMENT', 'OTHER');
-- CreateEnum
CREATE TYPE "ReportStatus" AS ENUM ('OPEN', 'REVIEWING', 'RESOLVED', 'DISMISSED');
-- CreateEnum
CREATE TYPE "ReportTargetType" AS ENUM ('LISTING', 'USER');
-- CreateEnum
CREATE TYPE "SubscriptionTier" AS ENUM ('BASIC', 'PRO', 'BUSINESS');
-- CreateEnum
CREATE TYPE "SubscriptionStatus" AS ENUM ('ACTIVE', 'CANCELLED', 'EXPIRED', 'PAST_DUE');
-- CreateEnum
CREATE TYPE "PaymentType" AS ENUM ('LISTING_FEE', 'COMMISSION', 'PROMOTION', 'SUBSCRIPTION');
-- CreateEnum
CREATE TYPE "ModerationAction" AS ENUM ('APPROVED', 'REJECTED', 'WARNING', 'BAN', 'UNBAN', 'LISTING_DELETED', 'LISTING_FEATURED');
-- AlterEnum
ALTER TYPE "ListingStatus" ADD VALUE 'PENDING_REVIEW';
-- AlterEnum
-- This migration adds more than one value to an enum.
-- With PostgreSQL versions 11 and earlier, this is not possible
-- in a single migration. This can be worked around by creating
-- multiple migrations, each migration adding only one value to
-- the enum.
ALTER TYPE "NotificationType" ADD VALUE 'LISTING_APPROVED';
ALTER TYPE "NotificationType" ADD VALUE 'LISTING_REJECTED';
ALTER TYPE "NotificationType" ADD VALUE 'MODERATION_WARNING';
ALTER TYPE "NotificationType" ADD VALUE 'ACCOUNT_BANNED';
ALTER TYPE "NotificationType" ADD VALUE 'ACCOUNT_UNBANNED';
ALTER TYPE "NotificationType" ADD VALUE 'REPORT_RESOLVED';
-- AlterTable
ALTER TABLE "Listing" ADD COLUMN "isFeatured" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "rejectionReason" TEXT,
ADD COLUMN "reviewedAt" TIMESTAMP(3),
ADD COLUMN "reviewedBy" TEXT;
-- AlterTable
ALTER TABLE "Payment" ADD COLUMN "description" TEXT,
ADD COLUMN "type" "PaymentType" NOT NULL DEFAULT 'LISTING_FEE';
-- AlterTable
ALTER TABLE "User" ADD COLUMN "banReason" TEXT,
ADD COLUMN "bannedAt" TIMESTAMP(3),
ADD COLUMN "bannedBy" TEXT,
ADD COLUMN "isBanned" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "role" "UserRole" NOT NULL DEFAULT 'USER';
-- CreateTable
CREATE TABLE "Report" (
"id" TEXT NOT NULL,
"reporterId" TEXT NOT NULL,
"targetType" "ReportTargetType" NOT NULL,
"targetId" TEXT NOT NULL,
"reason" "ReportReason" NOT NULL,
"description" TEXT,
"status" "ReportStatus" NOT NULL DEFAULT 'OPEN',
"resolvedBy" TEXT,
"resolution" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Report_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PlatformConfig" (
"id" TEXT NOT NULL,
"listingFee" DOUBLE PRECISION NOT NULL DEFAULT 5.00,
"commissionPercent" DOUBLE PRECISION NOT NULL DEFAULT 5.0,
"autoApprove" BOOLEAN NOT NULL DEFAULT true,
"maxImagesPerListing" INTEGER NOT NULL DEFAULT 6,
"maxListingsFreeTier" INTEGER NOT NULL DEFAULT 5,
"proPrice" DOUBLE PRECISION NOT NULL DEFAULT 9.99,
"businessPrice" DOUBLE PRECISION NOT NULL DEFAULT 29.99,
"promotionDayPrice" DOUBLE PRECISION NOT NULL DEFAULT 2.99,
"blockedKeywords" TEXT[] DEFAULT ARRAY[]::TEXT[],
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "PlatformConfig_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Subscription" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"tier" "SubscriptionTier" NOT NULL DEFAULT 'BASIC',
"status" "SubscriptionStatus" NOT NULL DEFAULT 'ACTIVE',
"stripeSubscriptionId" TEXT,
"currentPeriodEnd" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Subscription_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PromotedListing" (
"id" TEXT NOT NULL,
"listingId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"startDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"endDate" TIMESTAMP(3) NOT NULL,
"amountPaid" DOUBLE PRECISION NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PromotedListing_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ModerationLog" (
"id" TEXT NOT NULL,
"moderatorId" TEXT NOT NULL,
"targetUserId" TEXT,
"targetListingId" TEXT,
"action" "ModerationAction" NOT NULL,
"reason" TEXT,
"details" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ModerationLog_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Report_status_idx" ON "Report"("status");
-- CreateIndex
CREATE INDEX "Report_targetType_targetId_idx" ON "Report"("targetType", "targetId");
-- CreateIndex
CREATE UNIQUE INDEX "Subscription_userId_key" ON "Subscription"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "Subscription_stripeSubscriptionId_key" ON "Subscription"("stripeSubscriptionId");
-- CreateIndex
CREATE UNIQUE INDEX "PromotedListing_listingId_key" ON "PromotedListing"("listingId");
-- CreateIndex
CREATE INDEX "PromotedListing_isActive_endDate_idx" ON "PromotedListing"("isActive", "endDate");
-- CreateIndex
CREATE INDEX "ModerationLog_moderatorId_idx" ON "ModerationLog"("moderatorId");
-- CreateIndex
CREATE INDEX "ModerationLog_createdAt_idx" ON "ModerationLog"("createdAt");
-- AddForeignKey
ALTER TABLE "Report" ADD CONSTRAINT "Report_reporterId_fkey" FOREIGN KEY ("reporterId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PromotedListing" ADD CONSTRAINT "PromotedListing_listingId_fkey" FOREIGN KEY ("listingId") REFERENCES "Listing"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PromotedListing" ADD CONSTRAINT "PromotedListing_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ModerationLog" ADD CONSTRAINT "ModerationLog_moderatorId_fkey" FOREIGN KEY ("moderatorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,336 @@
-- CreateEnum
CREATE TYPE "RentalCategory" AS ENUM ('APARTMENT', 'HOUSE', 'CAR', 'MOTORCYCLE', 'BICYCLE', 'EBIKE');
-- CreateEnum
CREATE TYPE "RentalPeriodType" AS ENUM ('DAILY', 'MONTHLY');
-- CreateEnum
CREATE TYPE "RentalListingStatus" AS ENUM ('DRAFT', 'PENDING_REVIEW', 'ACTIVE', 'PAUSED', 'DELETED');
-- CreateEnum
CREATE TYPE "BookingStatus" AS ENUM ('PENDING', 'CONFIRMED', 'ACTIVE', 'COMPLETED', 'CANCELLED_BY_TENANT', 'CANCELLED_BY_LANDLORD', 'REJECTED', 'EXPIRED');
-- CreateEnum
CREATE TYPE "PayoutStatus" AS ENUM ('PENDING', 'PROCESSING', 'COMPLETED', 'FAILED');
-- CreateEnum
CREATE TYPE "CancellationPolicy" AS ENUM ('FLEXIBLE', 'MODERATE', 'STRICT');
-- AlterEnum
-- This migration adds more than one value to an enum.
-- With PostgreSQL versions 11 and earlier, this is not possible
-- in a single migration. This can be worked around by creating
-- multiple migrations, each migration adding only one value to
-- the enum.
ALTER TYPE "NotificationType" ADD VALUE 'BOOKING_REQUEST';
ALTER TYPE "NotificationType" ADD VALUE 'BOOKING_CONFIRMED';
ALTER TYPE "NotificationType" ADD VALUE 'BOOKING_REJECTED';
ALTER TYPE "NotificationType" ADD VALUE 'BOOKING_CANCELLED';
ALTER TYPE "NotificationType" ADD VALUE 'BOOKING_STARTED';
ALTER TYPE "NotificationType" ADD VALUE 'BOOKING_COMPLETED';
ALTER TYPE "NotificationType" ADD VALUE 'RENTAL_REVIEW';
ALTER TYPE "NotificationType" ADD VALUE 'PAYOUT_SENT';
ALTER TYPE "NotificationType" ADD VALUE 'PAYOUT_FAILED';
-- AlterEnum
-- This migration adds more than one value to an enum.
-- With PostgreSQL versions 11 and earlier, this is not possible
-- in a single migration. This can be worked around by creating
-- multiple migrations, each migration adding only one value to
-- the enum.
ALTER TYPE "PaymentType" ADD VALUE 'RENTAL_BOOKING';
ALTER TYPE "PaymentType" ADD VALUE 'RENTAL_COMMISSION';
ALTER TYPE "PaymentType" ADD VALUE 'RENTAL_DEPOSIT';
ALTER TYPE "PaymentType" ADD VALUE 'RENTAL_DEPOSIT_REFUND';
ALTER TYPE "PaymentType" ADD VALUE 'RENTAL_PAYOUT';
-- AlterTable
ALTER TABLE "Conversation" ADD COLUMN "rentalListingId" TEXT;
-- AlterTable
ALTER TABLE "PlatformConfig" ADD COLUMN "bookingExpiryHours" INTEGER NOT NULL DEFAULT 48,
ADD COLUMN "maxRentalImagesPerListing" INTEGER NOT NULL DEFAULT 10,
ADD COLUMN "rentalAutoApprove" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "rentalCommissionPercent" DOUBLE PRECISION NOT NULL DEFAULT 10.0,
ADD COLUMN "rentalPromotionDayPrice" DOUBLE PRECISION NOT NULL DEFAULT 3.99;
-- AlterTable
ALTER TABLE "User" ADD COLUMN "isLandlord" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "landlordVerified" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "stripeAccountId" TEXT;
-- CreateTable
CREATE TABLE "RentalListing" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"category" "RentalCategory" NOT NULL,
"location" TEXT NOT NULL,
"dailyPrice" DOUBLE PRECISION,
"monthlyPrice" DOUBLE PRECISION,
"depositAmount" DOUBLE PRECISION,
"details" JSONB,
"amenities" TEXT[] DEFAULT ARRAY[]::TEXT[],
"rules" TEXT[] DEFAULT ARRAY[]::TEXT[],
"cancellationPolicy" "CancellationPolicy" NOT NULL DEFAULT 'FLEXIBLE',
"minDays" INTEGER,
"maxDays" INTEGER,
"minMonths" INTEGER,
"maxMonths" INTEGER,
"status" "RentalListingStatus" NOT NULL DEFAULT 'DRAFT',
"viewCount" INTEGER NOT NULL DEFAULT 0,
"isFeatured" BOOLEAN NOT NULL DEFAULT false,
"isVerified" BOOLEAN NOT NULL DEFAULT false,
"rejectionReason" TEXT,
"reviewedBy" TEXT,
"reviewedAt" TIMESTAMP(3),
"landlordId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "RentalListing_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RentalImage" (
"id" TEXT NOT NULL,
"url" TEXT NOT NULL,
"order" INTEGER NOT NULL DEFAULT 0,
"rentalListingId" TEXT NOT NULL,
CONSTRAINT "RentalImage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AvailabilityBlock" (
"id" TEXT NOT NULL,
"rentalListingId" TEXT NOT NULL,
"startDate" TIMESTAMP(3) NOT NULL,
"endDate" TIMESTAMP(3) NOT NULL,
"isBlocked" BOOLEAN NOT NULL DEFAULT true,
"reason" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AvailabilityBlock_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Booking" (
"id" TEXT NOT NULL,
"rentalListingId" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"landlordId" TEXT NOT NULL,
"periodType" "RentalPeriodType" NOT NULL,
"startDate" TIMESTAMP(3) NOT NULL,
"endDate" TIMESTAMP(3) NOT NULL,
"pricePerPeriod" DOUBLE PRECISION NOT NULL,
"totalPeriods" INTEGER NOT NULL,
"subtotal" DOUBLE PRECISION NOT NULL,
"commissionRate" DOUBLE PRECISION NOT NULL DEFAULT 10.0,
"commissionAmount" DOUBLE PRECISION NOT NULL,
"depositAmount" DOUBLE PRECISION NOT NULL DEFAULT 0,
"totalAmount" DOUBLE PRECISION NOT NULL,
"status" "BookingStatus" NOT NULL DEFAULT 'PENDING',
"message" TEXT,
"rejectionReason" TEXT,
"cancellationReason" TEXT,
"stripePaymentIntentId" TEXT,
"expiresAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Booking_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BookingPayment" (
"id" TEXT NOT NULL,
"bookingId" TEXT NOT NULL,
"stripePaymentId" TEXT,
"amount" DOUBLE PRECISION NOT NULL,
"type" "PaymentType" NOT NULL,
"status" "PaymentStatus" NOT NULL DEFAULT 'PENDING',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "BookingPayment_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Payout" (
"id" TEXT NOT NULL,
"bookingId" TEXT NOT NULL,
"landlordId" TEXT NOT NULL,
"grossAmount" DOUBLE PRECISION NOT NULL,
"commissionAmount" DOUBLE PRECISION NOT NULL,
"netAmount" DOUBLE PRECISION NOT NULL,
"status" "PayoutStatus" NOT NULL DEFAULT 'PENDING',
"stripeTransferId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Payout_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RentalReview" (
"id" TEXT NOT NULL,
"bookingId" TEXT NOT NULL,
"rentalListingId" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"landlordId" TEXT NOT NULL,
"rating" INTEGER NOT NULL,
"comment" TEXT,
"landlordResponse" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "RentalReview_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RentalFavorite" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"rentalListingId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "RentalFavorite_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PromotedRental" (
"id" TEXT NOT NULL,
"rentalListingId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"startDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"endDate" TIMESTAMP(3) NOT NULL,
"amountPaid" DOUBLE PRECISION NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PromotedRental_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "RentalListing_landlordId_idx" ON "RentalListing"("landlordId");
-- CreateIndex
CREATE INDEX "RentalListing_category_idx" ON "RentalListing"("category");
-- CreateIndex
CREATE INDEX "RentalListing_status_idx" ON "RentalListing"("status");
-- CreateIndex
CREATE INDEX "RentalListing_createdAt_idx" ON "RentalListing"("createdAt");
-- CreateIndex
CREATE INDEX "RentalImage_rentalListingId_idx" ON "RentalImage"("rentalListingId");
-- CreateIndex
CREATE INDEX "AvailabilityBlock_rentalListingId_idx" ON "AvailabilityBlock"("rentalListingId");
-- CreateIndex
CREATE INDEX "AvailabilityBlock_startDate_endDate_idx" ON "AvailabilityBlock"("startDate", "endDate");
-- CreateIndex
CREATE INDEX "Booking_rentalListingId_idx" ON "Booking"("rentalListingId");
-- CreateIndex
CREATE INDEX "Booking_tenantId_idx" ON "Booking"("tenantId");
-- CreateIndex
CREATE INDEX "Booking_landlordId_idx" ON "Booking"("landlordId");
-- CreateIndex
CREATE INDEX "Booking_status_idx" ON "Booking"("status");
-- CreateIndex
CREATE UNIQUE INDEX "BookingPayment_stripePaymentId_key" ON "BookingPayment"("stripePaymentId");
-- CreateIndex
CREATE INDEX "BookingPayment_bookingId_idx" ON "BookingPayment"("bookingId");
-- CreateIndex
CREATE UNIQUE INDEX "Payout_bookingId_key" ON "Payout"("bookingId");
-- CreateIndex
CREATE INDEX "Payout_landlordId_idx" ON "Payout"("landlordId");
-- CreateIndex
CREATE INDEX "Payout_status_idx" ON "Payout"("status");
-- CreateIndex
CREATE UNIQUE INDEX "RentalReview_bookingId_key" ON "RentalReview"("bookingId");
-- CreateIndex
CREATE INDEX "RentalReview_rentalListingId_idx" ON "RentalReview"("rentalListingId");
-- CreateIndex
CREATE INDEX "RentalReview_landlordId_idx" ON "RentalReview"("landlordId");
-- CreateIndex
CREATE UNIQUE INDEX "RentalFavorite_userId_rentalListingId_key" ON "RentalFavorite"("userId", "rentalListingId");
-- CreateIndex
CREATE UNIQUE INDEX "PromotedRental_rentalListingId_key" ON "PromotedRental"("rentalListingId");
-- CreateIndex
CREATE INDEX "PromotedRental_isActive_endDate_idx" ON "PromotedRental"("isActive", "endDate");
-- AddForeignKey
ALTER TABLE "Conversation" ADD CONSTRAINT "Conversation_rentalListingId_fkey" FOREIGN KEY ("rentalListingId") REFERENCES "RentalListing"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RentalListing" ADD CONSTRAINT "RentalListing_landlordId_fkey" FOREIGN KEY ("landlordId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RentalImage" ADD CONSTRAINT "RentalImage_rentalListingId_fkey" FOREIGN KEY ("rentalListingId") REFERENCES "RentalListing"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AvailabilityBlock" ADD CONSTRAINT "AvailabilityBlock_rentalListingId_fkey" FOREIGN KEY ("rentalListingId") REFERENCES "RentalListing"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Booking" ADD CONSTRAINT "Booking_rentalListingId_fkey" FOREIGN KEY ("rentalListingId") REFERENCES "RentalListing"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Booking" ADD CONSTRAINT "Booking_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Booking" ADD CONSTRAINT "Booking_landlordId_fkey" FOREIGN KEY ("landlordId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BookingPayment" ADD CONSTRAINT "BookingPayment_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Payout" ADD CONSTRAINT "Payout_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Payout" ADD CONSTRAINT "Payout_landlordId_fkey" FOREIGN KEY ("landlordId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RentalReview" ADD CONSTRAINT "RentalReview_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RentalReview" ADD CONSTRAINT "RentalReview_rentalListingId_fkey" FOREIGN KEY ("rentalListingId") REFERENCES "RentalListing"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RentalReview" ADD CONSTRAINT "RentalReview_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RentalReview" ADD CONSTRAINT "RentalReview_landlordId_fkey" FOREIGN KEY ("landlordId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RentalFavorite" ADD CONSTRAINT "RentalFavorite_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "RentalFavorite" ADD CONSTRAINT "RentalFavorite_rentalListingId_fkey" FOREIGN KEY ("rentalListingId") REFERENCES "RentalListing"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PromotedRental" ADD CONSTRAINT "PromotedRental_rentalListingId_fkey" FOREIGN KEY ("rentalListingId") REFERENCES "RentalListing"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PromotedRental" ADD CONSTRAINT "PromotedRental_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

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

View File

@@ -11,6 +11,20 @@ async function main() {
// ── Clear existing data (reverse dependency order) ──────────────────
await prisma.$transaction([
prisma.promotedRental.deleteMany(),
prisma.rentalFavorite.deleteMany(),
prisma.rentalReview.deleteMany(),
prisma.payout.deleteMany(),
prisma.bookingPayment.deleteMany(),
prisma.booking.deleteMany(),
prisma.availabilityBlock.deleteMany(),
prisma.rentalImage.deleteMany(),
prisma.rentalListing.deleteMany(),
prisma.moderationLog.deleteMany(),
prisma.promotedListing.deleteMany(),
prisma.subscription.deleteMany(),
prisma.report.deleteMany(),
prisma.platformConfig.deleteMany(),
prisma.message.deleteMany(),
prisma.conversation.deleteMany(),
prisma.notification.deleteMany(),
@@ -780,6 +794,310 @@ async function main() {
console.log('Created 1 blocked user entry.');
// ── Platform Config ───────────────────────────────────────────────
await prisma.platformConfig.deleteMany();
await prisma.platformConfig.create({
data: {
listingFee: 5.00,
commissionPercent: 5.0,
autoApprove: true,
maxImagesPerListing: 6,
maxListingsFreeTier: 5,
proPrice: 9.99,
businessPrice: 29.99,
promotionDayPrice: 2.99,
blockedKeywords: ['illegal', 'drugs', 'weapons'],
rentalCommissionPercent: 10.0,
rentalAutoApprove: false,
maxRentalImagesPerListing: 10,
bookingExpiryHours: 48,
rentalPromotionDayPrice: 3.99,
},
});
console.log('Created PlatformConfig.');
// ── Assign roles to test users ────────────────────────────────────
await prisma.user.update({ where: { id: 'user-alice' }, data: { role: 'SUPER_ADMIN' } });
await prisma.user.update({ where: { id: 'user-bob' }, data: { role: 'ADMIN' } });
await prisma.user.update({ where: { id: 'user-carol' }, data: { role: 'MODERATOR' } });
console.log('Assigned roles: alice=SUPER_ADMIN, bob=ADMIN, carol=MODERATOR.');
// ── Rental Listings ─────────────────────────────────────────────────
// Make david a landlord
await prisma.user.update({ where: { id: 'user-david' }, data: { isLandlord: true, landlordVerified: true } });
// Make eva a landlord too
await prisma.user.update({ where: { id: 'user-eva' }, data: { isLandlord: true } });
const rentalListingsData = [
{
id: 'rental-01',
title: 'Modern Downtown Apartment — 2BR',
description: 'Spacious 2-bedroom apartment in the heart of downtown Chicago. Fully furnished with modern appliances, high-speed WiFi, and stunning city views from the 15th floor. Walking distance to restaurants, shops, and public transit.',
category: 'APARTMENT' as const,
location: 'Chicago, IL',
dailyPrice: 120.00,
monthlyPrice: 2800.00,
depositAmount: 500.00,
amenities: ['WiFi', 'Air Conditioning', 'Washer/Dryer', 'Parking', 'Gym', 'Elevator'],
rules: ['No smoking', 'No pets', 'Quiet hours 10PM-8AM'],
cancellationPolicy: 'MODERATE' as const,
minDays: 2,
maxDays: 90,
minMonths: 1,
maxMonths: 12,
status: 'ACTIVE' as const,
viewCount: 234,
isVerified: true,
landlordId: 'user-david',
},
{
id: 'rental-02',
title: 'Cozy Lake House with Private Dock',
description: 'Beautiful 3-bedroom lake house with private dock and boat access. Perfect for families or groups. Includes kayaks, paddleboards, and fishing equipment. Surrounded by nature with hiking trails nearby.',
category: 'HOUSE' as const,
location: 'Lake Geneva, WI',
dailyPrice: 250.00,
monthlyPrice: 5000.00,
depositAmount: 1000.00,
amenities: ['WiFi', 'Fireplace', 'BBQ', 'Boat Dock', 'Kayaks', 'Hot Tub'],
rules: ['No parties', 'No smoking indoors', 'Pets allowed with deposit'],
cancellationPolicy: 'STRICT' as const,
minDays: 3,
maxDays: 30,
status: 'ACTIVE' as const,
viewCount: 189,
landlordId: 'user-david',
},
{
id: 'rental-03',
title: 'Tesla Model 3 — Daily/Monthly Rental',
description: 'Clean 2024 Tesla Model 3 Long Range in Pearl White. Autopilot included. Full charge gives 350+ miles range. Insurance included in daily rate. Must be 25+ with valid license.',
category: 'CAR' as const,
location: 'Seattle, WA',
dailyPrice: 85.00,
monthlyPrice: 1800.00,
depositAmount: 500.00,
details: { year: 2024, make: 'Tesla', model: 'Model 3', color: 'Pearl White', mileage: 12000 },
amenities: ['Autopilot', 'Premium Audio', 'Heated Seats', 'Phone Charger'],
rules: ['No smoking', 'Must be 25+', 'Valid license required', 'Return fully charged'],
cancellationPolicy: 'FLEXIBLE' as const,
minDays: 1,
maxDays: 60,
status: 'ACTIVE' as const,
viewCount: 156,
landlordId: 'user-eva',
},
{
id: 'rental-04',
title: 'Harley-Davidson Sportster 883',
description: 'Classic Harley-Davidson Sportster 883 in Vivid Black. Perfect for weekend rides or road trips. Helmet included. Motorcycle license required.',
category: 'MOTORCYCLE' as const,
location: 'Portland, OR',
dailyPrice: 65.00,
depositAmount: 300.00,
amenities: ['Helmet Included', 'Saddlebags', 'Phone Mount'],
rules: ['Motorcycle license required', 'Must be 21+', 'No off-road use'],
cancellationPolicy: 'MODERATE' as const,
minDays: 1,
maxDays: 14,
status: 'ACTIVE' as const,
viewCount: 98,
landlordId: 'user-eva',
},
{
id: 'rental-05',
title: 'Trek City Bicycle — Daily Rental',
description: 'Comfortable Trek city bicycle perfect for exploring Portland. Includes lock, lights, and basket. Helmet available on request.',
category: 'BICYCLE' as const,
location: 'Portland, OR',
dailyPrice: 15.00,
amenities: ['Lock', 'Lights', 'Basket', 'Helmet Available'],
rules: ['Return by 8PM', 'Lock when unattended'],
cancellationPolicy: 'FLEXIBLE' as const,
minDays: 1,
maxDays: 7,
status: 'ACTIVE' as const,
viewCount: 67,
landlordId: 'user-eva',
},
{
id: 'rental-06',
title: 'VanMoof S5 Electric Bike',
description: 'Premium VanMoof S5 e-bike with boost button. Range up to 90 miles. Built-in anti-theft. Perfect for daily commuting or weekend exploring.',
category: 'EBIKE' as const,
location: 'San Francisco, CA',
dailyPrice: 35.00,
monthlyPrice: 600.00,
depositAmount: 200.00,
amenities: ['Anti-theft', 'Boost Button', 'Phone Mount', 'Lock'],
rules: ['Must wear helmet', 'Return charged', 'No off-road'],
cancellationPolicy: 'FLEXIBLE' as const,
minDays: 1,
maxDays: 30,
status: 'ACTIVE' as const,
viewCount: 112,
landlordId: 'user-david',
},
{
id: 'rental-07',
title: 'Luxury Penthouse — Pending Review',
description: 'Stunning penthouse apartment with panoramic views. 3 bedrooms, chef kitchen, private terrace. Under review.',
category: 'APARTMENT' as const,
location: 'Los Angeles, CA',
dailyPrice: 450.00,
monthlyPrice: 9000.00,
depositAmount: 2000.00,
amenities: ['Pool', 'Gym', 'Concierge', 'Parking', 'Terrace'],
rules: ['No parties', 'No smoking'],
cancellationPolicy: 'STRICT' as const,
status: 'PENDING_REVIEW' as const,
viewCount: 0,
landlordId: 'user-david',
},
];
const rentalListings = await prisma.$transaction(
rentalListingsData.map((r) => prisma.rentalListing.create({ data: r }))
);
console.log(`Created ${rentalListings.length} rental listings.`);
// ── Rental Images ─────────────────────────────────────────────────
const rentalImageRecords = [
{ rentalListingId: 'rental-01', url: '/uploads/placeholder-r1.jpg', order: 0 },
{ rentalListingId: 'rental-01', url: '/uploads/placeholder-r2.jpg', order: 1 },
{ rentalListingId: 'rental-02', url: '/uploads/placeholder-r3.jpg', order: 0 },
{ rentalListingId: 'rental-02', url: '/uploads/placeholder-r4.jpg', order: 1 },
{ rentalListingId: 'rental-03', url: '/uploads/placeholder-r5.jpg', order: 0 },
{ rentalListingId: 'rental-04', url: '/uploads/placeholder-r6.jpg', order: 0 },
{ rentalListingId: 'rental-05', url: '/uploads/placeholder-r7.jpg', order: 0 },
{ rentalListingId: 'rental-06', url: '/uploads/placeholder-r8.jpg', order: 0 },
];
await prisma.$transaction(
rentalImageRecords.map((img) => prisma.rentalImage.create({ data: img }))
);
console.log(`Created ${rentalImageRecords.length} rental images.`);
// ── Bookings ──────────────────────────────────────────────────────
const bookingsData = [
{
id: 'booking-01',
rentalListingId: 'rental-01',
tenantId: 'user-eva',
landlordId: 'user-david',
periodType: 'DAILY' as const,
startDate: daysAgo(-5),
endDate: daysAgo(-2),
pricePerPeriod: 120,
totalPeriods: 3,
subtotal: 360,
commissionRate: 10,
commissionAmount: 36,
depositAmount: 500,
totalAmount: 860,
status: 'COMPLETED' as const,
createdAt: daysAgo(10),
},
{
id: 'booking-02',
rentalListingId: 'rental-03',
tenantId: 'user-alice',
landlordId: 'user-eva',
periodType: 'DAILY' as const,
startDate: daysAgo(2),
endDate: daysAgo(-5),
pricePerPeriod: 85,
totalPeriods: 7,
subtotal: 595,
commissionRate: 10,
commissionAmount: 59.5,
depositAmount: 500,
totalAmount: 1095,
status: 'ACTIVE' as const,
createdAt: daysAgo(5),
},
{
id: 'booking-03',
rentalListingId: 'rental-02',
tenantId: 'user-bob',
landlordId: 'user-david',
periodType: 'DAILY' as const,
startDate: daysAgo(-10),
endDate: daysAgo(-7),
pricePerPeriod: 250,
totalPeriods: 3,
subtotal: 750,
commissionRate: 10,
commissionAmount: 75,
depositAmount: 1000,
totalAmount: 1750,
status: 'CONFIRMED' as const,
createdAt: daysAgo(3),
},
{
id: 'booking-04',
rentalListingId: 'rental-05',
tenantId: 'user-carol',
landlordId: 'user-eva',
periodType: 'DAILY' as const,
startDate: daysAgo(-14),
endDate: daysAgo(-12),
pricePerPeriod: 15,
totalPeriods: 2,
subtotal: 30,
commissionRate: 10,
commissionAmount: 3,
depositAmount: 0,
totalAmount: 30,
status: 'PENDING' as const,
expiresAt: daysAgo(-12),
createdAt: daysAgo(1),
},
];
const bookings = await prisma.$transaction(
bookingsData.map((b) => prisma.booking.create({ data: b }))
);
console.log(`Created ${bookings.length} bookings.`);
// ── Payouts for completed bookings ────────────────────────────────
await prisma.payout.create({
data: {
bookingId: 'booking-01',
landlordId: 'user-david',
grossAmount: 360,
commissionAmount: 36,
netAmount: 324,
status: 'COMPLETED',
},
});
console.log('Created 1 payout.');
// ── Rental Reviews ────────────────────────────────────────────────
await prisma.rentalReview.create({
data: {
bookingId: 'booking-01',
rentalListingId: 'rental-01',
tenantId: 'user-eva',
landlordId: 'user-david',
rating: 5,
comment: 'Amazing apartment! Super clean, great location, and David was an excellent host. The views were even better than the photos. Would definitely stay again.',
landlordResponse: 'Thank you Eva! You were a wonderful guest. Welcome back anytime!',
},
});
console.log('Created 1 rental review.');
// ── Rental Favorites ──────────────────────────────────────────────
await prisma.$transaction([
prisma.rentalFavorite.create({ data: { userId: 'user-alice', rentalListingId: 'rental-01' } }),
prisma.rentalFavorite.create({ data: { userId: 'user-alice', rentalListingId: 'rental-02' } }),
prisma.rentalFavorite.create({ data: { userId: 'user-bob', rentalListingId: 'rental-03' } }),
prisma.rentalFavorite.create({ data: { userId: 'user-carol', rentalListingId: 'rental-06' } }),
]);
console.log('Created 4 rental favorites.');
console.log('\nSeed completed successfully!');
}