# 📘 BLUEPRINT: KONVERTER MULTI-VARIANT — SINGLE STANDARD VARIANT ARCHITECTURE

**Module:** `konversi`  
**Workspace:** `w:/san_staging/application/modules/konversi`  
**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. Satu tabel stok: **`stock_locker_variant`** menggantikan `stock_locker` + `stock_locker_variant`.
4. Satu jalur processing: **`rsltItems2`** menggantikan `rsltItems` + `rsltItems2`.
5. Backward compatibility: flow existing tetap jalan selama masa migrasi.

### 1.3 In Scope

- Modul `konversi` — semua flow (1334 pusat, 334 cabang).
- Session contract: `items`, `items2`, `items2_sum`, `rsltItems`, `rsltItems2`.
- Selector, shopping cart, validator, followup (posting stok & accounting).
- Variant default generator untuk produk non-varian.

### 1.4 Out of Scope

- Modul lain di luar `konversi` (pembelian, penjualan, dll) — akan menyusul di fase rollout.
- Rewrite total UI template.
- Perubahan skema database secara destruktif.

---

## 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 |

Dokumen acuan existing di proyek:
- `docs/blueprint-upgrade-modul-konversi-support-varian.md`
- `docs/blueprint-uat-fix-konversi-selector-shoppingcart-varian.md`
- `docs/blueprint_konversi_stok.md`
- `application/modules/konversi/WORKFLOW_MODUL_KONVERSI.md`
- `application/modules/variant_cutover/sap_variant_conversion_rules.md`
- `application/modules/variant_cutover/STANDAR_MODUL_VARIANT_CUTOVER_ISO25010.md`

---

## 3. BASELINE MASALAH (ROOT CAUSE)

### 3.1 Dual Session Key

| Item | Key Saat Ini | Standar Target |
|------|-------------|----------------|
| Non-varian | `"1743"` (produk_id) | ❌ |
| Varian | `"variant:1743:15"` | ✅ |

**Akibat:** Fungsi `buildVariantKeyCandidate()` (`FollowUp.php:219`) harus menebak-nebak format key.

### 3.2 Ghost Key Harus Dibersihkan

`FollowUp.php:281-284`:
```php
if ($isVariantSibling && $vid < 1 && strpos($ck, "variant:") !== 0) {
    // Ghost row pattern: master key muncul padahal ada varian siblings
    $dropItemKeys[] = $k;  // ← HAPUS paksa
}
```

**Akibat:** Overhead runtime + potensi data hilang jika ghost key lolos.

### 3.3 Dual Processing Pipeline

| Pipeline | Gate | Untuk |
|----------|------|-------|
| `rsltItems` | `items` → `tableIn_detail_rsltItems` | Non-varian |
| `rsltItems2` | `items2` → `tableIn_detail_rsltItems2` | Varian |

**Akibat:** Logic berulang di `FollowUp.php:1448-1523`, rawan inkonsistensi.

### 3.4 Dual Stock Table

```sql
-- Non-varian
FROM stock_locker WHERE produk_id = ?
-- Varian
FROM stock_locker_variant WHERE produk_id = ? AND variant_id = ?
```

**Akibat:** Config `coTransaksiCore.php` harus daftarkan 2 komponen paralel.

### 3.5 Dual HPP Injector

```php
"afterPreProcessorInjector"  → gateSource: "rsltItems"   (non-varian)
"afterPreProcessorInjector2" → gateSource: "rsltItems2"  (varian)
```

**Akibat:** Config membengkak, rawan salah mapping.

---

## 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`.

```
 ┌──────────────────────────────────────────┐
 │              PRODUK (Entity)              │
 │       produk_id = 1743                    │
 │       nama = "T-Shirt"                    │
 └──────────┬───────────────────────────────┘
            │ selalu memiliki minimal 1 variant
            ▼
 ┌──────────────────────────────────────────┐
 │         VARIANT (Identity Stok)           │
 │                                           │
 │  ╔═══════════════════════════════════╗    │
 │  ║ variant_id = -1 (DEFAULT)        ║    │ ← Produk non-varian
 │  ║ variant_label = "Default"        ║    │
 │  ║ cart_key = "variant:1743:-1"     ║    │
 │  ╚═══════════════════════════════════╝    │
 │                                           │
 │  ┌──────────────┐  ┌──────────────┐       │
 │  │ variant=15   │  │ variant=16   │       │ ← Produk multi-varian
 │  │ label=Hitam  │  │ label=Putih  │       │
 │  │ cart=v:1743:15│  │ cart=v:1743:16│      │
 │  └──────────────┘  └──────────────┘       │
 └──────────────────────────────────────────┘
```

