add cron and partial update refund schema

This commit is contained in:
arifal
2026-05-10 22:27:21 +07:00
parent 9a163c4f13
commit 744ee3446b
7 changed files with 466 additions and 17 deletions
+144
View File
@@ -0,0 +1,144 @@
# 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<ReturnType<typeof reviewRepo.findByOrganizer>>[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.