# Matriks Kebutuhan KabarNikah

Status: **Terpenuhi** (diimplementasi + ada bukti uji), **Sebagian** (berjalan dengan batas yang dijelaskan), **Butuh konfigurasi** (kode siap, perlu kredensial/aset pemilik), **Belum diuji nyata** (kode siap, belum diuji pada layanan sungguhan).
Rujukan tes: `T#` = kelompok di `kabarnikah/tests/run.php` (lihat `docs/bukti/hasil-tes-otomatis.txt`), `B:` = skrip uji browser di `tools/` (sumber pengembangan), `F:` = bukti gambar di `docs/bukti/qa/`.

Jalur file relatif terhadap folder privat `kabarnikah/` kecuali diawali `public_html/`.

## A. Merek, landing, katalog

| Kebutuhan | Implementasi | Bukti | Status |
|---|---|---|---|
| Merek KabarNikah konsisten, logo pemilik di semua tempat | `public_html/assets/brand/*`, `views/partials/header.php`, `views/layouts/*`, footer template diganti (`tools/build_templates.py`) | F:01, F:13, T2 | Terpenuhi |
| Layar pembuka amplop → 2 cincin → hati putih | `public_html/assets/{css/kn-loader.css,js/kn-loader.js}`, `views/partials/loader.php` (sekali per sesi, hormati reduced-motion) | video `public_html/assets/video/kabarnikah-demo.mp4` (detik 0–4) | Terpenuhi |
| Landing lengkap (header, hero, bukti fitur, katalog, video, fitur, cara kerja, harga, FAQ, CTA, footer) | `views/public/home.php`, `Service/Defaults.php`, `Service/ContentService.php` | B:qa_viewports, F:12 | Terpenuhi |
| Video landing dengan media nyata | Rekaman aplikasi 31 dtk + poster + transkrip; dapat diganti admin (`AdminController::contentMedia`) | `public_html/assets/video/` | Terpenuhi |
| Tanpa statistik/testimoni/logo klien palsu | Konten default tanpa angka/testimoni; peringatan di admin konten | T2 ("Tidak ada statistik/testimoni karangan") | Terpenuhi |
| Katalog 4 desain + detail + demo publik | `PublicController::catalog/product`, `InvitationController::demo`, `views/public/product.php` (video nyata, cover, 3 tangkapan) | T2 (4 kartu, 4 demo), F:13, F:14 | Terpenuhi |
| Harga & cover katalog diatur admin tanpa menimpa undangan lama | `AdminController::saveProduct/productMedia`, snapshot `order_items.snapshot_json` | T4 ("Ubah harga … tidak mengubah pesanan lama") | Terpenuhi |
| SEO & sharing (meta, OG image, sitemap, robots) | `views/layouts/public.php`, `PublicController::sitemap/robots`, `assets/brand/og-image.jpg` | T2 (robots) | Terpenuhi |

## B. Akun, peran, otorisasi

| Kebutuhan | Implementasi | Bukti | Status |
|---|---|---|---|
| Dua peran (admin, member) ditentukan server; daftar publik selalu member | `Service/AuthService::register`, kolom `users.role` CHECK | T3 | Terpenuhi |
| Login/daftar/lupa/reset/verifikasi email/keluar/sesi lain | `Http/AuthController`, `Core/Session` (sesi di DB, token di-hash) | T3, B:qa_flow | Terpenuhi (email butuh SMTP) |
| CSRF, rate limit, hash Argon2id/bcrypt | `Core/App::middleware`, `Core/Support::RateLimiter`, `AuthService` | T3 | Terpenuhi |
| Pemeriksaan kepemilikan di API, media, invoice, pratinjau, publikasi | `Service/Access` (`invitation`, `editable`, `order`), `MediaService::serve` | T4, T5, T7, T8 (akses silang 404) | Terpenuhi |
| Matriks izin admin vs member | Opsi rute `role` di `src/routes.php` | T3 (member → /admin 403) | Terpenuhi |

## C. Pembelian, pembayaran, hak, masa aktif

