# PRD — Papan Apresiasi Kelas dan Integrasi XP Penghargaan

- **Status:** Implementasi source selesai sampai integrasi XP; browser QA siswa selesai, QA role lain dan rollout production belum dilakukan
- **Versi:** 1.0
- **Tanggal:** 26 Agustus 2026
- **Produk:** UtiVerse — UtiClass / Homeroom Workspace
- **Nama fitur:** Papan Apresiasi Kelas, Pencapaian Saya, dan XP Penghargaan
- **Dokumen induk:** `PRD-PEMBINAAN-APRESIASI-PRESTASI.md`

---

## 0. Cara menggunakan PRD ini

Dokumen ini adalah kontrak implementasi lanjutan untuk modul Apresiasi & Prestasi yang sudah berjalan. Agent/IDE yang melanjutkan pekerjaan wajib:

1. Membaca dokumen ini dan `PRD-PEMBINAAN-APRESIASI-PRESTASI.md` sampai selesai sebelum mengubah kode.
2. Memeriksa `git status`, branch, migration, route, model, service, halaman, test, dan data aktual karena repository dapat berubah setelah PRD dibuat.
3. Mengerjakan fase secara berurutan. Jangan memulai fase berikutnya sebelum Definition of Done fase aktif terpenuhi dan checkpoint ditulis.
4. Tidak mengedit migration lama yang sudah pernah dijalankan. Semua perubahan schema harus memakai migration baru yang additive dan dapat di-rollback.
5. Mempertahankan seluruh `student_recognitions` existing. Record `internal` dan `student` tidak boleh otomatis menjadi publik untuk kelas.
6. Menjaga Catatan Pelanggaran dan Pembinaan tetap berada di domainnya sendiri. Tidak ada catatan negatif, pembinaan personal, bukti, atau informasi sensitif yang boleh masuk Papan Apresiasi Kelas.
7. Memakai `HomeroomAccessService`, `PeriodContext`, `StudentEnrollment`, dan otorisasi backend untuk isolasi kelas/periode. Menu frontend bukan bukti otorisasi.
8. Menyusun response Papan Kelas menggunakan allow-list field publik. Jangan mengirim model `StudentRecognition::toArray()` ke teman sekelas.
9. Menjalankan verification commands pada fase aktif dan mencatat hasilnya di checkpoint.
10. Mencentang checklist hanya setelah implementasi dan bukti verifikasinya selesai. Kode yang baru ditulis tetapi belum diuji tetap `[ ]`.
11. Tidak mengaktifkan XP penghargaan sebelum Phase 4 selesai, migration aman, idempotensi terbukti, dan feature flag dinyalakan secara eksplisit.
12. Tidak melakukan commit, push, deployment, migration production, atau aktivasi feature flag kecuali diminta secara eksplisit.

### Status implementasi

- [x] Keputusan produk mode hibrida disetujui.
- [x] PRD v1 disusun berdasarkan audit kode aktual.
- [x] Phase 0 — Baseline, kontrak keamanan, dan test safety net.
- [x] Phase 1 — Schema publikasi kelas dan kompatibilitas data lama.
- [ ] Phase 2 — Backend publikasi kelas, feed, dan otorisasi periode.
- [ ] Phase 3 — UI compact Papan Kelas, Pencapaian Saya, dan widget Beranda.
- [ ] Phase 4 — Ledger XP penghargaan, idempotensi, dan feature flag (source/test selesai; uji concurrency nyata belum dilakukan, default off).
- [ ] Phase 5 — Hardening, QA, dokumentasi, dan rollout.

**Checkpoint terakhir:** 26 Agustus 2026 — source mode hibrida, publikasi kelas aman, UI compact, widget Beranda, dan ledger XP telah diimplementasikan. Full suite 135 test/1.600 assertion, typecheck, build, scoped Pint, route check, migration rollback/rerun, dan diff check lulus. Browser QA siswa lulus pada desktop 1536×703, mobile 320×568 dan 390×844, serta tema gelap/terang tanpa horizontal overflow. QA UI wali/admin dan data feed kelas berisi belum dilakukan. XP tetap off; belum ada commit, push, deploy, migration production, atau aktivasi XP.

### Format checkpoint wajib setelah setiap fase

```text
Tanggal/waktu:
Branch dan HEAD:
Fase aktif:
Checklist yang baru selesai:
File yang diubah:
Migration/data command yang dijalankan:
Feature flag:
Test lulus:
Test belum dijalankan:
QA manual/browser:
Risiko/temuan tersisa:
Langkah berikutnya:
```

---

## 1. Ringkasan keputusan produk

UtiClass memakai **mode hibrida** dengan dua ruang yang berbeda:

Dokumen ini merevisi hanya dua batas pada PRD induk: publikasi yang semula hanya kepada pemilik diperluas dengan publikasi kelas yang aman, dan XP recognition diperbolehkan pada fase terpisah. Larangan ranking publik tetap berlaku. Seluruh batas Pelanggaran, Pembinaan, legacy, DOCX, kelas/periode, dan audit pada PRD induk tetap berlaku.

1. **Papan Apresiasi Kelas** adalah ruang bersama untuk merayakan apresiasi dan prestasi yang sengaja diterbitkan wali kelas kepada seluruh anggota kelas pada periode yang sah.
2. **Pencapaian Saya** adalah portofolio pribadi siswa. Halaman ini tetap memuat semua Apresiasi/Prestasi miliknya yang dipublikasikan kepada pemilik, termasuk yang juga muncul di Papan Kelas.
3. **Catatan sumber dan publikasi kelas dipisahkan.** `student_recognitions` tetap menjadi rekam penghargaan individual. Publikasi kelas memakai entitas publikasi khusus dengan ringkasan aman untuk teman sekelas.
4. **Papan Kelas bukan ranking.** Tampilannya boleh terasa seperti celebration board/leaderboard, tetapi urutannya kronologis, bukan berdasarkan jumlah penghargaan, XP, atau posisi siswa.
5. **Tidak ada daftar siswa terbawah atau siswa yang belum mendapat penghargaan di portal siswa.** Informasi pemerataan hanya boleh menjadi insight internal wali kelas/admin.
6. **Penghargaan kelompok tampil satu kali.** Satu publikasi dapat mempunyai beberapa penerima tanpa membuat kartu identik berulang.
7. **XP diberikan kepada pemilik recognition, bukan kepada publikasi kelas.** Scope `Pribadi` dan `Kelas` memperoleh XP yang sama sehingga wali tidak terdorong membuka data ke kelas hanya demi poin.
8. **XP tidak tampil pada kartu publik kelas.** Nominal dan riwayat XP hanya terlihat oleh siswa penerima dan pihak berwenang.
9. **Tidak ada komentar bebas, like, voting, atau peer nomination pada versi ini.** Fokusnya pengakuan resmi wali kelas dengan risiko moderasi minimal.

### Keputusan terkait istilah “leaderboard”

Fitur ini menggunakan nama **Papan Apresiasi Kelas**, bukan Leaderboard Penghargaan. Alasan produk:

- leaderboard memberi kesan anak diurutkan dari terbaik hingga terendah;
- jumlah kesempatan lomba dan kondisi setiap siswa tidak sama;
- apresiasi karakter mudah berubah menjadi kegiatan berburu poin jika dihitung secara publik;
- tujuan utama adalah memberi teladan dan pengakuan, bukan memenangkan kompetisi popularitas.

Elemen visual yang tetap dapat mengambil semangat leaderboard:

- sorotan terbaru;
- kartu prestasi dengan treatment visual khusus;
- ringkasan kelas tanpa nama/ranking, misalnya jumlah momen positif dan ragam kategori;
- animasi ringan saat publikasi baru dilihat pertama kali.

