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:
@@ -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