# Bug Sweep Report — Ronde 2 (keluargamart, semua app) Tanggal: 2026-09-29. Fokus ronde ini: (1) audit kode yang baru berubah di ronde perbaikan sebelumnya, (2) re-verifikasi status 79 temuan ronde 1, (3) modul yang belum tersentuh. Baseline: backend `go test ./...` + `go vet` green saat commit `d7b6580`; frontend ERP 383/383, B2B 136/136 + typecheck clean, POS 35/35. **Ringkasan: 6 temuan baru (1 P1, 5 P2) + 1 catatan minor. Sekitar 20 temuan ronde 1 ternyata SUDAH tertutup di kode meski laporan lama mengatakan sebaliknya; daftar rekonsiliasi ada di bawah.** --- ## Temuan baru ### R2-1 (P1, backend) — B2B orders halaman 2+ rusak: `per_page` kumulatif dipotong clamp 100 - Di mana: frontend `keluargamart-b2b-frontend/app/services/http/repos.ts:1621` vs backend `keluargamart-backend/internal/sales/service.go:1538` (`MyOrders`). - Pola pagination kumulatif multi-status mengirim `per_page = pageSize × page` (mis. halaman 3 × 20 = 60… dst). Backend `MyOrders` menjepit `if f.PerPage < 1 || f.PerPage > 100 { f.PerPage = 20 }` → untuk halaman ≥ 6 (per_page > 100) window terpotong ke 20 baris per status, slice `((page-1)*20, page*20)` menghasilkan baris salah / halaman kosong meski data ada. - Perbaikan disarankan (pilih satu): - Frontend: batasi jendela, atau fetch per-status halaman sesungguhnya dan gabung dua arah; atau - Backend: naikkan clamp untuk route `/me` (mis. clamp 500 khusus e-commerce buyer), atau dukung `page` + `per_page` per-status dan merge di frontend. - Catatan: clamp sama ada di 4 tempat (`ListOrders` :288, `MyOrders` :1538, `ListSalesVouchers` :2861, `ListReturns` :3188) — hanya MyOrders yang kena pola kumulatif frontend saat ini. ### R2-2 (P2, backend) — Re-verifikasi pembayaran menimpa catatan manusia - Di mana: `keluargamart-backend/db/query/finance.sql` (`SetPaymentReview`) + `internal/finance/service.go`. - `split_part(notes, ' — ', 2)` mengembalikan seluruh string bila delimiter tidak ada. Jadi notes lama `[Verified] ...` TANPA catatan manusia, ketika di-reject lalu diverifikasi ulang tanpa catatan, menghasilkan `[Verified] ... — ` (ekor `- — ` yatim). Kosmetik; perbaikan satu baris: trim hasil split bila kosong/duplicate. ### R2-3 (P2, backend) — `PayablesAging` masih menghitung PO draft/approved - Di mana: `db/query/finance.sql` (`PayablesAging`/`SumPayablesAging`) / `internal/finance/service.go`. - Belum ada filter status PO (`received` saja) sehingga hutang usaha melebihi realisasi. (Masih terbuka dari ronde 1; dikonfirmasi ulang tetap ada.) ### R2-4 (P2, backend) — `cart PATCH/DELETE` tanpa cek rows-affected (tetap terbuka) - `db/query/cart.sql` `UpdateCartItemQuantity`/`DeleteCartItem` = `:exec`, di-gen `_, err := Exec` dan dibuang. PATCH kuantitas / DELETE item untuk kombinasi (cart_id, product_id) yang tidak ada menjawab sukses. Sebaiknya `:execrows` + `ErrNotFound` bila 0 baris. ### R2-5 (P2, B2B) — Stub profil/pengguna live mode (tetap terbuka, dikonfirmasi) - `repos.ts:1221-1226`: `getCustomer: async () => null`, `updateProfile: notImplemented`, `getUsers: []`, invite/update/remove tidak diimplementasi. Halaman `account/profile.vue` dan `account/users.vue` mati total di live mode; `users.vue` juga tanpa gate admin. Kandidat perbaikan cepat: petakan `getCustomer` ke profil yang sudah tersedia via endpoint lain / perbaiki stub dengan endpoint partners yang ada. ### R2-6 (P2, keamanan/sesi, backend) — Sinkronisasi deaktivasi user (tetap terbuka) - (a) Approval/deaktivasi buyer (partners) hanya menulis `customers.is_active`; login memeriksa `users.is_active` dan token memeriksa `users.is_active` per-request → buyer yang ditolak/dinonaktifkan tetap bisa browse dengan token lama sampai token kedaluwarsa. - (b) Ganti password via admin (system `UpdateUser` with password) tidak mencabut token target — sesi lama tetap hidup setelah password diganti. - Perbaikan: saat kedua perubahan, cabut token tokenable tersebut (dan set `users.is_active` mengikuti `customers.is_active`). ### Catatan minor (tidak diberi nomor bug) - Gate konfirmasi-terima pembeli yang dulu 403 kini memakai `RequireAnyPermission("sales.sales-orders.update", "sales.sales-orders.view")` (main.go:1074) — buyer tidak memegang keduanya → alur "pesanan diterima" pembeli kemungkinan masih 403. Perlu endpoint khusus pembeli atau glob `sales.*` untuk buyer; verifikasi manual perlu akun buyer. --- ## Rekonsiliasi ronde 1 — ternyata SUDAH tertutup di kode (laporan lama basi) Diverifikasi langsung ke kode saat ini: 1. **P0 #2** UpdateUser email-takeover — guard `guardAssignableRole(existing.Role)` ada terlepas dari field yang dikirim. ✔ tertutup 2. **P0 #4** buyer-invoice cross-customer — `custResolver` + ownership check `inv.CustomerID != ownerCustomerID → 404`. ✔ tertutup 3. **P0 #5** CreateReturn cross-tenant — `checkScope(order.StoreID)` kini benar dipanggil. ✔ tertutup 4. **P0 #3** Cancel paid order — kini `ErrPaidCannotCancel`; uang hanya via return/refund. ✔ tertutup 5. **P1 #6** Cash refund tidak masuk drawer — seam `refundDrawer` ada & ter-wire (`salesSvc.WithRefundDrawer(cashierRepo)`), menulis cash_out di shift terbuka. ✔ tertutup 6. **P1 #8** Galon pending forever — POS kini auto-`/complete`; backend Complete commit payment atomik (`WithPayments`). ✔ tertutup (unit_price masih client-supplied → tetap jadi P2 kecil bila ingin server-authoritative) 7. **P1 #9/#10** PayDebt guard + VerifyPayment tulis nyata — diperbaiki ronde ini (komit `d7b6580`). ✔ tertutup 8. **P1 #11** Bocoran `customer_id <= 0` — guard kosong ada di MyInvoices, MyReceivables, MyDeliveryOrders; MyPayments ber-guard di service. ✔ tertutup 9. **P1 #16/#17** pos-receipt/voucher-print decimal — `toNumber` normalisasi string/float dipakai. ✔ tertutup 10. **P1 #14** AddStoreUser tanpa cek role — `guardAssignableRole(target.Role)` ada. ✔ tertutup 11. **P1 #23** POS undo crash + receipt prefs — sudah diperbaiki ronde ini (komit POS `3766644`). ✔ tertutup 12. **P1 #20/#21/#22** B2B mapPayment/orderId, pagination multi-status, harga galon — diperbaiki ronde ini (komit B2B `a1eb149`)… dengan catatan R2-1 di atas. 13. **P1 #25** ERP bulk-approve swallow — kini menghitung sukses/gagal per order dan menamai order yang gagal. ✔ tertutup 14. **P2 POS** Kas Masuk/Kas Keluar export — kini membaca `cashIn/cashOut` hasil query batch `SumDrawerByShifts` (bukan lagi 0). ✔ tertutup ## Masih terbuka dari ronde 1 (belum diverifikasi tertutup, prioritas turun) - **P1 #12** sinkronisasi deaktivasi buyer (lihat R2-6a). - **P1 #13** admin password change tidak mencabut sesi (R2-6b). - **P1 #15** `employeeLookupFn` fallback "first active employee" (main.go ~:723) — masih memungkinkan kebocoran data antar-employee di /me endpoints. - **P2 lain**: galon `/galon/status` hardcoded; double-open-DO; idempotency regenerasi key per request (double-click); register email `contains("@")`; settings cache; dsb. (daftar lengkap di ronde 1). ## Saran urutan perbaikan ronde berikut 1. R2-1 (pagination kumulatif vs clamp 100) — bug fungsional nyata di halaman order B2B. 2. R2-6 (pencabutan token saat deaktivasi/ganti password) — dampak keamanan langsung. 3. R2-4 (cart rows-affected) + R2-3 (payables aging status) — kecil, murah. 4. #15 employee fallback → fail-closed. 5. R2-2 kosmetik notes split. --- ## Rekonsiliasi 2026-09-30 (verifikasi kode, bukan asumsi) Laporan ini ditulis dari baseline `d7b6580`, sebelum beberapa perbaikan yang kemudian masuk. Lima dari enam temuan **sudah tertutup**; status aslinya di atas salah dan jangan dipakai untuk memprioritaskan pekerjaan. | Item | Laporan bilang | Kenyataan di kode | Commit | |---|---|---|---| | R2-1 | terbuka | **tertutup** — `MyOrders` clamps `per_page` ke 500, bukan 100; `ListOrders` tetap 100 | `858d4e7` | | R2-2 | terbuka | **tertutup** — `SetPaymentReview` memakai `NULLIF(TRIM(split_part(notes,' — ',2)),'')` sehingga sisa kosong tidak menghasilkan ekor orphan | `600e79c` | | R2-3 | terbuka | **tertutup** — `PayablesAging` (internal/reports/breakdown_repository.go) memfilter `po.status IN ('received','partial')`. Catatan: Laporan menyebut `db/query/finance.sql`, yang salah lokasi — query-nya di package `reports` | `da4e011` | | R2-4 | terbuka | **tertutup** — `UpdateCartItemQuantity`/`DeleteCartItem` sudah `:execrows` dan repository mengembalikan `ErrItemNotFound` bila 0 baris | `ce30c56` | | R2-6 | terbuka | **tertutup** — `DeleteTokensByUser` dipanggil di admin password change; hook `onCustomerDeactivated` (main.go) menonaktifkan `users` + mencabut token | `2d62046` | | R2-5 | terbuka | **benar terbuka**, tapi terbelah — lihat di bawah | — | R2-6 sudah tertutup 13 jam sebelum laporan ini ditulis, jadi "tetap terbuka" di atas berasal dari baseline yang usang, bukan dari pemeriksaan ulang. **R2-5 terbuka sepenuhnya, dan bukan perbaikan cepat.** Catatan awal di atas ("kandidat perbaikan cepat: petakan `getCustomer` ke profil yang tersedia") tidak akan berhasil, dan alasannya tiga, bertumpuk: 1. **Endpoint yang ada tidak yang tepat.** `GET /me` mengembalikan `user`, sedangkan halaman profil butuh record **customer**. Keduanya kebetulan sama-sama punya `name`/`email`, tapi bukan hal yang sama. 2. **Buyer tidak boleh membaca direktori pelanggan, dan itu disengaja.** `/customers/*` dijaga `ecomOrPermGate("masterdata.customers.read")` dengan komentar eksplisit bahwa direktori itu master data PII dan pembeli storefront tidak boleh menombaknya. Tidak ada `GET /customers/me` — jadi perbaikannya harus menambah endpoint sendiri, bukan memetakan yang ada. 3. **Tiga dari enam field tidak punya kolom.** `account/profile.vue` membaca `website`, `billingAddressId` dan `shippingAddressId`; tabel `customers` tidak punya satu pun dari ketiganya. Halaman itu ditulis terhadap model data yang belum pernah ada, jadi mapping seadanya akan tetap meninggalkan form kosong. Artinya R2-5 adalah pekerjaan fitur (skema + API + UI) dari awal, dan harus dianggap sebesar itu — bukan ditambal sambil menyalin `getCustomer` ke `/me`. ### Ronde 1 yang juga sudah tertutup - **P1 #12 / R2-6a** — sinkronisasi deaktivasi buyer: tertutup di `2d62046`. - **P1 #13 / R2-6b** — admin password change mencabut sesi: tertutup di `2d62046`. - **P1 #15** — `employeeLookupFn` fallback "first active employee": sudah fail-closed (`23bde14`) **dan** jalur yang memakainya kini dibatasi RBAC (`7837538`). Catatan penting: `/galon/status` yang disebut di bawah sudah diperbaiki di `a9c3f38` — angka hardcode 45/12/3 diganti hitungan nyata dengan `diToko` = `null` karena tidak ada registri unit galon. **Yang benar-benar masih terbuka setelah rekonsiliasi ini:** R2-5 (fitur, bukan bug), idempotency key yang diregenerasi per request, register email `contains("@")`, cache settings, koordinat warehouse (butuh data operator).