# 📘 BLUEPRINT: KONVERTER MULTI-VARIANT — SINGLE STANDARD VARIANT ARCHITECTURE

**Workspace:** `w:/new_san_variant`
**Acuan Standar:** ISO/IEC 25010 · SAP LO-VC · SAP Movement Types 561/562/701/702

---

## 1. TUJUAN

### 1.1 Visi

Menghilangkan **dual standard** (non-varian vs varian) dengan mengadopsi **single standard variant**:
setiap produk diperlakukan sebagai entity yang memiliki varian. Produk yang saat ini "non-varian"
secara otomatis memiliki **1 varian default implisit** (`variant_id = -1`).

### 1.2 Target Bisnis

1. Semua produk memiliki `variant_id` — tidak ada `variant_id = 0` sebagai pengecualian.
2. Session key **seragam**: `variant:{produk_id}:{variant_id}` untuk semua item.
3. Dua level granularitas stok: **`stock_locker`** (product-level aggregate untuk laporan & 36 modul) dan **`stock_locker_variant`** (variant-level detail). Dual-write permanen ke kedua tabel.
4. Satu jalur processing: **`rsltItems`** — semua item sudah punya `variant_id` (berkat middleware). Dual-write hanya di level komponen (LockerStock+LockerStockVariant, dll).
5. Backward compatibility: 36 modul out-of-scope tetap baca stock_locker tanpa perubahan. Tidak ada fase cleanup.

### 1.3 In Scope — 19 Modul Inventory

| Agent | Modul | Jumlah |
|:-----:|-------|:------:|
| **1** | `konversi`, `konversi_varian` | 2 |
| **4 bis** | `openbalance` | 1 (stok awal — lihat catatan) |
| **2** | `pembelian`, `pembelianimport`, `pembelianjasa`, `pembelianprojek` | 4 |
| **3** | `penjualan`, `penjualanproject` | 2 |
| **4** | `pindahgudang`, `opname` | 2 |
| **5** | `distribusifg`, `distribusiproduksi`, `distribusisupplies`, `distribusijasa` | 4 |
| **6** | `produksi`, `produksiproses`, `adjustment`, `asetmanagement` | 4 |

### 1.4 Out of Scope — Tidak Perlu Diubah

| Kategori | Modul | Alasan |
|----------|-------|--------|
| **Finance/AR** | `penerimaan`, `penerimaanprojek` | Piutang — tidak ada komponen stok |
| **Finance** | `kas`, `banking`, `biaya`, `akunting`, `valas`, `saham`, `deviden`, `pettycast`, `taxes` | Tidak ada stok |
| **Finance** | `pembatalan`, `pembayaran`, `adjustmentjurnal` | Transaksi finansial |
| **Approval** | `requeststok` | Hanya request — tidak posting stok |
| **Utility** | `addons`, `api`, `dashboard`, `tools`, `webservice` | Tidak ada stok |
| **Report** | `laporan`, `laporankeuangan` + backup | Report-only |
| **Backup** | `kasOLD`, `opname_ori`, `opname_v0`, `pembelian_*`, `penerimaanOLD`, `pindahgudang_`, `produksi_sebelum_*`, `taxes_*` | Backup — tidak aktif |

---

## 2. ACUAN STANDAR

| Standar | Kegunaan | Status |
|---------|----------|--------|
| **ISO/IEC 25010** | Kualitas perangkat lunak (8 karakteristik + SoD) | ✅ Sudah dipakai |
| **SAP LO-VC** | Data model variant configuration | ✅ Sudah (implisit) |
| **SAP Movement Types** (561/562/701/702) | Posting stok & jurnal konversi | ✅ Sudah dipakai |
| **ISO 8000** | Data quality master produk & varian | ➕ Tambahan baru |
| **ISO 9001:2015** | Traceability parent-variant | ✅ Sudah dipakai |
| **ISO 27001:2022** | Audit trail immutable | ✅ Sudah dipakai |
| **ISO 28000:2007** | Integritas kuantitas stok | ✅ Sudah dipakai |

