11 KiB
Setrip — Trust & Trip Detail Roadmap
Status implementasi yang menaikkan kepercayaan calon peserta — trip detail experience yang meyakinkan + sistem reputasi organizer yang transparan.
Prinsip: trust = fungsi dari (a) kelengkapan informasi trip dan (b) reputasi organizer yang transparan. Setiap fitur dievaluasi: apakah memberi calon peserta alasan obyektif untuk percaya?
Audit state sekarang (baseline)
Trip detail (~80% sudah ada):
- ✅ Itinerary, include/exclude, meeting point — schema lengkap di
Trip, ditampilkan viaTripProgramBlock. - ✅ Participant preview (kartu confirmed peserta dengan avatar, kota, interests).
- ✅ Slot tersisa (X / Y) dengan progress bar berwarna.
- ⚠️ Urgency message ada tapi tidak mencolok saat slot menipis.
- ⚠️ Itinerary text bebas — organizer butuh hint format supaya konsisten isi lengkap.
Review/trust (~40% sudah ada):
- ✅ Model
TripReview(rating + comment per trip+user, unique). - ✅ Trust panel di trip detail (verified badge, trips created, avg rating, review count).
- ❌ Profil organizer publik
/u/[id]belum tampilkan rating/review aggregate. - ❌ Belum ada total participants served, completion rate, rating breakdown.
- ❌ Belum ada list ulasan terkumpul per organizer.
- ❌
TripStatus.COMPLETEDenum-nya ada tapi tidak pernah di-set.
File baseline: app/trips/[id]/page.tsx, server/services/trust.service.ts, server/services/review.service.ts, app/u/[id]/page.tsx.
PR-1 — Trip Detail Polish (UI only) ✅
Selesai. Cosmetic. Tidak ada migration, tidak ada perubahan service/repo.
| # | Item | Status | File |
|---|---|---|---|
| 1.1 | Urgency badge mencolok saat spotsLeft <= 3 ("⚡ Tinggal X spot!") di header progress bar |
✅ | app/trips/[id]/page.tsx |
| 1.2 | Participant preview ringkas di blok progress ("👥 Sudah join: Andi, Rina, Budi +4") — first impression tanpa scroll | ✅ | app/trips/[id]/page.tsx |
| 1.3 | Hint deskriptif + placeholder lebih konkret di field itinerary form create-trip | ✅ | features/trip/components/create-trip-form.tsx |
Tindakan manual: tidak ada.
PR-2 — Organizer Trust Aggregates (service + UI, tanpa migration) ✅
Selesai. tsc --noEmit lulus. Tanpa schema baru, tanpa migration.
Keputusan asumsi yang dipakai:
tripsCompleted≠Trip.status = COMPLETED(status itu tidak pernah di-set). PakaiendDate < now()(fallbackdate < now()) ANDstatus != CLOSED.tripsCancelled=Trip.status = CLOSED(organizer batalkan trip eksplisit).completionRatebutuh sample ≥ 3 (COMPLETION_RATE_MIN_SAMPLEdi lib/trust.ts) supaya tidak menyesatkan organizer baru.- Rating breakdown di-render sebagai bar chart kecil (visual cue lebih kuat dari angka mentah).
OrganizerStatsPaneldi profil publik tidak di-render untuk user yang murni peserta — query Prisma juga di-skip kalauorganizedTrips.length === 0 && !isVerifiedOrganizer.- Trip detail: stat box "Trip dibuat" diganti jadi "Trip selesai" (lebih meaningful) + tambah "Peserta dilayani". Total 3 stat box, masih kompak.
| # | Item | Status | File |
|---|---|---|---|
| 2.1 | Extend OrganizerTrust type: tripsCompleted, tripsCancelled, totalParticipantsServed, completionRate, ratingBreakdown |
✅ | server/services/trust.service.ts |
| 2.2 | tripsCompleted di-derive dari endDate < now() (fallback date < now()) AND status != CLOSED |
✅ | server/services/trust.service.ts |
| 2.3 | totalParticipantsServed = count TripParticipant CONFIRMED di trip yang sudah lewat & tidak dibatalkan |
✅ | server/services/trust.service.ts |
| 2.4 | completionRate = tripsCompleted / (tripsCompleted + tripsCancelled). Null bila sample < 3 |
✅ | server/services/trust.service.ts, lib/trust.ts |
| 2.5 | ratingBreakdown via prisma.tripReview.groupBy({ by: ['rating'] }) |
✅ | server/services/trust.service.ts |
| 2.6 | Komponen OrganizerStatsPanel (badges + 4 stat box + bar chart breakdown) |
✅ | features/profile/components/organizer-stats-panel.tsx |
| 2.7 | Update OrganizerTrustPanel di trip detail — Trip selesai (+ subtitle "berjalan"), Peserta dilayani, Rating |
✅ | features/trip/components/organizer-trust-panel.tsx |
| 2.8 | Render OrganizerStatsPanel di /u/[id] (skip query untuk non-organizer) |
✅ | app/u/[id]/page.tsx |
Tindakan manual: tidak ada.
PR-3 — Organizer Reviews Aggregator (service + UI, tanpa migration) ✅
Selesai. tsc --noEmit lulus. Tanpa schema baru, tanpa migration.
Keputusan asumsi yang dipakai:
- Default limit 20 ulasan terbaru — cukup untuk MVP, tidak perlu pagination dulu.
- Komponen RSC (server component, no
"use client") — pure render, tidak ada interaktivitas. - Tipe
OrganizerReviewItemdi-extract dariAwaited<ReturnType<typeof reviewRepo.findByOrganizer>>[number]supaya schema repo = sumber kebenaran tanpa duplikasi tipe. - Fetch trust + reviews via single
Promise.alldi/u/[id](paralel, hemat 1 round-trip). - Komponen di-skip kalau
reviews.length === 0— biar tidak makin "kosong" di profil organizer baru. Stats panel sudah punya pesan "Belum ada ulasan". - Rating ditampilkan sebagai bintang penuh (
★★★★☆) bukan angka — visual cue lebih kuat untuk testimoni. - Header section: "X terbaru dari Y ulasan" kalau di-limit, atau cuma "Y ulasan" kalau seluruh list ditampilkan.
| # | Item | Status | File |
|---|---|---|---|
| 3.1 | reviewService.getReviewsByOrganizer(organizerId, limit?) + tipe OrganizerReviewItem |
✅ | server/services/review.service.ts |
| 3.2 | Repo helper findByOrganizer (default limit 20, urut newest, include user + trip) |
✅ | server/repositories/review.repo.ts |
| 3.3 | Komponen OrganizerReviewsList (avatar + name + bintang + trip link + tanggal + comment) |
✅ | features/review/components/organizer-reviews-list.tsx |
| 3.4 | Render di /u/[id] di bawah OrganizerStatsPanel, fetch via Promise.all paralel |
✅ | app/u/[id]/page.tsx |
Tindakan manual: tidak ada.
PR-4 — Trip Completion Mechanism (cron daily) ✅
Selesai. Pilihan B (cron daily) — Trip.status = COMPLETED di-set otomatis untuk trip yang endDate (atau date kalau endDate null) sudah lewat hari ini UTC.
Keputusan asumsi yang dipakai:
- Cutoff =
utcStartOfDay(new Date())(start of today UTC). Trip denganendDate < cutoffdi-flip; trip yang berakhir hari ini belum. - Hanya trip dengan status
OPENatauFULLyang di-flip.CLOSEDtidak disentuh (organizer eksplisit membatalkan; tetap dibedakan dari COMPLETED untuk perhitungantripsCancelleddi trust panel). - Idempotent: dua kali run di hari yang sama, run kedua match 0 row.
- Endpoint diproteksi via
Authorization: Bearer ${CRON_SECRET}. KalauCRON_SECRETtidak di-set, endpoint hard-fail 500 (mencegah accidentally jalan tanpa proteksi). - Schedule cron:
0 18 * * *(jam 18:00 UTC = 01:00 WIB hari berikutnya) — buffer ~7 jam pasca-akhir hari WIB sebelum flip. trustServicetetap pakai computed-from-endDate(tidak diganti kestatus = COMPLETED). Alasan: trust calc tetap correct walau cron telat / down sehari, dan backward-compat untuk trip lama yang dibuat sebelum cron aktif.- Vercel Cron via
vercel.json— host lain tinggal panggil endpoint yang sama dari cron eksternal apa saja (GitHub Actions, cron-job.org, dst) dengan header yang sama.
| # | Item | Status | File |
|---|---|---|---|
| 4.1 | Repo helper bulkCompletePastTrips(cutoff) (idempotent, batch update) |
✅ | server/repositories/trip.repo.ts |
| 4.2 | Service tripService.autoCompletePastTrips() |
✅ | server/services/trip.service.ts |
| 4.3 | API route /api/cron/auto-complete-trips (GET, proteksi CRON_SECRET) |
✅ | app/api/cron/auto-complete-trips/route.ts |
| 4.4 | Schedule 0 18 * * * di Vercel Cron |
✅ | vercel.json |
Tindakan manual:
- Set env
CRON_SECRETdi hosting (random ≥32 char). Generate cepat:openssl rand -hex 32. - Kalau host bukan Vercel: panggil endpoint dari cron eksternal apa saja (GitHub Actions schedule, cron-job.org, EasyCron, dst) dengan header
Authorization: Bearer ${CRON_SECRET}.vercel.jsonbisa dihapus.
❌ Anti-list (yang harus DITOLAK kalau muncul)
- Model
OrganizerReviewterpisah —TripReviewsudah cukup, 1 trip = 1 organizer. Bikin model baru = duplikasi data + sumber kebenaran ambigu. - Denormalisasi cache (mis.
User.cachedAvgRating) sebelum aggregate query terbukti lambat. Premature optimization → drift jadi tech debt cepat. - Auto-hapus review buruk atau organizer "respon" review (untuk MVP). Bisa nanti — fokus dulu menampilkan data jujur.
- "Trust score" gabungan satu angka — kasih breakdown agar calon peserta evaluasi sendiri. Single number gampang dimanipulasi & menyesatkan.
- Review user/peserta (no-show, kooperatif?) — itu C6 di SOCIAL_ROADMAP.md. Scope berbeda, jangan campur.
- Rating dengan setengah bintang / kustom 1-10 — tetap 1-5 integer (sudah di schema). Granularitas lebih halus tidak meningkatkan trust, hanya menambah noise.
Saran phasing
PR berurutan. Setiap PR mandiri (siap di-deploy):
- PR-1 — Trip detail polish. Cepat, low-risk, no migration. Mulai dari sini.
- PR-2 — Trust aggregates di service + UI. Read-only, no migration.
- PR-3 — Reviews list per organizer di service + UI. Read-only, no migration.
- PR-4 — Diskusi opsi completion mechanism (atau skip kalau opsi C dipilih).
Pertanyaan terbuka sebelum PR-2:
- Apakah
tripsCompletedcount termasuk trip denganstatus = CLOSEDyangendDate < now()? Saran: tidak — CLOSED = dibatalkan, dipisah ketripsCancelled. - Threshold minimum supaya
completionRateditampilkan? Saran: min 3 trip selesai supaya angka tidak menyesatkan (1 trip dibatalkan dari 1 trip = 0% looks bad untuk organizer baru). - Tampilkan rating breakdown sebagai bar chart atau hanya angka? Saran: bar chart kecil — visual cue lebih kuat untuk credibility.