| Kebutuhan | Implementasi | Bukti | Status |
|---|---|---|---|
| Satu pembelian = satu hak = satu undangan untuk template yang dibeli | `EntitlementService::createFromOrderItem` (UNIQUE `order_item_id`), `invitations.entitlement_id` UNIQUE | T4 | Terpenuhi |
| Hak edit hanya untuk template yang dibeli, dicek tiap permintaan | `Access::editable` (template hak = template undangan) | T5 ("Mengganti template lewat data …") | Terpenuhi |
| Transfer manual: bukti privat, verifikasi admin, idempoten | `OrderService::submitProof/approve/reject/refund` (transaksi IMMEDIATE) | T4, B:qa_flow | Terpenuhi |
| Produk terkunci sebelum lisensi aset & metode bayar | `CatalogService::readiness`, `OrderService::create` | T4 | Terpenuhi |
| Gateway Midtrans: callback ≠ bukti; verifikasi tanda tangan + cek status API; dedupe | `OrderService::handleMidtrans`, tabel `payment_events` | T6 | Terpenuhi (logika); **Belum diuji nyata** (tanpa kunci sandbox) |
| Masa aktif 365 hari default, perpanjangan undangan yang sama | `EntitlementService::extend`, `OrderService::createRenewal` | T5 | Terpenuhi |
| Saat habis: editor terkunci, publik nonaktif tanpa data pribadi | `Access::editable`, `InvitationController::show` (410) | T5 | Terpenuhi |
| Pembayaran platform bukan amplop digital | Hadiah tamu hanya info rekening pasangan; tidak ada dompet | — | Terpenuhi |

## D. Empat template, editor, foto, warna

| Kebutuhan | Implementasi | Bukti | Status |
|---|---|---|---|
| Empat ZIP menjadi empat renderer berbeda yang setia sumber | `tools/build_templates.py`, `templates/*/view.html`, `public_html/tpl/*/kn/adapter.js`, `Service/TemplateRenderer` | AUDIT_4_TEMPLATE.md, F:14, baseline | Terpenuhi (lihat batas di audit) |
| Manifest template (slot, token, batas, kemampuan, versi) | `templates/*/manifest.json`, `Service/TemplateRegistry` | — | Terpenuhi |
| Editor: autosave, draft vs terbit, revisi, konflik, cadangan lokal | `public_html/assets/js/editor.js`, `InvitationService::saveDraft/publish/restore` | T5 (409), B:qa_flow, F:02 | Terpenuhi |
| Unggah & crop foto (zoom/geser/putar), validasi server, EXIF dihapus, varian WebP | `MediaService::uploadForInvitation/processImage/cropVariant` | B:qa_flow, F:07 | Terpenuhi |
| Foto tidak merusak ilustrasi/sprite; foto contoh tidak bocor | Slot terpetakan per template; `samplePhotos` + placeholder netral | T5, AUDIT §4 | Terpenuhi |
| Warna berubah nyata per template dengan batas jujur | `kn-runtime.js applyColors`, `tpl/*/kn/theme.css`, `theme-extra.css` | F:08, F:10, F:11 | Terpenuhi (token terbatas sesuai manifest) |
| Animasi/interaksi khas tetap berjalan | Adaptor mengikat data tanpa membangun ulang DOM | video hero 4 template | Terpenuhi (Chromium); perangkat fisik belum |
| Musik/video: unggah musik, YouTube; musik bawaan dikunci lisensi | `editor.js` bagian media, `allow_bundled_music` | — | Terpenuhi |

## E. Tamu, RSVP, ucapan, publikasi

| Kebutuhan | Implementasi | Bukti | Status |
|---|---|---|---|
| Tamu & tautan personal, mode privat dengan token | `MemberController::guests/guestLink`, `InvitationController::show` | B:qa_flow | Terpenuhi |
| RSVP & ucapan tersimpan di server, moderasi, ekspor CSV/XLSX aman formula | `InvitationController::postRsvp/postWish`, `MemberController::export`, `Spreadsheet::safeCell` | T5, T7 | Terpenuhi |
| Slug, alias 301, ICS kalender | `InvitationService::changeSlug`, `slug_aliases`, `InvitationController::ics` | — | Terpenuhi (tanpa tes otomatis khusus) |

