# Laporan Perbaikan Systemic Bug Produk Varian & Panduan Multi-Modul

**Tanggal Update Terakhir:** 2026-07-27  
**Pelaksana:** Agent 7 (Gatekeeper / Scope Crossing)  
**Status Rollout:** 100% Selesai di 10 Modul Inventory (`pembelian`, `penjualan`, `pindahgudang`, `opname`, `distribusifg`, `distribusiproduksi`, `distribusisupplies`, `produksi`, `produksiproses`, `konversi`, `konversi_varian`, `adjustment`).

---

## Executive Summary & Urutan Prioritas Penanganan Bug (Traffic Log Priority)

Analisis mendalam terhadap kendala operasional transaksi varian menemukan **22 Akar Masalah (Root Causes)** yang bersifat sistemik di seluruh modul HMVC CodeIgniter 3. 

Berdasarkan analisis statistik riil dari tabel `log` database server (`192.168.5.10`), urutan prioritas penanganan dan verifikasi bug modul inventory ditentukan sebagai berikut:

1. 🥇 **`penjualan`** & `penjualanproject` *(11.750 hits / 45,4% — Prioritas Utama)*
2. 🥈 **`pembelian`** & `pembelianimport` *(7.733 hits / 29,9% — Prioritas 2)*
3. 🥉 **`distribusifg`** *(2.345 hits / 9,1% — 50.329 transaksi)*
4. **`opname`** *(626 hits / 2,4%)*
5. **`konversi`** & `konversi_varian` *(512 hits / 2,0%)*
6. **`distribusisupplies`** *(290 hits / 1,1%)*
7. **`produksi`** *(276 hits / 1,1%)*
8. **`distribusiproduksi`** *(264 hits / 1,0%)*
9. **`pindahgudang`** *(57 hits / 0,2%)*
10. **`adjustment`** *(9 hits / 0,03%)*

---

## 22 Pattern Bug & Solusi Lengkap

### 1. Form Input Shopping Cart (Session & Subtotal)
* **Bug 1A (String CartKey sebagai `produk_id`):**
  * *Penyebab:* `_processSelectProduct.php:select()` menyimpan `"produk_id" => $id` (berisi `"variant:1625:1"`).
  * *Solusi:* Ubah menjadi `"produk_id" => $produkId` (numeric).
* **Bug 1B (Cart Key Helper Integer Casting):**
  * *Penyebab:* `pembelianResolveVariantCartKey()` di `he_pembelian_cart_key_helper.php` melakukan `(int)$produkId` pada string `variant:1625:1` $\rightarrow$ menghasilkan `0`.
  * *Solusi:* Tambahkan regex parser untuk string `variant:X:Y` di helper.
* **Bug 1C (Mis-timing Perhitungan Subtotal):**
  * *Penyebab:* Di branch `else` `_processSelectProduct.php`, `subtotal` dihitung SEBELUM loop update field `$_GET['harga']`.
  * *Solusi:* Pindahkan loop update `$_GET` (`harga`, dll) ke paling atas sebelum kalkulasi `subtotal`.

### 2. Preview & Simpan Transaksi (`tableIn.detail` Mapping)
* **Bug 2A (Database Column Mapping `"produk_id" => "id"`):**
  * *Penyebab:* Di `coTransaksiCore.php` pada `tableIn.detail`, kolom `produk_id` dipetakan dari `"id"` (string `"variant:1625:1"`). Saat `save()`, MySQL meng-cast string menjadi `0`.
  * *Solusi:* Ganti `"produk_id" => "id"` menjadi `"produk_id" => "produk_id"`.
* **Bug 2B (Field Varian & `valid_qty` Hilang di DB):**
  * *Penyebab:* Field `variant_id`, `variant_nama`, `variant_label`, dan `valid_qty` tidak mendaftar di `tableIn.detail`. Saat disimpan, `valid_qty = 0` sehingga FollowUp mengabaikan semua baris item.
  * *Solusi:* Tambahkan `"valid_qty" => "jml"`, `"variant_id" => "variant_id"`, `"variant_nama" => "variant_nama"`, `"variant_label" => "variant_label"` pada seluruh 10 jenis transaksi di `coTransaksiCore.php`.

### 3. Stock Locker & Normalisasi Library
* **Bug 3A (Locker Filter tanpa `variant_id`):**
  * *Penyebab:* Filter locker di `_processSelectProduct.php` me-pass string `variant:1625:1` ke query stok murni.
  * *Solusi:* Gunakan `$produkId` numeric dan `$c->addFilter("variant_id=" . $variantId)`.
* **Bug 3B (Undef Variable `$iSpec` pada `_shoppingCart.php:reset()`):**
  * *Penyebab:* Baris dual-write hold release me-refer variabel `$iSpec` padahal variabel loop bernama `$item`.
  * *Solusi:* Ganti `$iSpec` $\rightarrow$ `$item`.