Leaderboard Quiz/Arena existing tidak diubah oleh PRD ini.

---

## 2. Latar belakang dan masalah saat ini

Halaman `/student/recognitions` saat ini:

- memfilter `student_recognitions.student_id` berdasarkan siswa yang login;
- hanya mengambil record `visibility=student` dengan `published_at` terisi;
- tidak dapat menampilkan penghargaan siswa lain di kelas;
- memakai hero dan dua kartu statistik besar walaupun informasinya ringkas;
- menampilkan satu kartu besar untuk setiap record;
- belum mempunyai publikasi aman untuk kelas, pengelompokan penerima, XP penghargaan, atau pemerataan apresiasi.

Struktur `student_recognitions` sudah menyimpan `class_id`, tahun pelajaran, semester, siswa, tipe, kategori, tanggal, dan metadata publikasi. Fondasi ini cukup untuk mempertahankan portofolio pribadi, tetapi tidak ideal dijadikan response langsung Papan Kelas karena:

- `description` dapat ditulis untuk konsumsi pribadi siswa;
- `evidence_reference` bersifat internal dan tidak boleh bocor;
- bulk create menghasilkan satu record per siswa sehingga feed dapat berisi duplikat;
- mengubah arti `visibility=student` menjadi “semua siswa” akan merusak compatibility dan privasi record existing.

Gamification existing mempunyai `gamification_profiles`, `xp_logs`, dan `GamificationService::awardXp()`. Namun integrasi penghargaan belum aman dilakukan langsung karena:

- pengecekan duplikasi masih dilakukan sebelum transaction;
- `xp_logs` belum mempunyai idempotency key/unique constraint generik;
- `gamification_profiles.user_id` perlu diaudit sebelum dibuat unique;
- belum ada mekanisme koreksi/reversal XP penghargaan yang dapat diaudit;
- belum ada aturan cap untuk apresiasi berulang.

---

## 3. Tujuan, indikator keberhasilan, dan non-goals

### 3.1 Tujuan utama

- Setiap siswa dalam kelas dapat melihat momen apresiasi/prestasi kelas yang secara eksplisit dipublikasikan wali kelas.
- Siswa penerima tetap mempunyai portofolio pribadi yang lebih lengkap daripada kartu publik kelas.
- Wali kelas dapat memilih `Internal`, `Pribadi`, atau `Kelas` dengan arti yang jelas.
- Konten kelas selalu memakai ringkasan publik khusus dan tidak pernah mengambil field sensitif secara tidak sengaja.
- Penghargaan untuk beberapa siswa tampil sebagai satu momen bersama.
- UI jauh lebih compact, mudah dipindai, nyaman pada 320×568, 390×844, dan desktop.
- Beranda siswa menampilkan preview ringkas momen terbaru agar Papan Kelas tidak menjadi halaman yang terlupakan.
- XP penghargaan diberikan adil, idempotent, dapat dikoreksi, dan tidak memengaruhi scope publikasi.
- Wali kelas memperoleh insight internal untuk pemerataan apresiasi tanpa mempermalukan siswa.

### 3.2 Indikator keberhasilan teknis

- Siswa hanya dapat melihat publikasi kelas jika mempunyai `StudentEnrollment` pada `class_id` dan `academic_year_id` publikasi tersebut.
- Siswa dari kelas atau tahun pelajaran lain menerima 403/tidak memperoleh data walaupun mengetahui ID/URL.
- Response feed tidak memuat `description`, `evidence_reference`, NISN, email, ID akun, atau metadata audit internal.
- Publikasi kelompok dengan lima penerima menghasilkan satu kartu dan lima penerima yang sah.
- Record `internal` dan record private existing tidak pernah masuk Papan Kelas tanpa tindakan eksplisit wali.
- Perubahan scope `Kelas → Pribadi` segera menghilangkan publikasi dari teman sekelas tetapi tetap mempertahankan portofolio pemilik.
- Query feed dan widget Beranda tetap bounded, tidak bertambah linear terhadap jumlah penerima.
- Satu recognition menghasilkan maksimal satu award XP, termasuk pada retry, double-click, atau perubahan scope.
- Membatalkan recognition menghasilkan satu reversal XP dan audit log; perubahan kelas/pribadi tidak menghasilkan XP tambahan/reversal.
- Feature flag off mempertahankan perilaku XP existing tanpa perubahan angka.

### 3.3 Indikator keberhasilan produk

- Siswa dapat memahami perbedaan `Papan Kelas` dan `Pencapaian Saya` tanpa petunjuk panjang.
- Pada mobile, minimal tiga momen ringkas dapat dipindai dalam satu layar standar setelah header.
- Tidak ada urutan “siswa terbaik”, “top XP penghargaan”, atau daftar siswa tanpa penghargaan pada portal siswa.
- Setiap kartu publik menjawab: siapa, mendapat pengakuan apa, kapan, dan dari kategori apa.
- Wali dapat mempublikasikan satu penghargaan kelas dalam alur yang sama tanpa menyalin data dua kali.

### 3.4 Non-goals

- Tidak menampilkan catatan pelanggaran, pembinaan, konseling, komunikasi orang tua, atau catatan negatif apa pun.
- Tidak menampilkan bukti, URL bukti, nomor dokumen, NISN, atau deskripsi internal kepada teman sekelas.
- Tidak membuat ranking penghargaan berdasarkan jumlah, bobot, XP, atau posisi 1–N.
- Tidak mengubah leaderboard Quiz, Arena, Live Game, atau Team Quest existing.
- Tidak memberi siswa kemampuan membuat/mengedit penghargaan sendiri.
- Tidak membuat komentar bebas, reaksi emoji, like, dislike, nominasi teman, voting, atau share publik ke internet.
- Tidak mengirim WhatsApp/email/push notification massal pada versi ini.
- Tidak memberi akses publik tanpa login atau akses lintas kelas/sekolah.
- Tidak mengubah nilai akademik berdasarkan penghargaan.
- Tidak membuat marketplace/redeem XP.
- Tidak melakukan backfill XP retroaktif untuk recognition existing secara otomatis.

---

## 4. Terminologi dan batas domain

- **Recognition:** satu `student_recognitions` milik satu siswa; menjadi sumber portofolio pribadi dan sumber award XP.
- **Publikasi kelas:** salinan publik aman dari satu atau beberapa recognition yang diterbitkan sebagai satu momen ke kelas.
- **Penerima:** siswa yang memperoleh recognition dan namanya dapat tampil pada publikasi kelas.
- **Internal:** hanya pihak Homeroom/admin yang berwenang; siswa pemilik tidak melihatnya.
- **Pribadi:** recognition dipublikasikan hanya kepada siswa pemilik.
- **Kelas:** recognition terlihat oleh pemilik dan mempunyai publikasi kelas aktif yang dapat dilihat anggota kelas/periode sah.
- **Ringkasan publik:** kalimat singkat yang sengaja ditulis/dikonfirmasi untuk dibaca teman sekelas; berbeda dari `description` internal/personal.
- **Pencapaian Saya:** portofolio individual published-only existing, dengan tampilan baru yang lebih compact.
- **Papan Apresiasi Kelas:** feed kronologis publikasi kelas aktif.
- **Sorotan:** treatment visual untuk momen terbaru/tertentu, bukan ranking.
- **XP penghargaan:** XP individual yang berasal dari recognition valid setelah dipublikasikan kepada pemilik.
- **Reversal:** log XP negatif yang mengoreksi award yang dibatalkan; histori awal tidak dihapus.

### Matriks keterlihatan