## F. WhatsApp melalui OpenWA

| Kebutuhan | Implementasi | Bukti | Status |
|---|---|---|---|
| Kontak manual + impor XLSX/CSV (pemetaan, validasi, dedupe, baris ditolak) | `Service/ContactService`, `Service/Spreadsheet`, `Http/WaController` | T7, B:qa_wa, F:04 | Terpenuhi |
| Berkas contoh kontak tanpa nomor nyata | `/unduh/TEMPLATE_KONTAK_KABARNIKAH.{xlsx,csv}`, berkas di paket | — | Terpenuhi |
| Teks pesan per member, variabel daftar-putih, 3 preset | `Service/MessageTemplate`, `member_message_settings` | T7, B:qa_wa | Terpenuhi |
| Koneksi WA per member terisolasi; API key tidak ke browser | `WaService::connect/status/disconnect`, `OpenWAAdapter` | T8, B:qa_wa, F:05 | Terpenuhi |
| Antrean persisten, jadwal, jeda, pause/resume/cancel, retry aman, kuota, pengecualian | `WaService::claim/report/control/retry/suppress`, `worker/kabarnikah-openwa-worker.mjs`, `bin/wa-dispatch.php` | T8, uji worker | Terpenuhi |
| Status submitted/delivered/read/failed/unknown berdasarkan bukti | `WaService::webhook/applyAck` (HMAC, dedupe, monoton) | T8 | Terpenuhi |
| Integrasi OpenWA diverifikasi pada versi terpasang | `OpenWAAdapter` diuji ke OpenWA v0.30.0 asli | LAPORAN_PENGUJIAN §3 | Terpenuhi (tanpa akun WA nyata) |
| Deployment OpenWA/worker + panduan, tidak memalsukan kebutuhan host | `KabarNikah_OpenWA_Worker.zip`, `PANDUAN_OPENWA.md` | — | Terpenuhi; **pengiriman nyata belum diuji** |

## G. Admin & operasional

| Kebutuhan | Implementasi | Bukti | Status |
|---|---|---|---|
| Ringkasan, produk/harga/cover/lisensi, member & hak, undangan, pesanan, konten, pengaturan, WA, dukungan, audit, email, cadangan, sistem | `Http/AdminController`, `views/admin/*` | B:qa_admin (14 halaman 200), T9, F:01 | Terpenuhi |
| Rahasia terenkripsi & tersamarkan | `Core/Support::Settings` (sodium) | T6 | Terpenuhi |
| Cadangan & pemulihan aman | `Service/BackupService` | T9 | Sebagian (penukaran lintas server belum diuji) |
| Pemeriksaan sistem + uji akses storage | `Service/SystemCheck` | T9 | Terpenuhi |

## H. Instalasi, keamanan, kualitas

| Kebutuhan | Implementasi | Bukti | Status |
|---|---|---|---|
| Wizard instalasi satu kali dengan kode bootstrap, tanpa admin bawaan | `Http/InstallController` | T1 | Terpenuhi |
| Storage terlindungi, folder privat di luar docroot | `public_html/index.php`, `.htaccess`, `SystemCheck::storageProbe` | T2 (traversal), PANDUAN_INSTALASI | Terpenuhi (verifikasi di server tujuan) |
| CSP ketat, header keamanan, tanpa eval | `Core/App::finalize/csp`, patch lax.min.js | T2 | Terpenuhi |
| Migrasi otomatis saat pembaruan | `Core/App::upgradeIfNeeded` | dipakai saat migrasi 002 | Terpenuhi |
| Aksesibilitas (label, fokus, skip link, reduced motion) & responsif 360–1920 px | `views/*`, `app.css` | B:qa_viewports (0 overflow) | Terpenuhi (tanpa audit pembaca layar) |
| Paket dapat dipasang dari ZIP pada instalasi bersih | `KabarNikah_Siap_Upload.zip` | LAPORAN_PENGUJIAN (uji instal bersih dari ZIP) | Terpenuhi (server PHP bawaan) |
