# Bug JS Shopping Cart: Cross-Row Reset Diskon/Premi & Risiko Lain

> **Sumber:** Sesi Codex (15-16 Mei 2026) — "Cari proses CRM ke shopping cart"
> **Turn C:** Bug cross-row reset → row-scoped fix
> **Turn D:** Risk assessment hardening
> **File:** `application/modules/penjualan/template/transaksi_reguler.html`
> **Lokasi history:** `C:\Users\vansgare\.codex\sessions\2026\05\15\rollout-2026-05-15T17-36-32-...jsonl`

---

## Turn C: Bug Cross-Row Reset (Selesai ✅ — Sudah Diperbaiki)

### Masalah

Saat user mengisi **`Disc (%)`** di satu row (misal row 5), handler JS lama memproses **banyak row sekaligus** (bukan hanya row aktif), sehingga row lain yang memiliki **premi** (misal row 2-4 dengan premi 20.000) ikut di-reset ke `0` dan `Price(Net)` balik ke `Harga Jual Excl. Ppn` (contoh 700.000).

### Dampak Bisnis
- Nilai `nett/subtotal` antar-item jadi tidak konsisten
- Potensi salah total transaksi (berpengaruh ke nilai penjualan/diskon di jurnal)

### File yang Diperbaiki

**File:** `application/modules/penjualan/template/transaksi_reguler.html`

| Line | Event | Perbaikan |
|------|-------|-----------|
| 1245 | `disc_percent` keyup | Row-scoped by `noid` |
| 1310 | `_diskon_nilai` keyup | Row-scoped by `noid` |
| 1360 | `premi_percent` keyup | Row-scoped by `noid` |
| 1400 | `premi` keyup | Row-scoped by `noid` |
| 1441 | `nett1` keyup | Row-scoped by `noid` |

### Kode Perbaikan (Inti)

**Pola yang digunakan untuk semua 5 handler:**

```javascript
$("input[keyid=disc_percent]").on('keyup', function(){
    var noid = $(this).attr('noid');                    // <-- AMBIL ROW SAJA
    var diskon_persen = parseFloat(removeCommas($('#disc_percent_'+noid).val())) || 0;
    var harga_non = parseFloat(removeCommas($('#harga_exclude_ppn_'+noid).html())) || 0;
    // update hanya row noid ini — tidak menyentuh row lain
});
```

### Fungsi Kalkulasi Total di `calcShoppingCartPettycash()`

**Line 1118-1199** — Fungsi ini membaca semua row dengan `jQuery.each(indexed, ...)` tetapi **tidak mengubah** nilai diskon/premi row lain — hanya membaca untuk kalkulasi grand total.

**Line 1133-1156** — Kalkulasi premi vs diskon per row:
```javascript
var diskon_absolute = parseFloat(removeCommas($('#_diskon_nilai_'+noid).val())) || 0;
var premi_absolute = 0;
var premi_persen = 0;

if (harga_non <= 0) {
    diskon_absolute = 0;
    premi_absolute = hargaNettInput > 0 ? hargaNettInput : 0;
}
else if (diskon_absolute > harga_non) {
    diskon_absolute = harga_non;
}
else if (diskon_absolute <= 0 && hargaNettInput > harga_non) {
    premi_absolute = hargaNettInput - harga_non;
    premi_persen = (premi_absolute / harga_non) * 100;
    diskon_absolute = 0;
}
```

### Verifikasi (Manual Test)

1. Isi premi di row 2/3/4 (misal 20.000)
2. Di row 5 isi `Disc (%)` (misal 10)
3. **Pastikan row 2/3/4 tetap premi**, tidak kembali 0, dan `Price(Net)` tetap 720.000
4. Cek subtotal per row + grand total berubah hanya sesuai row yang diedit

**Edge cases:**
- `Harga Jual Excl. Ppn = 0`: diskon dipaksa 0, premi diperlakukan sesuai input nett/premi
- Diskon > 100%: ditolak dan dikembalikan aman
- Input negatif diskon/premi: dinormalisasi ke 0

**Validasi sintaks:** Lolos: `php -l application/modules/penjualan/template/transaksi_reguler.html`

---

## Turn D: Risk Assessment — Potensi Bug Lain (Belum Diperbaiki)