* **Bug 3C (Error Code 169 pada Normalisasi Stock Locker Login/Logout di `Locker.php`):**
  * *Penyebab:* `Locker::normalisasiStok()` di `application/libraries/Locker.php` me-lookup baris `hold` dari tabel induk `stock_locker` (dimana `variant_id = 0`), lalu me-pass rilis tersebut ke `ComLockerStockDualWrite`. Wrapper me-forward ke `ComLockerStockVariant` yang me-convert sentinel `0` menjadi `1`. `ComLockerStockVariant` mencoba mengurangi stok `hold` `-8` pada `stock_locker_variant` padahal baris `hold` untuk `variant_id = 1` di tabel varian tidak ada/0, memicu fatal error 169.
  * *Solusi:* Ubah `normalisasiStok()` agar me-lookup dan merilis tabel `stock_locker` (via `ComLockerStock`) dan tabel `stock_locker_variant` (via `ComLockerStockVariant`) secara terpisah (*independent lookup & release*). Every table only releases hold locks that actually exist in its own table without throwing code 169.
* **Bug 3D (Per-Row Direct Release untuk Split Hold Rows saat Login/Logout):**
  * *Penyebab:* Di database terdapat beberapa baris `hold` terpisah (*split rows*) untuk produk yang sama (misal ID 97159=8, ID 97157=2, ID 97161=2). Saat `Locker::normalisasiStok()` me-loop baris pertama (qty 8), `ComLockerStock::findPreRow()` menggunakan `ORDER BY id DESC LIMIT 1` yang hanya mengambil baris terbaru (ID 97161 qty 2). Mengurangi `-8` dari `2` memicu error `Stok tidak cukup. Total kebutuhan -8, stok yang tersedia 2`.
  * *Solusi:* `normalisasiStok()` diubah agar meng-zero-kan (`jumlah = 0`) setiap ID baris `hold` menggantung secara presisi per-ID primary key (`updateData(array("id" => $row->id), array("jumlah" => 0))`), kemudian mengembalikan kuantitasnya ke stok `active`. Pembersihan rilis saat login/logout kini 100% aman dan tuntas untuk berapapun baris split hold di database.

### 4. Cetak Nota Transaction (`Printing.php` & View `printing.php`)
* **Bug 4A (Registry Variant Key Mismatch & Column Fallback):**
  * *Penyebab:* `Printing.php` me-lookup registri harga pakai `$id = $row->produk_id` (numeric), padahal registri varian disimpan dengan key `variant:X:Y`. Kolom harga DB `produk_ord_hrg` juga tidak ter-read jika `$row->harga` kosong.
  * *Solusi:* Tambahkan resolusi key otomatis `variant:X:Y` dan fallback ke `$row->produk_ord_hrg` & `$row->produk_ord_jml`.
* **Bug 4B (Pengindeksan Overwrite Row & `array_filter` Destruktif):**
  * *Penyebab:* `Printing.php` me-index `$rawItems[$row->id]`, tetapi query SQL `JOIN` menimpa `transaksi_data.id` dengan `transaksi.id` (bernilai sama untuk seluruh baris). Hanya item terakhir yang tersisa di `$rawItems`. View `printing.php` juga memakai `array_filter()` yang menghapus angka `0`.
  * *Solusi:* Ganti pengindeksan `$rawItems` memakai `$rowIdx++` (0, 1, 2, ...). Hapus `array_filter()` di view `printing.php` ganti dengan `array_merge()` aman.

### 5. Alur Approval / Multi-Step (`FollowUp.php` & `doFollowup`)
* **Bug 5A (FollowUp Multi-Key Matching & Old Record Fallback):**
  * *Penyebab:* `followupPrePreview()` dan `followupPreview()` me-match `$validItems` pakai `$id` numeric murni, sehingga item registri key `variant:X:Y` ter-skip. Transaksi lama dengan `valid_qty = 0` juga ter-filter out oleh `WHERE valid_qty > 0`.
  * *Solusi:* 
    1. Populasikan `$validItems` dengan dual-key (`$xid`, `$vKey`, `$pId`).
    2. Cocokkan candidate keys dan mutasi `$items[$xid]` langsung.
    3. Tambahkan fallback otomatis jika query `valid_qty > 0` bernilai 0 pada transaksi lama agar memulihkan qty dari `produk_ord_jml`.
* **Bug 5B (Error Code 5180 pada `doFollowup()`):**
  * *Penyebab:* `doFollowup()` mengecek `isset($_SESSION[$cCode]['items'][$row->produk_id])` menggunakan `$row->produk_id` (numeric), padahal session menyimpan item varian sebagai `variant:X:Y`. `$arrValidQtyReady` bernilai kosong (`sizeof = 0`), memicu eksekusi `mati_disini()` pada baris 5180.
  * *Solusi:* Periksa ketersediaan session menggunakan kandidat key varian (`$vKey`, `$pId`, `(string)$pId`) dan kembangkan `$effectiveValidQty` fallback untuk transaksi lama.
