Files
marketplace/server/prisma/seed.ts
delta-lynx-89e8 dcd2dcb841 feat: subscription tiers, period filter, dashboards, docs
- Add subscription tiers (Basic/Pro/Business) with listing limits and dynamic commission
- Add daily/monthly period filter on rentals page
- Add landlord dashboard with earnings chart, stat cards, property performance
- Add landlord subscription management page
- Add tenant dashboard with upcoming stays
- Add business model documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 16:19:33 -08:00

1119 lines
47 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { PrismaClient } from '@prisma/client';
import bcryptjs from 'bcryptjs';
const { hashSync } = bcryptjs;
const prisma = new PrismaClient();
async function main() {
console.log('Seeding database...');
const passwordHash = hashSync('password123', 10);
// ── 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(),
prisma.payment.deleteMany(),
prisma.favorite.deleteMany(),
prisma.offer.deleteMany(),
prisma.listingImage.deleteMany(),
prisma.listing.deleteMany(),
prisma.blockedUser.deleteMany(),
prisma.session.deleteMany(),
prisma.user.deleteMany(),
]);
console.log('Cleared existing data.');
// ── Users ───────────────────────────────────────────────────────────
const users = await prisma.$transaction([
prisma.user.create({
data: {
id: 'user-alice',
email: 'alice.chen@example.com',
passwordHash,
fullName: 'Alice Chen',
nickname: 'alice',
avatar: '/uploads/avatars/alice.jpg',
phone: '(415) 555-0101',
location: 'San Francisco, CA',
bio: 'Tech enthusiast and avid reader. Always looking for great deals on gadgets and books.',
rating: 4.8,
ratingCount: 24,
showEmail: false,
showPhone: true,
showLocation: true,
showOnline: true,
showRating: true,
isActive: true,
marketingEmail: true,
},
}),
prisma.user.create({
data: {
id: 'user-bob',
email: 'bob.martinez@example.com',
passwordHash,
fullName: 'Bob Martinez',
nickname: 'bobby_m',
avatar: '/uploads/avatars/bob.jpg',
phone: '(213) 555-0202',
location: 'Los Angeles, CA',
bio: 'Sports memorabilia collector and weekend warrior. Selling gear I no longer use.',
rating: 4.5,
ratingCount: 18,
showEmail: true,
showPhone: true,
showLocation: true,
showOnline: true,
showRating: true,
isActive: true,
},
}),
prisma.user.create({
data: {
id: 'user-carol',
email: 'carol.nguyen@example.com',
passwordHash,
fullName: 'Carol Nguyen',
nickname: 'carol_n',
avatar: '/uploads/avatars/carol.jpg',
phone: '(503) 555-0303',
location: 'Portland, OR',
bio: 'Interior designer with an eye for vintage furniture. Downsizing my collection.',
rating: 4.9,
ratingCount: 32,
showEmail: false,
showPhone: false,
showLocation: true,
showOnline: false,
showRating: true,
isActive: true,
twoFactorEnabled: true,
},
}),
prisma.user.create({
data: {
id: 'user-david',
email: 'david.kim@example.com',
passwordHash,
fullName: 'David Kim',
nickname: 'dave_k',
avatar: '/uploads/avatars/david.jpg',
phone: '(312) 555-0404',
location: 'Chicago, IL',
bio: 'Gamer and music lover. Clearing out my shelves to make room for new hobbies.',
rating: 4.2,
ratingCount: 11,
showEmail: false,
showPhone: true,
showLocation: true,
showOnline: true,
showRating: true,
isActive: true,
},
}),
prisma.user.create({
data: {
id: 'user-eva',
email: 'eva.johnson@example.com',
passwordHash,
fullName: 'Eva Johnson',
nickname: 'eva_j',
avatar: '/uploads/avatars/eva.jpg',
phone: '(206) 555-0505',
location: 'Seattle, WA',
bio: 'Outdoor enthusiast and plant mom. Selling items that need a new home.',
rating: 4.7,
ratingCount: 15,
showEmail: true,
showPhone: true,
showLocation: true,
showOnline: true,
showRating: true,
isActive: true,
notifFavorites: false,
},
}),
]);
console.log(`Created ${users.length} users.`);
// ── Listings ────────────────────────────────────────────────────────
const listingsData = [
// ACTIVE listings (10)
{
id: 'listing-01',
title: 'MacBook Pro 14" M3 Pro — 18GB RAM',
description: 'Barely used MacBook Pro 14-inch with M3 Pro chip, 18GB unified memory, 512GB SSD. Includes original charger and box. AppleCare+ until March 2027. No scratches or dents, always used with a case.',
price: 1650.00,
obo: true,
category: 'ELECTRONICS' as const,
condition: 'LIKE_NEW' as const,
status: 'ACTIVE' as const,
location: 'San Francisco, CA',
viewCount: 142,
sellerId: 'user-alice',
},
{
id: 'listing-02',
title: 'Mid-Century Modern Walnut Coffee Table',
description: 'Beautiful solid walnut coffee table with tapered legs. Dimensions: 48"L x 24"W x 16"H. Minor surface wear consistent with age. A real statement piece for any living room.',
price: 320.00,
obo: true,
category: 'FURNITURE' as const,
condition: 'GENTLY_USED' as const,
status: 'ACTIVE' as const,
location: 'Portland, OR',
viewCount: 87,
sellerId: 'user-carol',
},
{
id: 'listing-03',
title: 'Patagonia Better Sweater Jacket — Men\'s Large',
description: 'Classic Patagonia fleece jacket in "New Navy" colorway, size Large. Worn a handful of times, no pilling or stains. Great for layering or casual wear.',
price: 75.00,
obo: false,
category: 'CLOTHING' as const,
condition: 'LIKE_NEW' as const,
status: 'ACTIVE' as const,
location: 'Seattle, WA',
viewCount: 63,
sellerId: 'user-eva',
},
{
id: 'listing-04',
title: 'Weber Spirit II E-310 Gas Grill',
description: 'Three-burner propane grill with side tables. Used for two summers. GS4 grilling system, porcelain-enameled cast-iron grates. Includes cover and propane tank. Pickup only.',
price: 280.00,
obo: true,
category: 'HOME_GARDEN' as const,
condition: 'USED' as const,
status: 'ACTIVE' as const,
location: 'Chicago, IL',
viewCount: 54,
sellerId: 'user-david',
},
{
id: 'listing-05',
title: 'Trek Marlin 7 Mountain Bike — Size M',
description: '2024 Trek Marlin 7 in Matte Nautical Navy. Size Medium (fits 5\'5"5\'9"). Shimano Deore 1x10 drivetrain, RockShox Judy fork. About 300 miles on it. Ready to ride.',
price: 620.00,
obo: true,
category: 'SPORTS' as const,
condition: 'GENTLY_USED' as const,
status: 'ACTIVE' as const,
location: 'Portland, OR',
viewCount: 109,
sellerId: 'user-carol',
},
{
id: 'listing-06',
title: 'Complete Dune Series (6 Books, Hardcover)',
description: 'The original six Dune novels by Frank Herbert in hardcover. Ace/Putnam editions with dust jackets. All in very good condition — no writing, highlighting, or torn pages.',
price: 95.00,
obo: false,
category: 'BOOKS' as const,
condition: 'GENTLY_USED' as const,
status: 'ACTIVE' as const,
location: 'San Francisco, CA',
viewCount: 76,
sellerId: 'user-alice',
},
{
id: 'listing-07',
title: 'PlayStation 5 Disc Edition + 4 Games',
description: 'PS5 Disc Edition console with two DualSense controllers, charging dock, and four games: Spider-Man 2, FF7 Rebirth, Elden Ring, and Baldur\'s Gate 3. Everything works perfectly.',
price: 420.00,
obo: true,
category: 'GAMES' as const,
condition: 'GENTLY_USED' as const,
status: 'ACTIVE' as const,
location: 'Chicago, IL',
viewCount: 203,
sellerId: 'user-david',
},
{
id: 'listing-08',
title: 'Sony WH-1000XM5 Wireless Headphones',
description: 'Industry-leading noise cancelling headphones in black. Includes carrying case, USB-C cable, and audio cable. Battery still holds 30+ hours. Ear pads in excellent shape.',
price: 199.00,
obo: false,
category: 'ELECTRONICS' as const,
condition: 'LIKE_NEW' as const,
status: 'ACTIVE' as const,
location: 'Los Angeles, CA',
viewCount: 91,
sellerId: 'user-bob',
},
{
id: 'listing-09',
title: 'Dyson V12 Detect Slim Cordless Vacuum',
description: 'Powerful cordless stick vacuum with laser dust detection. Comes with all original attachments and wall-mount dock. About 1 year old, works flawlessly.',
price: 340.00,
obo: true,
category: 'HOME_GARDEN' as const,
condition: 'GENTLY_USED' as const,
status: 'ACTIVE' as const,
location: 'Seattle, WA',
viewCount: 68,
sellerId: 'user-eva',
},
{
id: 'listing-10',
title: 'Vintage Schwinn Varsity Road Bike',
description: '1975 Schwinn Varsity 10-speed in original Kool Lemon color. All-original components, rides well. Some cosmetic patina adds character. A classic commuter or collector piece.',
price: 250.00,
obo: true,
category: 'VEHICLES' as const,
condition: 'FAIR' as const,
status: 'ACTIVE' as const,
location: 'Los Angeles, CA',
viewCount: 45,
sellerId: 'user-bob',
},
// SOLD listings (3)
{
id: 'listing-11',
title: 'Nintendo Switch OLED + Pro Controller',
description: 'White Nintendo Switch OLED model with Pro Controller, dock, and carrying case. Screen is pristine. Includes 3 physical game cartridges.',
price: 300.00,
obo: false,
category: 'GAMES' as const,
condition: 'GENTLY_USED' as const,
status: 'SOLD' as const,
location: 'San Francisco, CA',
viewCount: 178,
sellerId: 'user-alice',
},
{
id: 'listing-12',
title: 'Herman Miller Aeron Chair — Size B',
description: 'Fully loaded Aeron chair in graphite. Size B (medium). PostureFit SL, adjustable arms, tilt limiter. Purchased from authorized dealer in 2022. 12-year warranty still active.',
price: 750.00,
obo: true,
category: 'FURNITURE' as const,
condition: 'LIKE_NEW' as const,
status: 'SOLD' as const,
location: 'Portland, OR',
viewCount: 256,
sellerId: 'user-carol',
},
{
id: 'listing-13',
title: 'Canon EOS R6 Mark II Body Only',
description: 'Professional mirrorless camera body. 24.2MP full-frame sensor, 4K 60fps video, IBIS. Shutter count under 5,000. Includes battery, charger, and strap. No box.',
price: 1800.00,
obo: true,
category: 'ELECTRONICS' as const,
condition: 'LIKE_NEW' as const,
status: 'SOLD' as const,
location: 'Chicago, IL',
viewCount: 312,
sellerId: 'user-david',
},
// DRAFT listings (2)
{
id: 'listing-14',
title: 'Garmin Fenix 7X Solar Smartwatch',
description: 'Multi-sport GPS watch with solar charging. Titanium bezel, sapphire lens. Includes QuickFit bands. Still drafting — need to take photos.',
price: 450.00,
obo: true,
category: 'ELECTRONICS' as const,
condition: 'GENTLY_USED' as const,
status: 'DRAFT' as const,
location: 'Seattle, WA',
viewCount: 0,
sellerId: 'user-eva',
},
{
id: 'listing-15',
title: 'Restoration Hardware Cloud Sofa — 2 Seat',
description: 'RH Cloud sofa in white perennials fabric. Extremely comfortable. Moving and cannot bring it. Will update with measurements and photos shortly.',
price: 2200.00,
obo: true,
category: 'FURNITURE' as const,
condition: 'USED' as const,
status: 'DRAFT' as const,
location: 'Los Angeles, CA',
viewCount: 0,
sellerId: 'user-bob',
},
];
const listings = await prisma.$transaction(
listingsData.map((l) => prisma.listing.create({ data: l }))
);
console.log(`Created ${listings.length} listings.`);
// ── Listing Images ──────────────────────────────────────────────────
const imageRecords = [
{ listingId: 'listing-01', uploadedBy: 'user-alice', url: '/uploads/placeholder-1.jpg', order: 0 },
{ listingId: 'listing-01', uploadedBy: 'user-alice', url: '/uploads/placeholder-2.jpg', order: 1 },
{ listingId: 'listing-02', uploadedBy: 'user-carol', url: '/uploads/placeholder-3.jpg', order: 0 },
{ listingId: 'listing-02', uploadedBy: 'user-carol', url: '/uploads/placeholder-4.jpg', order: 1 },
{ listingId: 'listing-03', uploadedBy: 'user-eva', url: '/uploads/placeholder-5.jpg', order: 0 },
{ listingId: 'listing-04', uploadedBy: 'user-david', url: '/uploads/placeholder-6.jpg', order: 0 },
{ listingId: 'listing-04', uploadedBy: 'user-david', url: '/uploads/placeholder-7.jpg', order: 1 },
{ listingId: 'listing-05', uploadedBy: 'user-carol', url: '/uploads/placeholder-8.jpg', order: 0 },
{ listingId: 'listing-05', uploadedBy: 'user-carol', url: '/uploads/placeholder-9.jpg', order: 1 },
{ listingId: 'listing-06', uploadedBy: 'user-alice', url: '/uploads/placeholder-10.jpg', order: 0 },
{ listingId: 'listing-07', uploadedBy: 'user-david', url: '/uploads/placeholder-11.jpg', order: 0 },
{ listingId: 'listing-07', uploadedBy: 'user-david', url: '/uploads/placeholder-12.jpg', order: 1 },
{ listingId: 'listing-08', uploadedBy: 'user-bob', url: '/uploads/placeholder-13.jpg', order: 0 },
{ listingId: 'listing-09', uploadedBy: 'user-eva', url: '/uploads/placeholder-14.jpg', order: 0 },
{ listingId: 'listing-10', uploadedBy: 'user-bob', url: '/uploads/placeholder-15.jpg', order: 0 },
{ listingId: 'listing-10', uploadedBy: 'user-bob', url: '/uploads/placeholder-16.jpg', order: 1 },
{ listingId: 'listing-11', uploadedBy: 'user-alice', url: '/uploads/placeholder-17.jpg', order: 0 },
{ listingId: 'listing-12', uploadedBy: 'user-carol', url: '/uploads/placeholder-18.jpg', order: 0 },
{ listingId: 'listing-12', uploadedBy: 'user-carol', url: '/uploads/placeholder-19.jpg', order: 1 },
{ listingId: 'listing-13', uploadedBy: 'user-david', url: '/uploads/placeholder-20.jpg', order: 0 },
{ listingId: 'listing-13', uploadedBy: 'user-david', url: '/uploads/placeholder-21.jpg', order: 1 },
];
const images = await prisma.$transaction(
imageRecords.map((img) => prisma.listingImage.create({ data: img }))
);
console.log(`Created ${images.length} listing images.`);
// ── Offers ──────────────────────────────────────────────────────────
const now = new Date();
const inOneWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
const daysAgo = (d: number) => new Date(now.getTime() - d * 24 * 60 * 60 * 1000);
const offersData = [
// ACCEPTED offers on SOLD listings
{
id: 'offer-01',
amount: 275.00,
message: 'Would you take $275? I can pick it up today.',
status: 'ACCEPTED' as const,
buyerId: 'user-bob',
sellerId: 'user-alice',
listingId: 'listing-11',
expiresAt: daysAgo(-2),
createdAt: daysAgo(10),
},
{
id: 'offer-02',
amount: 680.00,
message: 'Beautiful chair! I have been looking for one of these. Would $680 work?',
status: 'ACCEPTED' as const,
counterAmount: 720.00,
buyerId: 'user-eva',
sellerId: 'user-carol',
listingId: 'listing-12',
expiresAt: daysAgo(-1),
createdAt: daysAgo(14),
},
{
id: 'offer-03',
amount: 1700.00,
message: 'Interested in the R6 II. Offering $1700 — can meet anywhere in Chicago.',
status: 'ACCEPTED' as const,
buyerId: 'user-alice',
sellerId: 'user-david',
listingId: 'listing-13',
expiresAt: daysAgo(-3),
createdAt: daysAgo(7),
},
// PENDING offers
{
id: 'offer-04',
amount: 1500.00,
message: 'Is there any flexibility on price? I see similar configs going for around $1500.',
status: 'PENDING' as const,
buyerId: 'user-david',
sellerId: 'user-alice',
listingId: 'listing-01',
expiresAt: inOneWeek,
createdAt: daysAgo(1),
},
{
id: 'offer-05',
amount: 380.00,
message: 'Great deal on the PS5 bundle. Would you accept $380?',
status: 'PENDING' as const,
buyerId: 'user-carol',
sellerId: 'user-david',
listingId: 'listing-07',
expiresAt: inOneWeek,
createdAt: daysAgo(2),
},
{
id: 'offer-06',
amount: 550.00,
message: 'Love this bike! Would you consider $550?',
status: 'PENDING' as const,
buyerId: 'user-alice',
sellerId: 'user-carol',
listingId: 'listing-05',
expiresAt: inOneWeek,
createdAt: daysAgo(1),
},
// DECLINED offers
{
id: 'offer-07',
amount: 200.00,
message: 'I can offer $200 for the coffee table.',
status: 'DECLINED' as const,
buyerId: 'user-david',
sellerId: 'user-carol',
listingId: 'listing-02',
expiresAt: daysAgo(1),
createdAt: daysAgo(5),
},
{
id: 'offer-08',
amount: 150.00,
message: 'Budget is tight but really want those headphones. $150?',
status: 'DECLINED' as const,
buyerId: 'user-eva',
sellerId: 'user-bob',
listingId: 'listing-08',
expiresAt: daysAgo(2),
createdAt: daysAgo(8),
},
// COUNTERED offers
{
id: 'offer-09',
amount: 250.00,
message: 'Interested in the grill. How about $250?',
status: 'COUNTERED' as const,
counterAmount: 270.00,
buyerId: 'user-alice',
sellerId: 'user-david',
listingId: 'listing-04',
expiresAt: inOneWeek,
createdAt: daysAgo(3),
},
{
id: 'offer-10',
amount: 290.00,
message: 'Would $290 work for the vacuum? I am in the Ballard area and can pick up.',
status: 'COUNTERED' as const,
counterAmount: 310.00,
buyerId: 'user-bob',
sellerId: 'user-eva',
listingId: 'listing-09',
expiresAt: inOneWeek,
createdAt: daysAgo(2),
},
];
const offers = await prisma.$transaction(
offersData.map((o) => prisma.offer.create({ data: o }))
);
console.log(`Created ${offers.length} offers.`);
// ── Conversations & Messages ────────────────────────────────────────
const conv1 = await prisma.conversation.create({
data: {
id: 'conv-01',
user1Id: 'user-bob',
user2Id: 'user-alice',
listingId: 'listing-11',
},
});
const conv2 = await prisma.conversation.create({
data: {
id: 'conv-02',
user1Id: 'user-eva',
user2Id: 'user-carol',
listingId: 'listing-12',
},
});
const conv3 = await prisma.conversation.create({
data: {
id: 'conv-03',
user1Id: 'user-david',
user2Id: 'user-alice',
listingId: 'listing-01',
},
});
const conv4 = await prisma.conversation.create({
data: {
id: 'conv-04',
user1Id: 'user-alice',
user2Id: 'user-david',
listingId: 'listing-04',
},
});
console.log('Created 4 conversations.');
const messagesData = [
// Conv 1: Bob buying Switch from Alice (completed sale)
{ content: 'Hi! Is the Switch still available?', senderId: 'user-bob', conversationId: 'conv-01', isRead: true, createdAt: daysAgo(11) },
{ content: 'Yes it is! Everything works great.', senderId: 'user-alice', conversationId: 'conv-01', isRead: true, createdAt: daysAgo(11) },
{ content: 'Awesome. Would you take $275 for it?', senderId: 'user-bob', conversationId: 'conv-01', isRead: true, offerAmount: 275.00, createdAt: daysAgo(10) },
{ content: 'That works for me. When can you pick up?', senderId: 'user-alice', conversationId: 'conv-01', isRead: true, createdAt: daysAgo(10) },
{ content: 'How about Saturday around noon? I can come to the Mission district.', senderId: 'user-bob', conversationId: 'conv-01', isRead: true, createdAt: daysAgo(9) },
{ content: 'Perfect. I will DM you the exact address on Saturday morning.', senderId: 'user-alice', conversationId: 'conv-01', isRead: true, createdAt: daysAgo(9) },
{ content: 'Picked it up — everything looks great. Thanks Alice!', senderId: 'user-bob', conversationId: 'conv-01', isRead: true, createdAt: daysAgo(7) },
// Conv 2: Eva buying Aeron from Carol (completed sale)
{ content: 'Hi Carol, love the Aeron listing! Is the warranty transferable?', senderId: 'user-eva', conversationId: 'conv-02', isRead: true, createdAt: daysAgo(15) },
{ content: 'Hi Eva! Yes, Herman Miller warranties follow the chair, not the owner.', senderId: 'user-carol', conversationId: 'conv-02', isRead: true, createdAt: daysAgo(15) },
{ content: 'Great to know. Would $680 be fair? I see refurbs going for about that.', senderId: 'user-eva', conversationId: 'conv-02', isRead: true, offerAmount: 680.00, createdAt: daysAgo(14) },
{ content: 'This is from an authorized dealer though. How about we meet at $720?', senderId: 'user-carol', conversationId: 'conv-02', isRead: true, offerAmount: 720.00, createdAt: daysAgo(14) },
{ content: '$720 is fair. Deal! Can you ship to Seattle or is it pickup only?', senderId: 'user-eva', conversationId: 'conv-02', isRead: true, createdAt: daysAgo(13) },
{ content: 'I can ship via UPS freight. I will factor that into the price — still $720 total.', senderId: 'user-carol', conversationId: 'conv-02', isRead: true, createdAt: daysAgo(13) },
{ content: 'That is incredibly generous. Sending payment now.', senderId: 'user-eva', conversationId: 'conv-02', isRead: true, createdAt: daysAgo(12) },
{ content: 'Payment received! Shipping Monday. Will send tracking.', senderId: 'user-carol', conversationId: 'conv-02', isRead: true, createdAt: daysAgo(12) },
// Conv 3: David interested in MacBook from Alice (ongoing)
{ content: 'Hey, quick question about the MacBook — what is the battery cycle count?', senderId: 'user-david', conversationId: 'conv-03', isRead: true, createdAt: daysAgo(3) },
{ content: 'Let me check... 47 cycles. Basically brand new battery.', senderId: 'user-alice', conversationId: 'conv-03', isRead: true, createdAt: daysAgo(3) },
{ content: 'Nice. Any issues with the keyboard or trackpad?', senderId: 'user-david', conversationId: 'conv-03', isRead: true, createdAt: daysAgo(2) },
{ content: 'Nope, everything is perfect. I mostly used it with an external display and keyboard.', senderId: 'user-alice', conversationId: 'conv-03', isRead: true, createdAt: daysAgo(2) },
{ content: 'I submitted an offer for $1500 — let me know what you think.', senderId: 'user-david', conversationId: 'conv-03', isRead: true, offerAmount: 1500.00, createdAt: daysAgo(1) },
{ content: 'I will take a look. That is a bit lower than I was hoping for but let me think about it.', senderId: 'user-alice', conversationId: 'conv-03', isRead: false, createdAt: daysAgo(0) },
// Conv 4: Alice interested in grill from David (ongoing)
{ content: 'Hi David! Does the grill come with the cover shown in the photo?', senderId: 'user-alice', conversationId: 'conv-04', isRead: true, createdAt: daysAgo(4) },
{ content: 'Yes, the cover and propane tank are both included.', senderId: 'user-david', conversationId: 'conv-04', isRead: true, createdAt: daysAgo(4) },
{ content: 'I put in an offer for $250. Let me know if that works.', senderId: 'user-alice', conversationId: 'conv-04', isRead: true, offerAmount: 250.00, createdAt: daysAgo(3) },
{ content: 'Appreciate the offer! Could you do $270? I paid $500 new last year.', senderId: 'user-david', conversationId: 'conv-04', isRead: true, offerAmount: 270.00, createdAt: daysAgo(3) },
{ content: 'Let me think on it and get back to you.', senderId: 'user-alice', conversationId: 'conv-04', isRead: false, createdAt: daysAgo(2) },
];
const messages = await prisma.$transaction(
messagesData.map((m) => prisma.message.create({ data: m }))
);
console.log(`Created ${messages.length} messages.`);
// ── Favorites ───────────────────────────────────────────────────────
const favoritesData = [
{ userId: 'user-bob', listingId: 'listing-01' },
{ userId: 'user-bob', listingId: 'listing-06' },
{ userId: 'user-carol', listingId: 'listing-07' },
{ userId: 'user-carol', listingId: 'listing-08' },
{ userId: 'user-david', listingId: 'listing-02' },
{ userId: 'user-david', listingId: 'listing-05' },
{ userId: 'user-eva', listingId: 'listing-01' },
{ userId: 'user-eva', listingId: 'listing-07' },
{ userId: 'user-alice', listingId: 'listing-04' },
{ userId: 'user-alice', listingId: 'listing-05' },
{ userId: 'user-alice', listingId: 'listing-08' },
];
const favorites = await prisma.$transaction(
favoritesData.map((f) => prisma.favorite.create({ data: f }))
);
console.log(`Created ${favorites.length} favorites.`);
// ── Notifications ───────────────────────────────────────────────────
const notificationsData = [
// NEW_OFFER
{
userId: 'user-alice',
type: 'NEW_OFFER' as const,
title: 'New Offer Received',
body: 'David Kim offered $1,500 for your MacBook Pro 14" M3 Pro.',
data: { offerId: 'offer-04', listingId: 'listing-01' },
isRead: false,
createdAt: daysAgo(1),
},
{
userId: 'user-david',
type: 'NEW_OFFER' as const,
title: 'New Offer Received',
body: 'Carol Nguyen offered $380 for your PlayStation 5 Disc Edition.',
data: { offerId: 'offer-05', listingId: 'listing-07' },
isRead: true,
createdAt: daysAgo(2),
},
// OFFER_ACCEPTED
{
userId: 'user-bob',
type: 'OFFER_ACCEPTED' as const,
title: 'Offer Accepted!',
body: 'Alice Chen accepted your offer of $275 for the Nintendo Switch OLED.',
data: { offerId: 'offer-01', listingId: 'listing-11' },
isRead: true,
createdAt: daysAgo(10),
},
{
userId: 'user-alice',
type: 'OFFER_ACCEPTED' as const,
title: 'Offer Accepted!',
body: 'David Kim accepted your offer of $1,700 for the Canon EOS R6 Mark II.',
data: { offerId: 'offer-03', listingId: 'listing-13' },
isRead: true,
createdAt: daysAgo(7),
},
// OFFER_DECLINED
{
userId: 'user-david',
type: 'OFFER_DECLINED' as const,
title: 'Offer Declined',
body: 'Carol Nguyen declined your offer of $200 for the Mid-Century Coffee Table.',
data: { offerId: 'offer-07', listingId: 'listing-02' },
isRead: true,
createdAt: daysAgo(5),
},
{
userId: 'user-eva',
type: 'OFFER_DECLINED' as const,
title: 'Offer Declined',
body: 'Bob Martinez declined your offer of $150 for the Sony WH-1000XM5.',
data: { offerId: 'offer-08', listingId: 'listing-08' },
isRead: false,
createdAt: daysAgo(8),
},
// ITEM_SOLD
{
userId: 'user-alice',
type: 'ITEM_SOLD' as const,
title: 'Item Sold!',
body: 'Your Nintendo Switch OLED has been sold for $275.',
data: { listingId: 'listing-11', amount: 275 },
isRead: true,
createdAt: daysAgo(9),
},
{
userId: 'user-carol',
type: 'ITEM_SOLD' as const,
title: 'Item Sold!',
body: 'Your Herman Miller Aeron Chair has been sold for $720.',
data: { listingId: 'listing-12', amount: 720 },
isRead: true,
createdAt: daysAgo(12),
},
// NEW_MESSAGE
{
userId: 'user-alice',
type: 'NEW_MESSAGE' as const,
title: 'New Message',
body: 'David Kim sent you a message about MacBook Pro 14" M3 Pro.',
data: { conversationId: 'conv-03', listingId: 'listing-01' },
isRead: true,
createdAt: daysAgo(3),
},
{
userId: 'user-david',
type: 'NEW_MESSAGE' as const,
title: 'New Message',
body: 'Alice Chen sent you a message about Weber Spirit II Gas Grill.',
data: { conversationId: 'conv-04', listingId: 'listing-04' },
isRead: false,
createdAt: daysAgo(2),
},
// ITEM_FAVORITED
{
userId: 'user-alice',
type: 'ITEM_FAVORITED' as const,
title: 'Item Favorited',
body: 'Someone favorited your MacBook Pro 14" M3 Pro listing.',
data: { listingId: 'listing-01' },
isRead: false,
createdAt: daysAgo(1),
},
{
userId: 'user-carol',
type: 'ITEM_FAVORITED' as const,
title: 'Item Favorited',
body: 'Someone favorited your Trek Marlin 7 Mountain Bike listing.',
data: { listingId: 'listing-05' },
isRead: true,
createdAt: daysAgo(4),
},
];
const notifications = await prisma.$transaction(
notificationsData.map((n) => prisma.notification.create({ data: n }))
);
console.log(`Created ${notifications.length} notifications.`);
// ── Payments (3 completed for sold items) ───────────────────────────
const paymentsData = [
{
userId: 'user-bob',
listingId: 'listing-11',
stripePaymentId: 'pi_3PxSwitch001',
amount: 275.00,
status: 'COMPLETED' as const,
createdAt: daysAgo(9),
},
{
userId: 'user-eva',
listingId: 'listing-12',
stripePaymentId: 'pi_3PxAeron002',
amount: 720.00,
status: 'COMPLETED' as const,
createdAt: daysAgo(12),
},
{
userId: 'user-alice',
listingId: 'listing-13',
stripePaymentId: 'pi_3PxCanon003',
amount: 1700.00,
status: 'COMPLETED' as const,
createdAt: daysAgo(6),
},
];
const payments = await prisma.$transaction(
paymentsData.map((p) => prisma.payment.create({ data: p }))
);
console.log(`Created ${payments.length} payments.`);
// ── Blocked Users ───────────────────────────────────────────────────
await prisma.blockedUser.create({
data: {
blockerId: 'user-carol',
blockedId: 'user-david',
},
});
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: 'Sun-Drenched Loop Apartment — 2BR / River Views',
description: 'Wake up to panoramic views of the Chicago River from the 15th floor of a modern high-rise in the Loop. This fully furnished two-bedroom features floor-to-ceiling windows, a chef-ready kitchen with quartz countertops and stainless steel appliances, an in-unit washer/dryer, and a dedicated workspace perfect for remote work.\n\nThe building offers 24-hour concierge, a rooftop fitness center, heated parking, and direct access to the Riverwalk. Step outside to Millennium Park, the Art Institute, and dozens of restaurants within a five-minute walk. The Blue and Red CTA lines are one block away.\n\nLinens, towels, cookware, and high-speed fiber WiFi (500 Mbps) are all included — just bring your suitcase.',
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: 'Lakefront Retreat — Private Dock & Hot Tub',
description: 'Escape to this charming three-bedroom lake house on the north shore of Lake Geneva. Nestled among mature oaks, the property features a wraparound deck, a private dock with a boat lift, and a six-person hot tub overlooking the water.\n\nInside you will find an open-concept living room with a stone fireplace, a fully equipped kitchen, and three bedrooms that comfortably sleep eight. We provide two kayaks, stand-up paddleboards, fishing rods, and life jackets for all ages.\n\nThe property backs up to a nature preserve with 4+ miles of hiking trails. Downtown Lake Geneva — with its shops, restaurants, and the famous Shore Path — is a ten-minute drive. Ideal for family vacations, friend getaways, or a quiet creative retreat.',
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: '2024 Tesla Model 3 Long Range — Autopilot Included',
description: 'Hit the road in a pristine 2024 Tesla Model 3 Long Range finished in Pearl White. This car has just 12,000 miles, a spotless interior, and every software update installed.\n\nAutopilot comes standard, and a full charge delivers 350+ miles of range — enough for a Seattle-to-Portland round trip with room to spare. The premium audio system, heated seats, and glass roof make every drive feel special.\n\nInsurance is included in the daily rate. Pickup and drop-off happen at a secure garage in Capitol Hill, or I can deliver within Seattle for a small fee. Supercharger network access included. You just need to be 25+ with a clean driving record.',
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 — Weekend Warrior Ready',
description: 'There is nothing like cruising the Pacific Northwest on a classic Harley. This Sportster 883 Iron in Vivid Black is tuned, serviced, and ready for your next adventure — whether that is a day trip down the Columbia River Gorge or a weekend ride along the Oregon Coast.\n\nDOT-approved helmet, saddlebags, and a phone mount are included. I will walk you through the bike at pickup, and I am always a text away if you have questions on the road. Motorcycle endorsement on your license is 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 FX City Bike — Explore Portland on Two Wheels',
description: 'The easiest way to see Portland is by bike, and this Trek FX is the perfect ride for it. Lightweight aluminum frame, 21 speeds, comfortable upright geometry, and puncture-resistant tires so you can cruise from the Pearl District to Hawthorne without a worry.\n\nEvery rental includes a U-lock, front and rear lights, a handlebar basket, and a city cycling map with curated routes. Helmet available on request at no extra charge. Pickup and drop-off at my home in the Alberta Arts District.',
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 E-Bike — Commute or Explore SF in Style',
description: 'Skip the traffic and see San Francisco the fun way on a VanMoof S5 electric bike. The integrated boost button launches you up Market Street hills effortlessly, and the 90-mile range means you can ride all day without worrying about battery.\n\nBuilt-in GPS anti-theft tracking, automatic electronic shifting, and integrated front/rear lights make this one of the smartest bikes on the road. Perfect for daily commuting, weekend rides across the Golden Gate Bridge, or exploring the city by the bay.\n\nPickup at my place in the Mission. I will adjust the seat and give you a quick demo before you head out.',
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: 'Hollywood Hills Penthouse — Skyline & Ocean Views',
description: 'Breathtaking three-bedroom penthouse perched above the Sunset Strip with unobstructed views from downtown LA to the Pacific. The open-plan living space features a chef kitchen with Miele appliances, floor-to-ceiling glass, and a private wraparound terrace ideal for sunset entertaining.\n\nBuilding amenities include an infinity pool, a state-of-the-art gym, 24-hour concierge, valet parking, and secure elevator access. Currently under platform review — listing will go live once verified.',
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/rental-apartment-1.jpg', order: 0 },
{ rentalListingId: 'rental-01', url: '/uploads/rental-apartment-2.jpg', order: 1 },
{ rentalListingId: 'rental-01', url: '/uploads/rental-apartment-3.jpg', order: 2 },
{ rentalListingId: 'rental-02', url: '/uploads/rental-house-1.jpg', order: 0 },
{ rentalListingId: 'rental-02', url: '/uploads/rental-house-2.jpg', order: 1 },
{ rentalListingId: 'rental-02', url: '/uploads/rental-house-3.jpg', order: 2 },
{ rentalListingId: 'rental-03', url: '/uploads/rental-car-1.jpg', order: 0 },
{ rentalListingId: 'rental-03', url: '/uploads/rental-car-2.jpg', order: 1 },
{ rentalListingId: 'rental-04', url: '/uploads/rental-motorcycle-1.jpg', order: 0 },
{ rentalListingId: 'rental-04', url: '/uploads/rental-motorcycle-2.jpg', order: 1 },
{ rentalListingId: 'rental-05', url: '/uploads/rental-bicycle-1.jpg', order: 0 },
{ rentalListingId: 'rental-05', url: '/uploads/rental-bicycle-2.jpg', order: 1 },
{ rentalListingId: 'rental-06', url: '/uploads/rental-ebike-1.jpg', order: 0 },
{ rentalListingId: 'rental-06', url: '/uploads/rental-ebike-2.jpg', order: 1 },
{ rentalListingId: 'rental-07', url: '/uploads/rental-apartment-4.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!');
}
main()
.catch((e) => {
console.error('Seed failed:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});