================================================================================ CATATAN RILIS & PEMBARUAN SISTEM KELUARGAMART (VERSI 1.5.0) ================================================================================ Tanggal rilis dokumentasi : 1 November 2026 Status : Terverifikasi di stack lokal (unit, sweep, dan alur data end-to-end). Bukan pernyataan bahwa perubahan sudah berjalan di server produksi. Cakupan : apps/backend (Go), apps/erp, apps/pos, apps/b2b, packages/contracts, packages/utils Migrasi database : 000140-000146 (total 146 migrasi) Kontrak API : 395 route, 207 permission (drift check bersih) -------------------------------------------------------------------------------- RINGKASAN -------------------------------------------------------------------------------- Rilis 1.5.0 menuntaskan satu keluarga bug yang berakar sama: pesanan e-commerce memiliki DUA mode pemenuhan (ambil sendiri di toko dan kirim ke alamat), tetapi diperlakukan sebagai satu alur yang seragam. Akibatnya daftar kasir memuat pesanan kurir, daftar gudang memuat pesanan pickup, pesanan pickup dibuatkan Delivery Order otomatis, dan serah terima di kasir tidak pernah berhasil. Rilis ini juga mengembalikan verifikasi manusia pada pesanan yang sudah dibayar, menghentikan penimpaan status "delivered" menjadi "invoiced", mengganti seluruh angka dashboard dari proksi menjadi agregat tabel yang sama dengan rinciannya, menampilkan diskon member pada detail sales order, dan membuka layar kasir "Ambil Toko" yang sebelumnya tidak pernah bisa diakses. -------------------------------------------------------------------------------- 1. PEMISAHAN MODE PEMENUHAN: AMBIL DI TOKO vs KIRIM KE ALAMAT -------------------------------------------------------------------------------- • Masalah Sebelumnya: Checkout menulis penanda "[Ambil Sendiri di Toko] ..." ke shipping_address, tetapi seluruh daftar dan filter menyaring memakai order_type saja. Karena pesanan e-commerce adalah KEDUA mode sekaligus, order_type tidak pernah bisa memisahkannya. Akibat yang dilaporkan pengguna: - Pesanan kirim ke alamat muncul di daftar "Ambil Toko" kasir. - Pesanan ambil di toko muncul di antrean "Packing Order" gudang. - Pesanan ambil di toko dibuatkan Delivery Order otomatis saat dikonfirmasi, sehingga tampil "Sedang Diantar" di peta dispatch. - Tombol "Serahkan Barang" di kasir selalu gagal. • Solusi: - Satu predikat kanonik isPickupOrder (backend Go) yang membaca penanda shipping_address, dipakai bersama oleh daftar kasir, filter gudang, dan pengecekan transisi status. - Daftar kasir memakai filter pickup_only di SQL (bentuk positif), daftar Packing Order memakai exclude_pickup (bentuk negatif). Keduanya diterapkan di dalam query, sebelum LIMIT/OFFSET, sehingga paging dan total konsisten. - onOrderConfirmed melewati pesanan pickup: tidak ada Delivery Order otomatis untuk barang yang diserahkan di kasir. - Halaman pesanan B2B menyembunyikan tombol "Lacak Pengiriman", stepper kurir, dan kartu Dokumen untuk pesanan pickup. • Penanda yang sama dipakai tiga konsumen dan dijaga identik: - apps/backend/internal/sales/service.go -> isPickupOrder - apps/b2b/app/utils/paymentMethods.ts -> isPickupOrder - apps/backend/db/query/sales.sql -> pola regex '[[]ambil sendiri|[]]ambil di toko|\[ambil sendiri' • Commit: 8a23628, 57c0271, b278bb6, 3a71081, 020f740, 7d75a63 • Pengujian: - Unit: pickup muncul dan delivery tersaring pada daftar kasir; exclude_pickup menyisakan delivery pada daftar gudang. - Live (stack lokal), satu pesanan pickup dan satu pesanan kurir nyata: Daftar Ambil Toko kasir : pickup tampil, kurir TIDAK tampil Daftar Packing Order : kurir tampil, pickup TIDAK tampil Delivery Order otomatis : pickup 0 DO, kurir 1 DO Halaman B2B : pickup tanpa lacak/stepper/dokumen -------------------------------------------------------------------------------- 2. VERIFIKASI MANUSIA: PEMBAYARAN TIDAK LAGI MENERIMA PESANAN -------------------------------------------------------------------------------- • Masalah Sebelumnya: SettleOrderPayment memajukan status pending_approval menjadi invoiced begitu pembayaran Midtrans lunas. Pesanan yang dibayar otomatis dianggap diterima, melewati verifikasi manusia sepenuhnya. • Solusi: - pending_approval TIDAK lagi ada dalam daftar status yang dimajukan oleh pelunasan. Uang tercatat; status tetap "Menunggu Konfirmasi". - Confirm tetap menjadi satu-satunya jalan penerimaan dan mencatat approved_by (siapa yang menerima). Status confirmed/draft tetap boleh maju; status fulfillment tidak pernah mundur. • Commit: 8a23628 • Pengujian: pesanan yang sudah dibayar penuh tetap pending_approval; Confirm pada pesanan tersebut berhasil dan mencatat approver. -------------------------------------------------------------------------------- 3. SERAH TERIMA DI KASIR (POS) BERFUNGSI TANPA MEMBUKA CELAH -------------------------------------------------------------------------------- • Masalah Sebelumnya: Dua cacat berlawanan arah: - Transisi pending_approval -> delivered ditolak ("invalid order status"), sehingga tombol "Serahkan Barang" tidak pernah berhasil. - Cabang fast-forward lama menerima KASIR untuk pesanan apa pun yang sudah confirmed/invoiced/paid, sehingga pesanan kurir bisa dilompatkan langsung ke delivered dari meja kasir (terbukti HTTP 200 saat verifikasi). • Solusi: - Lompatan pending_approval -> delivered diizinkan HANYA untuk pickup tunai: isCashierHandover && isPickupOrder && isCashOrder. - Fast-forward ke delivered dibatasi untuk warehouse/admin/owner. Kasir tidak lagi dapat melewati rantai picking -> shipped pada pesanan kurir. - Pelunasan COD saat serah terima memakai seam SettleOrderPayment yang sama dengan pelunasan Midtrans, menjaga fake store dan SQL tetap sinkron. • Commit: 8a23628, 915f525 • Pengujian: - Kasir menyerahkan pickup tunai dari pending_approval -> delivered, lunas. - Kasir mencoba delivered pada pesanan kurir -> ditolak. - Pesanan transfer yang belum lunas -> ditolak. -------------------------------------------------------------------------------- 4. STATUS TERMINAL: "DELIVERED" TIDAK LAGI DITIMPA MENJADI "INVOICED" -------------------------------------------------------------------------------- • Masalah Sebelumnya: Setiap serah terima berakhir dengan status "invoiced" dan sebuah invoice terbuka hantu. Penelusuran menunjukkan AdvanceFulfillment memicu hook onConfirm untuk SETIAP transisi, dan hook tersebut membuat invoice yang menulis status invoiced tanpa syarat — sehingga menimpa delivered yang baru saja ditulis. Unit test lolos karena fake store tidak memiliki hook. • Solusi: - onConfirm hanya menyala ketika pesanan benar-benar mencapai confirmed. - CreateInvoice hanya menstempel invoiced bila pesanan masih pada status pra-fulfillment (draft / pending_approval / confirmed). Konfirmasi yang datang terlambat tidak lagi menarik pesanan mundur. - DeleteInvoice menangani konsekuensinya: invoice yang dibuat tanpa stempel status dapat dihapus bersih tanpa mengubah status pesanan. • Commit: 504da82 • Pengujian: serah terima kasir berakhir delivered dengan kas lunas; pembuatan invoice pada pesanan delivered tidak menulis status maupun riwayat. -------------------------------------------------------------------------------- 5. DASHBOARD: SETIAP KARTU DIHITUNG DARI TABEL YANG SAMA DENGAN RINCIANNYA -------------------------------------------------------------------------------- • Masalah Sebelumnya (dilaporkan pengguna: "angka dashboard dari mana, detailnya 0 semua"): - Penjualan Hari Ini = MonthSales / 30 (bukan hari ini) - Order Aktif = len(RecentActivity) (10 aktivitas, bukan order) - PO Aktif = konstanta 2 (angka literal) - Net Cashflow = MonthSales - MonthPurchase (bukan arus kas) - Piutang = agregat nyata, tetapi handler tidak pernah mengisi pendingInvoices -> modal selalu kosong - Modal PO Aktif = tidak pernah diisi -> selalu kosong • Solusi: - Setiap angka menjadi agregat nyata dari tabelnya sendiri. - Setiap kartu yang dapat diklik menyajikan baris yang mendasarinya, dengan SATU predikat dipakai bersama oleh COUNT dan daftar (activeOrderStatuses). - Drilldown baru: activeOrders, todaySales, pendingInvoices, activePOs. - Modal Net Cashflow melaporkan bulan berjalan (sama dengan kartu) dan menyimpan total 12 bulan sebagai tren berlabel. • Commit: 0a14d89, ba36507 • Hasil verifikasi live (kartu = rincian): Order Aktif 17 = 17 baris Penjualan Hari Ini 252.300 = jumlah baris 252.300 Piutang 307.520 = jumlah baris 307.520 PO Aktif 10 = 10 baris Net Cashflow 120.000 = net bulan berjalan 120.000 • Regresi: TestDashboard_SummaryMatchesDrilldown menyajikan satu store melalui kedua endpoint dan mengunci kelima pasangan, termasuk array yang tidak kosong. -------------------------------------------------------------------------------- 6. FILTER DAFTAR ORDER: TANGGAL DAN CAKUPAN TOKO BENAR-BENAR DITERAPKAN -------------------------------------------------------------------------------- • Masalah Sebelumnya: ListOrders mengirim search/status/type ke query baris, tetapi TIDAK mengirim date_from, date_to, store_id, dan allowed_store_ids — padahal COUNT memakai semuanya. Akibatnya: - Pemilih tanggal tidak berpengaruh (baris dari semua bulan tetap kembali). - Pemanggil yang dibatasi toko menerima baris toko lain pada payload, sementara totalnya tetap ter-scope. Ini melanggar aturan isolasi tenant meskipun predikat SQL-nya sudah ada. • Solusi: query baris kini menerima filter yang persis sama dengan COUNT. • Commit: 3a71081 • Pengujian live: ?date_from=2026-11-01&date_to=2026-11-01 -> hanya baris tanggal tersebut ?store_id=1 -> hanya baris toko 1 -------------------------------------------------------------------------------- 7. LAYAR KASIR "AMBIL TOKO" AKHIRNYA TERKIRIM -------------------------------------------------------------------------------- • Masalah Sebelumnya: Halaman Ambil Toko sudah lengkap ditulis (daftar, dialog serah terima, struk), tetapi TIDAK dapat dibuka pada build mana pun: tidak ada entri router, tidak ada menu sidebar, dan memanggil dua method service yang tidak ada (orderService.forPickup dan completePickupHandover). Setiap mount akan error. Tidak ada unit test yang bisa menangkapnya karena komponennya tidak pernah di-mount. Sementara itu backend dan filternya sudah aktif, sehingga satu- satunya cara menyerahkan barang adalah lewat modul Packing Order ERP — justru jalur lintas-mode yang dilarang. • Solusi: - Rute /pickup dan menu sidebar di bawah permission sales.pos.create. - orderService.forPickup membaca /sales-orders/for-pickup dan mempertahankan status mentah, alamat pickup, dan catatan yang dirender layar. - orderService.completePickupHandover mengirim PATCH status delivered; server tetap yang memutuskan (pickup tunai dari pending_approval saja, transfer wajib lunas lebih dulu). - Jalur negatif terlihat: pickup transfer yang belum lunas diberi badge "Menunggu Transfer", tanpa tombol Serahkan, dengan penjelasan di dialog — bukan dibiarkan memicu penolakan server. - Dialog serah terima memakai v-model pada komponen yang mengharapkan v-model:open, sehingga modal tidak pernah terbuka; diperbaiki untuk kedua dialog (serah terima dan struk). • Commit: 7df125b • Pengujian (Playwright, 15/15): menu -> /pickup; pickup tampil, delivery tidak; badge COD vs transfer; blocker transfer; dialog terbuka; kembalian dihitung; struk muncul; pesanan berpindah ke tab Selesai; tanpa error halaman. -------------------------------------------------------------------------------- 8. BUKTI FOTO PENERIMAAN BARANG (POD) DAN BUKTI TERIMA PEMBELI -------------------------------------------------------------------------------- • Masalah Sebelumnya: Panel "Bukti Foto Penerimaan Barang (POD)" dan badge "Foto POD" di ERP membaca field proof_photo yang tidak pernah diproduksi query mana pun, tidak punya kolom, dan tidak punya endpoint — panel itu selamanya hanya bisa menampilkan "Belum Ada Foto POD". • Solusi: - Migrasi 000143: delivery_orders memperoleh proof_photo_url dan recipient_name. POST /delivery-orders/{id}/deliver menerima body multipart OPSIONAL (foto + nama penerima) yang disimpan melalui pipeline unggah bersama (maks 5 MB, validasi tipe gambar, kunci proof-), di dalam transaksi penyelesaian sehingga bukti tersimpan atomik dengan status yang dibuktikan. Panggilan tanpa body berperilaku persis seperti sebelumnya. - Migrasi 000142: sales_orders memperoleh received_proof_url, dengan PATCH /sales-orders/{id}/received-proof untuk bukti terima dari pembeli. Bersifat bukti, bukan gerbang: kegagalan unggah tidak membatalkan konfirmasi, tetapi pembeli diberi tahu. Kepemilikan diperiksa terhadap customer pesanan tersebut, dan pesanan terminal menolak penulisan. - internal/uploads memperoleh SaveFileHeader agar pemanggil yang sudah memegang header multipart memakai aturan penyimpanan yang sama. • Commit: 020f740, 8a23628 • Pengujian: unggah -> simpan -> baca kembali pada stack lokal; penolakan kepemilikan, URL eksternal, dan pesanan terminal. -------------------------------------------------------------------------------- 9. CHECKOUT B2B: TIDAK ADA LAGI 500 GENERIK -------------------------------------------------------------------------------- • Masalah Sebelumnya: Checkout menampilkan "Something went wrong". Lima sentinel sales tidak ada di tabel pemetaan: ErrUnpaidOrderFulfillment, ErrFulfillmentForbidden, ErrPaymentNotAllowed, ErrPaidCannotCancel, ErrGatewayDisabled. • Solusi: dipetakan mengikuti status internal/sales sendiri — 409 pelanggaran bisnis, 403 izin fulfillment, 400 aturan metode pembayaran, 503 gateway mati dengan pesan tersamar (tidak pernah menampilkan kunci konfigurasi). Sebuah test tabel mengunci seluruh 15 sentinel sales agar tidak ada yang kembali menjadi 500. • Commit: 66724db • Pengujian live: checkout dengan voucher tidak valid mengembalikan 409 "voucher is not usable: ... not found". -------------------------------------------------------------------------------- 10. TAMPILAN: DISKON MEMBER DAN DETAIL SALES ORDER -------------------------------------------------------------------------------- • Masalah Sebelumnya: Detail sales order menampilkan Harga 15.000 dan Subtotal 14.700 dengan Diskon 0 — karena diskon member disimpan pada kolom terpisah (membership_discount_amount) dan tidak pernah dirender. Aritmatika di layar tidak menutup, dan dilaporkan pengguna sebagai bug harga. • Solusi: - Kolom Diskon menampilkan diskon baris + diskon member, dengan tooltip tier, serta catatan "Termasuk diskon member ..." di bawah tabel. Tidak ada uang yang diubah: nilai tersimpan sudah benar, tampilannya yang belum lengkap. - Panel "Status Pengiriman & Distribusi" dan tombol "Buka di Modul Packing Order" dihapus dari halaman sales order (proses gudang milik modul Packing Order). "Batalkan Order" dipindahkan ke header agar tidak hilang. - Nomor SO pada modal dashboard membaca so_number (sebelumnya order_number, sehingga setiap baris tampil "#SO-1"). - Status baca notifikasi POS disimpan per id (tidak lagi kembali "belum dibaca" setiap reload); cache PWA dinaikkan ke km-pos-v4. • Commit: 1321ff4, ba36507, aacb4cb -------------------------------------------------------------------------------- 11. GERBANG PENGUJIAN BARU (AGENTS.md) -------------------------------------------------------------------------------- Empat kelas cacat dari rilis ini diubah menjadi gerbang yang dapat diperiksa: 7-A Pemisahan mode pemenuhan (pickup vs delivery) per permukaan. 7-B Pemeriksaan UI-orphan: field harus punya sumber, dan halaman tanpa rute dianggap belum terkirim. 7-C Status terminal & hook efek samping: hook milik transisinya, status tidak boleh mundur, uji keadaan akhir setelah hook. 7-D Aritmatika uang harus reconcile di layar. 8-E Satu predikat, satu jendela: kartu dan rincian wajib berbagi predikat. 8-F Verifikasi artefak yang berjalan (bukan komentar di binary). Peta sumber data dashboard (kartu -> endpoint -> query -> drilldown) juga ditambahkan pada docs/development/testing-strategy.md. -------------------------------------------------------------------------------- 12. HYDRATION B2B, COOKIE LOGIN, DAN VALIDASI VOUCHER -------------------------------------------------------------------------------- • Masalah Sebelumnya: - Setiap deep link tamu ke halaman terproteksi memicu peringatan "Hydration completed but contains mismatches". Penyebabnya: middleware auth hanya mengalihkan di sisi klien, sehingga server sudah mengirim halaman terproteksi lalu klien berpindah ke /login di tengah hidrasi. - Login B2B di origin http "berhasil" tetapi tidak menyimpan sesi; cookie token dibuang browser karena atribut secure mengikuti NODE_ENV. - Memasukkan kode voucher yang tidak ada menjawab HTTP 404, yang terbaca seperti endpoint rusak, bukan sebagai jawaban validasi. • Solusi: - Middleware auth dan admin memakai cookie token sebagai fakta auth yang tersedia saat SSR: tanpa cookie -> redirect 302 sebelum halaman terproteksi dikirim; dengan cookie -> render dan klien memulihkan sesi. Atribut juga dipasang lewat htmlAttrs agar SSR dan klien menghasilkan pohon yang sama. - Atribut secure pada cookie mengikuti protokol origin sebenarnya: https tetap Secure, http tidak. - Validasi voucher yang tidak dikenal menjawab 200 {valid:false, reason}; kegagalan lain (mis. timeout) tetap memakai status aslinya. • Commit: fdfbd53, a16330d, 540be14 • Pengujian: - 6 deep link tamu (/account/orders, /cart, /quick-order, /payments, /account/membership, /admin): 0 peringatan hidrasi, redirect di server. - Login nyata -> cookie non-Secure -> /account/orders bertahan setelah reload penuh, tanpa peringatan. - Deep sweep 74/74 dengan 0 uncaught JS (sebelumnya 5 peringatan hidrasi). -------------------------------------------------------------------------------- 13. MEMBERSHIP: AKUN PEMBELI MULAI DARI BRONZE -------------------------------------------------------------------------------- • Kebijakan: Akun pembeli (baik perseorangan maupun bisnis) mulai dari tingkat Bronze sehingga langsung mendapatkan benefit dasar membership. • Solusi: - Pendaftaran akun buyer otomatis meng-enroll tier Bronze (000141, 000145). - Dukungan akun reguler (non-member) tetap dipertahankan pada kode frontend dan backend sebagai fallback aman jika ada akun tanpa membership. • Commit: 000141, 000145 -------------------------------------------------------------------------------- 14. PICKER PRODUK TRANSFER ERP -------------------------------------------------------------------------------- • Masalah Sebelumnya: form transfer memuat satu halaman berisi 200 produk, sehingga produk ke-201 dan seterusnya tidak pernah bisa ditransfer tanpa pesan apa pun. • Solusi: KProductSelect mencari produk ke API (server-side), sehingga seluruh katalog dapat dipilih tanpa memuat semuanya ke browser. • Commit: a16330d, a749b5c -------------------------------------------------------------------------------- 15. NOTIFIKASI: STATUS BACA BERTAHAN SETELAH RELOAD (ERP & B2B) -------------------------------------------------------------------------------- • Masalah Sebelumnya: daftar notifikasi kembali menampilkan entri yang sudah dibaca setiap halaman dimuat ulang, sehingga lonceng terus menyala. • Solusi: read-state disimpan per id di localStorage dan dipakai bersama oleh store notifikasi ERP/B2B (POS sudah lebih dulu memakai pola yang sama). • Commit: 6fed8d1 -------------------------------------------------------------------------------- 16. PERSETUJUAN ORDER: SIAPA YANG BISA MENYETUJUI -------------------------------------------------------------------------------- • Masalah Sebelumnya: Setelah "Simpan & Ajukan Order", pesanan berstatus Menunggu Persetujuan dan antreannya ada di Sales -> Sales Order. Namun antrean itu hanya terlihat oleh admin/owner: role_permissions adalah satu-satunya sumber matriks izin (nol baris = deny-all) dan TIDAK ADA role yang memegang sales.sales-orders.confirm. GM, BM, dan SPV — pihak yang disebut layar sebagai "atasan/manajemen" — tidak pernah melihat banner maupun tombol Setujui. Di sisi lain, halaman Packing Order menampilkan tombol Setujui tanpa cek izin, sehingga peran gudang mendapat tombol yang endpoint-nya menjawab 403. • Solusi: - Migrasi 000146 memberikan sales.sales-orders.confirm kepada gm, bm, spv. - Sales sengaja TIDAK diberi: pemohon tidak boleh menyetujui ordernya sendiri (aturan pemisahan tugas yang sama dengan 000054 untuk PO). Gudang tetap view-only. - Tombol Setujui di Packing Order kini memakai izin yang sama dengan endpoint-nya. • Commit: 6349751 • Pengujian live: gm/bm/spv memiliki izin; sales/store_manager/warehouse tidak; admin tetap melihat antrean dan kartu "Persetujuan Atasan" di detail order. -------------------------------------------------------------------------------- 17. POS AMBIL TOKO: METODE PEMBAYARAN DITAMPILKAN -------------------------------------------------------------------------------- • Masalah Sebelumnya: Kartu Ambil Toko tidak menampilkan metode pembayaran. Satu-satunya isyarat adalah tebakan "apakah catatan berbunyi cash?", dan semua order lain dilabeli "Menunggu Transfer" — sehingga order QRIS/gateway yang belum lunas terbaca seolah menunggu transfer bank. • Solusi: - Backend mengekstrak penanda checkout "[Metode: …]" satu kali (paymentMethodFromNotes) dan mengirim payment_method pada payload sales order; baris lama tanpa penanda tetap memakai fallback isCashOrder. - Kasir melihat metode sebenarnya: Tunai di Kasir (COD), Transfer Bank, atau QRIS / VA / E-Wallet, beserta status Lunas / Belum dibayar — di kartu maupun di dialog serah terima. - Tombol Serahkan Barang tetap hanya untuk order tunai atau yang sudah lunas. • Commit: 6b1c9bf • Pengujian: 12 kasus parsing di backend; label dan mapper di POS; verifikasi live pada order gateway, transfer, dan tunai. -------------------------------------------------------------------------------- 18. B2B: TOMBOL DAFTAR DAN FEED NOTIFIKASI -------------------------------------------------------------------------------- • Masalah Sebelumnya: - Tombol "Daftar" tidak bereaksi. Aturan wajib menyetujui syarat dan ketentuan tidak dibungkus UFormField, sehingga tidak ada tempat untuk menampilkan pesan: tidak ada request terkirim dan tidak ada pesan apa pun. Akun tidak pernah dibuat — karena itu login dengan kredensial tersebut selalu "salah". - Feed notifikasi B2B memanggil `ORDERS` yang tidak pernah didefinisikan (konstanta yang benar `SALES_ORDERS`), sehingga setiap pemanggilan melempar ReferenceError dan ditelan catch; pembeli tidak melihat notifikasi. • Solusi: - Kotak centang syarat dibungkus UFormField; pesan "Anda harus menyetujui syarat dan ketentuan" tampil dan request dikirim setelah dicentang. - Referensi diperbaiki ke SALES_ORDERS; typecheck B2B bersih kembali. • Commit: 89848bd • Pengujian UI: tanpa centang -> 0 request + pesan tampil; dengan centang -> 201 dan langsung masuk; kata sandi salah -> pesan "Email atau kata sandi yang Anda masukkan salah". -------------------------------------------------------------------------------- 19. WIDGET CASHFLOW: LABEL BULAN MENGIKUTI DATA -------------------------------------------------------------------------------- • Masalah Sebelumnya: Grafik "Cashflow Bulanan" dan modal rinciannya memberi label bulan dengan daftar tetap Jan–Des. Padahal seri 12 bulan berakhir pada bulan berjalan, sehingga pada 1 November seluruh batang bergeser satu bulan dan penerimaan besar (18jt) tampil sebagai "Des" — padahal itu uang November. • Solusi: - Backend mengirim daftar bulan (YYYY-MM) yang dibangun dari jendela yang SAMA dengan penjumlahan datanya, jadi label tidak bisa lagi menyimpang. - Chart dan modal memformat slot menjadi "Nov 2026", sehingga jendela yang melewati pergantian tahun tetap terbaca benar. - Formatter dipakai bersama dan diuji unit. • Commit: ab15d8d • Pengujian live: baris terakhir "Nov 2026 Rp 18.724.400", sebelumnya "Okt 2026"; tidak ada lagi label "Des 2026" untuk bulan berjalan. -------------------------------------------------------------------------------- 20. PESANAN TANPA METODE PEMBAYARAN TIDAK LAGI MACET -------------------------------------------------------------------------------- • Masalah Sebelumnya: Aturan "harus bayar dulu sebelum dikemas" hanya mengecualikan pesanan non-e-commerce. Akibatnya pesanan e-commerce yang kolom metode pembayarannya kosong dianggap "transfer belum lunas": status tetap Menunggu Pembayaran dan tidak ada jalan untuk mengemas. • Solusi: - Hanya penanda eksplisit transfer / gateway / qris / VA (kolom metode atau catatan) yang menahan pesanan. "Metode tidak tercatat" bukan bukti transfer. - Aturan yang sama dipakai halaman Packing Order, sehingga badge "Menunggu Bayar" dan aksi gudang tidak bertentangan dengan backend. - Form Sales Order ERP tetap punya pilihan metode (Tunai / Transfer / Tempo) supaya kasus kosong tidak terulang. • Commit: df2dfc1 • Pengujian: 13 kasus aturan bayar-di-muka; order tunai e-commerce bisa dikemas tanpa badge, order transfer tetap "Menunggu Bayar". -------------------------------------------------------------------------------- 21. MASTER DATA PRODUK: URUT NAIK/TURUN BERFUNGSI -------------------------------------------------------------------------------- • Masalah Sebelumnya: Klik header "Harga Beli", "Harga Jual", atau "Stok" tidak mengubah urutan. Tabel mengirim sort_by/sort_dir, tetapi endpoint produk hanya memahami kosakata storefront (name, price_asc, price_desc, newest, popularity), sehingga empat kombinasi permintaan mengembalikan halaman yang identik. • Solusi: - Query menghormati sort_by/sort_dir untuk name, code, selling_price, purchase_price, created_at, dan stock, dengan arah per kolom. - Kolom Stok diurutkan memakai ekspresi yang sama dengan nilai yang ditampilkan (jumlah stok tersedia per gudang). - Whitelist terpusat (normalizeProductSort) supaya nilai dari query string tidak pernah menjadi identifier SQL; kolom tak dikenal kembali ke urutan default, dan nilai kosong tetap di bawah pada kedua arah (NULLS LAST). • Commit: f5fb6f2 • Pengujian live: Harga Jual asc 1 → 18.000.000, desc 18.000.000 → 1; Stok asc/desc juga terbalik; di UI klik header menghasilkan [1, 2000, 6000, 8000, 15000, 18000000] lalu kebalikannya. -------------------------------------------------------------------------------- 22. TAMPILAN TABEL ERP: HEADER & GARIS KOLOM -------------------------------------------------------------------------------- • Masalah Sebelumnya: Tabel (mis. Hutang Supplier) tidak punya garis pemisah antar kolom dan header-nya tipis, sehingga baris sulit diikuti saat kolom uang rata kanan bercampur kolom teks rata kiri. • Solusi: - Header menjadi pita tebal (700) dengan latar berbeda dan garis bawah. - Garis kolom tipis di antara semua kolom, tanpa garis pada kolom terakhir. - Baris yang disorot kursor diberi latar berbeda; angka memakai tabular figures agar digit lurus per kolom. - Diterapkan di level komponen tabel bersama (KTable, KDataTable, modal rincian dashboard) sehingga seluruh ERP konsisten. • Commit: d925716 -------------------------------------------------------------------------------- 23. DETAIL UI: SATU TANDA PLUS PER TOMBOL -------------------------------------------------------------------------------- • Masalah Sebelumnya: Tombol "Input Kunjungan Mandiri" di Sales Tracking menampilkan ikon plus sekaligus karakter "+" pada teksnya. • Solusi: Karakter "+" dihapus (ikon sudah menyampaikan makna), dan ditambahkan test yang memeriksa seluruh berkas Vue: tombol tidak boleh memuat ikon plus sekaligus "+" pada labelnya. Test tersebut menemukan tepat satu kejadian di ERP, POS, dan B2B. • Commit: 397af9c -------------------------------------------------------------------------------- 24. OPNAME: SELISIH MENGIKUTI SNAPSHOT STOK -------------------------------------------------------------------------------- • Masalah Sebelumnya: - Baris yang belum dihitung tampil sebagai selisih negatif penuh karena rumusnya COALESCE(fisik, 0) − sistem. Produk dengan stok 9.899 tampil −9.899 tanpa pernah diinput, sehingga lembar yang belum dikerjakan terbaca seolah seluruh barang hilang. - "Terbitkan & Kunci Stok" hanya mengubah status. Mutasi gudang tetap berjalan sehingga stok sistem di layar sudah bergeser — inilah sebab angka tidak cocok. - Salinan di layar menjanjikan pembekuan penuh. • Solusi: - Selisih bernilai kosong sampai baris dihitung, lalu dihitung sekali di converter (bukan di SQL). - Selama opname terbuka, mutasi gudang (stok masuk/keluar manual, transfer, penerimaan barang) ditolak dengan 409 dan pesan yang bisa ditindaklanjuti. Penyesuaian opname sendiri tetap diizinkan agar finalisasi bisa menulis koreksinya. - Penjualan kasir sengaja TIDAK diblokir: opname yang terlantar tidak boleh menghentikan toko berjualan. Selisih akibat penjualan selama hitung tampil sebagai selisih wajar. - Salinan UI diperbaiki menjadi "Kunci Mutasi Gudang" beserta penjelasan bahwa penjualan kasir tetap berjalan. • Commit: 735fb6b • Pengujian: baris belum dihitung tanpa selisih, baris dihitung mendapat selisih tepat, freeze menolak stok masuk (409), dan penjualan POS tetap memotong stok (210 → 209) di gudang yang terkunci. -------------------------------------------------------------------------------- 25. MEMBERSHIP: TIER AWAL BRONZE, ANGKA SAMA DENGAN BACKEND -------------------------------------------------------------------------------- • Masalah Sebelumnya: Halaman Keanggotaan dan ringkasan akun B2B menulis bronze 5%, silver 10%, gold 15%, platinum 20%. Padahal tabel membership_tiers yang dipakai mesin harga berisi bronze 2%, gold 5%, platinum 8% — dan tidak ada jalur kode yang pernah memberikan tier Silver. Pembeli baru membaca "diskon 5%" lalu menerima 2% saat checkout. • Solusi: - Halaman memakai angka yang sama dengan backend (2/5/8), bukan 5/10/15/20. - Entri Silver dihapus dari halaman, tipe TypeScript, dan fallback offline. - Pendaftaran tetap menulis 'bronze' pada membership dan riwayat tier; dua test baru mengunci SQL tersebut agar tidak bisa dinaikkan tanpa sengaja. - Kebijakan "tanpa non-member" tetap: setiap pembeli terdaftar mulai dari Bronze. • Commit: 97492d1 • Pengujian live: registrasi baru -> membership bronze dengan diskon 2%; halaman menampilkan "Bronze · Diskon 2% Semua Produk", tanpa Silver. -------------------------------------------------------------------------------- 26. PENYELESAIAN DELIVERY ORDER (500 PANIC) -------------------------------------------------------------------------------- • Masalah Sebelumnya: Setiap POST /delivery-orders/{id}/deliver menjawab 500. Log menunjukkan panic nil pointer di penulis kolom POD: SetDeliveryOrderPOD mengirim `nil` sebagai transaksi ke penulis baris, sehingga q.Exec panic. Semua penyelesaian DO gagal — dengan maupun tanpa foto. • Solusi: - SetDeliveryOrderPOD kini menerima transaksi pemanggil, dan DeliverDeliveryOrder mengoper transaksinya sehingga bukti pengiriman (foto + nama penerima) tersimpan atomik dengan status penyelesaian. • Commit: 49dc344 • Pengujian live: DO in_transit tanpa foto -> completed + SO delivered; DO in_transit dengan foto multipart -> completed dengan proof_photo_url /uploads/proof-… dan nama penerima tercatat. Tiga test mengunci ketiga jalur (tanpa foto, dengan foto, saver gagal). -------------------------------------------------------------------------------- 27. LAPORAN PENJUALAN: TRANSAKSI DEPOT GALON -------------------------------------------------------------------------------- • Masalah Sebelumnya: Data transaksi galon (isi ulang, pinjam, beli galon) tersimpan di tabel galon_transactions — terpisah dari sales_orders sesuai alur depot. Backend sudah mengirim galon_total dan rincian per hari lewat /reports/sales, tetapi halaman Laporan Penjualan ERP membuang field itu. Akibatnya kasir melihat uang galon di kasir namun tidak menemukannya di ERP, dan dianggap "tidak sinkron". • Solusi: - KPI baru "Penjualan Depot Galon" dengan keterangan bahwa nilainya berada di luar Total Penjualan (dua sumber pendapatan tetap terpisah agar tidak tertukar). - Grafik "Pasar vs Depot Galon": perbandingan per hari. - Ikon Drop ditambahkan ke vendor barrel untuk kartu tersebut. • Commit: 9bf362e • Pengujian live: KPI menampilkan Rp 95.000 (6 transaksi galon hari ini), breakdown per hari merender market dan galon berdampingan. -------------------------------------------------------------------------------- 28. JURNAL OTOMATIS PER TRANSAKSI GALON -------------------------------------------------------------------------------- • Masalah Sebelumnya: Uang galon tercatat di payments tetapi tidak pernah masuk buku besar — laporan penjualan menunjukkan galon, jurnal akuntansi tidak. • Solusi: - Setiap penyelesaian galon mem-posting satu jurnal cash_receipt: D Kas Tunai Toko K Pendapatan Depot Galon Air (penjualan) K Uang Muka Penjualan / Deposit Customer (deposit, liabilitas) - Deposit memakai baris kredit sendiri supaya pendapatan dan liabilitas tidak tertukar — pola yang sama dengan pemisahan baris payments. - Di-posting dengan role system (kasir tidak perlu izin jurnal); created_by tetap pengguna nyata. - Kegagalan jurnal dilaporkan ke pemanggil, bukan ditelan: handover tetap selesai (uangnya memang berpindah) tetapi kehilangan catatan buku besar tidak bisa terjadi diam-diam. • Commit: d414de1 • Pengujian live: GLN-202611-000007 (isi ulang Rp 6.000): D Kas Tunai Toko 6.000 / K Pendapatan Depot Galon 6.000 Buy galon + deposit Rp 70.000: D Kas Tunai Toko 126.000 / K Pendapatan 56.000 + K Deposit 70.000 -------------------------------------------------------------------------------- 29. HUTANG SUPPLIER: DRILL-DOWN DAN RETUR PEMBELIAN -------------------------------------------------------------------------------- • Masalah Sebelumnya: - Kartu hutang hanya menampilkan angka total tanpa asal-usul: finance tidak bisa melihat supplier mana, nomor referensi, atau apakah utang berasal dari pembelian reguler atau dari pembelian dengan retur barang cacat. - Form Penerimaan Barang punya kolom "Retur / Cacat" + foto bukti, tetapi backend tidak memiliki field-nya: nilai penolakan dibuang dan PO tetap berutang penuh atas barang yang ditolak. • Solusi: - Migrasi 000147: goods_receipt_items memperoleh quantity_rejected dan defect_notes — retur pembelian menjadi data terstruktur. - Saat penerimaan dibuat, nilai PO otomatis dikurangi (Σ quantity_rejected × harga PO); harga kontrak PO menang atas harga input. Validasi: received + rejected tidak boleh melebihi quantity. - Halaman Hutang Supplier memperoleh baris expandable (drill-down) per PO: supplier, no. referensi, sumber utang (pembelian reguler vs pembelian + retur), nilai retur, total PO, dan pembayaran. - KTable (komponen tabel bersama) kini mendukung expandable rows, selaras dengan KDataTable. • Commit: c017b7d (2 commit: backend + model yang tertinggal) • Pengujian live: PO Rp 50.000, terima 7 tolak 3 -> total PO turun menjadi sesuai nilai diterima, drill-down menampilkan nilai retur dan jumlah baris; PO tanpa retur tetap menampilkan "Pembelian reguler". -------------------------------------------------------------------------------- 30. TABEL HUTANG SUPPLIER: SORTIR VIA SERVER (OPSI B) -------------------------------------------------------------------------------- • Masalah Sebelumnya: Header tabel terlihat bisa diklik untuk mengurutkan, tetapi sortir hanya berlaku pada halaman aktif (client-side) dan kembali seperti semula saat pindah halaman — persis perilaku yang dilaporkan sebagai "tabel ikut bergeser saat navigasi". • Solusi (opsi yang dipilih pengguna): - useListPage memperoleh onSort: klik header menjadi ?sort_by=&sort_dir= dan halaman kembali ke 1. - ListDebts mengurutkan dengan whitelist kolom (po_number, supplier_name, order_date, due_date, total_amount, paid_amount, remaining_amount) plus tiebreak po.id agar paging stabil. - Kolom Supplier memakai sortKey -> supplier_name (nama kolom DB berbeda dari nama UI). - Whitelist di purchasing/sort.go: nilai dari query string tidak pernah menjadi identifier SQL (uji injeksi mengembalikan urutan default). • Commit: 5aa6bf7 • Pengujian live: Sisa Hutang asc [0,0,0,0,0,0] / desc [171jt, 500rb, 300rb, …]; klik header pertama dan kedua menghasilkan urutan yang berbeda. -------------------------------------------------------------------------------- HASIL VERIFIKASI -------------------------------------------------------------------------------- • Backend Go : 42 paket lulus; -race bersih pada sales/cashier/cart • Drift guard : 395 route dan 207 permission sinkron • Vitest ERP : 41 berkas, 361 test lulus • Vitest POS : 21 berkas, 123 test lulus • Vitest B2B : 22 berkas, 182 test lulus • Build produksi : ERP, POS, dan B2B bersih • Playwright sweep : 9/9 lulus • Exhaustive UI sweep : seluruh halaman/tombol/modal diaudit, 0 kegagalan, 0 kegagalan jaringan 5xx • Deep module sweep : 74/74 lulus, 0 uncaught JS (0 peringatan hidrasi) • Alur data end-to-end : 28/28 lulus, data tersimpan di database • Matriks 7-A di UI nyata: satu pesanan pickup dan satu pesanan kurir diperiksa pada daftar POS, daftar Packing ERP, dan halaman pesanan B2B • Quality gate : pnpm sweep / test:e2e / test:ui / sweep:deep / test:flow kini benar-benar ada di package.json dan terhubung ke scripts/ (sebelumnya hanya tertulis di AGENTS.md) -------------------------------------------------------------------------------- CATATAN PENERAPAN -------------------------------------------------------------------------------- • Produksi menjalankan image dari GHCR. Perubahan ini baru terlihat setelah server menarik image versi terbaru dan membuat ulang container; catatan rilis ini tidak menyatakan bahwa langkah tersebut sudah dilakukan. • Migrasi 000140-000145 berjalan otomatis saat backend start (dbmigrate). 000140 mengizinkan progres fulfillment gudang setelah shift kasir ditutup tanpa melonggarkan guard finansial; 000141 & 000145 memastikan akun pembeli terdaftar mulai dari tier Bronze; penanganan akun non-member tetap aman. • Angka pada contoh verifikasi berasal dari database pengembangan lokal, bukan data produksi. • Alur transfer stok antar gudang 6-tahap beserta voucher serah terima, rekonsiliasi kas & penutupan shift kasir, penomoran dokumen YYYYMMDD dengan reset bulanan, dan stepper Delivery Order 5-tahap sudah dirilis pada versi 1.2.0 sampai 1.4.0. Bagian itu tidak diulang di sini agar catatan rilis ini hanya memuat perubahan yang benar-benar baru pada 1.5.0. ================================================================================