# Review Modul Penjualan (`application/modules/penjualan/`)

> **Tanggal**: 9 Mei 2026  
> **Reviewer**: GitHub Copilot (DeepSeek V4 Flash)  
> **Scope**: Seluruh file di `application/modules/penjualan/` (controllers, models, config, views)

---

## Daftar Isi

1. [Arsitektur Umum](#1-arsitektur-umum)
2. [File-File yang Terlibat](#2-file-file-yang-terlibat)
3. [Best Practice Compliance](#3-best-practice-compliance)
4. [Persistent Risks (Risiko Berkelanjutan)](#4-persistent-risks-risiko-berkelanjutan)
5. [Latent Risks (Risiko Laten)](#5-latent-risks-risiko-laten)
6. [Rekomendasi Prioritas](#6-rekomendasi-prioritas)

---

## 1. Arsitektur Umum

Modul ini adalah implementasi **multi-step transaction workflow** dengan arsitektur **HMVC (CodeIgniter 3 + MX Module)**.

### Alur Bisnis (Step-chain untuk `582` - Sales)

```
Step 1: Create  (SALES PRE ORDER / 582spo)  →   user: o_seller
Step 2: FollowUp → 582so (SALES ORDER)       →   user: o_seller_spv
Step 3: FollowUp → 582pkd (PRE PACKING)      →   user: o_gudang
Step 4: FollowUp → 582spd (PACKING LIST)     →   user: o_gudang
Step 5: FollowUp → 582    (INVOICE)           →   user: o_finance
```

### Controller Map

| Controller | Fungsi | Metode Utama |
|---|---|---|
| `Transaksi.php` | Validasi shopping cart + view listing | `validate()`, `viewUndoneItems()` |
| `Create.php` | Membuat transaksi baru (step 1) | `index()`, `preview()` |
| `Create_total.php` | **DUPLICATE** `Create.php` | `index()`, `preview()` |
| `FollowUp.php` | Follow-up transaksi ke step berikutnya | `index()`, `doFollowup()` |
| `TransaksiCrm.php` | View order dari CRM (estimate) | `viewOrderCrm()` |
| `_shoppingCart.php` | Shopping cart engine | `viewCart()`, `addItem()` |
| `Rabbitmq.php` | Message queue (RabbitMQ) | `send_task()` |
| `History.php` | Riwayat transaksi | `showData()` |
| `Printing.php` | Cetak dokumen | `preview()`, `printing()` |
| `CustomerApprovalApi.php` | REST API approval customer CRM | `checkStatus()`, `saveCustomer()` |

### Model

| Model | Fungsi |
|---|---|
| `Rabbitmq_mdl.php` | RabbitMQ connection & publish |

### Config Files

| Config | Isi |
|---|---|
| `coTransaksiUi.php` | UI definitions (steps, fields, selectors, validators) untuk tiap jenis transaksi |
| `coTransaksiCore.php` | Value gates, formulas, processors, value builders |
| `coTransaksiValues.php` | **DUPLICATE** of `coTransaksiCore.php` - isinya identik untuk kunci `"582"` |
| `coTransaksiLayout.php` | Layout template definitions |

---

## 2. File-File yang Terlibat

```text
application/modules/penjualan/
├── config/
│   ├── coTransaksiCore.php       (value gates, processors, formulas)
│   ├── coTransaksiValues.php     (DUPLIKAT dari coTransaksiCore.php)
│   ├── coTransaksiLayout.php     (layout/view bindings)
│   ├── coTransaksiUi.php         (UI config: steps, fields, selectors, validators)
│   └── _coTransaksiCore.php      (file backup/tidak dipakai)
├── controllers/
│   ├── Modul_Controller.php      (base controller - extends MX_Controller)
│   ├── Transaksi.php             (validasi + listing)
│   ├── Create.php                (pembuatan transaksi baru)
│   ├── Create_total.php          (DUPLIKAT Create.php)
│   ├── FollowUp.php              (follow-up antar step)
│   ├── __FollowUp.php            (DUPLIKAT/LEGACY FollowUp.php)
│   ├── TransaksiCrm.php          (CRM order view)
│   ├── CustomerApprovalApi.php   (REST API approval customer)
│   ├── _shoppingCart.php         (shopping cart engine)
│   ├── _processSelectProduct.php (selector produk)
│   ├── _processSelectProductKomposit.php
│   ├── _processSelectProductPaket.php
│   ├── _processSelectProductPpn.php
│   ├── _processPihak.php         (selector pihak/customer)
│   ├── _processPihakMain.php
│   ├── _selectorItem.php
│   ├── _selectorPihak.php
│   ├── _selectorPihakMain.php
│   ├── ActivityReport.php
│   ├── Debug.php
│   ├── Pembelian.php            (PEMBELIAN - ada di modul penjualan?)
│   ├── History.php
│   ├── Printing.php
│   ├── Rabbitmq.php
│   ├── ViewDetails.php
│   ├── _construct_file.php
│   ├── _followupLiveEdit.php
│   ├── _processSelectNotaItem.php
│   └── _processSelectNotaItem.php
├── models/
│   └── Rabbitmq_mdl.php
├── template/                     (template HTML untuk printing)
└── views/
    ├── transaksi.php             (view utama - create & followup)
    ├── create.php
    ├── followUp.php
    ├── history.php
    ├── shoppingCart.php
    ├── printing.php
    ├── viewdetails.php
    └── ... (lainnya)
```

---

## 3. Best Practice Compliance

### ✅ Yang sudah baik

| Aspek | Status | Keterangan |
|---|---|---|
| **HMVC Architecture** | ✅ | Menggunakan MX_Controller modular |
| **Session-based cart** | ✅ | Shopping cart menggunakan session, pattern umum untuk multi-step wizard |
| **Config-driven workflow** | ✅ | Steps, fields, validasi diatur via config array (coTransaksiUi) |
| **Value Gates pattern** | ✅ | Pemisahan antara data mentah (`items`) vs data kalkulasi (`valueGates`) |
| **Separation of processors** | ✅ | Proses select produk/pihak dipisah ke controller sendiri |
| **Access control by user group** | ✅ | Setiap step punya `userGroup` dan dicek via `alowedAccess()` |
| **Template system** | ✅ | Template HTML terpisah untuk printing |

### ❌ Yang belum sesuai best practice

| Aspek | Masalah |
|---|---|
| **Duplikasi controller** | `Create_total.php` adalah duplikat dari `Create.php` (class, constructor, method identik). Ini melanggar DRY. |
| **Duplikasi duplikasi config** | `coTransaksiValues.php` adalah duplikat dari `coTransaksiCore.php` untuk key `"582"`. Keduanya memiliki konten valueGates, detail, rsltItems yang persis sama. |
| **Hardcoded credentials** | `models/Rabbitmq_mdl.php` baris ~15: kredensial RabbitMQ hardcoded (`192.168.5.14`, `sos`, `NetworkMGK`) |
| **No input validation layer** | Tidak ada centralized input validation/filtering - validasi dilakukan inline di controller. |
| **Mixed concerns in Transaksi.php** | Controller `Transaksi.php` sangat besar, mencampur validasi, logika bisnis, dan rendering view. |
| **Global session mutation** | Session dimutasi langsung dari controller tanpa abstraction layer - menyulitkan unit testing. |
| **No proper error handling** | Banyak `die()` langsung pada error daripada exception handling. |
| **Overly large methods** | `Transaksi::validate()` (>300 baris), `Create::index()` (>500 baris), `_shoppingCart::viewCart()` (>500 baris) |
| **Inline JavaScript injection** | Banyak `echo "<script>...</script>"` langsung dari controller PHP daripada memisahkan JS. |
| **No DTO/Value Object** | Semua data passing via array asosiatif - tidak ada type safety. |
| **File naming inconsistency** | Ada `Create_total.php` vs `Create.php`, `__FollowUp.php` vs `FollowUp.php`. |

---

## 4. Persistent Risks (Risiko Berkelanjutan)

Risiko yang **sudah ada** dan terus berdampak pada pengembangan/maintenance.

### Risk 1: Duplikasi Kode Masif -> Maintenance Nightmare

**File:**
- `Create.php` vs `Create_total.php` - 100% duplikasi (>1000 baris identik)
- `coTransaksiCore.php` vs `coTransaksiValues.php` - duplikasi untuk key `"582"` (~300 baris identik)
- `FollowUp.php` vs `__FollowUp.php` - legacy backup yang tidak dihapus
- `Transaksi.php` vs `___Transaksi.php` - legacy backup

**Dampak:** Setiap bug fix atau enhancement harus diubah di 2-3 tempat. Satu lupa update menyebabkan inkonsistensi.

### Risk 2: Session sebagai "Database" Sekunder

**Lokasi:** Semua controller - penggunaan ekstensif `$_SESSION[$cCode]`

**Cuplikan:**
```php
// Transaksi.php ~line 30+
$this->cCode = $cCode = "_TR_" . $this->jenisTr;
// ... digunakan di >100 tempat:
if (isset($_SESSION[$cCode]['main_elements'])) { ... }
if (isset($_SESSION[$cCode]['items'])) { ... }
$_SESSION[$cCode]['main']['pihakMainExec'] = $selectedExec;
```

**Dampak:**
- Tidak bisa di-scale ke load-balanced environment (session sticky required)
- Tidak bisa di-test secara terisolasi
- Mudah terjadi session collision jika user membuka multiple tab dengan jenisTr berbeda
- Data hilang jika session expired di tengah transaksi

### Risk 3: Hardcoded RabbitMQ Credentials

**File:** `penjualan/models/Rabbitmq_mdl.php`
```php
$this->connection = new AMQPStreamConnection('192.168.5.14', 5672, 'sos', 'NetworkMGK');
```

**Dampak:**
- Security exposure di VCS
- Tidak bisa berbeda per environment (dev/staging/prod)
- Credential rotation jadi sulit

### Risk 4: `__FollowUp.php` - Legacy File Masih Ada

**Cuplikan (raw SQL injection risk):**
```php
$this->db->query("UPDATE transaksi SET indexing_main_values = '$indexing' WHERE id = '$masterID'");
```

Jika `$indexing` atau `$masterID` mengandung data dari blob yang tidak difilter, ini rentan SQL injection (walaupun diklaim sudah dicegah).

### Risk 5: Tidak Ada Unit/E2E Testing

Tidak ditemukan test files di modul penjualan. Tidak ada folder `tests/`.

**Dampak:** Setiap perubahan adalah "blind deployment" - tidak ada safety net.

---

## 5. Latent Risks (Risiko Laten)

Risiko yang **belum muncul** tapi berpotensi menjadi masalah di masa depan.

### Risk A: Race Condition di Multi-Step Workflow

**Lokasi:** `FollowUp.php` - method `doFollowup()` dan `doCancelPacking()`

**Potensi masalah:**
- User A dan User B melakukan follow-up pada transaksi yang sama secara simultan
- Tidak ada pessimistic/optimistic locking
- Step counter bisa terlewat atau data duplicate

**Cuplikan indikasi:**
```php
// Tidak ada transaksi locking atau version check
// Langsung INSERT/UPDATE tanpa cek status terkini
```

### Risk B: Tidak Ada Logging Audit Trail yang Terstruktur

Tidak ada centralized logging (ke file/database/ELK). `error_log()` atau `log_message()` tidak digunakan secara konsisten.

**Dampak:** Troubleshooting masalah produksi sangat sulit karena tidak ada trace.

### Risk C: Performa Degradasi pada Shopping Cart Besar

**Lokasi:** `_shoppingCart.php` dan `Transaksi.php::preview()`

**Potensi masalah:**
- Shopping cart items disimpan di session
- Method `preview()` melakukan looping O(n * m) untuk items, items2, items2_sum, items3_sum
- Ada N+1 query pattern di beberapa tempat (misal: loop lookup per item)

### Risk D: Tidak Ada Middleware/Filter untuk Cross-Cutting Concerns

- CORS tidak dikonfigurasi
- CSRF protection tidak konsisten
- Rate limiting tidak ada (terutama di `CustomerApprovalApi.php` dan `Rabbitmq.php`)
- Request logging tidak ada

### Risk E: Config File Inflexibility

**Lokasi:** `coTransaksiUi.php` (~3000+ baris)

**Potensi masalah:**
- Semua konfigurasi dalam satu file array besar
- Tidak ada inheritance/override antar jenis transaksi
- Tidak bisa di-reload tanpa restart aplikasi
- Error syntax di satu jenis transaksi bisa merusak seluruh modul

### Risk F: PHP Die/Halt di Production

**Contoh di banyak tempat:**
```php
die("konfigurasi transaksi belum ditentukan @" . __LINE__);
mati_disini($msg);
```

Jika terjadi error konfigurasi di production, user akan melihat blank page/error message mentah. Seharusnya graceful error handling + logging.

### Risk G: SQL Injection Potensial di `__FollowUp.php`

**Lokasi:** File `__FollowUp.php` - masih ada query raw SQL tanpa parameter binding:
```php
$this->db->query("UPDATE transaksi SET indexing_main_values = '$indexing' WHERE id = '$masterID'");
```
Meskipun `$indexing` mungkin dari `blobEncode`, perlu dipastikan tidak ada celah.

### Risk H: Tidak Ada Monitoring/Observability

- Tidak ada metrics endpoint
- Tidak ada health check
- Tidak ada structured logging
- Error handling menggunakan `die()` atau `echo` saja

---

## 6. Rekomendasi Prioritas

| Priority | Action | File Terkait | Estimated Effort |
|---|---|---|---|
| **P0** | Hapus file duplikat (Create_total, coTransaksiValues, legacy backups) & update references | `Create_total.php`, `coTransaksiValues.php`, `___Transaksi.php`, `__FollowUp.php` | 1 hari |
| **P0** | Pindahkan RabbitMQ credentials ke config file + .env | `models/Rabbitmq_mdl.php` | 1 jam |
| **P0** | Review & fix SQL injection di `__FollowUp.php` | `__FollowUp.php` | 2 jam |
| **P1** | Implementasi database-level locking di `doFollowup()` | `FollowUp.php` | 1 hari |
| **P1** | Tambah centralized logging (log_message) di semua entry point | Semua controller | 1-2 hari |
| **P1** | Refactor session-dependent code ke cache/database driver | Semua controller | 3-5 hari |
| **P2** | Pisahkan coTransaksiUi ke file per jenis transaksi | `config/coTransaksiUi.php` | 2 hari |
| **P2** | Ganti `die()` dengan exception handler + user-friendly error page | Semua controller | 1 hari |
| **P2** | Tambah CSRF + rate limiting di API endpoints | `CustomerApprovalApi.php`, `Rabbitmq.php` | 1 hari |
| **P3** | Refactor Transaksi::validate() jadi terpisah per validator class | `Transaksi.php` | 2-3 hari |
| **P3** | Tambah unit test untuk critical path (doFollowup, save) | `tests/` folder baru | 3-5 hari |

---

## Ringkasan

| Metrik | Value |
|---|---|
| **Total file** | ~30 files |
| **File duplikat** | **4+ files** (Create_total, coTransaksiValues, __FollowUp, ___Transaksi) |
| **Hardcoded credentials** | **1 file** (Rabbitmq_mdl.php) |
| **Raw SQL queries** | Terdeteksi di `__FollowUp.php` |
| **Unused/legacy files** | `___Transaksi.php`, `__FollowUp.php`, `coTransaksiValues.php` (jika duplikat) |
| **Code coverage (tests)** | **0%** |
| **die()/mati_disini() usage** | >10 occurrences |

**Kesimpulan:** Modul penjualan memiliki arsitektur yang cukup baik (HMVC, config-driven, multi-step workflow), namun terkendala oleh duplikasi kode yang masif, penggunaan session yang berlebihan, dan tidak adanya testing. Risiko tertinggi adalah race condition di follow-up workflow dan potensi SQL injection di file legacy.

---

## 7. Root Cause: Customer Mismatch CRM → Subsidiary

### Problem Statement

Saat membuat **estimate di CRM** dan memilih konsumen **"CV Rizky Berkah Sendiri"**, setelah estimate di-save dan dikirim sebagai penjualan CRM ke **subsidiary (san_sarana_8apr)**, data customer di CRM dan di subsidiary **berbeda**.

### Arsitektur Data Flow

```mermaid
flowchart LR
    CRM[san_ibb_master<br/>CRM System]
    SAN[san_staging<br/>Holding Company]
    SUB[san_sarana_8apr<br/>Subsidiary]

    CRM -- "1. Buat Estimate + Pilih Customer" --> CRM
    CRM -- "2. Kirim data → penjualan_transaksi_data_crm_bridge" --> SAN
    SAN -- "3. Followup → buat transaksi (referensi customer_id SAN)" --> SAN
    SAN -- "4. Kirim penjualan → subsidiary" --> SUB
    SUB -- "5. Customer_id SAN dipakai langsung<br/>di tabel per_customers subsidiary" --> SUB
```

### Sistem Identitas Customer per Entitas

| Entitas | Tabel Customer | Primary Key | Contoh ID |
|---|---|---|---|
| **CRM (san_ibb_master)** | `leads` / `customers` | `lead_id` atau `client_id` (dari CRM) | `"lead_xyz_789"` |
| **SAN (Holding)** | `per_customers` | `id` (auto-increment) | `123` |
| **Subsidiary (san_sarana_8apr)** | `per_customers` | `id` (auto-increment) | **456** (berbeda!) |

### Kode yang Terlibat

#### 1. Bridge Table (`penjualan_transaksi_data_crm_bridge`)

**Model:** `MdlCrmDataBridge` → Tabel `penjualan_transaksi_data_crm_bridge`

Kolom relevan:
```
client_id    ↔ lead_id dari CRM (string identifier unik)
customer_id  ↔ id dari per_customers SAN (integer, auto-increment)
referensi_id ↔ estimate_id dari CRM
```

#### 2. Customer Resolution di TransaksiCrm

**File:** `MdlTransaksiCrm::resolveCustomerName()` (baris 335)

```php
// Skenario 1: customer_id terisi → lookup di per_customers (SAN)
if (!empty($orderData->customer_id) && $orderData->customer_id != 0) {
    if (isset($customerMapping[$orderData->customer_id])) {
        $customerName = $customerMapping[$orderData->customer_id]["nama"];
    }
}
// Skenario 2: customer_id=0, tapi client_id ada
elseif (!empty($orderData->client_id)) {
    if (isset($preCustomerMapping[$orderData->client_id])) {
        $customerName = $preCustomerMapping[$orderData->client_id]["nama"];
    }
}
```

**File:** `TransaksiCrm.php::viewOrderCrm()` (baris 93)

```php
$customerName = $crmModel->resolveCustomerName($firstOrder, $customer, $preCustomer);
```

#### 3. Preview CRM (saat followup dari SAN)

**File:** `Create.php::previewCrm()` (baris ~12264-12764)

```php
$customer_id = $tmp[0]->customer_id;  // Dari bridge table (SAN customer_id)
$c = new MdlCustomer();
$c->addFilter("id='$customer_id'");
$tempCust = $c->lookUpAll()->result();
$customer = (array)$tempCust[0];
```

#### 4. Model Subsidiary

**File:** `MdlSubsidiary.php`

```php
class MdlSubsidiary extends MdlMother
{
    protected $tableName = "per_customers";
    // Filter member_id='100' → memfilter per entitas
    protected $filters = array("status='1'", "trash='0'", "member_id='100'");
}
```

### 🎯 Akar Masalah (Root Cause)

**Masalah inti:** Tidak ada **cross-reference mapping** antara ID customer SAN dengan ID customer Subsidiary.

Ketika data penjualan CRM dikirim ke subsidiary, flow yang terjadi:

1. **CRM** membuat estimate dengan customer **"CV Rizky Berkah Sendiri"** → menyimpan `client_id` (dari CRM internal)
2. **SAN** menerima data → menyimpan di `penjualan_transaksi_data_crm_bridge` dengan:
   - `client_id` = lead_id dari CRM (string unik)
   - `customer_id` = ID di `per_customers` **milik SAN** (integer, misal: `123`)
3. **SAN membuat transaksi** → menggunakan `customer_id` = `123` (milik SAN) sebagai pihak
4. **Data dikirim ke Subsidiary** → Subsidiary menerima `customer_id = 123`
5. **Subsidiary lookup** di tabel `per_customers` miliknya sendiri:
   - Jika ada customer dengan `id = 123` → **customer SALAH** (orang/entitas berbeda)
   - Jika tidak ada → **error / N/A**
   - Jika ada duplikat nama → confusion

### Faktor Penguat

| Faktor | Detail |
|---|---|
| **Auto-increment ID** | Setiap database memiliki sequence ID sendiri. ID `123` di SAN bisa merujuk ke PT. ABC, sementara di subsidiary ID `123` bisa merujuk ke CV. XYZ |
| **Tidak ada mapping table** | Tidak ada tabel `customer_subsidiary_mapping` atau `customer_cross_reference` yang menyimpan relasi {san_id → subsidiary_id} |
| **client_id tidak dipakai** | Kolom `client_id` (dari CRM) ada di bridge table tapi tidak digunakan saat lookup customer di subsidiary. Padahal `client_id` bersifat **universal/unik antar sistem** |
| **referensi_id tidak dimanfaatkan** | `referensi_id` (estimate_id) ada di bridge table tapi tidak digunakan untuk cross-reference customer |

### 💡 Rekomendasi Perbaikan (Prioritas P0)

#### Opsi 1: Customer Cross-Reference Mapping (Rekomendasi Utama)

Buat tabel mapping:

```sql
CREATE TABLE customer_cross_reference (
    id INT AUTO_INCREMENT PRIMARY KEY,
    client_id VARCHAR(255) NOT NULL COMMENT 'lead_id dari CRM',
    san_customer_id INT NOT NULL COMMENT 'customer_id di SAN',
    subsidiary_customer_id INT DEFAULT NULL COMMENT 'customer_id di Subsidiary',
    subsidiary_db VARCHAR(100) DEFAULT 'san_sarana_8apr',
    nama_customer VARCHAR(255),
    created_at DATETIME,
    updated_at DATETIME,
    UNIQUE KEY uk_client_id (client_id),
    INDEX idx_san_customer (san_customer_id),
    INDEX idx_subsidiary_customer (subsidiary_customer_id)
);
```

**Implementasi:**
1. **Saat data CRM tiba di SAN**: Simpan `client_id` + `customer_id` SAN ke mapping
2. **Saat kirim data ke subsidiary**: Cari `subsidiary_customer_id` dari mapping berdasarkan `client_id`
3. **Jika belum ada mapping**: Buat customer baru di subsidiary, simpan mappingnya
4. **Gunakan `client_id`** sebagai kunci universal, bukan auto-increment ID

#### Opsi 2: Sync Customer via client_id (Alternatif)

Modifikasi logic pengiriman ke subsidiary agar:

```php
// Alih-alih mengirim customer_id (auto-increment SAN):
$dataToSubsidiary['customer_id'] = $sanCustomerId;  // ❌ BERBAHAYA

// Kirim client_id (universal identifier):
$dataToSubsidiary['client_id'] = $clientIdFromCrm;   // ✅ AMAN
// Subsidiary lookup/make customer by client_id
```

#### Opsi 3: Flag "Is Sync" + Manual Match

Jika data sudah terlanjur berbeda, buat UI di SAN untuk:
1. Menampilkan customer SAN → customer Subsidiary yang **tidak match**
2. Admin secara manual mapping-kan customer SAN → Subsidiary
3. Sistem menyimpan mapping untuk transaksi selanjutnya

### Kesimpulan Root Cause

```
Penyebab: ID customer (auto-increment) SAN dipakai langsung di subsidiary
           tanpa mapping → menyebabkan referensi ke entitas yang salah.

Solusi:   Gunakan client_id (dari CRM) sebagai universal identifier,
           atau buat tabel customer_cross_reference.