---

## 3. BASELINE — FRESH WORKSPACE

Workspace `new_san_variant` adalah fresh copy — belum ada kontaminasi multi-variant.

### 3.1 Config — Murni Non-Varian

```php
// coTransaksiCore.php — hanya non-varian components:
"comName" => "FifoProdukJadi",       // ✅ existing
"comName" => "LockerStock",           // ✅ existing
"comName" => "LockerStockMutasi",     // ✅ existing

// BELUM ADA — HARUS DITAMBAH:
"comName" => "FifoProdukJadiVarian",
"comName" => "LockerStockVariant",
"comName" => "LockerStockMutasiVariant",
```

### 3.2 Satu Pipeline — `rsltItems`

Semua item diproses via `rsltItems` yang sama. Karena setiap item sudah punya `variant_id`
(berkat middleware `normalizeSessionKeys`), posting gate bisa mengalirkan ke komponen
yang sesuai (LockerStock + LockerStockVariant).

**Tidak perlu `rsltItems2`** — dual-write terjadi di level komponen, bukan pipeline.

### 3.3 Keuntungan Fresh Workspace

- **Tidak ada ghost key** — `normalizeSessionDetailGatesByItems()` tidak ada
- **Satu pipeline** — `rsltItems` handle semua item. Dual-write di komponen locker/fifo
- **Tidak ada variant trap** — semua debug function tidak ada
- **22 modul non-stok tidak tersentuh** — zero regression risk

---

## 4. ARSITEKTUR TARGET

### 4.1 Prinsip Dasar

> **Setiap produk adalah varian.**
> Produk tanpa varian eksplisit secara otomatis memiliki **1 varian default** dengan `variant_id = -1`.

### 4.2 Session Key — Single Standard

| Kondisi | Key Lama | Key Baru |
|---------|----------|----------|
| Non-varian | `"1743"` | `"variant:1743:-1"` |
| Varian Hitam | `"variant:1743:15"` | `"variant:1743:15"` (sama) |
| Varian Putih | `"variant:1743:16"` | `"variant:1743:16"` (sama) |

### 4.3 Processing Pipeline — Single Standard

Semua item lewat **satu pipeline** (`rsltItems`). Posting gate mendistribusikan ke
dua level komponen secara dual-write:

| Layer | Komponen (1 pipeline) |
|-------|----------------------|
| Posting gate | `rsltItems` (handle `variant_id` di session) |
| FIFO value | `FifoProdukJadi` + `FifoProdukJadiVarian` (dual-write) |
| Locker stok | `LockerStock` + `LockerStockVariant` (dual-write) |
| Mutasi stok | `LockerStockMutasi` + `LockerStockMutasiVariant` (dual-write) |

### 4.4 Stock Table — Target Akhir

| Tabel | Status akhir |
|-------|:------------:|
| `stock_locker` | ✅ Tetap aktif — untuk laporan agregat & 36 modul |
| `stock_locker_variant` | ✅ Tetap aktif — untuk detail variant & transaksi |
| `stock_locker_mutasi` | ✅ Tetap aktif — mutasi product-level |
| `stock_locker_mutasi_variant` | ✅ Tetap aktif — mutasi variant-level |

### 4.5 Transaction Atomicity — Wajib

Setiap transaksi posting menulis ke **6 komponen** sekaligus:
- `FifoProdukJadi` + `FifoProdukJadiVarian`
- `LockerStock` + `LockerStockVariant`
- `LockerStockMutasi` + `LockerStockMutasiVarian`

>>> **WAJIB** gunakan database transaction (`$this->db->trans_start()` / `trans_complete()`) 
>>> di semua 19 modul. Jika satu write gagal, SEMUA harus rollback.
>>> Tanpa transaction, partial write = stok inkonsisten = **P0 bug**.

### 4.6 Reconciliation Otomatis

Jalankan query ini periodik (cron harian) untuk deteksi selisih stok:

```sql
SELECT fl.produk_id, fl.jumlah AS total_locker, SUM(fv.jumlah) AS total_variant
FROM stock_locker fl
JOIN stock_locker_variant fv USING(produk_id)
GROUP BY fl.produk_id
HAVING total_locker != total_variant;
```

Jika ada hasil = ada inkonsistensi dual-write. Fix MANUAL sebelum posting lanjutan.

---

## 5. DESAIN TEKNIS

### 5.1 Default Variant Sentinel

```php
define('VARIANT_DEFAULT_ID', -1);
define('VARIANT_DEFAULT_LABEL', 'Default');

function resolveVariantId($produkId, $variantId = 0)
{
    if ((int)$variantId > 0) {
        return (int)$variantId;
    }
    return VARIANT_DEFAULT_ID;
}
```

### 5.2 Session Key Normalizer — Auto di MdlMother::__construct()

Semua model extend `MdlMother`. `__construct()` otomatis menormalisasi
semua session `_TR_*` yang memiliki `items` — **tanpa perlu panggil manual**:

```php
public function __construct()
{
    parent::__construct();
    if (isset($_SESSION) && is_array($_SESSION)) {
        $this->load->helper('he_variant_unifier');
        foreach ($_SESSION as $key => $val) {
            if (strpos($key, '_TR_') === 0 && isset($val['items'])) {
                normalizeSessionKeys($_SESSION, $key);
            }
        }
    }
}
```

```php
function normalizeSessionKeys(&$session, $cCode)
{
    if (!isset($session[$cCode]['items'])) return;
    $newItems = array();
    foreach ($session[$cCode]['items'] as $key => $spec) {
        if (!is_array($spec)) continue;
        $pid = isset($spec['produk_id']) ? (int)$spec['produk_id'] : (int)$key;
        $vid = resolveVariantId($pid, isset($spec['variant_id']) ? (int)$spec['variant_id'] : 0);
        $newKey = 'variant:' . $pid . ':' . $vid;
        $spec['variant_id'] = $vid;
        $spec['cart_key'] = $newKey;
        $newItems[$newKey] = $spec;
    }
    $session[$cCode]['items'] = $newItems;
}
```

**Konsekuensi penting:**
- **0 perubahan** di `Modul_Controller.php` — normalisasi otomatis saat model di-load
- **0 method baru** — cukup modifikasi `__construct()` yang sudah ada
- **Idempoten** — normalisasi hanya tambah `variant_id` & `cart_key`, tidak hapus data apapun
- **Non-varian** → `variant_id = -1` (aman dibaca oleh `MdlLockerStock` yang aggregate)
- **Varian** → `variant_id` asli (diproses oleh `MdlLockerStockVariant` untuk detail)

### 5.3 View Database

```sql
CREATE OR REPLACE VIEW stock_unified AS
SELECT produk_id, variant_id, cabang_id, gudang_id, state, jumlah
FROM stock_locker_variant
UNION ALL
SELECT produk_id, -1 AS variant_id, cabang_id, gudang_id, state, jumlah
FROM stock_locker
WHERE produk_id NOT IN (SELECT DISTINCT produk_id FROM stock_locker_variant);
```

---

## 6. PERUBAHAN PER MODUL

### 6.1 Wajib untuk Semua 19 Modul Inventory

| File | Perubahan |
|------|-----------|
| `config/coTransaksiCore.php` | TAMBAH: 3 varian comNames (`FifoProdukJadiVarian`, `LockerStockVariant`, `LockerStockMutasiVariant`) + feature flag `singleVariantStandard` (`.3-6 baris`) |

> FollowUp loop GENERIC — otomatis proses comName apapun dari config.
> Modul_Controller, _selectorItem, FollowUp: **0 perubahan**.
> Normalisasi session otomatis via MdlMother::__construct().

### 6.2 Khusus per Agent