| Data | Wali/Admin berwenang | Siswa pemilik | Teman sekelas sah | Siswa kelas lain |
|---|---:|---:|---:|---:|
| Recognition internal | Ya | Tidak | Tidak | Tidak |
| Recognition pribadi | Ya | Ya | Tidak | Tidak |
| Recognition dengan publikasi kelas aktif | Ya | Ya | Ringkasan publik saja | Tidak |
| Description lengkap | Ya | Ya jika published | Tidak | Tidak |
| Evidence/reference | Ya | Tidak secara default | Tidak | Tidak |
| Nama penerima pada publikasi | Ya | Ya | Ya | Tidak |
| XP penghargaan individual | Ya | Ya | Tidak | Tidak |
| Insight siswa belum terjangkau | Ya | Tidak | Tidak | Tidak |

---

## 5. Pengguna dan otorisasi

### 5.1 Wali kelas

Wali kelas aktif dapat:

- membuat recognition pada kelas/periode yang menjadi tanggung jawabnya;
- memilih audiens Internal, Pribadi, atau Kelas;
- menulis/mengoreksi ringkasan publik sebelum publikasi kelas;
- melihat preview persis seperti yang akan dilihat siswa;
- menarik publikasi kelas menjadi Pribadi tanpa menghapus recognition;
- menarik recognition menjadi Internal sesuai kewenangan;
- melihat insight internal pemerataan apresiasi;
- melihat arsip yang sah dalam mode read-only.

Mutation historis tetap mengikuti kontrak existing: pencatat atau admin pada periode aktif. Pergantian wali tidak otomatis memindahkan hak edit record lama.

### 5.2 Admin

Admin dapat mengoreksi dan mengelola publikasi sesuai capability existing. Semua create, publish, scope change, withdraw, delete, award, reversal, dan export harus tercatat di audit log.

### 5.3 Guru non-wali

Guru non-wali tidak memperoleh akses hanya karena mengetahui URL atau melihat menu. Semua endpoint Homeroom tetap memakai `HomeroomAccessService`/policy backend.

### 5.4 Siswa pemilik

Siswa dapat:

- melihat tab Pencapaian Saya;
- melihat detail personal dari recognition yang dipublikasikan kepada dirinya;
- melihat nominal XP yang berasal dari recognition jika Phase 4 aktif;
- mengunduh DOCX riwayatnya sendiri seperti saat ini;
- melihat Papan Kelas untuk enrollment yang sah.

### 5.5 Teman sekelas

Teman sekelas hanya dapat melihat payload publik yang telah di-allow-list. Akses diberikan jika siswa login mempunyai enrollment dengan pasangan:

- `student_enrollments.class_id = recognition_publications.class_id`; dan
- `student_enrollments.academic_year_id = recognition_publications.academic_year_id`.

Semester digunakan untuk filter/display, sementara membership historis mengikuti enrollment tahun pelajaran existing. Record yang ditarik atau soft-deleted tidak boleh terlihat.

---

## 6. Pengalaman pengguna yang dituju

### 6.1 Navigasi siswa

Label menu tetap **Penghargaan** agar perubahan tidak memutus mental model. Halaman berisi dua tab:

1. **Papan Kelas** — default jika ada publikasi kelas aktif pada periode berjalan.
2. **Pencapaian Saya** — portofolio pribadi dan DOCX.

Deep-link memakai query yang stabil:

- `/student/recognitions?tab=class`
- `/student/recognitions?tab=mine`

Jika siswa belum mempunyai enrollment/kelas, tab Papan Kelas menampilkan empty state aman dan Pencapaian Saya tetap dapat dibuka.

### 6.2 Papan Kelas

Bagian atas dibuat pendek dan compact:

- nama kelas dan periode;
- kalimat `Kebaikan dan pencapaian layak mendapat pengakuan`;
- chip agregat kelas: jumlah momen, apresiasi, dan prestasi;
- filter `Semua`, `Apresiasi`, `Prestasi` serta kategori opsional;
- tidak ada count per siswa.

Urutan default:

1. `published_at DESC`;
2. `id DESC` sebagai tie-breaker.

Kartu publik desktop/mobile memuat:

- ikon Apresiasi/Prestasi;
- satu atau beberapa nama penerima;
- judul;
- ringkasan publik maksimal 280 karakter;
- kategori;
- tingkat dan hasil untuk Prestasi jika relevan;
- tanggal recognition;
- label `Diberikan oleh Wali Kelas` tanpa email/NIP.

Kartu tidak memuat XP, total penghargaan siswa, posisi, evidence, atau tombol melihat portofolio siswa lain.

Untuk penerima banyak:

- maksimal tiga nama tampil langsung;
- sisanya tampil sebagai `+N siswa`;
- expand membuka seluruh nama penerima dalam publikasi yang sama;
- satu publikasi tetap satu kartu dan satu pagination item.

### 6.3 Pencapaian Saya

Hero besar existing diganti menjadi header ringkas:

- sapaan singkat;
- chip `Apresiasi` dan `Prestasi`;
- tombol DOCX sebagai secondary action;
- jika Phase 4 aktif, chip `XP dari penghargaan` hanya untuk pemilik.

Daftar menggunakan compact cards/list:

- judul dan tipe dominan;
- deskripsi maksimal dua baris sebelum expand;
- hasil/tingkat/category/date sebagai metadata rapat;
- label `Pribadi` atau `Tampil di Papan Kelas`;
- detail lengkap dibuka melalui expand/drawer tanpa memaksa setiap card menjadi tinggi.

### 6.4 Widget Beranda siswa

Tambahkan widget compact **Kabar Baik dari Kelasmu**:

- maksimal tiga publikasi kelas terbaru;
- payload sama amannya dengan Papan Kelas;
- CTA `Lihat Papan Kelas`;
- tidak memakai polling atau notifikasi massal;
- jika tidak ada publikasi, widget tidak mengambil ruang besar.

### 6.5 Form wali kelas

Pilihan audiens memakai radio cards dengan copy tegas:

- **Internal** — hanya wali kelas/admin.
- **Pribadi** — tampil hanya kepada siswa penerima.
- **Kelas** — tampil kepada penerima dan teman sekelas pada periode ini.

Jika `Kelas` dipilih:

- field **Ringkasan untuk kelas** wajib, 20–280 karakter;
- preview publik wajib tersedia sebelum submit;
- UI memberi peringatan bahwa bukti dan description lengkap tidak ikut dibagikan;
- untuk banyak penerima, preview menampilkan format nama yang sama dengan feed.

Default create tetap **Internal** untuk mencegah publikasi tidak sengaja.

### 6.6 Insight internal pemerataan

Wali/admin memperoleh panel kecil yang tidak pernah dikirim ke siswa:

- jumlah siswa yang pernah menerima apresiasi pada semester aktif;
- jumlah siswa yang pernah menerima prestasi;
- distribusi kategori;
- daftar siswa yang belum pernah mendapat recognition pada semester sebagai pengingat observasi, bukan penilaian buruk.

Copy wajib netral, misalnya `Belum tercatat mendapat apresiasi semester ini`, bukan `Siswa tanpa prestasi`.

---

## 7. Alur produk

### 7.1 Membuat recognition Internal

1. Wali memilih satu/beberapa siswa dan mengisi field existing.
2. Audiens default Internal.
3. Server membuat satu `student_recognitions` per siswa dalam satu transaction.
4. Tidak dibuat publikasi kelas dan tidak diberikan XP.

### 7.2 Mempublikasikan Pribadi

1. Wali memilih Pribadi saat create atau dari detail record.
2. `student_recognitions.visibility=student`, `published_at`, dan `published_by` diset server-side.
3. Record muncul di Pencapaian Saya pemilik.
4. Jika XP feature flag aktif dan recognition belum pernah memperoleh XP, award dibuat satu kali.
5. Tidak dibuat publikasi kelas.

### 7.3 Mempublikasikan ke Kelas