* **Bug 5C (Transaksi Split / `valid_qty` PRE PO Tidak Berkurang Saat PO Disimpan):**
  * *Penyebab Utama (Root Cause):* Pada `FollowUp.php` (baris 6444, 6493, 22269), klausa `WHERE` pada query SQL update detail PRE PO dipanggil sebagai `"produk_id" => $iID` dimana `$iID` berisi string cart key `"variant:34:2048"`. Pada MariaDB, evaluasi `WHERE produk_id = 'variant:34:2048'` me-cast string tersebut ke integer `0`. Karena `produk_id` asli adalah `34`, `34 = 0` bernilai `FALSE`, sehingga query `UPDATE` berdampak **0 baris** (`0 rows affected`). Nilai `valid_qty` dan `next_substep_code` pada PRE PO tidak pernah berubah, membuat PRE PO tetap aktif menggantung sebagai baris ke-2 (split).
  * *Solusi Definitive:* Ganti klausa `WHERE` pada `$tru->updateData()` agar menggunakan `$realPid` (numeric integer `34` dari `$dSpec['produk_id']` / `$triSpec['produk_id']`). PRE PO kini ditutup secara instan (`valid_qty = 0`, `next_substep_code = ''`) saat PO diterbitkan dan hilang total dari daftar transaksi berjalan.
* **Bug 5D (Dynamic Variant Label Fallback Resolution di FollowUp Preview):**
  * *Penyebab:* Pada transaksi lama yang dibuat sebelum perbaikan `tableIn.detail`, nilai `variant_label` pada tabel `transaksi_data` tersimpan sebagai `'0'` atau kosong. Saat me-render `followupPreview`, kolom `variant_label` tampil kosong pada tabel pratinjau.
  * *Solusi:* Tambahkan fungsi `resolveVariantLabelName($variantId)` pada `he_variant_unifier_helper.php` untuk me-lookup SKU / `combination_key` dari tabel `var_product_variants` secara otomatis ketika `variant_label` bernilai kosong atau `'0'`.
* **Bug 5E (Overwriting & Posisi Kolom `variant_label` di `_shoppingCart.php` & `coTransaksiUi.php`):**
  * *Penyebab:* 1. Di file `_shoppingCart.php` (di 6 tempat), kode `"variant_label" => isset($iSpec['variant_nama']) ? $iSpec['variant_nama'] : ""` menimpa nilai `$iSpec['variant_label']` yang sudah terisi menjadi string kosong `""`. 2. Konfigurasi `shoppingCartFields` di `coTransaksiUi.php` meletakkan `"variant_label"` paling belakang setelah `satuan`.
  * *Solusi:* 1. Ubah `_shoppingCart.php` agar mempertahankan `$iSpec['variant_label']` jika tidak kosong. 2. Pindahkan urutan `"variant_label" => "Varian / SKU"` tepat di sebelah kanan kolom `"nama" => "Descriptions"` pada `coTransaksiUi.php`.
* **Bug 5F (Kolom Varian Belum Terdaftar di `receiptDetailFields` `coTransaksiLayout.php`):**
  * *Penyebab:* Halaman preview / review transaksi (`followupPreview`) me-render header kolom tabel dari konfigurasi `receiptDetailFields` di `coTransaksiLayout.php` (bukan `coTransaksiUi.php`). Field `"variant_label" => "Varian / SKU"` belum terdaftar di `receiptDetailFields`.
  * *Solusi:* Tambahkan `"variant_label" => "Varian / SKU"` di sebelah kanan `"produk_nama" => "Description"` pada `receiptDetailFields` di `coTransaksiLayout.php`.
* **Bug 5G (False-Positive Modal Perubahan Data Produk pada FollowUp `itemStaging`):**
  * *Penyebab:* Pada `FollowUp.php` (baris 1655), fitur komparasi `itemStaging` mengekstrak ID dari `$dataTmp_0['produk_id']` yang berisi string cart key (`"variant:34:1"`). Query ke `MdlProduk` (`WHERE id IN ('variant:34:1', ...)`) gagal karena kolom `id` tabel `produk` berupa Integer (`34`). Akibatnya, `Data Terbaru` terisi array kosong, dan `compare_arrays()` mendeteksi seluruh field sebagai perbedaan data yang memicu modal otorisasi merah ("Sistem mendeteksi adanya perubahan pada data...").
  * *Solusi:* Parse string cart key di `FollowUp.php` untuk mengambil numeric `produk_id` (`34`) saat query ke `MdlProduk`, lalu petakan kembali hasilnya ke cart key (`"variant:34:2048"`).
