# Code Generator — Dokumentasi Teknis

Sistem generate kode/nomor otomatis yang fleksibel dan reusable di berbagai modul aplikasi. Format kode dikonfigurasi per-cabang dan per-modul melalui UI admin, disimpan di tabel `cabang_config`, dan counter disimpan di tabel `nota_urut`.

---

## Daftar Isi

1. [Arsitektur & File](#1-arsitektur--file)
2. [Cara Kerja](#2-cara-kerja)
3. [Konfigurasi Pattern](#3-konfigurasi-pattern)
4. [Komponen Pattern](#4-komponen-pattern)
5. [Reset Cycle](#5-reset-cycle)
6. [Penyimpanan Data](#6-penyimpanan-data)
7. [Concurrency Safety](#7-concurrency-safety)
8. [Penggunaan dari Kode PHP](#8-penggunaan-dari-kode-php)
9. [Referensi API Service](#9-referensi-api-service)
10. [UI Admin](#10-ui-admin)
11. [Contoh Output](#11-contoh-output)
12. [Modul yang Tersedia](#12-modul-yang-tersedia)

---

## 1. Arsitektur & File

```
common/
├── services/
│   └── CodeGeneratorService.php   ← inti logika (generate, build, config CRUD)
├── models/
│   ├── CabangConfig.php           ← key-value store config per cabang
│   └── NotaUrut.php               ← tabel counter
└── base/
    └── MyHelper.php               ← facade global (generateModuleCode, peekModuleCode)

backend/
├── controllers/
│   └── CabangConfigController.php ← action HTTP: code-generator, save, preview, reset, delete
└── views/
    └── cabang-config/
        └── code_generator.php     ← UI admin (pattern builder, live preview)
```

### Dependency

```
MyHelper
  └── CodeGeneratorService
        ├── CabangConfig   (baca/tulis config JSON)
        ├── NotaUrut       (baca/tulis counter)
        └── Cabang         (ambil inisial cabang)
```

---

## 2. Cara Kerja

```
generateModuleCode($cabangId, 'sales')
        │
        ▼
CodeGeneratorService::generate()
        │
        ├─ getConfig($cabangId, 'sales')
        │      └─ SELECT value FROM cabang_config
        │         WHERE cabang_id = ? AND key = 'code_generator.sales'
        │
        ├─ getNotaType('sales', 'monthly')
        │      └─ → "cg_sales_202605"   (encode reset cycle ke type column)
        │
        ├─ BEGIN TRANSACTION
        │
        ├─ SELECT id, last_id FROM nota_urut
        │    WHERE cabang_id = ? AND type = 'cg_sales_202605'
        │    FOR UPDATE                  ← row lock: blokir request lain
        │
        ├─ counter = last_id + 1
        │    (atau 1 jika baris belum ada)
        │
        ├─ UPDATE/INSERT nota_urut
        │
        ├─ COMMIT
        │
        └─ buildCode(config, counter, cabangId)
               └─ susun segmen sesuai pattern → "SLS-2026-05-00001"
```

---

## 3. Konfigurasi Pattern

Konfigurasi disimpan sebagai JSON di `cabang_config`:

- **key**: `code_generator.{moduleKey}` — contoh: `code_generator.sales`
- **value**: JSON object

### Struktur JSON Config

```json
{
  "prefix":         "SLS",
  "separator":      "-",
  "running_digits": 5,
  "reset_cycle":    "monthly",
  "pattern":        ["PREFIX", "YEAR", "MONTH_ROMAN", "RUNNING_NUMBER"],
  "year_format":    "year_4_digit",
  "custom_text":    "",
  "module_key":     "sales"
}
```

| Field | Tipe | Deskripsi |
|---|---|---|
| `prefix` | string | Teks awalan, otomatis UPPERCASE |
| `separator` | string | Pemisah antar segmen (`-`, `/`, `.`, `_`, `""`) |
| `running_digits` | int | Jumlah digit running number (1–10), di-pad dengan `0` |
| `reset_cycle` | string | Kapan counter direset: `never`, `yearly`, `monthly`, `daily` |
| `pattern` | array | Urutan komponen yang menyusun kode |
| `year_format` | string | `year_4_digit` (2026) atau `year_2_digit` (26) |
| `custom_text` | string | Teks kustom bebas, otomatis UPPERCASE |
| `module_key` | string | Identifier modul (disimpan kembali ke dalam JSON) |

---

## 4. Komponen Pattern

Berikut daftar seluruh komponen yang bisa diposisikan dalam pattern:

| Konstanta | Nilai | Output Contoh | Keterangan |
|---|---|---|---|
| `COMPONENT_PREFIX` | `PREFIX` | `INV` | Nilai dari field `prefix` config |
| `COMPONENT_YEAR` | `YEAR` | `2026` atau `26` | Tahun saat ini (format sesuai `year_format`) |
| `COMPONENT_MONTH` | `MONTH` | `05` | Bulan saat ini, 2 digit |
| `COMPONENT_MONTH_ROMAN` | `MONTH_ROMAN` | `V` | Bulan saat ini, angka romawi (I–XII) |
| `COMPONENT_DAY` | `DAY` | `23` | Tanggal saat ini, 2 digit |
| `COMPONENT_RUNNING` | `RUNNING_NUMBER` | `00001` | Counter berurutan, di-pad sesuai `running_digits` |
| `COMPONENT_CABANG_CODE` | `CABANG_CODE` | `GSM` | Nilai kolom `inisial` di tabel `cabang` |
| `COMPONENT_CABANG_ID` | `CABANG_ID` | `3` | Nilai `cabang_id` (angka) |
| `COMPONENT_INITIAL` | `INITIAL` | `ADM` | Nilai kolom `inisial` user yang sedang login |
| `COMPONENT_CUSTOM_TEXT` | `CUSTOM_TEXT` | `BANQUET` | Nilai dari field `custom_text` config |

> **`RUNNING_NUMBER` wajib** ada dalam setiap pattern. Validasi ini diterapkan di controller dan UI.

Komponen `PREFIX`, `CABANG_CODE`, `CABANG_ID`, `CUSTOM_TEXT`, dan `INITIAL` **dilewati** (tidak ditambahkan ke output) jika nilainya kosong.

---

## 5. Reset Cycle

Reset cycle menentukan kapan counter dikembalikan ke 1. Mekanismenya **tidak memerlukan perubahan skema database** — siklus di-encode langsung ke kolom `type` di tabel `nota_urut`.

| Reset Cycle | Encoding di `nota_urut.type` | Contoh |
|---|---|---|
| `never` | `cg_{moduleKey}` | `cg_sales` |
| `yearly` | `cg_{moduleKey}_{YYYY}` | `cg_sales_2026` |
| `monthly` | `cg_{moduleKey}_{YYYYMM}` | `cg_sales_202605` |
| `daily` | `cg_{moduleKey}_{YYYYMMDD}` | `cg_sales_20260523` |

Ketika siklus baru dimulai (bulan baru, tahun baru, hari baru), baris baru dengan `type` yang berbeda otomatis dibuat di `nota_urut` dengan counter mulai dari `1`. Baris lama tetap ada sebagai histori.

---

## 6. Penyimpanan Data

### Tabel `cabang_config`

Menyimpan konfigurasi format per-modul per-cabang.

```
cabang_config
├── id          INT  PK
├── cabang_id   INT  FK cabang.id
├── key         VARCHAR(100)   → "code_generator.sales"
├── value       TEXT           → JSON config (lihat §3)
├── createdAt   INT  (unix timestamp, by TimestampBehavior)
└── updatedAt   INT
```

Konstanta prefix di `CabangConfig`:
```php
CabangConfig::KEY_CODE_GENERATOR_PREFIX  // = 'code_generator.'
```

### Tabel `nota_urut`

Menyimpan counter terakhir per-modul per-cabang per-siklus.

```
nota_urut
├── id          INT  PK
├── cabang_id   INT
├── type        VARCHAR   → "cg_sales_202605"
├── last_id     INT       → nilai counter terakhir
└── updateAt    DATETIME
```

---

## 7. Concurrency Safety

Generate kode menggunakan **database transaction + row-level locking** untuk memastikan tidak ada duplikasi saat beberapa request terjadi bersamaan.

```php
// Di dalam CodeGeneratorService::generate()
$transaction = $db->beginTransaction();
try {
    // Lock baris sebelum baca, blokir request lain pada baris yang sama
    $row = $db->createCommand(
        "SELECT id, last_id FROM nota_urut
         WHERE cabang_id = :cid AND type = :t FOR UPDATE",
        [':cid' => $cabangId, ':t' => $notaType]
    )->queryOne();

    // Increment atomic
    $counter = $row ? (int)$row['last_id'] + 1 : 1;

    // Update atau insert
    // ...

    $transaction->commit();
} catch (\Throwable $e) {
    $transaction->rollBack();
    throw $e;
}
```

`FOR UPDATE` memastikan:
- Request A dan B yang datang bersamaan: salah satu menunggu hingga yang lain selesai commit
- Tidak ada dua request yang mendapat nilai counter yang sama
- Jika terjadi error, counter **tidak** di-increment (rollback)

---

## 8. Penggunaan dari Kode PHP

### Via `MyHelper` (cara yang direkomendasikan)

Selalu gunakan konstanta `CodeGeneratorService::MODULE_*` untuk module key — jangan hardcode string.

#### Generate kode baru (increment counter)

```php
use common\base\MyHelper;
use common\services\CodeGeneratorService;

// Di controller atau model saat menyimpan record baru:
$noFaktur = MyHelper::generateModuleCode($cabangId, CodeGeneratorService::MODULE_SALES);
// → "SLS-2026-V-00001"

$noReservasi = MyHelper::generateModuleCode($cabangId, CodeGeneratorService::MODULE_RESERVATION_DATA);
// → "RSV/ADM/2026/05/00001"

$noPO = MyHelper::generateModuleCode($cabangId, CodeGeneratorService::MODULE_PURCHASE_ORDER);
```

#### Preview kode berikutnya (tanpa increment)

Gunakan di form sebelum user submit, atau di API preview.

```php
$preview = MyHelper::peekModuleCode($cabangId, CodeGeneratorService::MODULE_SALES);
// → "SLS-2026-V-00002"  (next yang akan di-generate, tapi counter belum naik)
```

#### Penanganan error

Kedua method melempar `\RuntimeException` jika modul belum dikonfigurasi:

```php
try {
    $kode = MyHelper::generateModuleCode($cabangId, 'sales');
} catch (\RuntimeException $e) {
    // Modul belum dikonfigurasi di Pengaturan → Code Generator
    Yii::$app->session->setFlash('error', $e->getMessage());
}
```

### Via `CodeGeneratorService` langsung (untuk kasus khusus)

```php
use common\services\CodeGeneratorService;

// Generate (sama seperti MyHelper, tapi tanpa pengecekan config exist)
$kode = CodeGeneratorService::generate($cabangId, CodeGeneratorService::MODULE_SALES);

// Preview tanpa increment
$config  = CodeGeneratorService::getConfig($cabangId, 'sales');
$counter = CodeGeneratorService::getCurrentCounter($cabangId, 'sales');
$preview = CodeGeneratorService::buildCode($config, $counter + 1, $cabangId);

// Baca counter saat ini
$current = CodeGeneratorService::getCurrentCounter($cabangId, 'sales');

// Reset counter ke 0
CodeGeneratorService::resetCounter($cabangId, 'sales');

// Simpan konfigurasi baru
CodeGeneratorService::saveConfig($cabangId, 'sales', [
    'prefix'         => 'SLS',
    'separator'      => '-',
    'running_digits' => 5,
    'reset_cycle'    => 'monthly',
    'pattern'        => ['PREFIX', 'YEAR', 'MONTH_ROMAN', 'RUNNING_NUMBER'],
    'year_format'    => 'year_4_digit',
    'custom_text'    => '',
]);

// Hapus konfigurasi modul
CodeGeneratorService::deleteConfig($cabangId, 'sales');

// Ambil semua konfigurasi untuk satu cabang
$allConfigs = CodeGeneratorService::getAllConfigs($cabangId);
```

---

## 9. Referensi API Service

### `CodeGeneratorService`

| Method | Signature | Keterangan |
|---|---|---|
| `generate` | `(int $cabangId, string $moduleKey): string` | Generate + increment counter (concurrency-safe) |
| `buildCode` | `(array $config, int $counter, int $cabangId): string` | Susun string kode dari config + counter, tanpa DB |
| `getConfig` | `(int $cabangId, string $moduleKey): ?array` | Baca config dari `cabang_config`, null jika tidak ada |
| `getAllConfigs` | `(int $cabangId): array` | Semua config modul untuk satu cabang |
| `saveConfig` | `(int $cabangId, string $moduleKey, array $config): bool` | Upsert config ke `cabang_config` |
| `deleteConfig` | `(int $cabangId, string $moduleKey): bool` | Hapus config satu modul |
| `getCurrentCounter` | `(int $cabangId, string $moduleKey): int` | Baca counter terakhir (tanpa increment) |
| `resetCounter` | `(int $cabangId, string $moduleKey): int` | Set counter ke 0 |
| `getNotaType` | `(string $moduleKey, string $resetCycle): string` | Encode reset cycle ke string `nota_urut.type` |

### `MyHelper`

| Method | Signature | Keterangan |
|---|---|---|
| `generateModuleCode` | `(int $cabangId, string $moduleKey): string` | Facade generate + throw jika config tidak ada |
| `peekModuleCode` | `(int $cabangId, string $moduleKey): string` | Preview kode berikutnya tanpa increment |

### Controller Endpoints

| Action | URL | Method | Keterangan |
|---|---|---|---|
| `actionCodeGenerator` | `/cabang-config/code-generator` | GET | Halaman UI admin |
| `actionSaveCodeGenerator` | `/cabang-config/save-code-generator` | POST | Simpan config modul |
| `actionAjaxCodegenPreview` | `/cabang-config/ajax-codegen-preview` | POST | Preview live dari config |
| `actionAjaxCodegenReset` | `/cabang-config/ajax-codegen-reset` | POST | Reset counter ke 0 |
| `actionAjaxCodegenDelete` | `/cabang-config/ajax-codegen-delete` | POST | Hapus config modul |

Semua endpoint dilindungi oleh permission RBAC `updateBranchConfig`.

---

## 10. UI Admin

Akses melalui **Pengaturan → Code Generator** (`/cabang-config/code-generator`).

### Tampilan

```
┌─────────────────────────────────────────────────────────┐
│  Modul Terkonfigurasi                    [+ Tambah Baru] │
├──────────┬────────┬──────────┬──────┬──────┬───────┬────┤
│ Module   │ Prefix │ Pattern  │ Sep  │ Digit│ Reset │    │
├──────────┼────────┼──────────┼──────┼──────┼───────┼────┤
│ sales    │ SLS    │ PRE-YR.. │  -   │  5   │Monthly│ ✎✗ │
│ customer │ CST    │ PRE-YR.. │  -   │  5   │Monthly│ ✎✗ │
└──────────┴────────┴──────────┴──────┴──────┴───────┴────┘
```

### Form Konfigurasi

Terdiri dari 3 panel:

**1. General Config**
- Module Key: dropdown (19 pilihan, opsi yang sudah dikonfigurasi di-disable)
- Prefix, Separator, Running Digits
- Reset Cycle, Format Tahun (muncul jika YEAR ada di pattern)
- Custom Text (muncul jika CUSTOM_TEXT ada di pattern)

**2. Pattern Builder**
- Dua zona drag-and-drop (SortableJS): *Komponen Tersedia* ↔ *Pattern Aktif*
- Urutan komponen di zona Aktif menentukan urutan segmen dalam kode
- Badge `required` pada `RUNNING_NUMBER`

**3. Live Preview** (panel kanan, sticky)
- Update otomatis saat field berubah (debounce 250ms)
- Preview client-side instan untuk komponen lokal
- Server call otomatis untuk komponen yang butuh DB (`CABANG_CODE`, `CABANG_ID`, `INITIAL`)
- Tampilkan counter saat ini, counter berikutnya, dan `nota_urut.type`
- Tombol Reset Counter (hanya muncul saat edit modul existing)

---

## 11. Contoh Output

### Pattern: `[PREFIX, YEAR, MONTH_ROMAN, RUNNING_NUMBER]`
Config: prefix=`INV`, separator=`-`, digits=5, reset=monthly

```
INV-2026-V-00001
INV-2026-V-00002
INV-2026-VI-00001   ← bulan berganti, counter reset
```

### Pattern: `[PREFIX, CABANG_CODE, YEAR, RUNNING_NUMBER]`
Config: prefix=`PO`, separator=`/`, digits=4, reset=yearly

```
PO/GSM/2026/0001
PO/GSM/2026/0002
PO/GSM/2027/0001   ← tahun berganti, counter reset
```

### Pattern: `[CABANG_CODE, INITIAL, MONTH_ROMAN, YEAR, RUNNING_NUMBER]`
Config: separator=`/`, digits=5, reset=monthly

```
GSM/ADM/V/2026/00001
GSM/ADM/V/2026/00002
GSM/MGR/V/2026/00003   ← user berbeda, counter tetap lanjut
```

### Pattern: `[PREFIX, CABANG_ID, YEAR, MONTH, DAY, RUNNING_NUMBER]`
Config: prefix=`TRX`, separator=`-`, digits=3, reset=daily

```
TRX-3-2026-05-23-001
TRX-3-2026-05-23-002
TRX-3-2026-05-24-001   ← hari berganti, counter reset
```

### Pattern: `[PREFIX, RUNNING_NUMBER]`
Config: prefix=`CUST`, separator=`-`, digits=6, reset=never

```
CUST-000001
CUST-000002
CUST-000099   ← tidak pernah reset
```

---

## 12. Modul yang Tersedia

Daftar modul yang dapat dikonfigurasi via UI admin:

| Module Key | Deskripsi |
|---|---|
| `consignment` | Konsinyasi |
| `customer` | Data pelanggan |
| `deposit` | Deposit pelanggan |
| `goods_receive` | Penerimaan barang |
| `menu` | Menu makanan/minuman |
| `petty_cash` | Kas kecil |
| `purchase_invoice` | Invoice pembelian |
| `purchase_order` | Purchase order |
| `purchase_payment` | Pembayaran pembelian |
| `raw_fnb` | Bahan baku F&B |
| `raw_non_fnb` | Bahan baku non-F&B |
| `recipe` | Resep |
| `reservation_data` | Data reservasi |
| `reservation_invoice` | Invoice reservasi |
| `reservation_payment` | Pembayaran reservasi |
| `sales` | Penjualan |
| `staff` | Data staf |
| `stock_opname` | Stock opname |
| `supplier` | Data supplier |

### Konstanta per Module Key

Gunakan konstanta ini saat memanggil generate dari kode PHP — jangan hardcode string.

```php
CodeGeneratorService::MODULE_CONSIGNMENT         // 'consignment'
CodeGeneratorService::MODULE_CUSTOMER            // 'customer'
CodeGeneratorService::MODULE_DEPOSIT             // 'deposit'
CodeGeneratorService::MODULE_GOODS_RECEIVE       // 'goods_receive'
CodeGeneratorService::MODULE_MENU                // 'menu'
CodeGeneratorService::MODULE_PETTY_CASH          // 'petty_cash'
CodeGeneratorService::MODULE_PURCHASE_INVOICE    // 'purchase_invoice'
CodeGeneratorService::MODULE_PURCHASE_ORDER      // 'purchase_order'
CodeGeneratorService::MODULE_PURCHASE_PAYMENT    // 'purchase_payment'
CodeGeneratorService::MODULE_RAW_FNB             // 'raw_fnb'
CodeGeneratorService::MODULE_RAW_NON_FNB         // 'raw_non_fnb'
CodeGeneratorService::MODULE_RECIPE              // 'recipe'
CodeGeneratorService::MODULE_RESERVATION_DATA    // 'reservation_data'
CodeGeneratorService::MODULE_RESERVATION_INVOICE // 'reservation_invoice'
CodeGeneratorService::MODULE_RESERVATION_PAYMENT // 'reservation_payment'
CodeGeneratorService::MODULE_SALES               // 'sales'
CodeGeneratorService::MODULE_STAFF               // 'staff'
CodeGeneratorService::MODULE_STOCK_OPNAME        // 'stock_opname'
CodeGeneratorService::MODULE_SUPPLIER            // 'supplier'

// Array semua module key yang valid:
CodeGeneratorService::MODULE_KEYS                // ['consignment', 'customer', ...]
```

> Untuk menambah modul baru: tambah konstanta `MODULE_*` baru di `CodeGeneratorService`, masukkan ke array `MODULE_KEYS`. View dan controller akan otomatis mengikuti karena keduanya membaca dari `CodeGeneratorService::MODULE_KEYS`.