1. Wali memilih Kelas dan menulis ringkasan publik.
2. Server memvalidasi seluruh penerima berada pada kelas/tahun yang sama.
3. Dalam satu transaction:
   - recognition individual dibuat/diubah menjadi published-to-owner;
   - satu `recognition_publication` dibuat/diaktifkan;
   - seluruh recognition penerima ditautkan melalui pivot;
   - audit log dibuat;
   - XP award dibuat per recognition hanya jika feature flag aktif.
4. Penerima melihat versi lengkap di Pencapaian Saya.
5. Teman sekelas melihat satu kartu ringkasan pada Papan Kelas dan widget Beranda.

### 7.4 Mengubah Kelas menjadi Pribadi

1. Wali memilih `Tarik dari Papan Kelas`.
2. Publikasi kelas ditandai withdrawn; row histori tidak dihapus.
3. Underlying recognition tetap `visibility=student`.
4. Pencapaian Saya pemilik tetap tampil.
5. XP tidak berubah karena validitas penghargaan tidak berubah.

### 7.5 Mengubah menjadi Internal

1. Wali memilih `Jadikan internal` dengan konfirmasi.
2. Publikasi kelas aktif ditarik.
3. Underlying recognition menjadi internal dan hilang dari Pencapaian Saya.
4. XP tidak otomatis direversal hanya karena perubahan privasi. Jika penghargaan memang tidak valid, gunakan alur Pembatalan Penghargaan.

### 7.6 Membatalkan penghargaan yang keliru

1. Wali/admin memilih `Batalkan penghargaan` dan mengisi alasan.
2. Server mengunci recognition dan award XP terkait.
3. Recognition di-soft-delete dan XP direversal satu kali jika pernah diberikan.
4. Audit menyimpan aktor, alasan, nominal, dan referensi log awal/reversal.
5. Recognition yang dibatalkan tidak dapat diaktifkan ulang. Koreksi substansial dibuat sebagai recognition baru agar jejak audit jelas.
6. Jika recognition merupakan bagian publication kelompok, recipient terkait ditandai `removed_at` tanpa menghapus pivot. Publication tetap aktif bila masih mempunyai penerima aktif; jika penerima aktif terakhir dibatalkan, publication otomatis ditarik dalam transaction yang sama.

### 7.7 Arsip kelas

- Siswa dapat melihat Papan Kelas historis hanya untuk kelas/tahun yang tercatat pada enrollment-nya.
- Default selalu periode berjalan.
- Wali dapat membaca arsip yang sah; mutation ditolak oleh `PeriodContext`/guard existing.
- Pencapaian Saya tetap dapat memuat seluruh riwayat published milik siswa lintas periode.

---

## 8. Desain data

Semua tabel/kolom dibuat melalui migration baru. Nama final dapat disesuaikan dengan convention repository, tetapi kontrak domainnya tidak boleh berubah tanpa memperbarui PRD.

### 8.1 `recognition_publications`

Tabel ini adalah konten publik aman untuk Papan Kelas. Ia bukan pengganti `student_recognitions`.

| Kolom | Tipe | Catatan |
|---|---|---|
| `id` | bigint | Primary key |
| `uuid` | uuid/string unique | Identifier publikasi, tidak memakai ID recognition sebagai URL publik |
| `class_id` | FK `synced_classes` | Kelas audiens |
| `academic_year_id` | FK `academic_years` | Wajib |
| `semester_id` | FK nullable `semesters` | Filter/periode |
| `type` | string(20) | `appreciation` atau `achievement` |
| `title` | string(180) | Copy publik terkontrol |
| `category` | string(30) | Allow-list sesuai type |
| `recognized_on` | date | Tanggal recognition |
| `public_summary` | string(280) | Wajib; aman untuk teman sekelas |
| `level` | string(30) nullable | Prestasi |
| `result` | string(160) nullable | Prestasi |
| `organizer` | string(180) nullable | Ditampilkan hanya jika aman dan diisi |
| `issuer_name` | string(160) | Snapshot nama wali/pemberi pengakuan yang disetujui untuk tampil |
| `published_at` | timestamp | Waktu masuk feed |
| `published_by` | FK `users` | Aktor publikasi |
| `withdrawn_at` | timestamp nullable | Waktu ditarik |
| `withdrawn_by` | FK nullable `users` | Aktor penarikan |
| `withdraw_reason` | string(500) nullable | Audit operasional |
| timestamps |  |  |

Index minimum:

- unique `uuid`;
- `(class_id, academic_year_id, semester_id, published_at)`;
- `(class_id, academic_year_id, type, published_at)`;
- `(withdrawn_at, published_at)` bila query planner membutuhkan.

Aturan:

- feed aktif mensyaratkan `withdrawn_at IS NULL`;
- tidak ada `description` atau `evidence_reference` pada tabel ini;
- perubahan copy publik tidak mengubah description individual;
- publication lama disimpan saat ditarik untuk audit, bukan hard delete.

### 8.2 `recognition_publication_recipients`

| Kolom | Tipe | Catatan |
|---|---|---|
| `id` | bigint | Primary key |
| `recognition_publication_id` | FK | Cascade bersama publikasi hanya pada rollback/dev, bukan operasi normal |
| `student_recognition_id` | FK | Underlying recognition individual |
| `student_id` | FK `synced_students` | Snapshot/query penerima |
| `display_name` | string(160) | Snapshot nama yang memang boleh tampil di kelas |
| `removed_at` | timestamp nullable | Penerima dikeluarkan dari publication tanpa menghapus histori |
| `removed_by` | FK nullable `users` | Aktor koreksi recipient |
| `removal_reason` | string(500) nullable | Wajib saat recipient dikeluarkan |
| timestamps |  |  |

Constraint minimum:

- unique `(recognition_publication_id, student_recognition_id)`;
- unique `student_recognition_id` agar satu recognition tidak berada dalam dua publikasi kelas;
- index `(recognition_publication_id, student_id)`.

Feed hanya memakai recipient dengan `removed_at IS NULL`. Snapshot `display_name` mencegah feed historis berubah karena sync nama/kelas dan menghindari kebutuhan mengirim model siswa lengkap.

Semua recipient harus:

- mempunyai `student_recognition.class_id`, tahun, semester, type, dan tanggal yang kompatibel dengan publication;
- menjadi anggota kelas/tahun melalui `StudentEnrollment` saat publikasi dibuat;
- berjumlah minimal satu.

### 8.3 `recognition_xp_awards`

Tabel ini baru dibuat pada Phase 4 dan tidak aktif sebelum feature flag on.

| Kolom | Tipe | Catatan |
|---|---|---|
| `id` | bigint | Primary key |
| `student_recognition_id` | FK unique | Satu award seumur hidup per recognition |
| `user_id` | FK `users` | Akun siswa penerima |
| `xp_amount` | integer unsigned | Snapshot nilai saat award |
| `rule_key` | string(80) | Mis. `appreciation.base.v1` |
| `xp_log_id` | FK unique nullable | Log positif |
| `awarded_at` | timestamp | Waktu award |
| `reversed_xp_log_id` | FK unique nullable | Log koreksi negatif |
| `reversed_at` | timestamp nullable | Waktu reversal |
| `reversed_by` | FK nullable `users` | Aktor pembatalan |
| `reversal_reason` | string(500) nullable | Wajib saat reversal |
| timestamps |  |  |

Aturan:

- tidak ada row jika feature flag off;
- satu recognition hanya mempunyai satu award positif;
- perubahan Pribadi ↔ Kelas tidak membuat row baru;
- reversal tidak menghapus row/log positif;
- recognition tanpa akun siswa tidak gagal dipublikasikan, tetapi XP berstatus tidak diberikan dan dicatat untuk insight admin; implementasi tidak boleh membuat akun palsu.

