﻿# Mapping & Desain Skema SO — GMP GLIMS

> **Versi:** 1.0 — 16 April 2026
> **Sumber Legacy:** `po_new.sql` (MySQL 8.0, dump 16 Apr 2026)
> **Migration Utama:** `2026_04_12_000001_create_sales_order_tables.php`
> **Migration Supplementary:** `2026_04_16_000001_create_sales_order_supplementary_tables.php`
> **Dokumen terkait:** `BLUEPRINT_MASTER_DATA.md`, `MAPPING_PO_TABLES.md`

---

## Daftar Isi

1. [Ringkasan Semua Tabel](#1-ringkasan-semua-tabel)
2. [ERD — Relasi Antar Tabel](#2-erd--relasi-antar-tabel)
3. [Koneksi ke Modul Master Data](#3-koneksi-ke-modul-master-data)
4. [Detail Tabel Core SO](#4-detail-tabel-core-SO)
   - 4.1 [glims_t_sales_orders](#41-glims_t_sales_orders)
   - 4.2 [glims_t_sales_order_items](#42-glims_t_sales_order_items)
   - 4.3 [glims_t_sales_order_item_params](#43-glims_t_sales_order_item_params)
   - 4.4 [glims_t_sales_order_payments](#44-glims_t_sales_order_payments)
5. [Detail Tabel Supplementary](#5-detail-tabel-supplementary)
   - 5.1 [glims_t_so_cashbacks](#51-glims_t_so_cashbacks)
   - 5.2 [glims_t_so_sampling_locations](#52-glims_t_so_sampling_locations)
   - 5.3 [glims_t_so_print_logs](#53-glims_t_so_print_logs)
   - 5.4 [glims_t_so_revision_logs](#54-glims_t_so_revision_logs)
   - 5.5 [glims_t_so_change_logs](#55-glims_t_so_change_logs)
   - 5.6 [glims_t_so_edit_requests](#56-glims_t_so_edit_requests)
6. [Kolom Legacy yang Tidak Dibawa](#6-kolom-legacy-yang-tidak-dibawa)
7. [Kolom Baru yang Ditambahkan](#7-kolom-baru-yang-ditambahkan)
8. [Alur Bisnis: Change Log & Edit Request](#8-alur-bisnis-change-log--edit-request)
9. [Enum & Status yang Dipakai](#9-enum--status-yang-dipakai)
10. [Checklist Implementasi](#10-checklist-implementasi)

---

## 1. Ringkasan Semua Tabel

### Grup Core SO (Migration #1)

| # | Tabel Baru | Tabel Legacy | Deskripsi |
|---|---|---|---|
| 1 | `glims_t_sales_orders` | `t_po` | Header utama Sales Order |
| 2 | `glims_t_sales_order_items` | `t_po_det` | Line item / detail jenis pengujian per SO |
| 3 | `glims_t_sales_order_item_params` | `t_po_det_param` | Parameter uji per item (pH, BOD, COD, dll.) |
| 4 | `glims_t_sales_order_payments` | `t_po_pembayaran` | Pembayaran dari customer |

### Grup Supplementary (Migration #2)

| # | Tabel Baru | Tabel Legacy | Deskripsi |
|---|---|---|---|
| 5 | `glims_t_so_cashbacks` | `t_po_pembayaran_cashback` | Cashback / pengembalian dana |
| 6 | `glims_t_so_sampling_locations` | `t_po_lokasi_sampling` | Daftar lokasi sampling per SO |
| 7 | `glims_t_so_print_logs` | `log_print` | Log cetak dokumen (immutable) |
| 8 | `glims_t_so_revision_logs` | `t_log_po_rev` | Log revisi resmi SO + snapshot |
| 9 | `glims_t_so_change_logs` | *(baru)* | Audit trail perubahan setiap field (immutable) |
| 10 | `glims_t_so_edit_requests` | *(baru)* | Workflow persetujuan edit SO yang sudah dikunci |

**Total: 10 tabel** (4 core + 6 supplementary)

---

## 2. ERD — Relasi Antar Tabel

```
                        ┌──────────────────────────────────┐
                        │     glims_t_sales_orders        │
                        │  (Header SO)                      │
                        │──────────────────────────────────│
                        │  id, uuid, so_number              │
                        │  customer_id                      │
                        │  sample_type_id → [Master Data]   │
                        │  branch_id, company_id            │
                        │  status, payment_status           │
                        │  grand_total, tax_amount          │
                        └──────────────┬───────────────────┘
                                       │ 1
           ┌───────────────────────────┼──────────────────────────────┐
           │                           │                              │
           │ N                         │ N                            │ N
┌──────────▼──────────┐   ┌───────────▼──────────┐   ┌──────────────▼────────┐
│ glims_t_sales_order_ │   │ glims_t_so_cashbacks   │   │ glims_t_so_sampling_   │
│ payments            │   │ (Cashback)            │   │ locations             │
│ (Pembayaran)        │   └───────────────────────┘   └───────────────────────┘
└─────────────────────┘
                                       │
                               ┌───────▼──────────────────────────┐
                               │   glims_t_sales_order_items    │
                               │   (Line Item Jenis Pengujian)    │
                               │──────────────────────────────────│
                               │   sales_order_id              │
                               │   test_type_id → [Master Data]   │
                               │   regulation_id → [Master Data]  │
                               │   sample_points, unit_price      │
                               └───────────────┬──────────────────┘
                                               │ 1
                                               │ N
                               ┌───────────────▼──────────────────┐
                               │ glims_t_sales_order_item_params│
                               │ (Parameter per Item)             │
                               │──────────────────────────────────│
                               │   sales_order_item_id         │
                               │   parameter_id → [Master Data]   │
                               │   method_id    → [Master Data]   │
                               │   parameter_name (snapshot)      │
                               │   unit_price, specification      │
                               └──────────────────────────────────┘


AUDIT, LOG & WORKFLOW (terhubung ke semua tabel di atas)
─────────────────────────────────────────────────────────

glims_t_sales_orders ─┬─► glims_t_so_revision_logs  (snapshot per revisi)
                        ├─► glims_t_so_print_logs      (log cetak dokumen)
                        └─► glims_t_so_edit_requests   (workflow edit)

glims_t_so_change_logs  ◄── (polymorphic, merekam semua tabel SO)
```

---

## 3. Koneksi ke Modul Master Data

Saat user membuat SO, tabel master data digunakan untuk auto-fill data ke tabel SO.

```
User buat SO
     │
     ▼
[1] Pilih sample_type_id
     └─► glims_m_sample_types
           = auto-load parameter default + baku mutu

[2] Pilih test_type_id (paket/group pengujian)
     └─► glims_m_sample_type_groups
           └─► glims_p_group_sample_types
                 = auto-fill glims_t_sales_order_items

[3] Pilih regulation_id
     └─► glims_m_regulations
           └─► glims_m_regulation_appendices
                 = filter baku mutu yang relevan

[4] Per item → auto-fill parameter dari pivot
     └─► glims_p_sample_type_parameters
           ├─► parameter_id → glims_m_parameters
           ├─► method_id    → glims_m_methods
           └─► quality_standard → glims_m_quality_standard_params
                 = isi kolom `specification` di item_params
```

| Kolom di Tabel SO | FK ke Tabel Master Data | Keterangan |
|---|---|---|
| `glims_t_sales_orders.sample_type_id` | `glims_m_sample_types` | Jenis sampel yang dipesan |
| `glims_t_sales_order_items.test_type_id` | `glims_m_sample_type_groups` | Paket/grup pengujian |
| `glims_t_sales_order_items.regulation_id` | `glims_m_regulations` | Regulasi acuan pengujian |
| `glims_t_sales_order_item_params.parameter_id` | `glims_m_parameters` | Parameter uji (pH, BOD, dll.) |
| `glims_t_sales_order_item_params.method_id` | `glims_m_methods` | Metode uji (SNI, APHA, dll.) |

> **Catatan snapshot:** `parameter_name`, `test_method`, `unit`, `specification` di tabel `item_params` disimpan sebagai **string snapshot** (bukan hanya FK). Ini penting agar histori SO lama tidak berubah meskipun data master diupdate di kemudian hari.

---

## 4. Detail Tabel Core SO

### 4.1 `glims_t_sales_orders`

> **Dari legacy:** `t_po`
> **Migration:** `2026_04_12_000001_create_sales_order_tables.php`

#### Mapping Kolom Lengkap

| Kolom Legacy | Tipe Legacy | Kolom Baru | Tipe Baru | Catatan |
|---|---|---|---|---|
| `id` | `int AI` | `id` | `bigint AI` | PK |
| — | — | `uuid` | `uuid unique` | Identifier aman untuk API |
| `nomor` | `varchar(150)` | `so_number` | `varchar(150) unique` | Nomor SO |
| — | — | `quotation_number` | `varchar(50) nullable` | Nomor surat penawaran |
| `nomor_rev` | `int` | `revision_number` | `integer default 0` | Nomor revisi |
| `is_re_check` | `int` | `is_revision` | `boolean default false` | Flag revisi |
| `duplicate_from` | `int` | `duplicate_from_id` | `unsignedBigInteger nullable` | Disalin dari SO mana |
| `m_branch` | `int` | `branch_id` | `unsignedBigInteger nullable` | Cabang |
| `m_company` | `int` | `company_id` | `unsignedBigInteger nullable` | Perusahaan |
| `m_customer` | `int` | `customer_id` | `unsignedBigInteger` | Customer |
| `m_segmen_customer` | `int` | `customer_segment_id` | `unsignedBigInteger nullable` | Segmen customer |
| `no_mou_customer` | `int` | `mou_id` | `unsignedBigInteger nullable` | FK ke tabel MoU |
| — | — | `sample_type_id` | `unsignedBigInteger nullable` | **Baru** — FK → `glims_m_sample_types` |
| `tanggal` | `date` | `transaction_date` | `date` | Tanggal SO |
| `is_repeat` | `int` | `is_repeat` | `boolean default false` | SO berulang |
| `periode_kontrak` | `int` | `contract_period` | `integer default 0` | Periode kontrak (bulan) |
| `jumlah_so_sharing_biaya` | `int` | `sharing_so_count` | `integer default 1` | Jumlah SO berbagi biaya |
| `is_sampel_dikirim` | `int` | `is_sample_sent` | `boolean default false` | Sampel dikirim customer |
| `is_plan_sampling` | `tinyint` | `is_plan_sampling` | `boolean default false` | Ada rencana sampling |
| `is_sampling_gmp` | *(dari t_po legacy lain)* | `is_gmp_sampling` | `boolean default false` | Flag sampling GMP |
| `propose_tanggal_sampling` | `date` | `sampling_proposed_start` | `date nullable` | Rencana awal sampling |
| `propose_tanggal_sampling_akhir` | `date` | `sampling_proposed_end` | `date nullable` | Rencana akhir sampling |
| `tanggal_realisasi_sampling` | `timestamp` | `actual_sampling_start` | `timestamp nullable` | Realisasi awal |
| `tanggal_realisasi_sampling_akhir` | `timestamp` | `actual_sampling_end` | `timestamp nullable` | Realisasi akhir |
| — | — | `deadline_date` | `date nullable` | **Baru** — batas penyelesaian |
| `tanggal_terima_sampel` | `datetime` | `sample_received_at` | `datetime nullable` | Sampel diterima lab |
| `id_user_terima_sampel` | `bigint` | `sample_received_by` | `unsignedBigInteger nullable` | Penerima sampel |
| `status` | `int` | `status` | `string default draft` | Status SO (int→string) |
| `m_status_pembayaran` | `int` | `payment_status` | `string default unpaid` | Status pembayaran |
| `m_status_penawaran` | *(dari legacy lain)* | `quotation_status` | `string nullable` | Status penawaran |
| `status_validasi_akhir` | `tinyint` | `final_validation_status` | `tinyint nullable` | Validasi akhir |
| `request_tim` | `int` | `requested_team_id` | `unsignedBigInteger nullable` | Tim yang menangani |
| `prioritas` | `char(20)` | `priority` | `char(20) nullable` | Prioritas SO |
| `is_re_check` | `int` | `is_recheck` | `boolean default false` | Perlu re-check |
| `is_percepatan` | `int` | `is_acceleration` | `boolean default false` | Mode percepatan |
| `sub_total` | `double` | `subtotal` | `decimal(15,2)` | Total sebelum diskon |
| `disc_persen` | `float` | `discount_percent` | `decimal(5,2)` | Persen diskon |
| `disc` | `double` | `discount_amount` | `decimal(15,2)` | Nominal diskon |
| `total` | `double` | `total_before_tax` | `decimal(15,2)` | Total sebelum pajak |
| `dpp_lain` | `double` | `other_dpp` | `decimal(15,2)` | DPP lainnya |
| `ppn_persen` | `float default 12` | `tax_percentage` | `decimal(5,2) default 12` | Persen PPN |
| `ppn` | `double` | `tax_amount` | `decimal(15,2)` | Nominal PPN |
| `grand_total` | `double` | `grand_total` | `decimal(15,2)` | Grand total |
| `piutang` | `double` | `outstanding` | `decimal(15,2)` | Sisa tagihan |
| `status_pembayaran_cashback` | `int` | `has_cashback_payment` | `boolean` | Ada cashback |
| `total_cou` | `double` | `total_cou` | `decimal(15,2)` | Total CoU |
| `persen_gpm` | `double` | `gpm_percent` | `decimal(5,2)` | Persentase GPM |
| `nomor_invoice` | `varchar(50)` | `invoice_number` | `varchar(50) nullable` | Nomor invoice |
| `tanggal_cetak_invoice` | `timestamp` | `invoice_printed_at` | `timestamp nullable` | Tanggal cetak invoice |
| `nomor_sj` | `varchar(50)` | `delivery_note_number` | `varchar(50) nullable` | Nomor surat jalan |
| `nomor_io` | `varchar(50)` | `io_number` | `varchar(50) nullable` | Nomor IO |
| `id_transaction_accurate` | `bigint` | `accurate_transaction_id` | `unsignedBigInteger nullable` | Integrasi Accurate |
| `alamat_kirim_dokumen` | `mediumtext` | `document_delivery_address` | `mediumtext nullable` | Alamat kirim dokumen |
| `narahubung_nama_lhu` | `varchar(100)` | `lhu_contact_name` | `varchar(100) nullable` | Narahubung LHU |
| `narahubung_telp_lhu` | `varchar(25)` | `lhu_contact_phone` | `varchar(25) nullable` | Telepon narahubung |
| `alamat_kirim_lhu` | `mediumtext` | `lhu_delivery_address` | `mediumtext nullable` | Alamat kirim LHU |
| `note_teknis` | `mediumtext` | `technical_note` | `mediumtext nullable` | Catatan teknis |
| `note_administrasi` | `mediumtext` | `admin_note` | `mediumtext nullable` | Catatan administrasi |
| `note_internal_marketing` | `mediumtext` | `internal_marketing_note` | `mediumtext nullable` | Catatan marketing |
| `keterangan` | `varchar(500)` | `notes` | `varchar(500) nullable` | Keterangan umum |
| `reason_rejected` | `mediumtext` | `reason_rejected` | `mediumtext nullable` | Alasan ditolak |
| `reason_gpm` | `text` | `reason_gpm` | `text nullable` | Alasan GPM |
| `id_pegawai_cek_penawaran` | `int` | `checker_id` | `unsignedBigInteger nullable` | Pemeriksa penawaran |
| `tanggal_status_cek_penawaran` | `timestamp` | `checked_at` | `timestamp nullable` | Tanggal pemeriksaan |
| — | — | `approved_by` | `unsignedBigInteger nullable` | **Baru** — approver |
| `tanggal_approved` | `timestamp` | `approved_at` | `timestamp nullable` | Tanggal approval |
| `created_user` | `int` | `created_by` | `unsignedBigInteger nullable` | Pembuat |
| `updated_user` | `int` | `updated_by` | `unsignedBigInteger nullable` | Pengubah terakhir |
| `deleted_user` | `int` | `deleted_by` | `unsignedBigInteger nullable` | Penghapus |
| `created_date` | `timestamp` | `created_at` | `timestamps` | Laravel timestamps |
| `updated_date` | `timestamp` | `updated_at` | `timestamps` | Laravel timestamps |
| `deleted_date` / `is_delete` | `timestamp` / `int` | `deleted_at` | `softDeletes` | Soft delete |

#### Kolom Legacy yang Tidak Dibawa

Kolom-kolom berikut ada di `po_new.sql` namun **tidak dimodelkan** di tabel baru karena sudah tertangani oleh tabel lain atau tidak relevan untuk skema baru:

| Kolom Legacy | Alasan |
|---|---|
| `sj_nomor_urut`, `sj_bulan`, `sj_tahun`, `sj_created_user`, `sj_created_date` | Data internal penomoran SJ — bisa dihitung dari `delivery_note_number` + timestamps |
| `nomor_urut`, `nomor_bulan`, `nomor_tahun` | Komponen penomoran otomatis — cukup simpan hasil akhirnya di `so_number` |

---

### 4.2 `glims_t_sales_order_items`

> **Dari legacy:** `t_po_det`

#### Mapping Kolom

| Kolom Legacy | Tipe Legacy | Kolom Baru | Tipe Baru | Catatan |
|---|---|---|---|---|
| `id` | `int AI` | `id` | `bigint AI` | PK |
| — | — | `uuid` | `uuid unique` | Identifier aman |
| `t_po` | `int` | `sales_order_id` | `foreignId` cascade | FK ke SO |
| `m_jenis_pengujian` | `int` | `test_type_id` | `unsignedBigInteger nullable` | **FK → `glims_m_sample_type_groups`** |
| `m_peraturan` | `int` | `regulation_id` | `unsignedBigInteger nullable` | **FK → `glims_m_regulations`** |
| `m_parameter_uji` | `varchar(750)` | `sample_type_snapshot` | `varchar(750) nullable` | Snapshot nama jenis pengujian |
| `provide_by_client` | `int` | `is_client_provided` | `boolean` | Sampel dari client |
| `titik` | `int` | `sample_points` | `integer` | Jumlah titik sampling |
| — | — | `quantity` | `integer default 1` | **Baru** — jumlah sampel |
| `tarif` | `double` | `unit_price` | `decimal(15,2)` | Harga satuan |
| `disc_persen` | `double` | `discount_percent` | `decimal(5,2)` | Persen diskon per item |
| `disc_rupiah` | `double` | `discount_amount` | `decimal(15,2)` | Nominal diskon per item |
| `jumlah` | `double` | `subtotal` | `decimal(15,2)` | Subtotal item |
| `cou_param` | `double` | `cou_param` | `decimal(15,2)` | CoU parameter |
| `sum_cou_param` | `double` | `total_cou` | `decimal(15,2)` | Total CoU |
| `is_subkon` | `tinyint` | `is_subcontract` | `boolean` | Subkontrak |
| `is_hide` | `tinyint` | `is_hidden` | `boolean` | Sembunyikan di LHU |
| `cabang_sampling` | `int` | `sampling_branch_id` | `unsignedBigInteger nullable` | Cabang sampling |
| `cabang_lab` | `int` | `lab_branch_id` | `unsignedBigInteger nullable` | Cabang lab |
| `note` | `text` | `notes` | `text nullable` | Catatan per item |
| `created_user` | `int` | `created_by` | `unsignedBigInteger nullable` | Audit |
| `updated_user` | `int` | `updated_by` | `unsignedBigInteger nullable` | Audit |
| `deleted_user` | `int` | `deleted_by` | `unsignedBigInteger nullable` | Audit |
| `created_date` | `timestamp` | `created_at` | `timestamps` | |
| `updated_date` | `timestamp` | `updated_at` | `timestamps` | |
| `deleted_date` / `is_delete` | `timestamp` / `int` | `deleted_at` | `softDeletes` | |

---

### 4.3 `glims_t_sales_order_item_params`

> **Dari legacy:** `t_po_det_param`

Parameter uji yang terdaftar per item SO. Setiap baris = 1 parameter (pH, BOD, COD, dll.).

Kolom `parameter_name`, `test_method`, `unit`, `specification` disimpan sebagai **snapshot string** — bukan hanya FK — agar histori SO tidak berubah jika data master diupdate.

#### Mapping Kolom

| Kolom Legacy | Tipe Legacy | Kolom Baru | Tipe Baru | Catatan |
|---|---|---|---|---|
| `id` | `int AI` | `id` | `bigint AI` | PK |
| — | — | `uuid` | `uuid unique` | Identifier aman |
| `t_po_det` | `int` | `sales_order_item_id` | `foreignId` cascade | FK ke item |
| — | — | `parameter_id` | `unsignedBigInteger nullable` | **Baru** — FK → `glims_m_parameters` |
| — | — | `method_id` | `unsignedBigInteger nullable` | **Baru** — FK → `glims_m_methods` |
| `m_parameter_uji` | `varchar(750)` | `parameter_name` | `varchar(750)` | Snapshot nama parameter |
| `name_alias` | `varchar(255)` | `parameter_alias` | `varchar(255) nullable` | Alias nama parameter |
| — | — | `test_method` | `string nullable` | **Baru** — snapshot kode metode |
| — | — | `unit` | `varchar(50) nullable` | **Baru** — snapshot satuan |
| — | — | `specification` | `string nullable` | **Baru** — dari baku mutu |
| `is_parameter_tambahan` | `int` | `is_additional` | `boolean` | Parameter tambahan |
| `harga` | `double` | `unit_price` | `decimal(15,2)` | Harga per parameter |
| `cou` | `double` | `cou` | `decimal(15,2)` | Cost of Uncertainty |
| `created_user` | `int` | `created_by` | `unsignedBigInteger nullable` | Audit |
| `updated_user` | `int` | `updated_by` | `unsignedBigInteger nullable` | Audit |
| `deleted_user` | `int` | `deleted_by` | `unsignedBigInteger nullable` | Audit |
| `created_date` / `updated_date` / `deleted_date` | `timestamp` | `created_at` / `updated_at` / `deleted_at` | `timestamps` + `softDeletes` | |

---

### 4.4 `glims_t_sales_order_payments`

> **Dari legacy:** `t_po_pembayaran`

#### Mapping Kolom

| Kolom Legacy | Tipe Legacy | Kolom Baru | Tipe Baru | Catatan |
|---|---|---|---|---|
| `id` | `int AI` | `id` | `bigint AI` | PK |
| — | — | `uuid` | `uuid unique` | Identifier aman |
| `t_po` | `int` | `sales_order_id` | `foreignId` cascade | FK ke SO |
| `nomor_bukti_bayar` | `varchar(100)` | `receipt_number` | `varchar(100) nullable` | Nomor bukti bayar |
| `tanggal_bayar` | `date` | `payment_date` | `date` | Tanggal bayar |
| — | — | `status` | `string default pending` | **Baru** — status pembayaran |
| — | — | `payment_method` | `string nullable` | **Baru** — transfer/cash/giro |
| `jumlah` | `double` | `amount` | `decimal(15,2)` | Jumlah dibayar |
| `diskon` | `int` | `discount_amount` | `decimal(15,2)` | Diskon |
| `pph` | `int` | `tax_withheld` | `decimal(15,2)` | PPh dipotong |
| — | — | `bank_name` | `string nullable` | **Baru** |
| — | — | `bank_account_number` | `string nullable` | **Baru** |
| `idempotency_key` | `varchar(255)` | `idempotency_key` | `varchar(255) unique nullable` | Dedup webhook |
| — | — | `verified_by` | `unsignedBigInteger nullable` | **Baru** |
| — | — | `verified_at` | `timestamp nullable` | **Baru** |
| `keterangan` | `varchar(500)` | `notes` | `text nullable` | Catatan |
| `created_user` / `updated_user` / `deleted_user` | `int` | `created_by` / `updated_by` / `deleted_by` | `unsignedBigInteger nullable` | Audit |
| `created_date` / `updated_date` / `deleted_date` / `is_delete` | `timestamp` / `int` | `created_at` / `updated_at` / `deleted_at` | `timestamps` + `softDeletes` | |

---

## 5. Detail Tabel Supplementary

### 5.1 `glims_t_so_cashbacks`

> **Dari legacy:** `t_po_pembayaran_cashback`
> Struktur identik dengan `glims_t_sales_order_payments`. Merupakan aliran balik dana (pengembalian/kelebihan bayar kepada customer).

| Kolom | Tipe | Keterangan |
|---|---|---|
| `id` | `bigint AI` | PK |
| `uuid` | `uuid unique` | |
| `sales_order_id` | `foreignId` | FK → `glims_t_sales_orders` cascade |
| `receipt_number` | `varchar(100) nullable` | Nomor bukti cashback |
| `payment_date` | `date` | Tanggal cashback |
| `status` | `string default pending` | pending / verified / rejected |
| `payment_method` | `string nullable` | Metode pengembalian |
| `amount` | `decimal(15,2)` | Jumlah cashback |
| `discount_amount` | `decimal(15,2)` | Diskon |
| `tax_withheld` | `decimal(15,2)` | PPh |
| `bank_name` | `string nullable` | Nama bank tujuan |
| `bank_account_number` | `string nullable` | Nomor rekening tujuan |
| `idempotency_key` | `varchar(255) unique nullable` | Dedup |
| `verified_by` | `unsignedBigInteger nullable` | Verifikator |
| `verified_at` | `timestamp nullable` | Waktu verifikasi |
| `notes` | `text nullable` | Keterangan |
| `created_by` / `updated_by` / `deleted_by` | `unsignedBigInteger nullable` | Audit |
| `created_at` / `updated_at` / `deleted_at` | `timestamps` + `softDeletes` | |

---

### 5.2 `glims_t_so_sampling_locations`

> **Dari legacy:** `t_po_lokasi_sampling`
> Satu SO bisa memiliki **banyak lokasi sampling**.

| Kolom | Tipe | Keterangan |
|---|---|---|
| `id` | `bigint AI` | PK |
| `uuid` | `uuid unique` | |
| `sales_order_id` | `foreignId` | FK → `glims_t_sales_orders` cascade |
| `location_name` | `varchar(250)` | Nama/deskripsi lokasi sampling |
| `province` | `varchar(250) nullable` | Provinsi |
| `city` | `varchar(250) nullable` | Kota/kabupaten |
| `sampling_validation_date` | `date nullable` | Tanggal validasi realisasi |
| `validator_id` | `unsignedBigInteger nullable` | PIC validasi sampling |
| `lead_category_id` | `unsignedBigInteger nullable` | Kategori lead validasi |
| `created_by` / `updated_by` / `deleted_by` | `unsignedBigInteger nullable` | Audit |
| `created_at` / `updated_at` / `deleted_at` | `timestamps` + `softDeletes` | |

---

### 5.3 `glims_t_so_print_logs`

> **Dari legacy:** `log_print` (disederhanakan)
> **Immutable** — tidak ada UPDATE/DELETE. Setiap cetak = satu baris baru.

| Kolom | Tipe | Keterangan |
|---|---|---|
| `id` | `bigint AI` | PK |
| `sales_order_id` | `foreignId` | FK → `glims_t_sales_orders` cascade |
| `document_type` | `varchar(50)` | Jenis dokumen: `SO`, `invoice`, `lhu`, `sj`, `io`, `penawaran` |
| `document_number` | `varchar(100) nullable` | Nomor dokumen yang dicetak |
| `copy_number` | `integer default 1` | Ini cetakan ke berapa |
| `printed_by` | `unsignedBigInteger nullable` | User yang mencetak |
| `printed_at` | `timestamp` | Waktu cetak |
| `ip_address` | `varchar(45) nullable` | IP address saat cetak |
| `created_at` | `timestamp` | Hanya `created_at` (immutable) |

> **Tidak ada** `updated_at`, `deleted_at` — log cetak bersifat permanen.

---

### 5.4 `glims_t_so_revision_logs`

> **Dari legacy:** `t_log_po_rev` (diperluas dengan snapshot)
> Menyimpan snapshot JSON lengkap state SO pada setiap revisi resmi.

| Kolom | Tipe | Keterangan |
|---|---|---|
| `id` | `bigint AI` | PK |
| `uuid` | `uuid unique` | |
| `sales_order_id` | `foreignId` | FK → `glims_t_sales_orders` cascade |
| `revision_number` | `integer` | Nomor revisi (sesuai `glims_t_sales_orders.revision_number`) |
| `so_snapshot` | `json` | **Snapshot lengkap** — header SO + semua items + params saat revisi dibuat |
| `change_summary` | `text nullable` | Ringkasan perubahan dari revisi sebelumnya |
| `reason` | `text nullable` | Alasan terjadinya revisi |
| `created_by` | `unsignedBigInteger nullable` | User yang membuat revisi |
| `created_at` / `updated_at` | `timestamps` | |

**Format `so_snapshot` (contoh):**
```json
{
  "header": {
    "so_number": "SO-2026-001",
    "grand_total": 5000000,
    "status": "submitted"
  },
  "items": [
    {
      "id": 1,
      "test_type_id": 3,
      "unit_price": 500000,
      "params": [
        { "parameter_name": "pH", "unit_price": 50000 },
        { "parameter_name": "BOD", "unit_price": 100000 }
      ]
    }
  ]
}
```

---

### 5.5 `glims_t_so_change_logs`

> **Tabel baru** (tidak ada di legacy).
> Audit trail **per-field** untuk semua tabel SO. **Immutable** — hanya INSERT, tidak boleh UPDATE/DELETE.

Tabel ini bersifat **polymorphic** — bisa merekam perubahan dari semua tabel SO:
- `glims_t_sales_orders`
- `glims_t_sales_order_items`
- `glims_t_sales_order_item_params`
- `glims_t_sales_order_payments`
- `glims_t_so_cashbacks`
- `glims_t_so_sampling_locations`

#### Struktur Kolom

| Kolom | Tipe | Keterangan |
|---|---|---|
| `id` | `bigint AI` | PK |
| `uuid` | `uuid unique` | |
| `loggable_type` | `varchar(100)` | Nama tabel tujuan: `sales_orders`, `sales_order_items`, dll. |
| `loggable_id` | `unsignedBigInteger` | ID record yang berubah |
| `sales_order_id` | `unsignedBigInteger` | ID SO induk (untuk query cepat semua log per SO) |
| `event` | `varchar(50)` | Tipe event (lihat daftar di bawah) |
| `changed_fields` | `json nullable` | Array nama field yang berubah: `["status","updated_by"]` |
| `old_values` | `json nullable` | Nilai sebelum: `{"status": "draft"}` |
| `new_values` | `json nullable` | Nilai sesudah: `{"status": "submitted"}` |
| `reason` | `text nullable` | Alasan perubahan (opsional, diisi user) |
| `ip_address` | `varchar(45) nullable` | IP address |
| `user_agent` | `text nullable` | Browser/UA yang dipakai |
| `causer_id` | `unsignedBigInteger nullable` | User pelaku perubahan |
| `causer_type` | `varchar(50) default user` | `user`, `system`, `import`, `webhook`, `cli` |
| `created_at` | `timestamp` | Waktu perubahan (hanya `created_at`) |

#### Daftar Event

| Event | Kapan Terjadi |
|---|---|
| `created` | Record baru dibuat |
| `updated` | Ada field yang berubah |
| `deleted` | Soft delete |
| `restored` | Direstorasi dari soft delete |
| `approved` | SO di-approve |
| `rejected` | SO ditolak |
| `status_changed` | `status` atau `payment_status` berubah |
| `payment_verified` | Pembayaran diverifikasi |
| `revision_created` | Revisi baru dibuat |

#### Implementasi di Laravel

Isi tabel ini otomatis melalui **Eloquent Observer** di setiap model SO:

```php
// app/Modules/SalesOrder/Domain/Observers/SalesOrderObserver.php
class SalesOrderObserver
{
    public function updated(SalesOrder $SO): void
    {
        $dirty = $SO->getDirty();
        if (empty($dirty)) return;

        SoChangeLog::create([
            'loggable_type'   => 'sales_orders',
            'loggable_id'     => $SO->id,
            'sales_order_id' => $SO->id,
            'event'           => 'updated',
            'changed_fields'  => array_keys($dirty),
            'old_values'      => collect($dirty)->mapWithKeys(
                fn($v, $k) => [$k => $SO->getOriginal($k)]
            )->toArray(),
            'new_values'      => $dirty,
            'causer_id'       => auth()->id(),
            'ip_address'      => request()->ip(),
        ]);
    }
}
```

---

### 5.6 `glims_t_so_edit_requests`

> **Tabel baru** (tidak ada di legacy).
> Workflow persetujuan edit data SO yang sudah berada di status tertentu (misalnya sudah `in_progress` atau `completed`).

Tujuan: mencegah edit langsung ke data SO yang sedang/sudah diproses lab. Semua permintaan perubahan harus melalui review.

#### Struktur Kolom

| Kolom | Tipe | Keterangan |
|---|---|---|
| `id` | `bigint AI` | PK |
| `uuid` | `uuid unique` | |
| `sales_order_id` | `foreignId` | FK → `glims_t_sales_orders` |
| `target_type` | `varchar(100)` | Tabel target: `sales_orders`, `sales_order_items`, dll. |
| `target_id` | `unsignedBigInteger` | ID record yang ingin diedit |
| `request_type` | `varchar(50)` | Tipe request (lihat di bawah) |
| `status` | `varchar(30) default pending` | `pending`, `approved`, `rejected`, `cancelled`, `applied` |
| `requested_changes` | `json` | Field & nilai baru yang diminta: `{"unit_price": 750000}` |
| `current_snapshot` | `json` | State record saat request dibuat (untuk perbandingan reviewer) |
| `reason` | `text` | Alasan mengapa perlu edit (**wajib diisi**) |
| `requested_by` | `unsignedBigInteger` | User pengaju |
| `requested_at` | `timestamp` | Waktu pengajuan |
| `reviewed_by` | `unsignedBigInteger nullable` | Admin/manajer reviewer |
| `reviewed_at` | `timestamp nullable` | Waktu review |
| `review_notes` | `text nullable` | Catatan reviewer (alasan approve/reject) |
| `applied_by` | `unsignedBigInteger nullable` | Yang menerapkan perubahan |
| `applied_at` | `timestamp nullable` | Waktu perubahan diterapkan |
| `created_by` / `updated_by` | `unsignedBigInteger nullable` | Audit |
| `created_at` / `updated_at` | `timestamps` | |
| `deleted_at` | `softDeletes` | |

#### Tipe Request

| `request_type` | Keterangan |
|---|---|
| `edit_header` | Edit kolom di header SO (tanggal, customer, dll.) |
| `edit_item` | Edit unit_price, quantity di `sales_order_items` |
| `add_item` | Tambah item baru ke SO |
| `delete_item` | Hapus item dari SO |
| `edit_payment` | Edit data pembayaran |
| `edit_param` | Edit parameter uji di item |
| `delete_param` | Hapus parameter dari item |
| `cancel_po` | Batalkan SO yang sudah berjalan |

---

## 6. Kolom Legacy yang Tidak Dibawa

Kolom berikut ada di `po_new.sql` namun tidak dimodelkan di skema baru karena redundan atau telah digantikan:

| Kolom Legacy | Tabel | Alasan |
|---|---|---|
| `sj_nomor_urut`, `sj_bulan`, `sj_tahun` | `t_po` | Komponen penomoran SJ — cukup `delivery_note_number` sebagai hasilnya |
| `sj_created_user`, `sj_created_date` | `t_po` | Dicatat di `glims_t_so_change_logs` via Observer |
| `is_delete` | semua tabel | Digantikan oleh `softDeletes()` Laravel (`deleted_at`) |
| `created_date`, `updated_date` | semua tabel | Digantikan oleh `timestamps()` Laravel |
| `created_user`, `updated_user`, `deleted_user` | semua tabel | Direname menjadi `created_by`, `updated_by`, `deleted_by` |

---

## 7. Kolom Baru yang Ditambahkan

Kolom berikut **tidak ada di legacy** namun ditambahkan untuk kebutuhan GLIMS baru:

| Kolom Baru | Tabel | Tujuan |
|---|---|---|
| `uuid` | semua tabel | Identifier aman untuk URL/API (tidak ekspos integer ID) |
| `sample_type_id` | `sales_orders` | FK ke master jenis sampel — trigger auto-fill parameter |
| `deadline_date` | `sales_orders` | Batas waktu penyelesaian pengujian |
| `is_revision`, `revision_number` | `sales_orders` | Tracking revisi resmi |
| `quantity` | `sales_order_items` | Jumlah sampel (legacy hanya punya `titik`) |
| `parameter_id`, `method_id` | `item_params` | FK ke master parameter & metode GLIMS |
| `test_method`, `unit`, `specification` | `item_params` | Snapshot data master saat order (historical accuracy) |
| `payment_method`, `bank_name`, `bank_account_number` | `payments` | Detail metode pembayaran |
| `verified_by`, `verified_at` | `payments` | Verifikasi pembayaran |
| `status` | `payments`, `cashbacks` | Status workflow (pending/verified/rejected) |
| `approved_by` | `sales_orders` | Pelaku approval |
| semua tabel `glims_t_so_*` | supplementary | Lihat [§5](#5-detail-tabel-supplementary) |

---

## 8. Alur Bisnis: Change Log & Edit Request

### 8.1 Change Log — Audit Trail Otomatis

```
User/System melakukan aksi
         │
         ▼
Eloquent Model Event terpicu (creating/updating/deleting/restoring)
         │
         ▼
Observer/Listener membuat record di glims_t_so_change_logs
         │
         ├── loggable_type  = nama tabel
         ├── loggable_id    = ID record
         ├── sales_order_id = ID SO induk
         ├── event          = created/updated/deleted/dll.
         ├── changed_fields = ["status","unit_price"]
         ├── old_values     = {"status":"draft","unit_price":500000}
         ├── new_values     = {"status":"submitted","unit_price":750000}
         └── causer_id      = auth()->id()

IMMUTABLE — tidak boleh diupdate/dihapus
```

### 8.2 Edit Request — Workflow Persetujuan

```
┌─────────────────────────────────────────────────────────────────┐
│                    ALUR EDIT REQUEST                             │
└─────────────────────────────────────────────────────────────────┘

[1] User klik "Ajukan Edit" pada SO yang sudah dikunci
         │
         ▼
[2] Sistem buat record di glims_t_so_edit_requests
    status = pending
    requested_changes = { field: new_value, ... }
    current_snapshot  = { field: current_value, ... }
    reason = "alasan yang diisi user" (WAJIB)
         │
         ▼
[3] Notifikasi dikirim ke Admin/Manajer

         │
    ┌────┴────────────────────────────────┐
    │                                    │
    ▼                                    ▼
[4a] APPROVE                         [4b] REJECT
    reviewed_by = admin_id               reviewed_by = admin_id
    reviewed_at = now()                  reviewed_at = now()
    status = approved                    status = rejected
    review_notes = "..."                 review_notes = "alasan ditolak"
         │
         ▼
[5] Perubahan diterapkan ke tabel asli
    (update record target) 
    applied_by = admin_id / system
    applied_at = now()
    status = applied
         │
         ▼
[6] Observer otomatis buat entry di glims_t_so_change_logs
    event = updated (atau event spesifik)
    dengan referensi ke edit_request_id di metadata


         │                                    │
         ▼                                    ▼
User juga bisa [CANCEL] sebelum direview   → status = cancelled
```

### 8.3 Skenario Contoh: Edit Harga Parameter Setelah Pengujian Berjalan

```
Situasi:
  SO-2026-042, status = in_progress
  Parameter BOD, unit_price = 500.000 → ingin diubah ke 750.000

Alur:
  1. User (marketing) ajukan edit request
     - target_type: sales_order_item_params
     - target_id: 87
     - request_type: edit_param
     - requested_changes: { "unit_price": 750000 }
     - current_snapshot: { "unit_price": 500000, "parameter_name": "BOD" }
     - reason: "Harga referensi naik sejak SK baru per 1 April 2026"

  2. Manajer review → APPROVE
     - review_notes: "Sesuai SK Tarif No. 12/2026"

  3. System terapkan → unit_price di sales_order_item_params ID 87 = 750000
     grand_total SO recalculated

  4. change_logs mencatat:
     - event: updated
     - changed_fields: ["unit_price"]
     - old_values: { "unit_price": 500000 }
     - new_values: { "unit_price": 750000 }
```

---

## 9. Enum & Status yang Dipakai

### Status SO (`glims_t_sales_orders.status`)

| Value | Keterangan |
|---|---|
| `draft` | SO sedang disusun, belum diajukan |
| `submitted` | SO diajukan ke admin/manajer |
| `in_review` | Sedang direview tim |
| `in_progress` | Pengujian sedang berjalan di lab |
| `completed` | Semua pengujian selesai, LHU terbit |
| `cancelled` | SO dibatalkan |

### Payment Status (`glims_t_sales_orders.payment_status`)

| Value | Keterangan |
|---|---|
| `unpaid` | Belum ada pembayaran |
| `partial` | Sebagian sudah dibayar |
| `paid` | Lunas |
| `overpaid` | Lebih bayar (trigger cashback) |
| `refunded` | Cashback sudah dikembalikan |

### Quotation Status (`glims_t_sales_orders.quotation_status`)

| Value | Keterangan |
|---|---|
| `draft` | SPH belum dikirim |
| `sent` | SPH sudah dikirim ke customer |
| `accepted` | Customer setuju |
| `rejected` | Customer menolak |
| `revised` | SPH direvisi dan dikirim ulang |

### Payment Status (tabel payments & cashbacks)

| Value | Keterangan |
|---|---|
| `pending` | Menunggu verifikasi |
| `verified` | Sudah diverifikasi oleh finance |
| `rejected` | Bukti pembayaran ditolak |

### Edit Request Status (`glims_t_so_edit_requests.status`)

| Value | Keterangan |
|---|---|
| `pending` | Menunggu review |
| `approved` | Disetujui reviewer |
| `rejected` | Ditolak reviewer |
| `cancelled` | Dibatalkan pengaju |
| `applied` | Perubahan sudah diterapkan ke data asli |

---

## 10. Checklist Implementasi

### Phase 1: Migration & Database

- [ ] Jalankan migration utama: `2026_04_12_000001_create_sales_order_tables.php`
- [ ] Jalankan migration supplementary: `2026_04_16_000001_create_sales_order_supplementary_tables.php`
- [ ] Buat seeder untuk data SO dari `po_new.sql` (transformasi kolom)
- [ ] Verifikasi FK ke tabel master data sudah ada setelah `2026_04_13_000001_create_glims_master_data_tables.php`

### Phase 2: Domain Layer

- [ ] Buat Model `SalesOrder` (tabel `glims_t_sales_orders`)
- [ ] Buat Model `SalesOrderItem`
- [ ] Buat Model `SalesOrderItemParam`
- [ ] Buat Model `SalesOrderPayment`
- [ ] Buat Model `SoCashback`
- [ ] Buat Model `SoSamplingLocation`
- [ ] Buat Model `SoChangeLog` (readonly — no update/delete)
- [ ] Buat Model `SoEditRequest`
- [ ] Buat Enums: `PoStatus`, `PaymentStatus`, `QuotationStatus`, `EditRequestStatus`, `PoDocumentType`

### Phase 3: Observers (Change Log Otomatis)

- [ ] `SalesOrderObserver` → log `created`, `updated`, `deleted`, `restored`
- [ ] `SalesOrderItemObserver`
- [ ] `SalesOrderItemParamObserver`
- [ ] `SalesOrderPaymentObserver` → tambah event `payment_verified`
- [ ] Register semua observer di `SalesOrderServiceProvider`

### Phase 4: Edit Request Workflow

- [ ] `SubmitEditRequestAction` — validasi, buat record + ambil `current_snapshot`
- [ ] `ApproveEditRequestAction` — update status, apply perubahan ke target, trigger change log
- [ ] `RejectEditRequestAction` — update status + review notes
- [ ] `CancelEditRequestAction` — hanya oleh pengaju, hanya jika masih `pending`
- [ ] Notifikasi ke reviewer (email/in-app) saat ada request baru
- [ ] Notifikasi ke pengaju saat request direview

### Phase 5: API / Controllers

- [ ] `SalesOrderController` (CRUD + status transitions)
- [ ] `SalesOrderPaymentController`
- [ ] `SoEditRequestController` (submit, review, cancel)
- [ ] `SoChangeLogController` (read-only, untuk halaman audit trail)
- [ ] `SoPrintLogController` (catat cetak + list riwayat cetak)

### Phase 6: Frontend

- [ ] Halaman daftar SO dengan filter status, tanggal, customer
- [ ] Form buat SO (dengan auto-fill parameter dari master data)
- [ ] Tab detail SO: Items, Payments, Sampling Locations, Revision History
- [ ] Tab **Audit Trail** — tampilkan `change_logs` per SO dalam bentuk timeline
- [ ] Halaman **Edit Request** — form ajukan edit, daftar request pending (untuk reviewer)
- [ ] Badge counter request pending di sidebar untuk admin/manajer

---

*Dokumen disiapkan: 16 April 2026*
*Migration files: `2026_04_12_000001` + `2026_04_16_000001`*
