add payment, trust badge, handle race condition, fix booking schema
This commit is contained in:
@@ -9,43 +9,93 @@ Stack: [Next.js](https://nextjs.org) (App Router), NextAuth, Prisma (PostgreSQL)
|
||||
### 1. Autentikasi
|
||||
|
||||
- Pengguna baru mendaftar di `/register` (nama, email, password disimpan di database).
|
||||
- Login di `/login` melalui NextAuth; sesi dipakai di server action dan di halaman client (misalnya navbar, form buat trip).
|
||||
- Login di `/login` melalui NextAuth; sesi dipakai di server action dan di halaman client (navbar, form buat trip, join).
|
||||
|
||||
Tanpa login, pengguna tetap bisa melihat daftar trip dan detail trip, tetapi tidak bisa membuat trip atau join.
|
||||
|
||||
### 2. Organizer: membuat trip
|
||||
|
||||
1. Setelah login, organizer membuka **Buat Trip** (`/create-trip`) dari navbar, halaman `/trips`, beranda, atau tombol mengambang (+).
|
||||
2. Halaman form (`app/create-trip/page.tsx`) memvalidasi sesi di client; jika belum login, ditampilkan ajakan login.
|
||||
3. Organizer mengisi judul, gunung, lokasi, deskripsi (opsional), rentang tanggal (DatePicker), maks peserta, harga (format Rupiah), dan URL gambar opsional (`ImageUrlInput`).
|
||||
4. Submit memanggil server action `createTripAction` (`features/trip/actions.ts`):
|
||||
- Memastikan ada sesi.
|
||||
- Mem-parse dan memvalidasi input dengan Zod (`features/trip/schemas.ts`).
|
||||
- `tripService.createTrip` menulis trip baru ke database lewat `tripRepo.create`, menghubungkan `organizerId` ke user yang login, dan menyimpan gambar jika ada.
|
||||
5. Trip baru berstatus **OPEN** (default schema), lalu pengguna diarahkan ke detail trip `/trips/[id]`.
|
||||
1. Setelah login, organizer membuka **Buat Trip** (`/create-trip`) dari navbar, `/trips`, beranda, atau tombol (+).
|
||||
2. Form memvalidasi sesi; jika belum login, ditampilkan ajakan login.
|
||||
3. Organizer mengisi judul, gunung, lokasi, deskripsi (opsional), **meeting point**, **itinerary**, **termasuk / tidak termasuk** (opsional), rentang tanggal berangkat–pulang, maks peserta, harga (Rupiah), dan URL gambar opsional.
|
||||
4. Submit → `createTripAction` → validasi Zod → `tripService.createTrip` menulis `Trip` ke database (status default **OPEN**) beserta gambar jika ada.
|
||||
5. Pengguna diarahkan ke detail trip `/trips/[id]`.
|
||||
|
||||
Organizer **tidak** bisa join trip sendiri; di detail trip tombol join diganti pesan bahwa user adalah organizer.
|
||||
Organizer **tidak** bisa join trip sendiri; di detail trip ditampilkan bahwa dia adalah organizer trip ini.
|
||||
|
||||
### 3. Peserta: mencari trip dan join
|
||||
### 3. Peserta: mencari trip
|
||||
|
||||
1. **Beranda** (`/`) dan **Open Trip** (`/trips`) menampilkan trip dengan status **OPEN** dan tanggal berangkat tidak di masa lalu (`tripService.getOpenTrips` + filter di repository).
|
||||
2. Filter pencarian (`TripFilter`) mengirim query string; daftar trip disaring di server.
|
||||
3. Dari kartu trip (`TripCard`), pengguna membuka **detail** `/trips/[id]` (`app/trips/[id]/page.tsx`):
|
||||
- Peserta aktif = baris `TripParticipant` yang statusnya bukan `CANCELLED`.
|
||||
- Slot tersisa dan progress bar memakai jumlah peserta aktif tersebut.
|
||||
4. **Join** (`JoinTripButton` + `joinTripAction`):
|
||||
- Jika belum login: tautan ke `/login`.
|
||||
- Jika trip bukan `OPEN` dan user belum join: pendaftaran ditutup (kecuali user sudah terdaftar dan ingin membatalkan, mengikuti logika UI).
|
||||
- `tripService.joinTrip` memeriksa: trip ada, status `OPEN`, bukan organizer, belum terdaftar aktif, kapasitas belum penuh; lalu menambah atau mengaktifkan kembali partisipasi (lihat bagian perbaikan bug di bawah).
|
||||
5. Jika jumlah peserta aktif mencapai `maxParticipants`, status trip diperbarui menjadi **FULL**.
|
||||
6. **Batal ikut** memanggil `cancelJoinAction` → `tripService.cancelJoin`: partisipasi ditandai `CANCELLED`; jika trip sebelumnya `FULL` dan setelah batal slot kosong lagi, status dikembalikan ke **OPEN**.
|
||||
1. **Beranda** (`/`) dan **Open Trip** (`/trips`) menampilkan trip **OPEN** dengan tanggal berangkat yang masih relevan (`tripService.getOpenTrips`).
|
||||
2. Filter pencarian (`TripFilter`) mengirim query string (`q`, `from`, `to`); penyaringan dilakukan di server.
|
||||
3. Dari **TripCard**, pengguna membuka **detail** `/trips/[id]`: melihat info trip, kuota, panel kepercayaan organizer (jumlah trip, rating, badge jika ada), daftar peserta yang sudah disetujui, dan ulasan.
|
||||
|
||||
### 4. Ringkasan peran data
|
||||
---
|
||||
|
||||
### 4. Alur lengkap: dari join hingga pembayaran sukses
|
||||
|
||||
Alur ini menggambarkan satu peserta dari pertama kali mendaftar sampai pembayaran dianggap selesai di aplikasi (pembayaran **manual** / transfer di luar gateway).
|
||||
|
||||
```text
|
||||
[Peserta] [Sistem] [Organizer]
|
||||
| | |
|
||||
|-- buka detail trip ------>| |
|
||||
| | |
|
||||
|-- "Join Trip" ------------>| status partisipasi: PENDING |
|
||||
| | (mengisi slot kuota trip) |
|
||||
|<-- menunggu persetujuan ---| |
|
||||
| |<-- lihat "Permintaan join" ------|
|
||||
| | |
|
||||
| |<-- "Setujui" atau "Tolak" -------|
|
||||
| | Setujui -> CONFIRMED |
|
||||
| | Tolak -> CANCELLED |
|
||||
|<-- terkonfirmasi ikut -----| (jika disetujui) |
|
||||
| | |
|
||||
|-- transfer uang (WA/rek) --| (di luar app) |
|
||||
| | |
|
||||
|-- "Saya sudah bayar" ----->| markedPaidAt = sekarang |
|
||||
|<-- menunggu konfirmasi ----| |
|
||||
| | |
|
||||
| |<-- "Konfirmasi pembayaran" ------|
|
||||
| | paymentConfirmedAt = sekarang |
|
||||
|<-- pembayaran dikonfirmasi-| |
|
||||
```
|
||||
|
||||
**Langkah per langkah**
|
||||
|
||||
1. **Join trip**
|
||||
Di halaman detail, peserta login menekan **Join Trip Sekarang** → `joinTripAction` → `tripService.joinTrip`.
|
||||
Dibuat atau diaktifkan kembali baris `TripParticipant` dengan status **`PENDING`**. Kuota trip (peserta aktif: PENDING + CONFIRMED) bertambah; trip bisa menjadi **FULL** jika slot habis.
|
||||
|
||||
2. **Menunggu organizer**
|
||||
Peserta melihat status bahwa permintaan **menunggu persetujuan organizer**. Dia bisa **Batal ikut** selama tanggal berangkat belum lewat (status menjadi `CANCELLED`, slot bisa longgar lagi).
|
||||
|
||||
3. **Organizer menyetujui atau menolak**
|
||||
Di blok **Permintaan join**, organizer menekan **Setujui** → status partisipasi menjadi **`CONFIRMED`**, atau **Tolak** → **`CANCELLED`**.
|
||||
Jika ada peserta yang sudah menandai bayar sebelum disetujui, penolakan juga membersihkan data tandai bayar pada baris itu.
|
||||
|
||||
4. **Peserta terkonfirmasi**
|
||||
Setelah **CONFIRMED**, peserta dianggap bagian dari daftar “peserta terkonfirmasi” di halaman trip. Fitur ulasan trip setelah selesai hanya relevan untuk partisipasi terkonfirmasi (sesuai aturan di service).
|
||||
|
||||
5. **Menandai sudah membayar**
|
||||
Peserta mentransfer sesuai instruksi organizer (di luar app). Di app dia menekan **Saya sudah bayar** → kolom **`markedPaidAt`** diisi. Tombol ini tidak muncul jika pembayaran sudah dikonfirmasi atau tanggal trip sudah lewat.
|
||||
Aksi ini **atomik** (aman dari double klik).
|
||||
|
||||
6. **Organizer mengonfirmasi pembayaran**
|
||||
Di blok **Konfirmasi pembayaran**, organizer mengecek mutasi/bukti lalu menekan **Konfirmasi pembayaran** untuk peserta bersangkutan → kolom **`paymentConfirmedAt`** diisi.
|
||||
Ini **idempoten** (konfirmasi ganda tidak mengubah hasil akhir yang salah).
|
||||
|
||||
7. **Selesai (sukses payment di app)**
|
||||
Peserta melihat bahwa pembayaran **sudah dikonfirmasi organizer**. Di sini alur “commit” peserta + uang dalam konteks SeTrip dianggap lengkap dari sisi data aplikasi.
|
||||
|
||||
**Catatan produk:** tidak ada payment gateway; bukti transfer dan nominal final tetap komunikasi organizer–peserta (misalnya WA).
|
||||
|
||||
### 5. Ringkasan peran data
|
||||
|
||||
| Konsep | Penyimpanan |
|
||||
|--------|-------------|
|
||||
| Trip | Model `Trip` (judul, gunung, lokasi, tanggal, kuota, harga, status, relasi ke organizer) |
|
||||
| Peserta | `TripParticipant` unik per `(tripId, userId)` dengan status `CONFIRMED` / `CANCELLED` (default schema juga mengenal `PENDING`; alur UI saat ini memakai `CONFIRMED` saat join) |
|
||||
| Trip | `Trip`: judul, gunung, lokasi, tanggal, kuota, harga, status trip (`OPEN` / `FULL` / …), meeting point, itinerary, termasuk/tidak termasuk, relasi ke organizer |
|
||||
| Peserta | `TripParticipant` unik per `(tripId, userId)`: status **`PENDING`** / **`CONFIRMED`** / **`CANCELLED`**, serta **`markedPaidAt`** & **`paymentConfirmedAt`** untuk alur bayar manual |
|
||||
| Organizer (kepercayaan) | `User.isVerified`; agregat rating & jumlah trip dibuat dihitung dari data ulasan & trip |
|
||||
|
||||
## Menjalankan secara lokal
|
||||
|
||||
@@ -63,13 +113,13 @@ Buka [http://localhost:3000](http://localhost:3000).
|
||||
## Perbaikan bug (yang relevan dengan join & listing)
|
||||
|
||||
1. **Join lagi setelah “Batal ikut”**
|
||||
Satu user hanya boleh satu baris partisipasi per trip (`@@unique([tripId, userId])`). Kode lama mencoba `create` lagi setelah status `CANCELLED`, sehingga bisa gagal dengan pelanggaran unik. Sekarang jika sudah ada baris `CANCELLED`, partisipasi **diaktifkan kembali** (`CONFIRMED`) lewat `participantRepo.reactivate`, bukan insert baru.
|
||||
Satu user hanya satu baris per trip (`@@unique([tripId, userId])`). Jika baris sudah `CANCELLED`, join berikutnya **mengaktifkan kembali** partisipasi ke **`PENDING`** (bukan insert baru).
|
||||
|
||||
2. **Jumlah peserta di kartu / daftar**
|
||||
`_count.participants` di query listing sebelumnya menghitung semua baris termasuk yang `CANCELLED`, sehingga “slot tersisa” di `TripCard` bisa salah. Count sekarang hanya menghitung peserta dengan status **bukan** `CANCELLED`.
|
||||
`_count.participants` hanya menghitung status **bukan** `CANCELLED`, agar slot dan label “penuh” konsisten.
|
||||
|
||||
3. **Segar halaman setelah join/batal/buat trip**
|
||||
Setelah aksi trip, cache halaman `/trips` dan `/` ikut di-`revalidatePath` agar jumlah slot dan daftar di beranda konsisten tanpa harus refresh manual.
|
||||
3. **Segar halaman setelah aksi**
|
||||
`revalidatePath` dipanggil setelah join, batal, buat trip, setujui/tolak peserta, dan konfirmasi pembayaran agar daftar dan detail konsisten.
|
||||
|
||||
## Learn More
|
||||
|
||||
|
||||
Reference in New Issue
Block a user