# 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 via `TripProgramBlock`. - ✅ 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.COMPLETED` enum-nya ada tapi tidak pernah di-set. File baseline: [app/trips/[id]/page.tsx](app/trips/%5Bid%5D/page.tsx), [server/services/trust.service.ts](server/services/trust.service.ts), [server/services/review.service.ts](server/services/review.service.ts), [app/u/[id]/page.tsx](app/u/%5Bid%5D/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](app/trips/%5Bid%5D/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](app/trips/%5Bid%5D/page.tsx) | | 1.3 | Hint deskriptif + placeholder lebih konkret di field itinerary form create-trip | ✅ | [features/trip/components/create-trip-form.tsx](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). Pakai `endDate < now()` (fallback `date < now()`) AND `status != CLOSED`. - `tripsCancelled` = `Trip.status = CLOSED` (organizer batalkan trip eksplisit). - `completionRate` butuh sample ≥ 3 (`COMPLETION_RATE_MIN_SAMPLE` di [lib/trust.ts](lib/trust.ts)) supaya tidak menyesatkan organizer baru. - Rating breakdown di-render sebagai bar chart kecil (visual cue lebih kuat dari angka mentah). - `OrganizerStatsPanel` di profil publik tidak di-render untuk user yang murni peserta — query Prisma juga di-skip kalau `organizedTrips.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](server/services/trust.service.ts) | | 2.2 | `tripsCompleted` di-derive dari `endDate < now()` (fallback `date < now()`) AND `status != CLOSED` | ✅ | [server/services/trust.service.ts](server/services/trust.service.ts) | | 2.3 | `totalParticipantsServed` = count `TripParticipant CONFIRMED` di trip yang sudah lewat & tidak dibatalkan | ✅ | [server/services/trust.service.ts](server/services/trust.service.ts) | | 2.4 | `completionRate` = `tripsCompleted / (tripsCompleted + tripsCancelled)`. Null bila sample < 3 | ✅ | [server/services/trust.service.ts](server/services/trust.service.ts), [lib/trust.ts](lib/trust.ts) | | 2.5 | `ratingBreakdown` via `prisma.tripReview.groupBy({ by: ['rating'] })` | ✅ | [server/services/trust.service.ts](server/services/trust.service.ts) | | 2.6 | Komponen `OrganizerStatsPanel` (badges + 4 stat box + bar chart breakdown) | ✅ | [features/profile/components/organizer-stats-panel.tsx](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](features/trip/components/organizer-trust-panel.tsx) | | 2.8 | Render `OrganizerStatsPanel` di `/u/[id]` (skip query untuk non-organizer) | ✅ | [app/u/[id]/page.tsx](app/u/%5Bid%5D/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 `OrganizerReviewItem` di-extract dari `Awaited>[number]` supaya schema repo = sumber kebenaran tanpa duplikasi tipe. - Fetch trust + reviews via single `Promise.all` di `/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](server/services/review.service.ts) | | 3.2 | Repo helper `findByOrganizer` (default limit 20, urut newest, include user + trip) | ✅ | [server/repositories/review.repo.ts](server/repositories/review.repo.ts) | | 3.3 | Komponen `OrganizerReviewsList` (avatar + name + bintang + trip link + tanggal + comment) | ✅ | [features/review/components/organizer-reviews-list.tsx](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](app/u/%5Bid%5D/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 dengan `endDate < cutoff` di-flip; trip yang berakhir *hari ini* belum. - Hanya trip dengan status `OPEN` atau `FULL` yang di-flip. `CLOSED` tidak disentuh (organizer eksplisit membatalkan; tetap dibedakan dari COMPLETED untuk perhitungan `tripsCancelled` di trust panel). - Idempotent: dua kali run di hari yang sama, run kedua match 0 row. - Endpoint diproteksi via `Authorization: Bearer ${CRON_SECRET}`. Kalau `CRON_SECRET` tidak 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. - **`trustService` tetap pakai computed-from-`endDate`** (tidak diganti ke `status = 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](server/repositories/trip.repo.ts) | | 4.2 | Service `tripService.autoCompletePastTrips()` | ✅ | [server/services/trip.service.ts](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](app/api/cron/auto-complete-trips/route.ts) | | 4.4 | Schedule `0 18 * * *` di Vercel Cron | ✅ | [vercel.json](vercel.json) | **Tindakan manual:** 1. Set env `CRON_SECRET` di hosting (random ≥32 char). Generate cepat: `openssl rand -hex 32`. 2. 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.json` bisa dihapus. --- ## ❌ Anti-list (yang harus DITOLAK kalau muncul) - **Model `OrganizerReview` terpisah** — `TripReview` sudah 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](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): 1. **PR-1** — Trip detail polish. Cepat, low-risk, no migration. **Mulai dari sini.** 2. **PR-2** — Trust aggregates di service + UI. Read-only, no migration. 3. **PR-3** — Reviews list per organizer di service + UI. Read-only, no migration. 4. **PR-4** — Diskusi opsi completion mechanism (atau skip kalau opsi C dipilih). **Pertanyaan terbuka sebelum PR-2:** 1. Apakah `tripsCompleted` count termasuk trip dengan `status = CLOSED` yang `endDate < now()`? Saran: tidak — CLOSED = dibatalkan, dipisah ke `tripsCancelled`. 2. Threshold minimum supaya `completionRate` ditampilkan? Saran: min 3 trip selesai supaya angka tidak menyesatkan (1 trip dibatalkan dari 1 trip = 0% looks bad untuk organizer baru). 3. Tampilkan rating breakdown sebagai bar chart atau hanya angka? Saran: bar chart kecil — visual cue lebih kuat untuk credibility.