### 8.4 Hardening schema gamification existing

Phase 4 wajib melakukan preflight read-only sebelum migration:

- cari duplikat `gamification_profiles.user_id`;
- cari duplikat `(user_id, activity_type, reference_id)` non-null pada `xp_logs`;
- cocokkan total `xp_logs` dengan profile untuk mengetahui debt existing;
- jangan memperbaiki data production otomatis tanpa rencana dan persetujuan terpisah.

Migration/additive change yang direkomendasikan:

- unique index `gamification_profiles.user_id` setelah preflight aman;
- `xp_logs.idempotency_key` nullable unique;
- service adjustment bertanda positif/negatif dengan transaction dan row lock;
- idempotency key recognition: `recognition:{recognition_id}:award:v1` dan `recognition:{recognition_id}:reverse:v1`.

---

## 9. Kontrak payload publik

Controller/service Papan Kelas harus membentuk DTO/array eksplisit. Field maksimum:

```text
uuid
type
title
category
recognized_on
public_summary
level
result
organizer
published_at
class: { id, name }
academic_year: { id, name }
semester: { id, type } | null
issuer_name
recipients: [{ display_name }]
recipient_count
```

Field berikut dilarang berada pada response teman sekelas:

```text
student_recognition_id
student_id
user_id
nisn
nis
email
description
evidence_reference
recorded_by
updated_by
published_by id
deleted_at
xp_amount
xp_log_id
audit metadata
```

Nama penerima adalah data publik internal kelas yang memang menjadi tujuan fitur. Jangan menambahkan tautan ke profil, NISN, atau endpoint detail siswa.

---

## 10. Aturan XP penghargaan

### 10.1 Kapan XP diberikan

- XP diberikan ketika recognition pertama kali dipublikasikan kepada pemilik, baik melalui scope Pribadi maupun Kelas.
- Internal tidak memberi XP.
- Scope Kelas tidak memberi bonus di atas Pribadi.
- Republish, retry, double-click, atau perubahan scope tidak menggandakan XP.
- Recognition existing sebelum feature flag aktif tidak mendapat XP retroaktif secara default.

### 10.2 Nilai awal v1

| Jenis | Tingkat | XP |
|---|---|---:|
| Apresiasi | Semua kategori | 5 |
| Prestasi | Kelas | 10 |
| Prestasi | Sekolah | 20 |
| Prestasi | Kecamatan | 25 |
| Prestasi | Kabupaten/Kota | 30 |
| Prestasi | Provinsi | 40 |
| Prestasi | Nasional | 55 |
| Prestasi | Internasional | 70 |

Prinsip:

- kategori akademik, karakter, sosial, seni, olahraga, dan lainnya tidak mempunyai multiplier berbeda;
- semua penerima recognition kelompok memperoleh XP individual yang sama sesuai rule;
- nilai disimpan sebagai snapshot bersama `rule_key`, sehingga perubahan aturan masa depan tidak mengubah histori.

### 10.3 Anti-farming Apresiasi

- maksimal XP Apresiasi yang dihitung adalah 15 XP per siswa per ISO week berdasarkan `recognized_on`;
- recognition keempat dan seterusnya tetap disimpan, tetap dapat dipublikasikan, dan tetap menjadi portofolio, tetapi award XP-nya 0 dengan reason `weekly_cap`;
- Prestasi tidak memakai weekly cap karena bersifat capaian formal;
- cap hanya memengaruhi XP, bukan keterlihatan atau validitas penghargaan.

### 10.4 Koreksi dan reversal

- perubahan privasi tidak melakukan reversal;
- pembatalan karena record keliru melakukan satu reversal penuh;
- reversal memakai log negatif, bukan menghapus log awal;
- profile XP diperbarui dalam transaction yang sama dan tidak boleh menjadi negatif;
- recognition yang sudah dibatalkan tidak dapat memperoleh award lagi;
- koreksi substansial dibuat sebagai recognition baru.

### 10.5 Tampilan XP

- Pencapaian Saya boleh menampilkan `+N XP` atau `Tidak mendapat XP karena batas mingguan` hanya kepada pemilik;
- Papan Kelas dan widget Beranda tidak mengirim/menampilkan XP;
- tidak ada leaderboard XP khusus recognition;
- dashboard gamification existing tetap menjadi sumber total XP.

### 10.6 Feature flag dan rollout

Gunakan config/feature flag server-side, default off:

```text
features.recognition_xp = false
```

Aktivasi hanya setelah:

- schema dan service idempotent lulus;
- baseline total XP dicatat;
- test retry/reversal/cap lulus;
- QA akun tanpa `syncedStudent.user` lulus;
- keputusan production activation diberikan eksplisit.

---

## 11. Query, pagination, dan performa

- Feed dipaginasi per publication, bukan per recipient; target 12 publication per page.
- Recipient di-eager-load dalam query bounded.
- Summary kelas dihitung melalui aggregate query, bukan loop per siswa.
- Widget Beranda mengambil maksimal tiga publication dan hanya field DTO publik.
- Pencapaian Saya mempertahankan pagination individual existing.
- Insight pemerataan wali memakai aggregate/subquery dan tidak membuat query per siswa.
- Index publication harus diverifikasi dengan dataset representatif.
- Response tidak boleh memuat seluruh roster jika hanya membutuhkan tiga recipient preview; bila seluruh recipient diperlukan untuk expand, tetap eager-load dalam batch yang bounded.

Target query test:

- menambah recipient dari 2 menjadi 40 tidak menambah query count lebih dari dua query;
- menambah publication dari 12 menjadi 120 tidak mengubah jumlah query pada satu page secara linear;
- widget Beranda tidak menambah N+1 terhadap recipient.

---

## 12. Audit dan observability

Audit action minimum:

```text
recognition_class_published
recognition_class_publication_updated
recognition_class_withdrawn
recognition_scope_changed
recognition_xp_awarded
recognition_xp_capped
recognition_xp_reversed
recognition_cancelled
```

Audit description tidak boleh menyimpan password/token atau menyalin evidence URL sensitif. Simpan ID internal, kelas, jumlah penerima, actor, dan reason yang diperlukan.

Metric/log operasional minimum:

- kegagalan membership validation;
- publication tanpa recipient (harus nol);
- recognition published tanpa akun siswa untuk XP;
- duplicate idempotency attempt yang berhasil diabaikan;
- mismatch award row dan XP log;
- error widget/feed yang tidak boleh menggagalkan seluruh Beranda siswa.

---

## 13. Compatibility dan migrasi data existing

1. `student_recognitions` existing tetap sumber resmi portofolio.
2. `visibility=internal` tetap internal.
3. `visibility=student` existing tetap berarti Pribadi/pemilik saja.
4. Tidak ada publication row yang dibuat otomatis dari record existing.
5. Tidak ada XP retroaktif otomatis.
6. Export DOCX siswa tetap hanya memuat recognition milik siswa, bukan feed kelas.
7. Rekap wali kelas tetap membaca `student_recognitions`, bukan publication snapshot.
8. Backfill legacy positive tetap internal sesuai PRD induk.
9. Route `/student/recognitions` dipertahankan; tab memakai query agar bookmark existing tetap valid.
10. Existing test yang memastikan siswa tidak melihat recognition milik siswa lain harus diperluas: siswa boleh melihat **DTO publik** teman sekelas hanya melalui Papan Kelas, tetapi tetap tidak boleh mengakses recognition mentah/detail pribadi.

---

## 14. Rencana implementasi berfase

### Phase 0 — Baseline, kontrak keamanan, dan safety net

- [x] Verifikasi branch, HEAD, worktree, migration status, dan route aktual.
- [x] Jalankan focused baseline `StudentRecognitionTest` dan `HomeroomReportExportTest`.
- [x] Catat baseline `npm run typecheck`, `npm run build`, scoped Pint, dan `git diff --check`.
- [x] Tambahkan test yang mengunci perilaku existing: internal tidak terlihat, private hanya pemilik, unpublish menyembunyikan dari pemilik.
- [x] Tambahkan test negatif bahwa teman sekelas belum dapat memperoleh description/evidence recognition lain.
- [x] Audit fixture enrollment lintas kelas, lintas tahun, wali lama, dan siswa tanpa enrollment.
- [x] Audit data/schema gamification read-only tanpa mengubah angka.
- [x] Tetapkan DTO publik dan snapshot contoh response dalam test.
- [x] Perbarui checkpoint.

**Definition of Done:** baseline tercatat, kebocoran field sensitif mempunyai regression test, dan tidak ada perubahan perilaku production.

### Phase 1 — Schema publikasi kelas dan compatibility

- [x] Buat migration `recognition_publications`.
- [x] Buat migration `recognition_publication_recipients`.
- [x] Buat model, relation, casts, constants/status, factory/fixture.
- [x] Tambahkan unique/index minimum.
- [x] Pastikan migration tidak mengubah visibility record existing.
- [x] Pastikan migration tidak membuat publikasi atau XP retroaktif.
- [x] Test migration up/down pada database testing/MySQL yang aman.
- [x] Test constraint recipient, publication kosong, dan lintas kelas/periode.
- [x] Jalankan focused test, scoped Pint, dan diff check.
- [x] Perbarui checkpoint.

**Definition of Done:** schema additive, rollback aman, data existing tidak berubah, dan publication tidak dapat mempunyai recipient yang tidak sah.

### Phase 2 — Backend publication, feed, dan authorization

- [x] Implementasikan service transaction create/update/withdraw publikasi kelas.
- [x] Perluas create recognition untuk audiens Internal/Pribadi/Kelas.
- [x] Implementasikan ringkasan publik wajib 20–280 karakter untuk Kelas.
- [x] Implementasikan satu publication dengan banyak recipient.
- [x] Implementasikan perubahan Kelas → Pribadi dan Kelas/Pribadi → Internal.
- [x] Implementasikan feed berdasarkan enrollment class/year.
- [x] Implementasikan period selector hanya untuk enrollment siswa yang sah.
- [x] Implementasikan DTO allow-list dan larangan field sensitif.
- [x] Implementasikan aggregate summary dan pagination per publication.
- [x] Implementasikan audit log seluruh mutation publication.
- [ ] Test wali aktif/admin/guru non-wali/wali lama. (wali aktif dan guru non-wali lulus; admin/wali lama belum spesifik)
- [ ] Test teman sekelas, kelas lain, tahun lain, tanpa enrollment, dan IDOR/direct URL. (teman sekelas, kelas lain, dan tanpa enrollment lulus; tahun lain/IDOR belum spesifik)
- [x] Test bulk recipient satu kartu, all-or-nothing transaction, withdraw, dan republish aman.
- [ ] Test query count bounded.
- [ ] Perbarui checkpoint.

**Definition of Done:** publikasi kelas dapat dibuat/ditarik dengan aman; siswa hanya menerima feed kelas/periode yang sah; payload tidak mengandung data private.

### Phase 3 — UI compact dan discovery

- [x] Tambahkan tab Papan Kelas dan Pencapaian Saya pada route existing.
- [x] Ganti hero besar dengan header ringkas dan summary chips.
- [x] Implementasikan compact publication cards, filter, pagination, dan expand recipient.
- [x] Compact ulang cards Pencapaian Saya tanpa kehilangan detail/DOCX.
- [x] Tambahkan badge visibility `Pribadi`/`Papan Kelas` pada milik siswa.
- [x] Tambahkan pilihan audiens dan preview publik pada form wali.
- [x] Pastikan description/evidence tidak muncul pada preview publik.
- [x] Tambahkan widget Kabar Baik dari Kelasmu pada Beranda siswa.
- [x] Tambahkan insight pemerataan hanya pada halaman wali/admin.
- [x] Implementasikan loading, empty, error, dan archived states.
- [ ] QA light/dark theme. (portal siswa lulus; halaman wali/admin belum)
- [ ] QA 320×568, 390×844, tablet, dan 1440×900. (320×568, 390×844, dan desktop 1536×703 lulus; tablet/1440×900 belum)
- [x] Verifikasi tanpa horizontal overflow, card terlalu tinggi, atau fixed-nav overlap pada portal siswa.
- [ ] Verifikasi keyboard/focus, label form, dan contrast.
- [ ] Perbarui checkpoint.

**Definition of Done:** siswa dapat memindai minimal tiga momen ringkas setelah header pada mobile, tab mudah dipahami, dan wali melihat preview publik yang sama dengan hasil akhir.

### Phase 4 — XP recognition dan idempotency

- [x] Catat hasil preflight duplikat/profile mismatch; hentikan migration unique bila data tidak aman.
- [x] Buat migration `recognition_xp_awards`.
- [x] Tambahkan `xp_logs.idempotency_key` nullable unique.
- [x] Tambahkan unique `gamification_profiles.user_id` hanya setelah preflight aman.
- [x] Refactor GamificationService agar transaction, row lock, signed adjustment, dan idempotency aman.
- [x] Implementasikan rule XP v1 dan snapshot `rule_key`.
- [x] Implementasikan cap Apresiasi 15 XP per ISO week berdasarkan `recognized_on`.
- [x] Implementasikan award pada first publish Pribadi/Kelas.
- [x] Implementasikan no-op pada retry/republish/perubahan scope.
- [x] Implementasikan reversal hanya pada Pembatalan Penghargaan.
- [x] Tangani recognition yang pemiliknya belum mempunyai akun tanpa menggagalkan publication.
- [x] Tampilkan XP hanya pada Pencapaian Saya/pemilik.
- [x] Tambahkan config feature flag default off.
- [ ] Test concurrent/double request melalui unique constraint dan transaction.
- [x] Test cap, group recipient, reversal, nonnegative total, dan audit.
- [x] Test feature flag off tidak mengubah XP existing.
- [x] Perbarui checkpoint sebelum meminta izin aktivasi.

**Definition of Done:** XP idempotent dan dapat direversal, existing XP tidak berubah saat flag off, serta tidak ada nominal XP yang bocor ke feed kelas.

### Phase 5 — Hardening, QA, dokumentasi, dan rollout

- [x] Jalankan full PHP test suite.
- [x] Jalankan typecheck, production build, scoped/full Pint sesuai debt, dan `git diff --check`.
- [x] Jalankan route contract check.
- [x] Verifikasi migration rollback/rerun dan count data existing pada database aman.
- [ ] QA browser wali, admin, guru non-wali, siswa pemilik, teman sekelas, dan siswa kelas lain.
- [ ] QA periode aktif dan arsip.
- [ ] QA retry/double-click publication serta feature flag XP off/on di environment non-production.
- [ ] QA DOCX pemilik tetap benar dan tidak memuat feed siswa lain.
- [x] Perbarui `docs/PANDUAN-WALI-KELAS.md`.
- [x] Dokumentasikan arti Internal/Pribadi/Kelas dan alur pembatalan XP.
- [ ] Siapkan deployment checklist, backup, migration gate, cache clear, smoke test, dan rollback.
- [ ] Pisahkan laporan source-tested, pushed, deployed, migration-applied, flag-enabled, dan production-smoke-verified.
- [ ] Tandai complete hanya setelah semua acceptance criteria lulus.

**Definition of Done:** fitur aman digunakan sekolah, dokumentasi jelas, regression hijau, dan activation XP menjadi langkah eksplisit terpisah.

---

## 15. Acceptance test minimum

### 15.1 Privasi dan authorization

- [ ] Internal hanya terlihat wali/admin berwenang.
- [ ] Pribadi hanya terlihat siswa pemilik.
- [ ] Publikasi kelas terlihat siswa dengan enrollment class/year yang cocok.
- [ ] Siswa kelas lain dan tahun lain tidak memperoleh publication.
- [ ] Siswa tanpa enrollment tidak memperoleh feed kelas.
- [ ] Guru non-wali ditolak backend.
- [ ] Wali lama hanya membaca arsip yang sah.
- [ ] Description dan evidence tidak ada pada payload/feed/widget teman sekelas.
- [ ] NISN/email/user ID tidak ada pada payload publik.
- [ ] Direct URL/UUID guessing tidak membuka recognition private.

### 15.2 Publication lifecycle

- [ ] Internal create tidak membuat publication.
- [ ] Pribadi create tidak membuat publication kelas.
- [ ] Kelas create membuat source recognition dan satu publication secara atomik.
- [ ] Lima recipient tampil sebagai satu publication.
- [ ] Satu recipient invalid menggagalkan seluruh transaction.
- [ ] Kelas → Pribadi menarik feed tetapi mempertahankan Pencapaian Saya.
- [ ] Kelas/Pribadi → Internal menyembunyikan dari portal siswa.
- [ ] Withdraw tidak hard-delete histori publication.
- [ ] Existing visibility `student` tidak otomatis masuk feed.
- [ ] Record legacy positive tetap internal.

### 15.3 UI/UX

- [ ] Tab default dan URL query stabil.
- [ ] Header Pencapaian Saya lebih compact dari implementasi existing.
- [ ] Tiga kartu ringkas dapat dipindai pada mobile setelah header bila copy pendek.
- [ ] Tidak ada ranking/count per siswa pada Papan Kelas.
- [ ] Multi-recipient tidak menggandakan card.
- [ ] Empty/loading/error/archive state jelas.
- [ ] Widget Beranda maksimal tiga momen dan tidak membuat ruang kosong besar.
- [ ] Light/dark, keyboard, focus, contrast, dan screen width lulus.

### 15.4 XP

- [ ] Internal tidak memberi XP.
- [ ] First publish Pribadi memberi XP satu kali.
- [ ] First publish Kelas memberi XP yang sama dengan Pribadi.
- [ ] Republish/double-click/retry tidak menggandakan XP.
- [ ] Kelas ↔ Pribadi tidak mengubah XP.
- [ ] Pembatalan membuat tepat satu reversal.
- [ ] Reversal retry tidak menggandakan nilai negatif.
- [ ] Apresiasi melewati 15 XP/minggu tetap tercatat tetapi award 0.
- [ ] Prestasi memakai rule level v1.
- [ ] Group memberi satu award per recognition/penerima.
- [ ] Siswa tanpa akun tidak menggagalkan publication.
- [ ] Feature flag off tidak membuat award row/log.
- [ ] Tidak ada XP pada DTO publik kelas.

### 15.5 Regression

- [ ] Pencapaian Saya dan DOCX existing tetap ownership-bound.
- [ ] Homeroom recognition create/edit/delete/publish existing tetap berjalan.
- [ ] Dashboard/analytics Homeroom tetap bounded.
- [ ] Catatan Pelanggaran dan Pembinaan tidak masuk Papan Kelas.
- [ ] Absensi, Quiz, Arena, Live Game, dan Team Quest tetap memberi XP sesuai perilaku existing.
- [ ] Leaderboard Quiz/Arena existing tidak berubah.
- [ ] Arsip Homeroom tetap read-only.

---

## 16. Verification commands

Jalankan yang relevan setelah setiap fase dan simpan exit code/jumlah test pada checkpoint.

```powershell
php artisan route:list --path=student/recognitions
php artisan route:list --path=homeroom/recognitions
php artisan test --filter=StudentRecognitionTest
php artisan test --filter=HomeroomReportExportTest
php artisan test --filter=HomeroomDashboardTest
vendor\bin\pint --test <scoped-files>
npm run typecheck
npm run build
git diff --check
git status --short --branch
```

Focused test baru yang diharapkan:

```text
tests/Feature/StudentRecognitionClassPublicationTest.php
tests/Feature/RecognitionXpAwardTest.php
```

Final gate:

```powershell
php artisan test
php artisan route:list
vendor\bin\pint --test
npm run typecheck
npm run build
git diff --check
git status --short --branch
```

Aturan database:

- jangan menjalankan `migrate:fresh` pada database development/production berisi data;
- gunakan testing database dan rollback/rerun terkontrol;
- migration unique XP hanya dijalankan setelah query preflight menunjukkan data aman;
- catat row count `student_recognitions`, publication, recipient, XP award, XP log, dan profile sebelum/sesudah deployment;
- jangan mengaktifkan recognition XP pada deployment yang sama sebelum migration dan smoke test publication selesai bila risiko operasional belum dinilai.

---

## 17. QA browser minimum

### Role

- wali kelas aktif;
- admin;
- guru non-wali;
- siswa penerima;
- teman sekelas;
- siswa kelas lain;
- siswa tanpa enrollment/akun mapping bila fixture tersedia.

### Viewport

- 320×568;
- 390×844;
- tablet portrait;
- 1440×900.

### Skenario utama

1. Wali membuat Internal; tidak muncul di kedua portal siswa.
2. Wali membuat Pribadi; hanya pemilik melihat.
3. Wali membuat Kelas untuk satu siswa; pemilik melihat detail, kelas melihat ringkasan.
4. Wali membuat Kelas untuk lima siswa; feed hanya satu kartu.
5. Teman sekelas membuka expand; hanya nama dan field publik yang tampak.
6. Siswa kelas lain mencoba URL/query yang sama; data tidak muncul.
7. Wali menarik Kelas menjadi Pribadi; widget/feed teman hilang, pemilik tetap melihat.
8. Pencapaian Saya menampilkan kartu compact dan DOCX tetap dapat diunduh.
9. Feature flag XP off: tidak ada perubahan XP.
10. Feature flag XP on di non-production: award, cap, retry, dan reversal sesuai kontrak.

Tidak boleh ada horizontal overflow, console error, card kosong besar, fixed navigation overlap, atau flash/toast ganda.

---

## 18. Rollout dan rollback

### Rollout yang direkomendasikan

1. Backup database dan catat baseline count.
2. Deploy schema publication dengan XP flag off.
3. Jalankan migration dan cache clear sesuai panduan deployment.
4. Smoke test role wali/siswa untuk Internal, Pribadi, Kelas, dan withdraw.
5. Observasi error log/query/performance.
6. Aktifkan UI/widget setelah backend aman bila rollout bertahap diperlukan.
7. Jalankan preflight XP dan smoke test non-production.
8. Aktifkan recognition XP secara eksplisit pada langkah terpisah.
9. Verifikasi satu award XP nyata yang aman menggunakan fixture/test account, bukan memutasi penghargaan siswa production sembarangan.

### Rollback

- feature flag XP dimatikan terlebih dahulu;
- UI Papan Kelas dapat disembunyikan tanpa menghapus publication history;
- migration down hanya dilakukan jika aman dan belum menghasilkan data production yang harus dipertahankan;
- jika sudah ada publication production, prefer forward fix daripada menghapus tabel;
- XP reversal massal tidak boleh dijalankan otomatis; setiap koreksi harus mempunyai audit/reconciliation plan.

---

## 19. Current repo map yang diverifikasi pada 26 Agustus 2026