| Agent | Modul | Catatan |
|:-----:|-------|---------|
| **1** | `konversi`, `konversi_varian` | Flow multi-arah — perlu selector variant di shopping cart |
| **2** | `pembelian*` | Purchase — tambah stok via FIFO varian |
| **3** | `penjualan*` | Sales — kurangi stok via FIFO varian |
| **4** | `pindahgudang`, `opname` | Transfer 2 arah + stock opname |
| **5** | `distribusi*` | Distribusi lintas cabang — LockerStock* varian |
| **6** | `produksi*`, `adjustment`, `asetmanagement` | BOM + adjustment + aset |

---

## 7. VERIFIKASI

| ID | Skenario | Expected |
|----|----------|----------|
| T1 | Non-varian → Non-varian | Key seragam `variant:{pid}:-1` |
| T2 | Non-varian → Varian | Target variant_id=15 |
| T3 | Varian → Non-varian | Source variant_id=15 |
| T4 | Varian → Varian | Kedua variant_id > 0 |
| T5 | Multi source 1 parent | Tidak ada ghost key |

---

## 8. STRATEGI EKSEKUSI — 6-8 Minggu

```
Fase 1: Foundation (minggu 1-2)
├── he_variant_unifier.php, stock_unified view, feature flag
├── MdlMother: __construct() auto-normalize semua session _TR_* (0 method baru)
├── Wajib: verifikasi semua 19 modul punya database transaction
├── Wajib: reconciliation query (cron harian)

Fase 2: Rollout 19 Modul (minggu 3-6) — PARALEL 6 AGENT
├── Agent 1: konversi, konversi_varian
├── Agent 2: pembelian, pembelianimport, pembelianjasa, pembelianprojek
├── Agent 3: penjualan, penjualanproject
├── Agent 4: pindahgudang, opname
├── Agent 5: distribusifg, distribusiproduksi, distribusisupplies, distribusijasa
├── Agent 6: produksi, produksiproses, adjustment, asetmanagement

(Permanen — tidak ada fase cleanup — semua komponen non-varian TETAP ADA)
└── Final regression all 4 scenarios
```

---

## 9. RINGKASAN

| Metric | Arsitektur Lama (Non-Varian) | Arsitektur Baru (Dual-Write Permanen) |
|--------|:----------:|:--------------:|
| Session key format | `{produk_id}` | `variant:{pid}:{vid}` |
| Stock table (aggregate) | `stock_locker` | `stock_locker` ✅ tetap |
| Stock table (variant) | — | `stock_locker_variant` ✅ baru |
| Pipeline | `rsltItems` | `rsltItems` ✅ tetap (handle semua item via variant_id) |
| Modul diubah | 0 | **19 inventory** |
| Modul tidak diubah | - | **36 non-stok + backup** |
| Ghost key handler | Tidak perlu | **Tidak perlu** |
| Standar acuan | - | ISO 25010 + SAP LO-VC + ISO 8000 |
| Estimasi | - | **4-6 minggu (6 agent paralel)** |
| Atomicity | Tidak perlu | **WAJIB** database transaction (6 komponen) |
| Reconciliation | Tidak ada | Cron harian `stock_locker <> stock_locker_variant` |

---

## 10. DESIGN REQUIREMENTS CHECKLIST

Sebelum rollout, pastikan:

- [x] **Normalisasi session otomatis** via `MdlMother::__construct()` — **0 perubahan di controller**
- [ ] **6 model varian** di-copy dari `san_staging` (3 Coms + 3 Mdls)
- [ ] **Database transaction** aktif di semua 19 modul (`trans_start()` sebelum posting)
- [ ] **Reconciliation query** berjalan di cron harian
- [ ] **`stock_unified` view** sudah di-deploy (berlaku untuk transisi + permanen)
- [ ] Setiap agent verifikasi dual-write: `stock_locker.jumlah = SUM(stock_locker_variant.jumlah)` per produk
- [ ] **openbalance** dikerjakan oleh Agent 4 (stok awal, perlu dual-write)