> **Status:** Hanya analisis, belum ada perbaikan. AI menawarkan untuk patch hardening.

### Risiko 1: Validasi `Disc (%) > 100` Lemah

**Lokasi:** `transaksi_reguler.html:1268`

```javascript
if( Math.round(diskon_persen).toFixed(3) *1 > 100 ){
```

**Masalah:** Menggunakan `Math.round()` sebelum compare. Nilai seperti `100.4` akan di-round ke `100`, sehingga lolos dari warning padahal seharusnya ditolak.

**Rekomendasi:**
```javascript
if (diskon_persen > 100) { 
    /* reset + warning */ 
    return; 
}
```

### Risiko 2: `Disc (%)` Negatif Belum Di-clamp

**Lokasi:** `transaksi_reguler.html:1245`

**Masalah:** Input `Disc (%) = -5` tidak langsung di-clamp ke `0` di handler `disc_percent`. Nilai negatif bisa berubah jadi premi melalui alur kalkulasi total.

**Rekomendasi:**
```javascript
if (diskon_persen < 0) { diskon_persen = 0; }
```

### Risiko 3: Event Binding Hanya `keyup`

**Lokasi:** Line 1245, 1310, 1360, 1400, 1441

```javascript
$("input[keyid=disc_percent]").on('keyup', function(){ ... });
```

**Masalah:** Input via **paste** (Ctrl+V) atau **context-menu** hanya trigger event `input`/`change`, bukan `keyup`. Kalkulasi jadi telat sampai ada `keyup` berikutnya.

**Rekomendasi:**
```javascript
$("input[keyid=disc_percent]").on('input keyup change', function(){ ... });
```

### Ringkasan Risiko

| # | Risiko | Level | Status |
|---|--------|-------|--------|
| 1 | `disc_percent > 100` validasi pakai `Math.round` | 🟡 Medium | Belum diperbaiki |
| 2 | `disc_percent` negatif belum di-clamp | 🟢 Low | Belum diperbaiki |
| 3 | Event hanya `keyup`, paste tidak terdeteksi | 🟡 Medium | Belum diperbaiki |

### PHP Pricing Logic (`_processSelectProduct.php`)

**File:** `application/modules/penjualan/controllers/_processSelectProduct.php`

**Line 6331-6372** — CRM Price Lock logic:
```php
if ($isCrmPriceLocked) {
    $hargaStandar = $this->normalizeFloatValue($harga_yg_dipakai);
    $hargaNetCrm = $crmLockedPrice;
    
    // Jika CRM mengirim harga 0 (diskon 100%), nilai 0 harus tetap dipakai.
    // Fallback ke harga standar hanya untuk kasus payload harga CRM kosong.
    if ($crmLockedPriceIsExplicit === false) {
        $hargaNetCrm = $hargaStandar;
    }

    $diskonCrm = 0;
    $premiCrm = 0;
    if ($hargaNetCrm >= $hargaStandar) {
        $premiCrm = $hargaNetCrm - $hargaStandar;
    } else {
        $diskonCrm = $hargaStandar - $hargaNetCrm;
    }
    // ... set $tmp['discPersen'], $tmp['lastNett'], $tmp['nett1'], dll
}
```

---

## Flow Diagram Shopping Cart Kalkulasi

```
User input di shopping cart row
       │
       ▼
keyup/input event handler (row-scoped by noid)
       │
       ├─ disc_percent → hitung diskon absolute
       ├─ _diskon_nilai → hitung diskon persen
       ├─ premi_percent → hitung premi absolute
       ├─ premi_absolute → hitung premi persen
       └─ nett1 (Price Net) → hitung diskon/premi dari harga nett
       │
       ▼
calcShoppingCartPettycash()
       │
       ├─ Loop semua row via jQuery.each(indexed)
       ├─ Baca harga_non, diskon_absolute, premi dari DOM per row
       ├─ Hitung hrg_net = harga_non - diskon + premi
       ├─ Hitung subtotal per row
       └─ Hitung grand_total, grand_total_bruto, totalPPN
       │
       ▼
Update DOM:
       ├─ nett1_[noid] = hrg_net
       ├─ subtotal_[noid] = subtotal
       ├─ grand total display
       └─ PPN display
```