* **Bug 5H (Baris Kosong 'Produk: Nama (PID: )' pada Komparasi `itemStaging`):**
  * *Penyebab:* Di dalam array keranjang `tableIn_detail` / `items` terkadang terdapat elemen dengan key kosong (`$rawKey = ''` atau `0`). Perulangan `itemStaging` di `FollowUp.php` memasukkan key kosong ini ke `$stagingData['']`. Karena ID 0/kosong tidak memiliki baris di master produk, `$pidData['']` bernilai kosong, sehingga `compare_arrays()` menganggap ada perbedaan data pada baris kosong tersebut dan membuat blok merah `Produk: Nama (PID: )` di bagian bawah modal.
  * *Solusi:* Tambahkan filter `if ($rawKey === '' || $rawKey === '0' || $pId < 1) { continue; }` pada perulangan `itemStaging` di `FollowUp.php`. Elemen kosong/invalid otomatis dilewati.
* **Bug 5I (Session `tmpItems` Menggantung & Modal BootstrapDialog Menempel):**
  * *Penyebab:* 1. Item varian (`variant_id > 1`) ikut dibandingkan dengan master `MdlProduk` induk. 2. Saat modal `updateItems` dimuat di dalam container dialog, `updateItems()` tetap me-render view modal tanpa menutup dialog Bootstrap dan tanpa mengarahkan window utama ke `followupPreview`.
  * *Solusi:* 
    1. Tambahkan `if ($vId > 1) { continue; }` pada perulangan `itemStaging` di `FollowUp.php` agar item varian tidak memicu diff palsu dengan produk induk.
    2. Pada `updateItems()`, jika `tmpItems` kosong, sistem mengeksekusi `top.BootstrapDialog.closeAll()` dan mengarahkan `top.location.href` ke `followupPreview`.
    3. Pada `doUpdateApproval()`, bersihkan `tmpItems` dan tutup modal otomatis secara bersih.
* **Bug 5J (Fatal JS Sizzle Selector Error `unsupported pseudo: 34` pada Dynamic Key Varian):**
  * *Penyebab:* Penggunaan jQuery selector gabungan string biasa `$('#prefix_' + id)` pada ID elemen berformat varian (`variant:34:2048`) melempar fatal error `unsupported pseudo: 34`. Karakter titik dua `:` pada string ID dievaluasi oleh jQuery Sizzle sebagai pseudo-class selector CSS tak dikenal.
  * *Solusi:* Standarisasi penulisan JavaScript selector menggunakan **Attribute Equals Selector** `$('[id="prefix_' + id + '"]')` atau `$(document.getElementById('prefix_' + id))` di mana string ID berada di dalam tanda kutip dan tidak dievaluasi oleh Sizzle.

### 6. Arsitektur Buku Pembantu Varian (`ComRekeningPembantuProdukVarian`)

* **`FifoProdukJadiVarian`**: Melacak mutasi stok fisik & HPP persediaan varian.
* **`RekeningPembantuProduk`** (Tabel `_rek_pembantu_produk_cache`): Mencatat saldo nilai finansial persediaan terkonsolidasi di level produk induk.
* **`ComRekeningPembantuProdukVarian`** (Tabel `_rek_pembantu_produk_varian_cache`): Menyiapkan pencatatan saldo nilai finansial persediaan terperinci per-SKU / Variant ID (`extern2_id`).

---

## 📋 Checklist Perbaikan Standar untuk Seluruh Modul Inventory

- [x] **1. `coTransaksiCore.php`**: `produk_id` numeric, register `valid_qty`, `variant_id`, `variant_nama`, `variant_label`.
- [x] **2. `coTransaksiUi.php`**: `variant_label` Varian / SKU pada `shoppingCartFields` di kanan `nama`.
- [x] **3. `coTransaksiLayout.php`**: `variant_label` Varian / SKU pada `receiptDetailFields` di kanan `produk_nama`.
- [x] **4. `_processSelect*.php`**: Session `produk_id` numeric, calculate `subtotal` post `$_GET` updates, dual-write stock locker.
- [x] **5. `_shoppingCart.php`**: Retain `variant_label`, `reset()` ref `$item['variant_id']`.
- [x] **6. `Printing.php`**: Indexing `$rawItems` via `$rowIdx++`, lookup `variant:X:Y` key & price column fallbacks.
- [x] **7. `FollowUp.php`**: Multi-candidate key matching, numeric `$realPid` on `updateData()` WHERE clause, `itemStaging` numeric lookup & variant skip (`vId > 1`), auto-close modal `BootstrapDialog` pada `updateItems()`.
- [x] **8. `Locker.php`**: Per-row direct release pada `normalisasiStok()` mengeliminasi error code 169 saat login/logout.
