# Blueprint: Dual-Locker Co-Update untuk Variant (Parent + Variant)

## 1. Masalah

Saat ini 3 titik mutasi varian hanya mengupdate `stock_locker_variant`, tanpa menyentuh `stock_locker` parent. Ini menyebabkan **locker leak** pada parent:

| No | Titik Mutasi | File:Line | Hanya Update |
|----|-------------|-----------|-------------|
| 1 | `reserveVariantLockerForSelect()` | `_processSelectProductConvertion.php:363-481` | `stock_locker_variant` |
| 2 | `releaseVariantLockerOnRemove()` | `_processSelectProductConvertion.php:483-539` | `stock_locker_variant` |
| 3 | `reset()` — variant branch | `_shoppingCart.php:2303-2370` | `stock_locker_variant` |

### Dampak (P0 — Data Integrity)

- Stok parent tetap utuh meskipun varian di-hold → **overstatement stok**
- Saat user release/remove varian, parent hold tidak dikembalikan → **locker leak permanen**

## 2. Konsep

**Parent adalah aggregate.** Stok parent = 100 berarti total seluruh varian = 100.
Setiap mutasi `stock_locker_variant` (active↔hold) WAJIB diikuti mutasi SAMA JUMLAH dan SAMA ARAH di `stock_locker` parent, dalam **1 transaksi DB yang sama**.

### Struktur Parent Hold

Parent hold disimpan sebagai **1 row aggregate** per `(produk_id, gudang_id, oleh_id, state='hold')` di `stock_locker`.
- `produk_id` = ID produk utama
- `gudang_id` = gudang aktif user
- `oleh_id` = user yang memegang hold
- `state` = 'hold'
- `jumlah` = total seluruh varian yang di-hold oleh user tersebut

## 3. Skenario Lengkap

### Data Awal

| Entitas | Tabel | State | Qty |
|---------|-------|-------|-----|
| Produk A (parent) | `stock_locker` | active | 100 |
| Varian A-XL | `stock_locker_variant` | active | 30 |
| Varian A-L | `stock_locker_variant` | active | 40 |
| Varian A-M | `stock_locker_variant` | active | 30 |

### Step 1 — Pilih 5 A-XL + 10 A-L via modal variantPicker

| Entitas | Tabel | Perubahan | Hasil |
|---------|-------|-----------|-------|
| Produk A | `stock_locker` active | -15 | 85 |
| Produk A | `stock_locker` hold | +15 | 15 |
| A-XL | `stock_locker_variant` active | -5 | 25 |
| A-XL | `stock_locker_variant` hold | +5 | 5 |
| A-L | `stock_locker_variant` active | -10 | 30 |
| A-L | `stock_locker_variant` hold | +10 | 10 |

### Step 2 — Ubah A-XL dari 5 → 8 di shopping cart

| Entitas | Tabel | Delta | Hasil |
|---------|-------|-------|-------|
| Produk A | `stock_locker` active | -3 | 82 |
| Produk A | `stock_locker` hold | +3 | 18 |
| A-XL | `stock_locker_variant` active | -3 | 22 |
| A-XL | `stock_locker_variant` hold | +3 | 8 |

### Step 3 — Ubah A-L dari 10 → 4 di shopping cart

| Entitas | Tabel | Delta | Hasil |
|---------|-------|-------|-------|
| Produk A | `stock_locker` active | +6 | 88 |
| Produk A | `stock_locker` hold | -6 | 12 |
| A-L | `stock_locker_variant` active | +6 | 36 |
| A-L | `stock_locker_variant` hold | -6 | 4 |

### Step 4 — Remove A-XL (qty 8) dari cart

| Entitas | Tabel | Delta | Hasil |
|---------|-------|-------|-------|
| Produk A | `stock_locker` active | +8 | 96 |
| Produk A | `stock_locker` hold | -8 | 4 |
| A-XL | `stock_locker_variant` active | +8 | 30 |
| A-XL | `stock_locker_variant` hold | -8 | 0 |

### Step 5 — Reset cart (3 varian: XL=0, L=4, M=8)

Iterasi per varian (Opsi A):

**Iterasi A-L (hold=4):**
| Entitas | Tabel | Delta | Hasil |
|---------|-------|-------|-------|
| Produk A | `stock_locker` hold | -4 | 0 |
| Produk A | `stock_locker` active | +4 | 100 |
| A-L | `stock_locker_variant` hold | -4 | 0 |
| A-L | `stock_locker_variant` active | +4 | 40 |

**Iterasi A-M (hold=8):**
| Entitas | Tabel | Delta | Hasil |
|---------|-------|-------|-------|
| Produk A | `stock_locker` hold | -8 | -8 → clamp ke 0 |
| Produk A | `stock_locker` active | +8 | 108 |
| A-M | `stock_locker_variant` hold | -8 | 0 |
| A-M | `stock_locker_variant` active | +8 | 38 |

> **Catatan:** Setelah iterasi A-L, parent hold sudah 0. Iterasi A-M akan menemukan parent hold = 0 atau tidak ada. Maka parent clamp: hold=0, active +8 tetap dijalankan.

## 4. Desain Solusi

### 4.1 Helper Baru: `coUpdateParentLocker($identity, $jmlDelta, $produkRow)`

Logic:

```
$parentLocker = new MdlLockerStock()

// Query parent lockers (sebelum trans_start)
$parentActive = $parentLocker->cekLoker(cab, produkId, "active", 0, 0, gudang)
$parentHold   = $parentLocker->cekLoker(cab, produkId, "hold", userId, 0, gudang)

// Update parent active (kurangi jika jmlDelta positif, tambah jika negatif)
$newActiveJumlah = ($parentActive['jumlah'] ?? 0) - $jmlDelta
if (isset($parentActive['id'])) {
    $parentLocker->updateData(["id" => $parentActive['id']], ["jumlah" => $newActiveJumlah])
} elseif ($newActiveJumlah > 0) {
    $parentLocker->addData([...state='active', jumlah=$newActiveJumlah, ...])
}

// Update parent hold (tambah jika jmlDelta positif, kurangi jika negatif)
$holdExists = isset($parentHold['id'])
$currentHold = $parentHold['jumlah'] ?? 0
$newHoldJumlah = max(0, $currentHold + $jmlDelta)  // clamp ke 0

if ($holdExists) {
    $parentLocker->updateData(["id" => $parentHold['id']], ["jumlah" => $newHoldJumlah])
} elseif ($newHoldJumlah > 0) {
    $parentLocker->addData([...state='hold', jumlah=$newHoldJumlah, oleh_id=userId, ...])
}
```

### 4.2 Perubahan di `reserveVariantLockerForSelect()`

**Lokasi:** `_processSelectProductConvertion.php:363-481`

**Sebelum trans_start (line 410):**
- Tambah query parent active + parent hold via `MdlLockerStock`

**Di dalam trans block (line 410-473):**
- Setelah variant hold update (line 471), sebelum trans_complete (line 473):
  - Panggil `coUpdateParentLocker($identity, $jmlNambah, $produkRow)`

### 4.3 Perubahan di `releaseVariantLockerOnRemove()`

**Lokasi:** `_processSelectProductConvertion.php:483-539`

**Sebelum trans_start (line 504):**
- Tambah query parent active + parent hold via `MdlLockerStock`

**Di dalam trans block (line 504-535):**
- Setelah variant active update (line 534), sebelum trans_complete (line 535):
  - Panggil `coUpdateParentLocker($identity, -$holdQty, $sessionItem)`
  - Catatan: `$jmlDelta` = negatif karena arahnya hold→active (parent hold turun, parent active naik)

### 4.4 Perubahan di `reset()`

**Lokasi:** `_shoppingCart.php:2303-2370`

**Opsi A (per-variant, dalam loop):**
- Di dalam trans yang sudah ada (line 2334-2367):
  - Setelah variant active update (line 2364), sebelum trans_complete (line 2367):
    - Load `MdlLockerStock`
    - Query parent active + parent hold
    - Update parent hold -= holdQty, parent active += holdQty

## 5. File Impact

| File | Perubahan |
|------|-----------|
| `application/modules/konversi/controllers/_processSelectProductConvertion.php` | Modif `reserveVariantLockerForSelect()` — tambah parent co-update |
| `application/modules/konversi/controllers/_processSelectProductConvertion.php` | Modif `releaseVariantLockerOnRemove()` — tambah parent co-update |
| `application/modules/konversi/controllers/_shoppingCart.php` | Modif `reset()` variant branch — tambah parent co-update |

## 6. Verifikasi

### Skenario Regression Minimal Varian

| # | Skenario | Input | Cek Parent `stock_locker` | Cek `stock_locker_variant` |
|---|----------|-------|--------------------------|---------------------------|
| 1 | Varian→Varian | Select A-XL qty=5 | active -5, hold +5 | active -5, hold +5 |
| 2 | Non-varian→Non-varian | Select produk B qty=3 | Tidak berubah (existing code) | N/A |
| 3 | Varian→Non-varian | A-XL=5 lalu B=3 | A: active -5 hold +5; B: normal | A-XL: active -5 hold +5 |
| 4 | Non-varian→Varian | B=3 lalu A-XL=5 | B: normal; A: active -5 hold +5 | A-XL: active -5 hold +5 |
| 5 | Qty naik | A-XL 5→8 | active -3, hold +3 | active -3, hold +3 |
| 6 | Qty turun | A-XL 8→3 | active +5, hold -5 | active +5, hold -5 |
| 7 | Remove item | Hapus A-XL (hold=5) | hold -5, active +5 | hold=0, active +5 |
| 8 | Reset all (Opsi A) | XL=5, L=10, M=8 | hold=0, active +23 | masing-masing hold=0, active kembali |
| 9 | Concurrent user | User1 hold XL=5, User2 hold XL=3 | Masing-masing parent hold oleh_id beda | Tidak conflict |

### Edge Cases

- **Parent active tidak ada:** Jika tidak ditemukan, insert baru dengan jumlah = -delta (setelah delta negatif)
- **Parent hold tidak ada saat perlu nambah:** Insert baru
- **Delta negatif melebihi hold:** Clamp hold ke 0, active tetap ditambah sesuai delta absolut
- **Multiple variant 1 parent:** Parent hold = aggregate semua varian, update per-varian bertahap

## 7. Catatan Tambahan

- Tidak perlu mengubah tabel `stock_locker` — kolom sudah mencukupi (produk_id, gudang_id, state, oleh_id, jumlah)
- Tidak perlu mengubah `MdlLockerStockVariant` — hanya perlu `MdlLockerStock::cekLoker()`
- `reserveVariantLockerForSelect()` sudah punya `$produkRow` yang berisi `nama` dan `satuan`
- `coUpdateParentLocker()` bisa diimplementasikan sebagai method inline di masing-masing fungsi (demi menghindari cross-file dependency) atau sebagai private method di masing-masing controller