### 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

| Layer Saat Ini | Setelah Migrasi |
|----------------|-----------------|
| `rsltItems` (non-varian) | ❌ **Dihapus** |
| `rsltItems2` (varian) | ✅ **Jadi satu-satunya** |
| `tableIn_detail_rsltItems` | ❌ **Dihapus** |
| `tableIn_detail_rsltItems2` | ✅ **Jadi satu-satunya** |
| `afterPreProcessorInjector` | ❌ **Dihapus** |
| `afterPreProcessorInjector2` | ✅ **Jadi satu-satunya** |

### 4.4 Stock Table — Single Standard

| Tabel | Status |
|-------|--------|
| `stock_locker` | ❌ **Dihapus** — data dimigrasi ke `stock_locker_variant` dengan `variant_id=-1` |
| `stock_locker_variant` | ✅ **Jadi satu-satunya** |
| `stock_locker_mutasi` | ❌ **Dihapus** |
| `stock_locker_mutasi_variant` | ✅ **Jadi satu-satunya** |

### 4.5 Component Config — Single Standard

```php
// SEBELUM (dual):
"FifoProdukJadi"      → untuk non-varian
"FifoProdukJadiVarian" → untuk varian

// SESUDAH (single):
"FifoProdukJadiVarian" → untuk SEMUA item (variant_id menyertainya)
```

---

## 5. OPSI IMPLEMENTASI

### Opsi A (Recommended): Default Variant Generator + Migrasi Bertahap

**Inti:**
1. Setiap produk non-varian mendapatkan **1 varian default implisit** (`variant_id = -1`).
2. Session key diubah otomatis di entry point: `"1743"` → `"variant:1743:-1"`.
3. View database `stock_unified` menjembatani `stock_locker` dan `stock_locker_variant`.

**Kelebihan:**
- Tidak perlu migrasi data sekaligus.
- Backward compatible.
- Risiko rollback rendah.

**Konsekuensi:**
- Perlu view di database.
- Kode legacy tetap dipertahankan selama transisi.

### Opsi B: Rewrite Total Session Contract

**Inti:**
1. Ubah session contract total — semua key `variant:{pid}:{vid}`.
2. Tambah middleware normalizer di `Modul_Controller::__construct()`.
3. Refactor semua method yang membedakan varian/non-varian.

**Kelebihan:**
- Bersih — tidak ada utang teknis.
- Kode baru lebih sederhana.

**Konsekuensi:**
- Risiko sangat tinggi — banyak file berubah.
- UAT regression wajib mencakup semua skenario.

---

## 6. DESAIN TEKNIS

### 6.1 Default Variant Sentinel

```php
// Konstanta global
define('VARIANT_DEFAULT_ID', -1);
define('VARIANT_DEFAULT_LABEL', 'Default');

/**
 * Resolve variant_id untuk sebuah produk.
 * Jika produk non-varian, kembalikan sentinel -1.
 */
function resolveVariantId($produkId, $variantId = 0)
{
    if ((int)$variantId > 0) {
        return (int)$variantId;
    }
    return VARIANT_DEFAULT_ID; // -1
}
```

### 6.2 Session Key Normalizer (Middleware)

Letak: `Modul_Controller::__construct()`.

```php
/**
 * Normalisasi session key ke format "variant:{pid}:{vid}".
 * Non-varian → variant:{pid}:-1
 */
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;
}
```

### 6.3 Unified Stock Access

```php
/**
 * Satu method untuk baca stok — tanpa peduli varian/non-varian.
 */
function getUnifiedStock($produkId, $variantId, $cabangId, $gudangId)
{
    $vid = resolveVariantId($produkId, $variantId);
    $this->load->model("Mdls/MdlLockerStockVariant");
    $mdl = new MdlLockerStockVariant();
    $mdl->addFilter("produk_id='$produkId'");
    $mdl->addFilter("variant_id='$vid'");
    $mdl->addFilter("cabang_id='$cabangId'");
    $mdl->addFilter("gudang_id='$gudangId'");
    $mdl->addFilter("state='active'");
    return $mdl->lookupAll()->result();
}
```

### 6.4 Database View