- Branch `main` sinkron dengan `origin/main`; worktree bersih sebelum PRD ini dibuat.
- `routes/web.php`: route Homeroom recognition existing dan dua route student recognition berada pada middleware role masing-masing.
- `app/Http/Controllers/StudentRecognitionPortalController.php`: portal existing memfilter `student_id` login, `visibility=student`, dan `published_at`.
- `app/Services/StudentRecognitionService.php`: bulk create membuat satu row per siswa dan publication saat ini hanya owner-bound.
- `app/Models/StudentRecognition.php`: constants existing hanya `internal` dan `student`.
- `database/migrations/2026_08_24_000001_create_homeroom_guidance_and_recognition_tables.php`: recognition sudah menyimpan class/year/semester/student dan index individual.
- `app/Services/HomeroomAccessService.php`: akses kelas/periode dan membership roster existing.
- `app/Models/StudentEnrollment.php`: histori penempatan siswa per tahun pelajaran.
- `resources/js/pages/StudentPortal/Recognitions/Index.tsx`: hero, summary besar, dan cards existing akan di-compact.
- `resources/js/layouts/StudentLayout.tsx`: menu Penghargaan existing dipertahankan.
- `tests/Feature/StudentRecognitionTest.php`: coverage owner-only published recognition existing menjadi safety net utama.
- `app/Services/GamificationService.php`: award XP existing perlu hardening idempotency sebelum dipakai recognition.
- `database/migrations/2026_07_15_031128_create_gamification_profiles_table.php` dan `2026_07_15_031129_create_xp_logs_table.php`: schema XP existing perlu preflight sebelum unique/additive migration.
- `resources/js/pages/StudentPortal/GlobalLeaderboard.tsx` dan leaderboard game/quiz existing berada di domain permainan/nilai dan tidak diubah.

---

## 20. Risiko dan mitigasi

| Risiko | Dampak | Mitigasi |
|---|---|---|
| Description/evidence bocor ke kelas | Privasi tinggi | Tabel publikasi terpisah, DTO allow-list, regression payload |
| Existing private recognition tiba-tiba publik | Privasi tinggi | Tidak ada auto-backfill publication; `student` tetap private |
| Group membuat kartu duplikat | UX buruk | Satu publication + pivot banyak recipient |
| Ranking membuat siswa tertekan | Dampak pedagogis | Urutan kronologis, tanpa count/rank per siswa |
| Wali hanya mengapresiasi siswa yang sama | Ketidakadilan | Insight pemerataan internal tanpa publikasi daftar kekurangan |
| Double-click memberi XP ganda | Integritas data | Unique idempotency key + transaction + row lock |
| Unpublish menghapus XP yang masih sah | Kebingungan | Pisahkan privasi dari pembatalan validitas |
| Reversal menghilangkan histori | Audit lemah | Negative log dan link ke award awal |
| Siswa belum punya akun | Publication gagal | Publication tetap jalan; XP ditunda/tidak diberikan dan dilaporkan |
| Query recipient menjadi N+1 | Performa | Eager-load, pagination per publication, bounded query test |
| Migration unique gagal karena debt lama | Deployment gagal | Preflight production, jangan auto-dedupe |
| Widget membuat Beranda berat | UX/performa | Maksimal tiga item, DTO ringan, fail-soft |

---

## 21. Keputusan final implementasi

1. Mode hibrida dipakai.
2. Papan Apresiasi Kelas dan Pencapaian Saya berada pada halaman Penghargaan yang sama melalui tab.
3. Papan Kelas bersifat kronologis dan celebratory, bukan ranking.
4. Catatan sumber dan publikasi kelas dipisahkan melalui tabel publication khusus.
5. Ringkasan publik wajib berbeda dari description/evidence internal.
6. Publikasi kelompok menggunakan satu publication dan banyak recipient.
7. Existing `visibility=student` tetap private dan tidak diubah massal.
8. Kelas hanya dapat melihat publication berdasarkan enrollment class/year.
9. Beranda siswa memperoleh widget maksimal tiga momen terbaru.
10. Wali memperoleh insight pemerataan yang tidak pernah ditampilkan ke siswa.
11. XP diberikan per recognition ketika pertama kali published-to-owner, bukan karena scope kelas.
12. XP tidak tampil di Papan Kelas.
13. Apresiasi bernilai 5 XP dengan cap 15 XP/minggu; Prestasi mengikuti level v1.
14. XP memakai ledger idempotent, feature flag default off, dan reversal audit.
15. Tidak ada XP retroaktif otomatis.
16. Tidak ada komentar, like, voting, atau peer nomination pada versi ini.
17. DOCX pemilik dan laporan Homeroom existing tetap memakai recognition individual.
18. Aktivasi XP production merupakan checkpoint terpisah dari deployment Papan Kelas.

---

## 22. Definition of complete

Fitur dinyatakan selesai hanya jika:

1. Wali dapat memilih Internal, Pribadi, dan Kelas dengan dampak yang jelas.
2. Papan Kelas hanya menampilkan publikasi aman kepada anggota kelas/periode yang sah.
3. Description, evidence, identifier sensitif, dan XP tidak pernah masuk payload teman sekelas.
4. Penghargaan kelompok tampil satu kali dengan seluruh penerima yang benar.
5. Pencapaian Saya tetap ownership-bound, compact, dan DOCX tetap valid.
6. Widget Beranda memberi discovery tanpa polling/notifikasi massal.
7. Tidak ada ranking/count publik per siswa.
8. Insight pemerataan hanya terlihat wali/admin.
9. XP idempotent, capped, reversible, dan tetap off sampai aktivasi eksplisit.
10. Data existing/legacy tidak otomatis dipublikasikan atau diberi XP.
11. Otorisasi lintas role, kelas, periode, dan arsip lulus automated test serta browser QA.
12. Query feed/widget/insight bounded dan tidak N+1.
13. Full regression, typecheck, build, formatting/diff, migration gate, dan viewport QA selesai.
14. Checklist dan checkpoint diperbarui berdasarkan bukti nyata.
15. Source code, commit/push, deployment, migration, feature flag, dan production smoke verification dilaporkan sebagai status terpisah.

---

## 23. Checkpoint implementasi 26 Agustus 2026

```text
Tanggal/waktu: 26 Agustus 2026, Asia/Makassar
Branch dan HEAD awal: main, a0e6680533abdd0c17b141f2f7eed48e79f0bdf3
Fase aktif: Phase 5 hardening; Phase 2/3/4 belum ditutup karena matriks role/browser/concurrency belum seluruhnya diverifikasi
Checklist yang baru selesai: schema publikasi, DTO aman, feed enrollment-bound, UI compact, widget, ledger XP, dokumentasi, automated regression
Migration/data command yang dijalankan: migrate lokal; rollback dua migration baru saat tabel masih kosong; migrate ulang berhasil
Feature flag: RECOGNITION_XP_ENABLED=false (default; tidak diaktifkan)
Test lulus: full PHP suite 135 test/1.600 assertion; class publication terbaru 6/154; XP 4/21; StudentRecognition 5/138; HomeroomDashboard 7/122; HomeroomReportExport 9/99; migration 2/20
Frontend/quality lulus: npm run typecheck; npm run build; scoped Pint; git diff --check; route contract check
QA manual/browser: Chrome siswa lulus di 1536×703, 320×568, 390×844, dark/light; tab class/mine, empty state, kartu personal, bottom dock, dan dashboard tanpa widget kosong terverifikasi
Risiko/temuan tersisa: browser wali/admin, feed kelas berisi, tablet/1440×900, keyboard/focus menyeluruh; matriks wali lama; query-count assertion; concurrency request nyata; production activation
Langkah berikutnya: lengkapi QA role/data/viewport tersisa, lalu minta izin terpisah untuk commit/push/deploy/aktivasi XP
```
