import "dotenv/config"; import { PrismaClient } from "../app/generated/prisma/client"; import type { Prisma } from "../app/generated/prisma/client"; import type { ActivityCategory, Vibe, } from "../app/generated/prisma/enums"; import { PrismaPg } from "@prisma/adapter-pg"; import bcrypt from "bcryptjs"; import { encryptString, hmacHex } from "../lib/crypto"; const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL!, }); const prisma = new PrismaClient({ adapter }); // ============================================================================ // Helpers // ============================================================================ const utc = (y: number, m0: number, d: number, h = 12, min = 0) => new Date(Date.UTC(y, m0, d, h, min, 0, 0)); const img = (id: string) => `https://images.unsplash.com/photo-${id}?w=1200&q=80&auto=format&fit=crop`; const SEED_PASSWORD = "password123"; // ============================================================================ // Itineraries — disimpan sebagai const supaya data trip tetap padat. // Format: header `Hari N — ` lalu bullet `• HH:MM–HH:MM aktivitas`. // Lokasi, pos, dan spot diambil dari kasus nyata (Papandayan via Cisurupan, // Ciremai via Apuy, USS Liberty Tulamben, Karimun Jawa hopping, dst). // ============================================================================ const ITIN_PAPANDAYAN = `Hari 1 — Sabtu • 05:00–05:30 Meeting & briefing di Alun-alun Garut • 05:30–07:00 Perjalanan menuju basecamp Cisurupan • 07:00–08:00 Sarapan + repacking + pemanasan • 08:00–11:00 Trekking Camp David — Hutan Mati — Pondok Salada • 11:00–12:30 Setup tenda di Pondok Salada • 12:30–14:00 ISHOMA + games kenalan grup • 14:00–17:00 Eksplor Tegal Alun & Hutan Mati (golden hour foto) • 17:00–19:00 Masak bareng + makan malam • 19:00–21:00 Api unggun, kopi, sharing rencana summit • 21:00 Istirahat Hari 2 — Minggu • 03:30–04:00 Bangun + sarapan ringan + air panas • 04:00–05:30 Summit attack ke puncak Papandayan • 05:30–07:00 Sunrise + foto bareng di puncak • 07:00–09:00 Turun ke camp + sarapan utama • 09:00–11:00 Beres-beres tenda + repacking • 11:00–13:30 Turun ke basecamp Cisurupan • 13:30–14:30 Bersih-bersih + makan siang • 14:30–16:30 Perjalanan kembali ke Garut • 16:30 Sampai Garut, bubar grup`; const ITIN_CIREMAI = `Hari 1 — Sabtu • 04:00–04:30 Meeting & briefing di Stasiun Kuningan • 04:30–06:30 Perjalanan ke basecamp Apuy via Maja & Argapura • 06:30–07:30 Sarapan + registrasi simaksi + repacking • 07:30–10:30 Trek Pos 1 (Berod) → Pos 2 (Arban) → Pos 3 (Tegal Masawa) • 10:30–11:30 ISHOMA di Pos 4 (Tegal Jamuju) • 11:30–14:30 Lanjut Pos 5 (Sanghyang Rangkah) → Pos 6 (Goa Walet) • 14:30–16:00 Setup tenda di Pos 6 • 16:00–18:00 Acara bebas + makan sore + persiapan summit • 18:00–20:00 Briefing summit + early dinner • 20:00 Istirahat (bangun dini hari) Hari 2 — Minggu • 02:00–02:30 Bangun + cemilan + minuman hangat • 02:30–05:00 Summit attack ke puncak Sunan Cirebon (3.078 mdpl) • 05:00–06:30 Sunrise di puncak Ciremai • 06:30–08:30 Turun ke Pos 6 + sarapan • 08:30–10:30 Beres tenda + repacking • 10:30–14:00 Turun ke basecamp Apuy (track curam — hati-hati lutut) • 14:00–15:00 Bersih-bersih + makan siang di basecamp • 15:00–17:00 Kembali ke Stasiun Kuningan • 17:00 Bubar grup`; const ITIN_CAMPING = `Hari 1 — Sabtu • 13:00–13:30 Meeting di Pertigaan Pasar Lembang • 13:30–15:00 Perjalanan ke Ranca Upas via Ciwidey • 15:00–16:00 Check-in + setup tenda dome (sudah disiapkan tim) • 16:00–17:30 Tour camp area + ketemu rusa-rusa • 17:30–19:00 Persiapan BBQ + nyalakan api unggun • 19:00–21:00 Makan malam BBQ • 21:00–23:00 Live music akustik + games • 23:00 Istirahat Hari 2 — Minggu • 06:00–07:00 Sunrise + foto di hutan pinus • 07:00–08:30 Sarapan (nasi goreng + roti bakar + kopi) • 08:30–10:00 Memberi makan rusa + sesi foto • 10:00–11:00 Beres tenda + bersih-bersih • 11:00–11:30 Pulang menuju Lembang • 12:00 Sampai Lembang, bubar grup`; const ITIN_PAHAWANG = `Hari 1 — Sabtu (one-day trip) • 07:00–07:30 Meeting di Dermaga Ketapang, Lampung Selatan • 07:30–08:30 Briefing safety + fitting alat snorkel • 08:30–09:30 Sailing menuju Pulau Pahawang Kecil • 09:30–11:30 Snorkeling spot Cukuh Bedil — terumbu warna-warni • 11:30–12:30 Pindah spot ke Pulau Kelagian Kecil • 12:30–14:00 Makan siang + istirahat di pasir putih • 14:00–15:30 Snorkeling Tanjung Putus + sesi foto underwater • 15:30–16:30 Sailing kembali ke dermaga • 16:30–17:00 Bersih-bersih + bubar grup`; const ITIN_DIVING = `Hari 1 — Sabtu • 06:30–07:00 Meeting di dive shop Tulamben + welcome coffee • 07:00–08:00 Briefing dive plan + cek sertifikasi + fitting gear • 08:00–09:00 Surface interval + pengecekan tank/regulator • 09:00–10:30 Dive #1 — USS Liberty Wreck (5–28m, ~50 menit bottom time) • 10:30–12:00 Surface interval + brunch + log dive • 12:00–13:30 Dive #2 — Coral Garden / Drop Off (~50 menit) • 13:30–15:00 Debrief + makan siang • 15:00–17:00 Acara bebas (rest atau eksplor desa Tulamben) • 17:00–19:00 Sunset di pantai Tulamben + dinner • 19:00 Istirahat di homestay (mandiri) Hari 2 — Minggu • 06:00–06:30 Bangun + kopi • 06:30–07:30 Briefing dive #3 (early morning visibility tinggi) • 07:30–09:00 Dive #3 — Liberty Wreck pagi • 09:00–10:30 Surface interval + sarapan + log • 10:30–12:00 Dive #4 (opsional, fun dive shallow reef) • 12:00–13:00 Bersih gear + debrief akhir • 13:00–14:00 Makan siang penutupan • 14:00 Bubar grup`; const ITIN_ISLANDHOP = `Hari 1 — Jumat • 07:00–07:30 Meeting di Pelabuhan Kartini Jepara • 07:30–13:00 Penyeberangan kapal feri Jepara → Karimun Jawa • 13:00–14:00 Tiba di Pelabuhan Karimun + transfer homestay • 14:00–15:00 Check-in homestay + ISHOMA • 15:00–17:30 Sunset di Bukit Love + foto-foto • 17:30–19:00 Bersih-bersih + makan malam • 19:00–21:00 Alun-alun Karimun + jajan kuliner • 21:00 Istirahat Hari 2 — Sabtu • 06:30–07:30 Sarapan + briefing hopping • 07:30–09:30 Boat ke Pulau Menjangan Kecil — snorkeling spot terumbu • 09:30–11:30 Pulau Menjangan Besar — interaksi hiu (penangkaran) • 11:30–13:00 Makan siang BBQ ikan di Pulau Cemara Besar • 13:00–15:00 Pulau Cemara Kecil + foto pasir putih • 15:00–17:00 Pulau Cilik — sunset + snorkel terakhir • 17:00–18:30 Kembali ke homestay + bersih-bersih • 18:30–20:00 Makan malam seafood • 20:00 Acara bebas Hari 3 — Minggu • 06:00–07:00 Sunrise di Tanjung Gelam • 07:00–09:00 Sarapan + pack-up • 09:00–10:00 Belanja oleh-oleh di pelabuhan • 10:00–16:00 Penyeberangan kapal feri Karimun → Jepara • 16:00–17:00 Tiba di Pelabuhan Kartini, bubar grup`; const ITIN_CITYTRIP = `Hari 1 — Sabtu • 08:00–08:30 Meeting di Stasiun Tugu Yogyakarta • 08:30–10:00 Sarapan Gudeg Yu Djum + briefing rute • 10:00–12:00 Kotagede — kerajinan perak + Masjid Mataram • 12:00–13:30 Makan siang Sate Klathak Pak Pong • 13:30–16:00 Tamansari — pemandian Sultan + sumur Gumuling • 16:00–17:30 Coffee break di kedai lokal Prawirotaman • 17:30–19:30 Sunset di Bukit Bintang (Jl. Imogiri) • 19:30–21:30 Angkringan Lik Man — kopi joss + nasi kucing • 21:30 Drop ke penginapan masing-masing Hari 2 — Minggu • 06:00–07:00 Pickup dari penginapan • 07:00–09:30 Perjalanan ke Kalibiru, Kulon Progo • 09:30–11:30 Kalibiru — spot foto rumah pohon di tebing • 11:30–13:00 Makan siang pecel di warung lokal • 13:00–15:00 Pinus Pengger — instalasi seni alam • 15:00–16:30 Heha Sky View (opsional, cek cuaca) • 16:30–18:00 Kembali ke kota — drop di Stasiun Tugu / Bandara • 18:00 Bubar grup`; const ITIN_CULINARY = `Hari 1 — Sabtu (one-day food tour) • 09:00–09:30 Meeting di Stasiun Bandung pintu utara + briefing rute • 09:30–10:15 Stop 1: Surabi Enhaii (sarapan tradisional) • 10:15–11:00 Stop 2: Lotek Kalipah Apo • 11:00–11:45 Stop 3: Mie Kocok Mang Dadeng (Kebon Jukut) • 11:45–12:30 Stop 4: Bakso Akung (cabang Burangrang) • 12:30–13:30 Istirahat + jalan santai di Cihampelas • 13:30–14:15 Stop 5: Batagor Kingsley • 14:15–15:00 Stop 6: Cuanki Serayu • 15:00–15:45 Stop 7: Es Cendol Elizabeth • 15:45–16:30 Stop 8: Roti Gempol & Kopi Anjis (penutup) • 16:30–17:00 Closing + foto bareng di Braga`; const ITIN_CONCERT = `Hari 1 — Sabtu (showtime) • 17:00–17:30 Meeting di Plaza GBK, depan loket Cat 1 • 17:30–18:30 Foto bareng pre-show + obrolan singkat • 18:30–19:00 Masuk venue bareng (kategori tetap masing-masing) • 19:00–22:30 Konser Coldplay — Music of the Spheres • 22:30–23:00 Berkumpul lagi di luar gerbang utama GBK • 23:00–00:30 After-party dinner di Senayan (resto TBA via grup WA) • 00:30 Bubar`; const ITIN_WORKSHOP = `Hari 1 — Sabtu • 04:00–04:30 Meeting di Alun-alun Pangalengan • 04:30–05:30 Briefing teknis + setup peralatan • 05:30–07:30 Sunrise shoot di Perkebunan Teh Malabar • 07:30–09:00 Sarapan + sesi review foto bareng mentor • 09:00–11:00 Materi indoor: long exposure & filter ND • 11:00–13:00 ISHOMA + transfer ke Situ Cileunca • 13:00–16:00 Workshop on-field di Situ Cileunca (panorama, refleksi) • 16:00–18:00 Golden hour shoot di Bukit Nini • 18:00–19:30 Makan malam di villa • 19:30–22:00 Sesi malam — milky way / star trail (cuaca permitting) • 22:00 Istirahat di villa Hari 2 — Minggu • 05:00–07:00 Sunrise shoot di Pangalengan + foto siluet • 07:00–08:30 Sarapan + diskusi hasil • 08:30–11:00 Sesi editing Lightroom (laptop pribadi) • 11:00–12:00 Review akhir + sertifikat • 12:00–13:00 Makan siang penutupan • 13:00–15:00 Kembali ke Alun-alun Pangalengan • 15:00 Bubar grup`; const ITIN_RETREAT = `Hari 1 — Jumat • 14:00–15:00 Check-in Villa Sawah Ubud + welcome drink (jamu) • 15:00–16:00 Tour fasilitas + pembagian welcome kit • 16:00–17:30 Yin Yoga pembuka — release perjalanan • 17:30–18:30 Journaling: niat & ekspektasi retreat • 18:30–20:00 Dinner vegan (set menu) • 20:00–21:00 Circle pengenalan + meditasi singkat • 21:00 Lights-out Hari 2 — Sabtu • 06:00–07:30 Hatha Yoga matahari terbit • 07:30–09:00 Sarapan vegan + tea ceremony • 09:00–10:30 Meditasi guided: body scan • 10:30–12:00 Pranayama (latihan napas) • 12:00–13:30 Lunch + acara bebas (sawah walk) • 13:30–15:00 Sound healing dengan singing bowl • 15:00–16:30 Workshop: mindful eating + jamu making • 16:30–18:00 Yin Yoga sore + savasana panjang • 18:00–19:30 Dinner vegan • 19:30–21:00 Sharing circle + meditasi malam • 21:00 Lights-out Hari 3 — Minggu • 06:00–07:30 Vinyasa Flow penutupan • 07:30–09:00 Sarapan + closing journaling • 09:00–10:30 Closing circle + tukar pesan • 10:30–11:30 Check-out + pelukan perpisahan • 11:30–14:00 Acara bebas (rekomendasi spa/cafe sekitar) • 14:00 Trip resmi ditutup`; // ============================================================================ // 1. Cleanup — urutan FK aware (Payment → Booking → Review → Image → // Participant → Trip → OrgVerification → Profile → Account → User). // Seed wajib idempotent supaya `npm run seed` bisa di-run berulang. // ============================================================================ async function cleanup() { await prisma.payment.deleteMany(); await prisma.booking.deleteMany(); await prisma.tripReview.deleteMany(); await prisma.tripImage.deleteMany(); await prisma.tripParticipant.deleteMany(); await prisma.trip.deleteMany(); await prisma.organizerVerification.deleteMany(); await prisma.userProfile.deleteMany(); await prisma.account.deleteMany(); await prisma.user.deleteMany(); } // ============================================================================ // 2. Users + Profile // ============================================================================ interface SeedUser { key: string; name: string; email: string; isAdmin?: boolean; profile?: { bio?: string; city?: string; interests?: string[]; instagram?: string; vibe?: Vibe; }; } const SEED_USERS: SeedUser[] = [ // Admin { key: "admin", name: "Admin Setrip", email: "admin@setrip.id", isAdmin: true, }, // Organizers (akan di-link ke OrganizerVerification) { key: "dede", name: "Dede Inoen", email: "dede.inoen@setrip.id", profile: { bio: "Pendaki sejak 2015. Suka trip santai, bawa peserta pulang dengan selamat.", city: "Garut", interests: ["hiking", "camping", "fotografi"], instagram: "dede.inoen", vibe: "BALANCED", }, }, { key: "panji", name: "Panji Petualang", email: "panji@setrip.id", profile: { bio: "Trip leader 10+ tahun. Spesialis gunung & laut.", city: "Kuningan", interests: ["hiking", "diving", "snorkeling"], instagram: "panji.adv", vibe: "HARDCORE", }, }, { key: "fiersa", name: "Fiersa Besari", email: "fiersa@setrip.id", profile: { bio: "Buku, kopi, gunung. Kadang ngajak nonton konser bareng.", city: "Bandung", interests: ["hiking", "kuliner", "musik"], instagram: "fiersabesari", vibe: "CHILL", }, }, // Peserta { key: "budi", name: "Budi Santoso", email: "budi@gmail.com", profile: { bio: "Office worker yang lagi cari teman buat jalan akhir pekan.", city: "Jakarta", interests: ["hiking", "kuliner"], vibe: "BALANCED", }, }, { key: "sari", name: "Sari Dewi", email: "sari@gmail.com", profile: { bio: "Solo traveler. Suka pantai, ikan, dan tidur siang di hammock.", city: "Surabaya", interests: ["snorkeling", "yoga", "fotografi"], instagram: "saridew_id", vibe: "CHILL", }, }, { key: "doni", name: "Doni Prasetyo", email: "doni@gmail.com", profile: { bio: "Diver. Cari teman buddy fun-dive sebulan sekali.", city: "Denpasar", interests: ["diving", "freediving"], vibe: "HARDCORE", }, }, { key: "maya", name: "Maya Putri", email: "maya@gmail.com", profile: { bio: "Yoga teacher. Lagi belajar lebih hadir di setiap perjalanan.", city: "Yogyakarta", interests: ["yoga", "retreat", "kuliner"], instagram: "maya.putri", vibe: "CHILL", }, }, { key: "raka", name: "Raka Aditya", email: "raka@gmail.com", profile: { bio: "Anak baru di dunia trip. Energy tinggi, niat banyak.", city: "Bandung", interests: ["hiking", "konser"], vibe: "HARDCORE", }, }, ]; type UserMap = Record; async function seedUsers(): Promise { const password = await bcrypt.hash(SEED_PASSWORD, 12); const acceptedAt = new Date(); const map: UserMap = {}; for (const u of SEED_USERS) { const created = await prisma.user.create({ data: { name: u.name, email: u.email, password, emailVerified: acceptedAt, acceptedTermsAndPrivacy: true, acceptedAt, profile: u.profile ? { create: { bio: u.profile.bio, city: u.profile.city, interests: u.profile.interests ?? [], instagram: u.profile.instagram, vibe: u.profile.vibe, }, } : undefined, }, }); map[u.key] = { id: created.id, email: created.email, name: created.name }; } return map; } // ============================================================================ // 3. Organizer Verification — APPROVED, PENDING, REJECTED untuk demo admin queue // ============================================================================ interface SeedVerification { userKey: string; fullName: string; nik: string; birthDate: Date; address: string; bankName: string; bankAccountNumber: string; bankAccountName: string; status: "APPROVED" | "PENDING" | "REJECTED"; rejectionReason?: string; } const SEED_VERIFICATIONS: SeedVerification[] = [ { userKey: "dede", fullName: "Dede Inoen", nik: "3201010101010001", birthDate: utc(1990, 0, 1, 0, 0), address: "Jl. Pendaki No. 1, Garut, Jawa Barat", bankName: "BCA", bankAccountNumber: "1234567890", bankAccountName: "Dede Inoen", status: "APPROVED", }, { userKey: "panji", fullName: "Panji Petualang", nik: "3201010101010002", birthDate: utc(1985, 5, 15, 0, 0), address: "Jl. Adventure No. 7, Kuningan, Jawa Barat", bankName: "Mandiri", bankAccountNumber: "9876543210", bankAccountName: "Panji Petualang", status: "APPROVED", }, { userKey: "fiersa", fullName: "Fiersa Besari", nik: "3201010101010003", birthDate: utc(1988, 2, 3, 0, 0), address: "Jl. Cisitu No. 99, Bandung, Jawa Barat", bankName: "BNI", bankAccountNumber: "5566778899", bankAccountName: "Fiersa Besari", status: "APPROVED", }, // Demo admin queue: PENDING + REJECTED { userKey: "doni", fullName: "Doni Prasetyo", nik: "5101010202020001", birthDate: utc(1992, 8, 10, 0, 0), address: "Jl. Diving No. 12, Denpasar, Bali", bankName: "BCA", bankAccountNumber: "1112223334", bankAccountName: "Doni Prasetyo", status: "PENDING", }, { userKey: "raka", fullName: "Raka Aditya", nik: "3201010101010099", birthDate: utc(2000, 11, 31, 0, 0), address: "Alamat tidak jelas", bankName: "Bank lain", bankAccountNumber: "0001", bankAccountName: "RAKA", status: "REJECTED", rejectionReason: "Foto KTP tidak terbaca, alamat tidak lengkap.", }, ]; async function seedVerifications(users: UserMap, adminId: string) { const now = new Date(); for (const v of SEED_VERIFICATIONS) { const owner = users[v.userKey]; if (!owner) continue; const isApproved = v.status === "APPROVED"; const isReviewed = v.status !== "PENDING"; await prisma.organizerVerification.create({ data: { userId: owner.id, fullName: v.fullName, nikEncrypted: encryptString(v.nik), nikHash: hmacHex(v.nik), birthDate: v.birthDate, address: v.address, ktpImageKey: `ktp/seed-${v.userKey}.jpg`, livenessKey: `liveness/seed-${v.userKey}.jpg`, bankName: v.bankName, bankAccountNumber: v.bankAccountNumber, bankAccountName: v.bankAccountName, status: v.status, rejectionReason: v.rejectionReason, reviewedAt: isReviewed ? now : null, reviewedById: isReviewed ? adminId : null, verifiedAt: isApproved ? now : null, }, }); } } // ============================================================================ // 4. Trips — mix masa depan (OPEN) & masa lalu (COMPLETED) + 1 CLOSED. // Vibe diisi supaya filter & matching demo-able. // Trip masa lalu jadi pondasi review + trust score. // ============================================================================ interface SeedTripImage { url: string; caption: string; } interface SeedTrip { key: string; organizerKey: string; category: ActivityCategory; title: string; description: string; meetingPoint?: string; itinerary?: string; whatsIncluded?: string; whatsExcluded?: string; destination: string; location: string; date: Date; endDate: Date | null; maxParticipants: number; price: number; vibe: Vibe; status: "OPEN" | "FULL" | "CLOSED" | "COMPLETED"; images: SeedTripImage[]; } // 2026-05-10 = today (per CLAUDE.md). Trip masa lalu < hari ini, masa depan > hari ini. const SEED_TRIPS: SeedTrip[] = [ // ============ MASA LALU (sudah selesai — ada review) ============ { key: "past_papandayan", organizerKey: "dede", category: "HIKING", title: "Open Trip Papandayan — Maret 2026", description: `Pendakian santai ke Gunung Papandayan, batch Maret. Cocok untuk pemula.`, meetingPoint: "Alun-alun Garut, 05:00 WIB", itinerary: ITIN_PAPANDAYAN, destination: "Gunung Papandayan", location: "Garut, Jawa Barat", date: utc(2026, 2, 14, 22, 0), endDate: utc(2026, 2, 15, 18, 0), maxParticipants: 10, price: 250000, vibe: "BALANCED", status: "COMPLETED", images: [ { url: img("1554629947-334ff61d85dc"), caption: "Kawah Papandayan" }, { url: img("1464822759023-fed622ff2c3b"), caption: "Track menuju puncak" }, ], }, { key: "past_pahawang", organizerKey: "panji", category: "SNORKELING", title: "Snorkeling Pulau Pahawang — Februari 2026", description: `Trip snorkeling batch Februari. Cuaca bersahabat, visibility 10m+.`, meetingPoint: "Dermaga Ketapang, Lampung Selatan, 07:00 WIB", itinerary: ITIN_PAHAWANG, destination: "Pulau Pahawang Kecil", location: "Lampung Selatan, Lampung", date: utc(2026, 1, 21, 0, 0), endDate: null, maxParticipants: 10, price: 380000, vibe: "CHILL", status: "COMPLETED", images: [ { url: img("1583212292454-1fe6229603b7"), caption: "Snorkeling di Pahawang" }, ], }, { key: "past_culinary", organizerKey: "dede", category: "CULINARY", title: "Kulineran Street Food Bandung — April 2026", description: `Food tour batch April. 8 spot legend dilibas dalam satu hari.`, meetingPoint: "Stasiun Bandung pintu utara, 09:00 WIB", itinerary: ITIN_CULINARY, destination: "Street Food Tour Bandung", location: "Bandung, Jawa Barat", date: utc(2026, 3, 12, 0, 0), endDate: null, maxParticipants: 8, price: 175000, vibe: "CHILL", status: "COMPLETED", images: [ { url: img("1565299624946-b28f40a0ae38"), caption: "Street food" }, ], }, { key: "past_workshop_cancelled", organizerKey: "panji", category: "WORKSHOP", title: "Workshop Astrofotografi Bromo — Januari 2026 (BATAL)", description: `Cuaca tidak mendukung. Trip dibatalkan H-3, peserta refund 100%.`, meetingPoint: "-", destination: "Astrofotografi Bromo", location: "Probolinggo, Jawa Timur", date: utc(2026, 0, 18, 22, 0), endDate: utc(2026, 0, 19, 12, 0), maxParticipants: 6, price: 950000, vibe: "HARDCORE", status: "CLOSED", images: [], }, // ============ MASA DEPAN (OPEN — fitur join + bayar bisa di-test) ============ { key: "open_papandayan", organizerKey: "dede", category: "HIKING", title: "Open Trip Papandayan Weekend", description: `Pendakian santai ke Gunung Papandayan, cocok untuk pemula! ⚠️ Bawa: Sleeping bag, jaket, headlamp, air 2L`, meetingPoint: "Alun-alun Garut, Sabtu 05:00 WIB — detail grup WA.", itinerary: ITIN_PAPANDAYAN, whatsIncluded: `• Transport PP Garut–basecamp • Guide lokal • Tenda tim • Konsumsi 3x + snack`, whatsExcluded: `• Tiket masuk TN • Sleeping bag pribadi • Asuransi perjalanan`, destination: "Gunung Papandayan", location: "Garut, Jawa Barat", date: utc(2026, 5, 13, 22, 0), endDate: utc(2026, 5, 14, 18, 0), maxParticipants: 10, price: 250000, vibe: "BALANCED", status: "OPEN", images: [ { url: img("1554629947-334ff61d85dc"), caption: "Kawah Papandayan" }, { url: img("1464822759023-fed622ff2c3b"), caption: "Track menuju puncak" }, ], }, { key: "open_ciremai", organizerKey: "panji", category: "HIKING", title: "Pendakian Ciremai via Apuy", description: `Trip ke puncak tertinggi Jawa Barat! 🏔️ 📍 Meeting Point: Stasiun Kuningan, 04:00 WIB ⚠️ Level: Menengah — perlu stamina baik`, meetingPoint: "Stasiun Kuningan, Sabtu 04:00 WIB", itinerary: ITIN_CIREMAI, destination: "Gunung Ciremai", location: "Kuningan, Jawa Barat", date: utc(2026, 5, 23, 4, 0), endDate: utc(2026, 5, 24, 18, 0), maxParticipants: 8, price: 350000, vibe: "HARDCORE", status: "OPEN", images: [ { url: img("1480497490787-505ec076689f"), caption: "Puncak Ciremai 3.078 mdpl" }, { url: img("1502085671122-2d218cd434e6"), caption: "Sunrise dari puncak" }, ], }, { key: "open_camping", organizerKey: "dede", category: "CAMPING", title: "Camping Santai di Ranca Upas", description: `Camping bareng di tengah hutan pinus dengan ditemani rusa-rusa! Cocok buat first-timer. 📍 Meeting Point: Lembang, Sabtu 13:00 WIB 🎒 Fasilitas: Tenda dome, sleeping bag, BBQ, api unggun 🔥 Bonus: Live music akustik malam hari`, meetingPoint: "Pertigaan Pasar Lembang, Sabtu 13:00 WIB", itinerary: ITIN_CAMPING, whatsIncluded: `• Tenda + sleeping bag + matras • Logistik camp • Makan malam BBQ + sarapan • Tiket masuk lokasi`, whatsExcluded: `• Transport pribadi • Snack tambahan`, destination: "Ranca Upas", location: "Bandung Selatan, Jawa Barat", date: utc(2026, 5, 16, 6, 0), endDate: utc(2026, 5, 17, 12, 0), maxParticipants: 12, price: 220000, vibe: "CHILL", status: "OPEN", images: [ { url: img("1504280390367-361c6d9f38f4"), caption: "Tenda di hutan pinus" }, { url: img("1517824806704-9040b037703b"), caption: "Sunset di camp" }, ], }, { key: "open_snorkel", organizerKey: "panji", category: "SNORKELING", title: "Snorkeling Pulau Pahawang", description: `Air kristal-jernih, ikan warna-warni, dan pasir putih lembut. 🤿 Pemula friendly — guide profesional 📷 Underwater photo session included`, meetingPoint: "Dermaga Ketapang, Lampung Selatan, 07:00 WIB", itinerary: ITIN_PAHAWANG, whatsIncluded: `• Boat PP • Alat snorkel (masker, fin, life vest) • Guide & pemandu underwater • Konsumsi 2x`, whatsExcluded: `• Transport ke Lampung • Penginapan Sabtu malam`, destination: "Pulau Pahawang Kecil", location: "Lampung Selatan, Lampung", date: utc(2026, 5, 30, 0, 0), endDate: null, maxParticipants: 10, price: 380000, vibe: "CHILL", status: "OPEN", images: [ { url: img("1583212292454-1fe6229603b7"), caption: "Snorkeling di Pahawang" }, { url: img("1559825481-12a05cc00344"), caption: "Pasir putih Pahawang Kecil" }, ], }, { key: "open_diving", organizerKey: "fiersa", category: "DIVING", title: "Fun Dive Tulamben — USS Liberty Wreck", description: `Dive trip ke USS Liberty Wreck. ⚠️ Sertifikasi minimal: Open Water (PADI/SSI)`, meetingPoint: "Dive shop Tulamben, 06:30 WITA", itinerary: ITIN_DIVING, whatsIncluded: `• 2x dive guided • Full gear rental • Tank & weight • Konsumsi siang`, whatsExcluded: `• Transport ke Bali • Penginapan • Sertifikasi (cek validitas)`, destination: "USS Liberty Wreck", location: "Tulamben, Karangasem, Bali", date: utc(2026, 6, 4, 0, 0), endDate: utc(2026, 6, 5, 12, 0), maxParticipants: 6, price: 1850000, vibe: "HARDCORE", status: "OPEN", images: [ { url: img("1544551763-46a013bb70d5"), caption: "Wreck dive Tulamben" }, { url: img("1566024287286-457247b70310"), caption: "Reef Tulamben" }, ], }, { key: "open_islandhop", organizerKey: "panji", category: "ISLAND_HOPPING", title: "Karimun Jawa Island Hopping 3D2N", description: `Hopping 5 pulau favorit di Karimun Jawa. 🏝️ Cocok untuk solo traveler & couple`, meetingPoint: "Pelabuhan Kartini Jepara, Jumat 07:00 WIB", itinerary: ITIN_ISLANDHOP, whatsIncluded: `• Tiket kapal feri PP Jepara–Karimun • Homestay 2 malam (twin sharing) • Boat hopping 2 hari • Alat snorkel • Makan 6x`, whatsExcluded: `• Transport ke Jepara • Tiket pesawat`, destination: "Kepulauan Karimun Jawa", location: "Jepara, Jawa Tengah", date: utc(2026, 6, 12, 0, 0), endDate: utc(2026, 6, 14, 18, 0), maxParticipants: 12, price: 1450000, vibe: "BALANCED", status: "OPEN", images: [ { url: img("1507525428034-b723cf961d3e"), caption: "Pantai Karimun" }, { url: img("1519046904884-53103b34b206"), caption: "Boat hopping" }, ], }, { key: "open_citytrip", organizerKey: "fiersa", category: "CITY_TRIP", title: "City Trip Jogja Hidden Gems", description: `Bukan Malioboro lagi. Trip jelajah Jogja sisi 'lokal'. 🚐 Mobil grup, bukan tour bus`, meetingPoint: "Stasiun Tugu Yogyakarta, Sabtu 08:00 WIB", itinerary: ITIN_CITYTRIP, whatsIncluded: `• Transport mobil grup 2 hari • Tour leader lokal • Makan 3x (kuliner lokal) • Tiket masuk semua spot`, whatsExcluded: `• Transport ke Jogja • Penginapan (rekomendasi disediakan)`, destination: "Yogyakarta", location: "Yogyakarta", date: utc(2026, 5, 22, 0, 0), endDate: utc(2026, 5, 23, 20, 0), maxParticipants: 8, price: 650000, vibe: "BALANCED", status: "OPEN", images: [ { url: img("1596402184320-417e7178b2cd"), caption: "Tamansari" }, { url: img("1583309217394-d191d747bc66"), caption: "Sudut Jogja" }, ], }, { key: "open_culinary", organizerKey: "dede", category: "CULINARY", title: "Kulineran Street Food Bandung", description: `Hopping 8 spot kuliner legend Bandung dalam satu hari. 🍜 Cocok buat foodie & first-timer`, meetingPoint: "Stasiun Bandung pintu utara, 09:00 WIB", itinerary: ITIN_CULINARY, whatsIncluded: `• Transport angkot/grup • Tour leader food explorer • Sample setiap spot (8 tempat)`, whatsExcluded: `• Pembelian extra di luar sample`, destination: "Street Food Tour Bandung", location: "Bandung, Jawa Barat", date: utc(2026, 5, 17, 0, 0), endDate: null, maxParticipants: 8, price: 175000, vibe: "CHILL", status: "OPEN", images: [ { url: img("1565299624946-b28f40a0ae38"), caption: "Street food" }, ], }, { key: "open_concert", organizerKey: "fiersa", category: "CONCERT", title: "Nonton Coldplay Bareng — Music of the Spheres Jakarta", description: `Cari teman buat nonton Coldplay tapi gak mau nonton sendirian? Gabung grup ini. 🎤 Tiket BUKAN termasuk — peserta bawa tiket masing-masing 🤝 Grup hanya untuk koordinasi meet-up & after-party`, meetingPoint: "Plaza GBK, depan loket Cat 1, 17:00 WIB", itinerary: ITIN_CONCERT, whatsIncluded: `• Koordinasi grup • Foto bareng pre-show • After-party dinner di Senayan`, whatsExcluded: `• Tiket konser (bawa sendiri!) • Transport ke GBK`, destination: "Coldplay — Music of the Spheres", location: "Stadion Utama GBK, Jakarta", date: utc(2026, 6, 20, 10, 0), endDate: null, maxParticipants: 6, price: 0, // Free trip — booking langsung PAID saat di-confirm vibe: "BALANCED", status: "OPEN", images: [ { url: img("1470229722913-7c0e2dbbafd3"), caption: "Konser malam" }, ], }, { key: "open_workshop", organizerKey: "panji", category: "WORKSHOP", title: "Workshop Fotografi Lanskap — Pangalengan", description: `Belajar fotografi lanskap langsung di lapangan. 📷 Bawa kamera DSLR/mirrorless + tripod 👨‍🏫 Mentor: fotografer pro (10+ tahun pengalaman)`, meetingPoint: "Alun-alun Pangalengan, Sabtu 04:00 WIB", itinerary: ITIN_WORKSHOP, whatsIncluded: `• Materi workshop (briefing + on-field) • Tour leader & mentor • Penginapan villa 1 malam • Konsumsi 3x`, whatsExcluded: `• Kamera & tripod (bawa sendiri) • Transport ke Pangalengan`, destination: "Fotografi Lanskap", location: "Pangalengan, Bandung Selatan", date: utc(2026, 6, 6, 0, 0), endDate: utc(2026, 6, 7, 18, 0), maxParticipants: 6, price: 850000, vibe: "BALANCED", status: "OPEN", images: [ { url: img("1452587925148-ce544e77e70d"), caption: "Sunrise kebun teh" }, { url: img("1444080748397-f442aa95c3e5"), caption: "Workshop on-field" }, ], }, { key: "open_retreat", organizerKey: "fiersa", category: "RETREAT", title: "Mindfulness Retreat 3D — Ubud", description: `Retreat 3 hari di tengah sawah Ubud. 🧘 Untuk yang lagi burnout & butuh reset 👥 Grup kecil (max 8) — pengalaman akrab`, meetingPoint: "Villa Sawah Ubud (alamat dikirim H-3 via WA)", itinerary: ITIN_RETREAT, whatsIncluded: `• Penginapan villa 2 malam • Yoga 4 sesi + meditasi 6 sesi • Sound healing (1 sesi) • Konsumsi vegan 6x • Welcome kit (jurnal, herbal tea)`, whatsExcluded: `• Transport ke Ubud • Treatment spa opsional`, destination: "Mindfulness Retreat Ubud", location: "Ubud, Gianyar, Bali", date: utc(2026, 6, 26, 0, 0), endDate: utc(2026, 6, 28, 14, 0), maxParticipants: 8, price: 2400000, vibe: "CHILL", status: "OPEN", images: [ { url: img("1545389336-cf090694435e"), caption: "Yoga di sawah Ubud" }, { url: img("1518609878373-06d740f60d8b"), caption: "Villa retreat" }, ], }, ]; type TripMap = Record; async function seedTrips(users: UserMap): Promise { const map: TripMap = {}; for (const t of SEED_TRIPS) { const organizer = users[t.organizerKey]; if (!organizer) { throw new Error(`Organizer ${t.organizerKey} tidak ditemukan`); } const created = await prisma.trip.create({ data: { category: t.category, title: t.title, description: t.description, meetingPoint: t.meetingPoint, itinerary: t.itinerary, whatsIncluded: t.whatsIncluded, whatsExcluded: t.whatsExcluded, destination: t.destination, location: t.location, date: t.date, endDate: t.endDate, maxParticipants: t.maxParticipants, price: t.price, vibe: t.vibe, status: t.status, organizerId: organizer.id, images: t.images.length ? { create: t.images.map((im, order) => ({ url: im.url, caption: im.caption, order, })), } : undefined, }, }); map[t.key] = { id: created.id, price: created.price, status: t.status }; } return map; } // ============================================================================ // 5. Participants + Booking + Payment // Setiap baris = (tripKey, userKey, intent). `intent` mendeskripsikan // state akhir yang kita simulasikan, supaya UI menampilkan beragam kondisi. // // Mapping intent → (Participant, Booking, Payment): // - "PENDING_REQUEST" : Participant PENDING, Booking PENDING, no Payment // - "AWAITING_PAY" : Participant CONFIRMED, Booking AWAITING_PAY, no Payment // - "MARKED_PAID" : Participant CONFIRMED, Booking AWAITING_PAY, Payment AWAITING (peserta klik "sudah transfer") // - "PAID" : Participant CONFIRMED, Booking PAID, Payment PAID (organizer sudah konfirmasi) // - "FREE_CONFIRMED" : Participant CONFIRMED, Booking PAID, no Payment (trip gratis) // - "CANCELLED" : Participant CANCELLED, Booking CANCELLED, no Payment // ============================================================================ type Intent = | "PENDING_REQUEST" | "AWAITING_PAY" | "MARKED_PAID" | "PAID" | "FREE_CONFIRMED" | "CANCELLED"; interface SeedParticipant { tripKey: string; userKey: string; intent: Intent; } const SEED_PARTICIPANTS: SeedParticipant[] = [ // ---------- TRIP MASA LALU (COMPLETED) — semua PAID, jadi pondasi review ---------- { tripKey: "past_papandayan", userKey: "budi", intent: "PAID" }, { tripKey: "past_papandayan", userKey: "sari", intent: "PAID" }, { tripKey: "past_papandayan", userKey: "doni", intent: "PAID" }, { tripKey: "past_papandayan", userKey: "maya", intent: "PAID" }, { tripKey: "past_pahawang", userKey: "budi", intent: "PAID" }, { tripKey: "past_pahawang", userKey: "maya", intent: "PAID" }, { tripKey: "past_pahawang", userKey: "raka", intent: "PAID" }, { tripKey: "past_culinary", userKey: "sari", intent: "PAID" }, { tripKey: "past_culinary", userKey: "doni", intent: "PAID" }, { tripKey: "past_culinary", userKey: "raka", intent: "CANCELLED" }, // batal H-1 // CLOSED trip — peserta yang ke-refund (di-mark CANCELLED) { tripKey: "past_workshop_cancelled", userKey: "budi", intent: "CANCELLED" }, { tripKey: "past_workshop_cancelled", userKey: "doni", intent: "CANCELLED" }, // ---------- TRIP MASA DEPAN — mix state ---------- // Papandayan — campur semua state biar dashboard organizer kaya { tripKey: "open_papandayan", userKey: "budi", intent: "PAID" }, { tripKey: "open_papandayan", userKey: "sari", intent: "MARKED_PAID" }, { tripKey: "open_papandayan", userKey: "doni", intent: "AWAITING_PAY" }, { tripKey: "open_papandayan", userKey: "raka", intent: "PENDING_REQUEST" }, // Ciremai — 2 confirmed paid { tripKey: "open_ciremai", userKey: "budi", intent: "PAID" }, { tripKey: "open_ciremai", userKey: "maya", intent: "AWAITING_PAY" }, // Camping — 5 peserta variasi { tripKey: "open_camping", userKey: "budi", intent: "PAID" }, { tripKey: "open_camping", userKey: "sari", intent: "PAID" }, { tripKey: "open_camping", userKey: "doni", intent: "MARKED_PAID" }, { tripKey: "open_camping", userKey: "maya", intent: "AWAITING_PAY" }, { tripKey: "open_camping", userKey: "raka", intent: "PENDING_REQUEST" }, // Snorkeling { tripKey: "open_snorkel", userKey: "sari", intent: "PAID" }, { tripKey: "open_snorkel", userKey: "maya", intent: "MARKED_PAID" }, { tripKey: "open_snorkel", userKey: "raka", intent: "PENDING_REQUEST" }, // Diving — mahal, sedikit peserta { tripKey: "open_diving", userKey: "doni", intent: "AWAITING_PAY" }, // Island hop { tripKey: "open_islandhop", userKey: "budi", intent: "PAID" }, { tripKey: "open_islandhop", userKey: "sari", intent: "PAID" }, { tripKey: "open_islandhop", userKey: "maya", intent: "MARKED_PAID" }, { tripKey: "open_islandhop", userKey: "raka", intent: "PENDING_REQUEST" }, // City trip { tripKey: "open_citytrip", userKey: "budi", intent: "PAID" }, { tripKey: "open_citytrip", userKey: "maya", intent: "PAID" }, { tripKey: "open_citytrip", userKey: "sari", intent: "PENDING_REQUEST" }, // Culinary — penuh { tripKey: "open_culinary", userKey: "budi", intent: "PAID" }, { tripKey: "open_culinary", userKey: "sari", intent: "PAID" }, { tripKey: "open_culinary", userKey: "doni", intent: "PAID" }, { tripKey: "open_culinary", userKey: "maya", intent: "PAID" }, { tripKey: "open_culinary", userKey: "raka", intent: "PAID" }, // Concert (FREE) — Booking langsung PAID { tripKey: "open_concert", userKey: "maya", intent: "FREE_CONFIRMED" }, { tripKey: "open_concert", userKey: "raka", intent: "FREE_CONFIRMED" }, { tripKey: "open_concert", userKey: "sari", intent: "PENDING_REQUEST" }, // Workshop { tripKey: "open_workshop", userKey: "doni", intent: "PAID" }, { tripKey: "open_workshop", userKey: "sari", intent: "AWAITING_PAY" }, // Retreat — niche { tripKey: "open_retreat", userKey: "maya", intent: "PENDING_REQUEST" }, ]; interface BookingPlan { participantStatus: "PENDING" | "CONFIRMED" | "CANCELLED"; bookingStatus: "PENDING" | "AWAITING_PAY" | "PAID" | "CANCELLED"; payment: null | { status: "AWAITING" | "PAID" }; markedPaidAt: Date | null; paymentConfirmedAt: Date | null; } function planFromIntent(intent: Intent, now: Date): BookingPlan { switch (intent) { case "PENDING_REQUEST": return { participantStatus: "PENDING", bookingStatus: "PENDING", payment: null, markedPaidAt: null, paymentConfirmedAt: null, }; case "AWAITING_PAY": return { participantStatus: "CONFIRMED", bookingStatus: "AWAITING_PAY", payment: null, markedPaidAt: null, paymentConfirmedAt: null, }; case "MARKED_PAID": return { participantStatus: "CONFIRMED", bookingStatus: "AWAITING_PAY", payment: { status: "AWAITING" }, markedPaidAt: now, paymentConfirmedAt: null, }; case "PAID": return { participantStatus: "CONFIRMED", bookingStatus: "PAID", payment: { status: "PAID" }, markedPaidAt: now, paymentConfirmedAt: now, }; case "FREE_CONFIRMED": return { participantStatus: "CONFIRMED", bookingStatus: "PAID", payment: null, markedPaidAt: null, paymentConfirmedAt: null, }; case "CANCELLED": return { participantStatus: "CANCELLED", bookingStatus: "CANCELLED", payment: null, markedPaidAt: null, paymentConfirmedAt: null, }; } } async function seedParticipants(trips: TripMap, users: UserMap) { const now = new Date(); for (const sp of SEED_PARTICIPANTS) { const trip = trips[sp.tripKey]; const user = users[sp.userKey]; if (!trip || !user) continue; const plan = planFromIntent(sp.intent, now); const participant = await prisma.tripParticipant.create({ data: { tripId: trip.id, userId: user.id, status: plan.participantStatus, markedPaidAt: plan.markedPaidAt, paymentConfirmedAt: plan.paymentConfirmedAt, }, }); const booking = await prisma.booking.create({ data: { tripId: trip.id, userId: user.id, participantId: participant.id, amount: trip.price, status: plan.bookingStatus, }, }); if (plan.payment) { const data: Prisma.PaymentUncheckedCreateInput = { bookingId: booking.id, provider: "MANUAL", externalOrderId: `manual-${booking.id}`, amount: trip.price, status: plan.payment.status, method: "manual_transfer", paidAt: plan.payment.status === "PAID" ? now : null, }; await prisma.payment.create({ data }); } } } // ============================================================================ // 6. Reviews — hanya untuk trip COMPLETED, dari peserta CONFIRMED non-organizer. // ============================================================================ interface SeedReview { tripKey: string; userKey: string; rating: number; comment: string; } const SEED_REVIEWS: SeedReview[] = [ { tripKey: "past_papandayan", userKey: "budi", rating: 5, comment: "Trip-nya santai tapi tetep seru. Guide-nya sabar banget sama yang baru pertama mendaki.", }, { tripKey: "past_papandayan", userKey: "sari", rating: 5, comment: "Worth it banget! Sunrise di Tegal Alun cakep, makanan juga oke.", }, { tripKey: "past_papandayan", userKey: "doni", rating: 4, comment: "Trip aman dan terorganisir. Kurangin lama briefing pagi aja.", }, { tripKey: "past_papandayan", userKey: "maya", rating: 5, comment: "Bang Dede bener-bener jaga energi grup. Highly recommended.", }, { tripKey: "past_pahawang", userKey: "budi", rating: 4, comment: "Spot snorkeling-nya bagus, ikan rame. Kapal agak lama nunggu di dermaga.", }, { tripKey: "past_pahawang", userKey: "maya", rating: 5, comment: "Pertama kali snorkel & gak takut karena guide-nya stand-by. Mantap!", }, { tripKey: "past_pahawang", userKey: "raka", rating: 5, comment: "Sunset Pahawang Kecil top. Bakal ikut lagi.", }, { tripKey: "past_culinary", userKey: "sari", rating: 4, comment: "8 spot dalam 1 hari, perut sampe penuh. Mie kocoknya juara.", }, { tripKey: "past_culinary", userKey: "doni", rating: 3, comment: "Seru tapi rute pagi-siang kepadetan, jadwal molor 1 jam.", }, ]; async function seedReviews(trips: TripMap, users: UserMap) { for (const r of SEED_REVIEWS) { const trip = trips[r.tripKey]; const user = users[r.userKey]; if (!trip || !user) continue; await prisma.tripReview.create({ data: { tripId: trip.id, userId: user.id, rating: r.rating, comment: r.comment, }, }); } } // ============================================================================ // Main // ============================================================================ async function main() { console.log("🌱 Seeding database...\n"); console.log("⏳ Cleanup tabel lama..."); await cleanup(); console.log("⏳ Seed users + profile..."); const users = await seedUsers(); const admin = users["admin"]; if (!admin) throw new Error("Admin user gagal dibuat"); console.log("⏳ Seed organizer verifications..."); await seedVerifications(users, admin.id); console.log("⏳ Seed trips..."); const trips = await seedTrips(users); console.log("⏳ Seed participants + bookings + payments..."); await seedParticipants(trips, users); console.log("⏳ Seed reviews..."); await seedReviews(trips, users); console.log(` ✅ Selesai. Ringkasan: ${SEED_USERS.length} users (1 admin, 3 organizer, ${SEED_USERS.length - 4} peserta) ${SEED_VERIFICATIONS.length} organizer verifications (3 APPROVED, 1 PENDING, 1 REJECTED) ${SEED_TRIPS.length} trips (3 COMPLETED + 1 CLOSED + ${SEED_TRIPS.filter((t) => t.status === "OPEN").length} OPEN) ${SEED_PARTICIPANTS.length} participants (mix PENDING/CONFIRMED/CANCELLED) ${SEED_REVIEWS.length} reviews 🔐 Login info: Admin : admin@setrip.id Organizer : dede.inoen@setrip.id, panji@setrip.id, fiersa@setrip.id Peserta : budi/sari/doni/maya/raka @gmail.com Password : ${SEED_PASSWORD} `); } main() .catch((e) => { console.error("❌ Seed failed:", e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });