# Hal yang harus dilakukan di Production
Catatan langkah produksi per update. Yang terbaru di atas.

---

## [2026-06-17] WhatsApp Verification: Deep-Link Flow (wa.me + Webhook Confirmation)

**Ringkasan**
- Mengganti flow lama (sistem kirim OTP) dengan flow baru: user klik tombol → deep-link wa.me dengan template + nonce unik → user kirim pesan → webhook verifikasi + tandai `whatsapp_verified_at`.
- Sudah dipastikan: user yang sudah verified (atau admin) yang kembali ke `/verify-whatsapp` langsung di-redirect otomatis ke destinasi berikutnya (invitation/onboarding/dashboard) via `/api/v1/me`.
- File baru: migration + model + factory untuk `whatsapp_verification_intents`.
- Endpoint baru: `POST /api/v1/auth/whatsapp/init` (auth:sanctum) → return `wa_link` + `message_template`.
- Webhook (`/api/v1/webhook/whatsapp`) sekarang mengenali `MANABOU-VERIF-<nonce>`, bind ke intent + user, set verified, balas link balik.
- Keamanan: dukungan `FONNTE_WEBHOOK_SECRET` (query `?token=...` atau header `X-Webhook-Token`).
- Frontend: halaman verify-whatsapp sepenuhnya diganti ke tombol deep-link + panel "menunggu konfirmasi" + "Cek Status". Semua OTP grid/resend/countdown dihapus.
- Backward: route lama OTP tetap ada (untuk transisi); halaman baru tidak menggunakannya lagi.

**WAJIB dijalankan di terminal produksi (backend):**

```bash
php artisan migrate --force
php artisan config:clear
php artisan route:clear
```

**Environment baru (opsional tapi sangat disarankan untuk production):**

```env
FONNTE_WEBHOOK_SECRET=rahasia-acak-panjang
FONNTE_BUSINESS_NUMBER=6281234567890   # tanpa tanda +
```

> Setelah deploy, daftarkan webhook URL di Fonnte dashboard dengan `?token=...` (atau header) jika secret di-set.  
> Nomor bisnis (`FONNTE_BUSINESS_NUMBER`) adalah nomor WhatsApp resmi yang user akan kirim pesan verifikasinya.

Tidak ada seeder baru. Tidak ada permission baru.

**Testing yang sudah ditambahkan (Phase 2):**
- `WhatsappDeepLinkVerificationTest` (8 test cases, 36 assertions) mencakup:
  - Inisiasi deep-link oleh user terautentikasi + pembuatan intent + wa.me link.
  - Penggantian intent sebelumnya (hanya 1 aktif).
  - Webhook sukses: verifikasi user + consume intent + balasan WA.
  - Penolakan: nonce tidak dikenal, pengirim mismatch, intent kadaluarsa.
  - Penegakan secret webhook (`FONNTE_WEBHOOK_SECRET`) via query `?token=` dan header `X-Webhook-Token`.
  - Keberadaan route legacy OTP tetap ada (untuk transisi).

**UX registration → WhatsApp (Phase 3 – zero flash):**
- Register sekarang mengembalikan `whatsapp_verification` (wa_link + template + masked phone + expiry) **di samping** legacy OTP fields.
- Frontend register selalu set `pending_whatsapp`. Jika payload baru ada, juga simpan sebagai `pending_whatsapp_verification` di sessionStorage.
- Verify page menggunakan synchronous initial state dari sessionStorage → langsung render dalam mode "menunggu WhatsApp" dengan data yang benar (tidak ada flash form nomor telepon sama sekali untuk happy path registrasi).
- Efek khusus hanya untuk side-effect (buka wa.me sekali, consume transient, fallback INIT jika perlu).
- Setelah user kirim pesan dan kembali (atau webhook proses), `/me` check otomatis mengalihkan ke destinasi yang benar.
- Fallback tetap berfungsi untuk response registrasi lama.

Setelah deploy, jalankan:
```bash
php artisan test --filter=WhatsappDeepLinkVerificationTest
```
Semua harus hijau (saat ini 9 test / 53 assertions).

---

**Panduan transisi & penghapusan OTP lama (setelah periode transisi):**
- Frontend sekarang 100% menggunakan flow baru (INIT + wa.me + webhook). Legacy `send`/`verify`/`resend` tidak dipanggil lagi dari UI verifikasi.
- Route lama sengaja dibiarkan agar tidak memecah integrasi lain (jika ada) dan untuk rollback cepat.
- Ketika yakin (biasanya setelah 1-2 minggu atau setelah semua user sudah verified via flow baru):
  1. Hapus method `send`/`verify`/`resend` + helper lama di `WhatsappVerificationController`.
  2. Hapus route lama di `routes/api.php`.
  3. Drop table `whatsapp_otps` (buat migration `drop_whatsapp_otps_table`).
  4. Hapus model `WhatsappOtp` + factory jika ada.
  5. Update dokumentasi dan `must-do-in-production` untuk rilis berikutnya.
- Selama masa transisi, route lama tetap aman dan tidak mengganggu flow baru.

---

## [2026-06-16] JLPT Reading (読解) Feature Improvement — Phases 0-4

**Ringkasan**
- Roadmap "reading_material" objective sekarang mengarah ke practice berjenjang level di `/reading/{level}` (bukan PDF Irodori generik).
- Bank konten diperluas: `ReadingAssessmentSeeder` menjamin ≥10 passages + ≥30 questions per level (N5–N1). Data sintetik — **tidak butuh OpenAI key**.
- Mode latihan membaca khusus (`/reading/{level}`) dengan feedback langsung, sub-skill tags, penjelasan, toggle "Ujian Murni", dan riwayat attempt.
- Auto-complete objective roadmap saat skor practice ≥ 50% (`PASS_COMPONENT_PCT`).
- Review gate sumber bacaan (`reviewed_at` pada `reading_passages`) sebelum item bisa dipakai di assessment batch.
- Pertanyaan & pilihan dalam bahasa Jepang untuk N3+ (N5 tetap Indonesia).
- Saran kelemahan membaca muncul di `nextAction` Roadmap.
- Tabel/kolom baru: `reading_practice_attempts`, `text_type`/`reviewed_at`/`is_calibrated`/`is_active` pada passages, `sub_skill_tags`/`explanation` pada questions.

**WAJIB dijalankan di terminal produksi (backend):**

```bash
php artisan migrate --force
php artisan db:seed --class=ReadingAssessmentSeeder --force
php artisan config:clear
php artisan route:clear
```

> `ReadingAssessmentSeeder` juga dipanggil dari `DatabaseSeeder` (aman diulang). Setelah ini, halaman `/reading/N5` (dan level lain) punya konten dan Roadmap langsung ke sana.  
> Admin bisa tambah/generate konten via UI `/admin/assessments` (tombol "Generate Reading Source") atau CLI:  
> `php artisan nihongo:generate-reading --level=N5 --passages=10 --questions=3` (butuh `OPENAI_API_KEY`).

Tidak ada permission baru untuk fitur latihan membaca. Admin curation pakai `assessments.manage` yang sudah ada.

---

## [2026-06-10] Asesmen: item-bank Fase 5 (cooldown) + Fase 6 (statistik + migrasi)

**Ringkasan**
- Cooldown 1 jam setelah gagal asesmen (configurable via `ASSESSMENT_FAIL_COOLDOWN_MINUTES`).
- Statistik per item (`times_served`, `correct_count`) + command import data lama
  ke bank item.

**WAJIB dijalankan di terminal produksi (backend):**

```bash
php artisan migrate --force   # +times_served, +correct_count di assessment_items
```

**Opsional — import listening/reading lama ke bank item (idempoten):**

```bash
php artisan nihongo:migrate-assessment-bank --dry-run   # cek dulu
php artisan nihongo:migrate-assessment-bank             # import
```

**Opsional — atur cooldown** (default 60 menit; set 0 untuk nonaktif):

```
# .env
ASSESSMENT_FAIL_COOLDOWN_MINUTES=60
```

---

## [2026-06-10] Asesmen: item-bank Fase 4 (UI Admin)

**Ringkasan**
- Halaman admin `/admin/assessments`: generate soal, review/edit/regenerate/
  nonaktifkan item, rakit & aktifkan/arsipkan batch. Permission baru `assessments.manage`.

**WAJIB dijalankan di terminal produksi (backend):**

```bash
php artisan db:seed --class=RolePermissionSeeder --force   # tambah permission assessments.manage
php artisan config:clear
php artisan route:clear
```

> Tidak ada migrasi baru di fase ini. Setelah ini, admin (content-manager / super-admin)
> bisa membuka /admin/assessments untuk menyiapkan batch tiap level (lihat catatan Fase 3:
> level perlu batch aktif agar asesmennya bisa diambil user).

---

## [2026-06-10] Asesmen: item-bank Fase 3 (alur ujian pindah ke batch)

**Ringkasan**
- Alur ujian final kini **disajikan dari batch aktif** (rotasi anti-hafalan + acak
  urutan per section + snapshot). Kolom baru `is_retake` di `assessment_attempts`.
- **PENTING:** sebuah level kini **tidak menyajikan ujian** sampai ada **batch aktif**.
  Tanpa batch aktif, frontend menampilkan "Asesmen belum siap". Batch dibuat &
  diaktifkan lewat UI admin (Fase 4) atau service generator.

**WAJIB dijalankan di terminal produksi (backend):**

```bash
php artisan migrate --force
```

**Agar asesmen tiap level bisa diambil, admin harus menyiapkan batch:**
- Generate item per komponen, review, buildBatch, lalu **activate** (UI admin Fase 4,
  atau tinker/command bila belum ada UI). Tanpa batch aktif, asesmen level itu "belum
  siap" (tidak memblok belajar — hanya gerbang kelulusan level).

---

## [2026-06-10] Asesmen: item-bank Fase 1 (skema data layer)

**Ringkasan**
- Skema item bank: tabel `assessment_items`, `assessment_batches`,
  `assessment_batch_items` (untuk asesmen final berbasis batch yang dikurasi admin).
- Hanya menambah tabel — **belum mengubah alur ujian** (masih memakai snapshot Fase 0).
  Tidak ada perubahan permission/seeder/config.

**WAJIB dijalankan di terminal produksi (backend):**

```bash
php artisan migrate --force
```

> Tidak ada langkah lain (tanpa seeder/permission baru). Tabel masih kosong sampai
> Fase 2 (generator admin) dibangun.

---

## [2026-06-10] Asesmen Final: Tahap 5 (Listening) + redesign Fase 0 (snapshot per attempt)

**Ringkasan perubahan**
- Komponen **Menyimak (聴解)** untuk asesmen final: tabel `listening_items` (audio +
  transkrip + soal, dikelompokkan per `batch`, klip instruksi terdeteksi otomatis).
  Soal listening dibuat dari audio asli (Whisper transkrip → GPT soal JLPT, semua
  bahasa Jepang).
- Soal vocab/kanji/grammar diubah ke **pola JLPT bahasa Jepang**.
- **Snapshot per attempt (Fase 0 item-bank plan):** soal yang disajikan dibekukan ke
  `assessment_attempts` (kolom `snapshot`, `batch_id`, `status`); penilaian memakai
  snapshot, bukan data live. Menutup race build↔grade + submit buta = skor 0.
- Halaman admin **Listening** (`/admin/listening`) + permission baru `listening.manage`.

**WAJIB dijalankan di terminal produksi (backend):**

```bash
# 1. Tarik kode terbaru lalu pasang dependency (jika ada perubahan)
composer install --no-dev --optimize-autoloader

# 2. Jalankan migrasi baru
#    Membuat: card_flags, listening_items; menghapus kolom kanji_alt (cards),
#    batch/is_instruction (listening_items), snapshot/batch_id/status (assessment_attempts)
php artisan migrate --force

# 3. Re-seed permission (menambah permission 'listening.manage' ke content-manager)
#    Idempoten/aman diulang.
php artisan db:seed --class=RolePermissionSeeder --force

# 4. Bersihkan cache config/route (sidebar & route admin listening baru)
php artisan config:clear
php artisan route:clear
```

**Mengisi soal Listening (opsional, butuh OpenAI key + file audio):**

Listening TIDAK terisi otomatis tanpa OpenAI key. File audio harus ada di
`backend/public/audio/{level}/*.mp3` (mis. `public/audio/n5/001.mp3`). Pool & jumlah
soal per batch diatur di `config/listening.php`.

```bash
# Pastikan OPENAI_API_KEY ada di .env produksi.
# Generate 1 batch N5 (default 5 soal/batch; instruksi terdeteksi otomatis, tak dihitung):
php artisan nihongo:generate-listening --level=N5 --batch-size=5

# Tambah lebih banyak batch (rotasi anti-hafalan):
php artisan nihongo:generate-listening --level=N5 --batches=3

# (alternatif) ikut DatabaseSeeder — aman tanpa key (otomatis di-skip bila key kosong):
# php artisan db:seed --class=ListeningAssessmentSeeder --force
```

> Catatan: bila OpenAI key belum disiapkan, lewati langkah generate listening.
> Komponen lain (vocab/kanji/grammar/reading) tetap berfungsi tanpa listening.

**Catatan perilaku baru (tidak perlu aksi, hanya untuk diketahui):**
- Submit asesmen **tanpa membuka ujian lebih dulu** (tanpa `GET /assessments/{level}`)
  kini menghasilkan skor 0 — sengaja, untuk mencegah brute-force.

---

## [template — salin untuk update berikutnya]

```
## [YYYY-MM-DD] Judul update

Ringkasan singkat perubahan.

WAJIB dijalankan di terminal produksi:
1. ...
```

---

**Catatan perilaku baru (tidak perlu aksi, hanya untuk diketahui):**
- Submit practice tanpa data cukup → error 422 (validasi). Submit tanpa buka dulu (`GET /reading/{level}`) tidak dicegah (bukan asesmen final).
- Roadmap auto-complete hanya terjadi di `submit` path ketika `passed_component === true` (≥ 50%).
- Tanpa seeder, halaman practice akan tampil "Belum ada teks bacaan untuk level ini. Admin bisa generate via panel admin." (pesan UI).
- Bank sintetik seeder aman untuk production (teks pendek, level-appropriate, tidak sensitif).