```sql
-- View penyatu stock_locker + stock_locker_variant
CREATE 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.5 Session Contract — Contoh Sebelum & Sesudah

```php
// SEBELUM (dual):
$_SESSION[$cCode]['items'] = array(
    "1743" => array(                    // ← key = produk_id
        "id" => 1743,
        "nama" => "T-Shirt",
        "variant_id" => 0,
        "jml" => 10,
    ),
    "variant:1743:15" => array(         // ← key = variant:
        "id" => 1743,
        "nama" => "T-Shirt Hitam",
        "variant_id" => 15,
        "jml" => 5,
    ),
);

// SESUDAH (single):
$_SESSION[$cCode]['items'] = array(
    "variant:1743:-1" => array(         // ← key seragam
        "id" => 1743,
        "produk_id" => 1743,
        "nama" => "T-Shirt",
        "variant_id" => -1,
        "variant_label" => "Default",
        "cart_key" => "variant:1743:-1",
        "jml" => 10,
    ),
    "variant:1743:15" => array(
        "id" => 1743,
        "produk_id" => 1743,
        "nama" => "T-Shirt Hitam",
        "variant_id" => 15,
        "variant_label" => "Hitam",
        "cart_key" => "variant:1743:15",
        "jml" => 5,
    ),
);
```

---

## 7. MAPPING PERUBAHAN FILE

### 7.1 New Files

| File | Isi |
|------|-----|
| `helpers/he_variant_unifier.php` | Helper: `resolveVariantId()`, `normalizeSessionKeys()`, `getUnifiedStock()` |
| `database/migrations/XXX_create_view_stock_unified.sql` | View `stock_unified` |

### 7.2 Modified Files — Config

| File | Perubahan |
|------|-----------|
| `config/coTransaksiUi.php` | Hapus `shoppingCartPairedItemVarian` (tidak perlu — semua sudah varian) |
| `config/coTransaksiCore.php` | Hapus `afterPreProcessorInjector` — hanya pertahankan `Injector2` |
| `config/coTransaksiCore.php` | Hapus komponen `FifoProdukJadi` — hanya `FifoProdukJadiVarian` |
| `config/coTransaksiCore.php` | Hapus `LockerStock` — hanya `LockerStockVariant` |
| `config/coTransaksiCore.php` | Hapus `LockerStockMutasi` — hanya `LockerStockMutasiVariant` |

### 7.3 Modified Files — Controller

| File | Perubahan |
|------|-----------|
| `controllers/Modul_Controller.php` | Tambah `normalizeSessionKeys()` di `__construct()` |
| `controllers/FollowUp.php` | **Hapus** `normalizeSessionDetailGatesByItems()` |
| `controllers/FollowUp.php` | **Hapus** `buildVariantKeyCandidate()` |
| `controllers/FollowUp.php` | **Hapus** `applyCounterRowsToExistingGate()` |
| `controllers/FollowUp.php` | **Gabung** `rsltItems` + `rsltItems2` → cukup `rsltItems2` |
| `controllers/FollowUp.php` | **Hapus** `isVariantTrapEnabled()`, `logVariantTrap()`, `writeVariantTrapFile()` |
| `controllers/_processSelectProductConvertion.php` | Hapus branch `if ($variantId > 0)` — semua punya variant_id |
| `controllers/_shoppingCart.php` | Sederhanakan `parsePairedSelectionValue()` |
| `controllers/_shoppingCart.php` | Hapus `buildVariantSessionKey()` — pakai helper |

### 7.4 Deleted Code

| Fungsi/Method | Baris | Alasan |
|---------------|:-----:|--------|
| `normalizeSessionDetailGatesByItems()` | ~146 | Tidak ada ghost key |
| `buildVariantKeyCandidate()` | ~18 | Key seragam `variant:` |
| `applyCounterRowsToExistingGate()` | ~53 | Logic counter disederhanakan |
| `isVariantTrapEnabled()` | ~14 | Debug tidak relevan |
| `logVariantTrap()` | ~16 | Tidak perlu tracing ghost key |
| `writeVariantTrapFile()` | ~16 | Tidak perlu |
| Separuh `_processSelectProductConvertion.php` | ~400 | Branch `if variant` dihapus |
| Separuh `coTransaksiCore.php` | ~100 | Komponen ganda dihapus |
| **Total estimasi** | **~763 baris** | |

---

## 8. VERIFIKASI

### 8.1 Skenario Test

| ID | Skenario | Source | Target | Expected |
|----|----------|--------|--------|----------|
| T1 | Non-varian → Non-varian | `variant:100:-1` | `variant:200:-1` | ✅ Key seragam, ghost key = 0 |
| T2 | Non-varian → Varian | `variant:100:-1` | `variant:200:15` | ✅ Target variant_id=15 |
| T3 | Varian → Non-varian | `variant:100:15` | `variant:200:-1` | ✅ Source variant_id=15 |
| T4 | Varian → Varian | `variant:100:15` | `variant:200:16` | ✅ Kedua variant_id > 0 |
| T5 | Multi source 1 parent | `variant:100:15` + `variant:100:16` | `variant:200:-1` | ✅ Tidak ada ghost key "100" |

### 8.2 Edge Cases

| ID | Edge Case | Handling |
|----|-----------|----------|
| E1 | Produk tanpa varian di DB | Default sentinel `variant_id=-1` |
| E2 | Stok lama di `stock_locker` | View `stock_unified` menyerapnya |
| E3 | Key session lama `"1743"` masih ada | Normalizer di `Modul_Controller` mengubah ke `variant:1743:-1` |
| E4 | Produk varian dengan stok 0 | Tetap bisa dipilih (validasi di followup) |
| E5 | Rollback ke session lama | `normalizeSessionKeys()` jalan setiap request |

### 8.3 Regression Minimal

Wajib lulus sebelum rilis:
1. Non-varian → Non-varian
2. Non-varian → Varian
3. Varian → Non-varian
4. Varian → Varian

---

## 9. ROLLBACK PLAN

| Skenario | Tindakan |
|----------|----------|
| Session error setelah normalizer | Matikan `normalizeSessionKeys()` via feature flag |
| Stok tidak balance | Matikan view `stock_unified`, aktifkan ulang query `stock_locker` |
| Key format bentrok | Aktifkan `buildVariantKeyCandidate()` sebagai fallback |
| Approval gagal | Kembalikan `afterPreProcessorInjector` + `rsltItems` |

**Feature flag** di `config/coTransaksiCore.php`:

```php
"singleVariantStandard" => array(
    "enabled" => false,   // true untuk aktifkan
    "defaultVariantId" => -1,
    "forceKeyNormalization" => true,
),
```

---

## 10. STRATEGI MIGRASI LINTAS MODUL

```
Fase 1: Foundation (2-3 minggu)
├── Buat view `stock_unified`
├── Helper `he_variant_unifier.php`
├── Normalizer middleware di `Modul_Controller`
└── Default variant sentinel

