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:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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!');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user