Fase 2: Pilot — Modul konversi + variant_cutover (1-2 minggu)
├── Hapus dual pipeline (rsltItems → rsltItems2)
├── Hapus ghost key normalizer
├── Hapus buildVariantKeyCandidate
└── UAT regression T1-T5

Fase 3: Rollout ke modul stok bertahap (4-6 minggu)
├── Pembelian, Penjualan, Penerimaan
├── Distribusi, Produksi
└── Sisanya (~15 modul)

Fase 4: Cleanup (1-2 minggu)
├── Hapus tabel `stock_locker` + `stock_locker_mutasi`
├── Hapus komponen non-varian dari config
└── Hapus helper legacy
```

**Total estimasi:** ~100+ file berubah, 8-12 minggu.

---

## 11. ISO/IEC 25010 COMPLIANCE CHECKLIST

| ID | Sub-Karakteristik | Standar | Metode Verifikasi |
|----|-------------------|---------|-------------------|
| FS-01 | Functional Completeness | Semua produk punya variant_id, session key seragam `variant:` | Cek 50 produk random |
| FS-02 | Functional Correctness | `∑qty source = ∑qty target` setelah migrasi | Uji T1-T5 |
| RL-03 | Fault Tolerance | Normalizer jalan tanpa exception di semua session state | Inject session corrupt |
| MN-01 | Modularity | Helper terpisah, tidak inline di controller | Code review |
| MN-02 | Reusability | `normalizeSessionKeys()` reusable di modul lain | Dokumentasi API |
| SE-01 | Data Integrity | Tidak ada ghost key terlewat ke followup | Debug trap log |

---

## 12. RINGKASAN

| Metric | Sebelum (Dual) | Sesudah (Single) |
|--------|:---------------:|:-----------------:|
| Session key format | 2 format | 1 format `variant:{pid}:{vid}` |
| Stock table | 2 tabel | 1 tabel (`stock_locker_variant`) |
| Processing pipeline | 2 (`rsltItems` + `rsltItems2`) | 1 (`rsltItems2`) |
| HPP injector | 2 | 1 |
| Ghost key handler | Wajib | **Tidak perlu** |
| Kode dibersihkan | 0 | ~763 baris |
| Standar acuan | ISO 25010 | ISO 25010 + SAP LO-VC + ISO 8000 |
