# Dokumentasi Lengkap Alur Ekstraksi: Dari Teks Mentah (Raw Markdown) ke Data Bersih (JSON/Database)
Dokumen ini mendokumentasikan secara rinci bagaimana teks mentah (*Raw Markdown*) hasil pembacaan OCR & vLLM diolah langkah demi langkah oleh **Next.js API Gateway** (melalui utilitas [parser.ts](file:///d:/Client/Data%20Bisnis%20Solusi/app-pfm-ocr-v2/backend/pfm-web-app/src/utils/parser.ts)) hingga menjadi data terstruktur bersih (JSON) yang disimpan di database PostgreSQL.
---
## 1. Input: Teks Mentah (Raw Markdown) Hasil OCR & vLLM
Di bawah ini adalah contoh representatif dari teks Markdown mentah yang dihasilkan oleh pipeline **FastAPI & vLLM** (menggunakan model **PaddleOCR-VL-1.6**).
Teks ini memiliki beberapa masalah umum OCR:
* **Visual Distortions / Typo**: OCR salah mengenali karakter (misal `PQ` untuk `PO`, huruf `O`/`I`/`S` tercampur di dalam angka).
* **Shifted Layout**: Teks label terpisah dari nilainya karena tata letak baris yang bergeser.
* **Messy HTML Tables**: Struktur tabel mengandung tag HTML, spasi bising, baris kosong, dan data tanda air (*watermark*).
```markdown
PT. CHAROEN POKPHAND INDONESIA TBK
Kawasan Industri Modern Cikande, Serang, Banten
===================================================
Kepada Yth : PT.PRIMAFOOD INTERNATIONAL
Order Untuk : PFM ANCOL II (ANC-02)
Alamat Kirim : JL. ANCOL BARAT VIII NO. 1, PADEMANGAN, JAKARTA UTARA
Tanggal :
No. SO :
No. DO :
No. PO :
23 Jun 2026
16O19B032l
165998O277
PQ/26/000023082B
Plat Nomor : B 9427 UXT (ASLI)
Nama Driver : Ahmad Supriadi
| Kode Barang |
Nama Barang |
Banyak |
Jumlah |
| 11310014 |
Griller Size 2 (0.8-0.9) KG Frozen |
2 |
40 PC |
| 1131OO24 |
AYAM SIZE A PR FROZEN (O.9-1)KG/PC |
1 |
20 PC |
| 999999 |
Tanda Tangan Supir (Noise) |
|
|
| Barang dikirim dalam keadaan baik |
Jumlah |
60 PC |
Lembar 3 : Customer (ASLI)
```
---
## 2. Tahapan Pemrosesan & Ekstraksi Data (Step-by-Step)
Proses pemrosesan data mentah di atas dibagi menjadi 5 tahapan utama:
### Tahap 1: Ekstraksi Regex Awal (Lapis 1)
Sistem pertama kali mencoba mengekstrak field utama menggunakan pola Regex.
* **Ekstraksi Tanggal**:
* *Regex*: `/Tanggal\s*[:\-.]?\s*([^\n]{6,100})/i`
* *Hasil*: Karena baris di samping label `"Tanggal :"` kosong, pencarian langsung menghasilkan **`"Not Found"`**.
* **Ekstraksi No SO, DO, PO**:
* *Regex SO*: `/(?:No\.?[ \t]*SO|SO[ \t]*No\.?)[ \t]*[:\-][ \t]*([A-Z0-9\-]+)/i`
* *Regex DO*: `/(?:No\.?[ \t]*DO|Delivery Order[ \t]*No|D\.O\.[ \t]*No|Order[ \t]*No)[ \t]*[:\- \t]*([A-Z0-9\-]+)/i`
* *Regex PO*: `/(?:No\.?[ \t]*PO|PO[ \t]*No\.?)[ \t]*[:\-][ \t]*([A-Z0-9\-\/]+)/i`
* *Hasil*: Sama seperti tanggal, label `"No. SO"`, `"No. DO"`, dan `"No. PO"` tidak memiliki nilai di baris yang sama, sehingga ketiganya menghasilkan **`"Not Found"`**.
---
### Tahap 2: Penyelarasan Layout Tergeser (*Shifted Layout Re-alignment*)
Karena semua field bernilai `"Not Found"`, sistem mendeteksi adanya pergeseran layout. Program mengaktifkan algoritma pencarian baris terdekat (realinyasi indeks baris):
1. Program mengambil potongan baris tepat di bawah baris label yang kosong.
2. Ditemukan kumpulan baris berisi nilai:
* Baris 1: `23 Jun 2026`
* Baris 2: `16O19B032l`
* Baris 3: `165998O277`
* Baris 4: `PQ/26/000023082B`
3. **Pemetaan Realinyasi**:
* `tanggal` diarahkan mengambil baris yang memiliki pola tanggal $\rightarrow$ `"23 Jun 2026"`.
* `noSO` diarahkan mengambil angka 10-digit pertama (termasuk karakter typo) $\rightarrow$ `"16O19B032l"`.
* `noDO` diarahkan mengambil angka 10-digit kedua $\rightarrow$ `"165998O277"`.
* `noPO` diarahkan mengambil baris berpola PO $\rightarrow$ `"PQ/26/000023082B"`.
---
### Tahap 3: Sanitasi Data & Koreksi Karakter Typo (Lapis 2)
Setelah field berhasil dipetakan, program menjalankan fungsi `sanitizeParsedMetadata()` untuk membersihkan kesalahan visual OCR:
#### 1. Koreksi & Sanitasi Tanggal
* *Teks Awal*: `"23 Jun 2026"`
* *Proses*: Regex membagi string menjadi Hari (`23`), Bulan (`Jun`), dan Tahun (`2026`). Bulan `"Jun"` dicocokkan ke map kamus bulan untuk dikembangkan.
* *Hasil Bersih*: **`"23 June 2026"`**
#### 2. Koreksi & Sanitasi Nomor SO
* *Teks Awal*: `"16O19B032l"`
* *Proses*: Fungsi `correctVisualDigits()` memindai karakter non-angka dan menggantinya berdasarkan tabel kemiripan bentuk:
* Huruf `O` diganti menjadi angka `0` (indeks 2).
* Huruf `B` diganti menjadi angka `8` (indeks 5).
* Huruf `l` (L kecil) diganti menjadi angka `1` (indeks 9).
* *Hasil Bersih*: **`"1601980321"`** (Lolos validasi panjang 7-12 digit angka).
#### 3. Koreksi & Sanitasi Nomor DO
* *Teks Awal*: `"165998O277"`
* *Proses*: Huruf `O` diganti menjadi angka `0` (indeks 6).
* *Hasil Bersih*: **`"1659980277"`** (Lolos validasi panjang 7-12 digit angka).
#### 4. Koreksi & Sanitasi Nomor PO
* *Teks Awal*: `"PQ/26/000023082B"`
* *Proses*:
* Huruf `PQ` di depan diidentifikasi sebagai kesalahan baca dari label `PO`. Kode dibersihkan via `cleanAndFormatPO()`.
* Huruf `B` di bagian belakang angka diganti menjadi angka `8`.
* Tahun berjalan disesuaikan dengan segmentasi tahun dokumen (`26`).
* *Hasil Bersih*: **`"PO/26/0000230828"`**
#### 5. Koreksi Plat Nomor
* *Teks Awal*: `"B 9427 UXT (ASLI)"`
* *Proses*: Kata sampingan `(ASLI)` dibuang. Pola plat nomor Indonesia (`B 9427 UXT`) dicocokkan dengan kode prefix wilayah terdaftar.
* *Hasil Bersih*: **`"B 9427 UXT"`**
---
### Tahap 4: Fuzzy Store Resolution (Pencocokan Toko)
Teks customer *"PT.PRIMAFOOD INTERNATIONAL Order Untuk: PFM ANCOL II (ANC-02) Alamat Kirim: JL. ANCOL BARAT VIII..."* dianalisis:
1. **Tokenisasi**: Teks dipecah menjadi token-token kata: `['pfm', 'ancol', 'anc', '02', 'pademangan']` (kata seperti "jalan", "ke", "untuk" dibuang).
2. **Kalkulasi Irisan Token**:
Sistem mengompilasi token store dari database `store_master`.
* *Entri Toko*: `nama_toko: "PX HEAD OFFICE ANCOL"`, `alamat: "JL. ANCOL BARAT VIII NO. 1"`
* *Irisan*: Token `'ancol'`, `'barat'`, `'viii'`, dan `'pademangan'` memiliki kecocokan tinggi (skor $>0.8$).
3. **Hasil Resolusi**:
* `orderUntuk` $\rightarrow$ **`"PX HEAD OFFICE ANCOL"`** (Nama resmi dari DB).
* `alamat` $\rightarrow$ **`"JL. ANCOL BARAT VIII/1 KEL. ANCOL, KEC. PADEMANGAN JAKARTA UTARA, DKI JAKARTA"`** (Alamat resmi dari DB).
---
### Tahap 5: Parsing Tabel & Validasi Triple-Check Barang
Sistem mengurai tag `` menjadi grid dua dimensi (baris & kolom), lalu menyaring baris:
```markdown
Baris 1: | 11310014 | Griller Size 2 (0.8-0.9) KG Frozen | 2 | 40 PC |
Baris 2: | 1131OO24 | AYAM SIZE A PR FROZEN (O.9-1)KG/PC | 1 | 20 PC |
Baris 3: | 999999 | Tanda Tangan Supir (Noise) | | |
```
#### Baris 1:
* **OCR SKU**: `"11310014"` (Valid 8-digit).
* **Fuzzy Database Matching (Triple-Check)**:
* Dilakukan pencarian di database `sku_master`. Kode SKU `"11310014"` terdaftar sebagai `'AYAM SIZE 2 FROZEN (0.8-0.9)KG(*)'`.
* Karena SKU cocok 100%, sistem mengunci data master ini (Skor = 1.0).
* **Standardisasi Satuan**:
* Kuantitas `"2"` diautocomplete dengan kemasan luar master (`jenis_outer: "Karung"`) $\rightarrow$ **`"2 KRG"`**.
* Total `"40 PC"` dikoreksi dengan kemasan dalam master (`standar_jumlah: "PC"`) $\rightarrow$ **`"40 PC"`**.
#### Baris 2:
* **OCR SKU**: `"1131OO24"` (Typo huruf `O`).
* **Koreksi Visual Digit**: Huruf `O` diubah menjadi angka `0` $\rightarrow$ `"11310024"`.
* **Fuzzy Database Matching (Triple-Check)**:
* SKU hasil koreksi `"11310024"` dicari di DB, terdaftar sebagai `'AYAM SIZE A FROZEN (0.9-1)KG/PC(*)'`.
* SKU cocok 100%. Data master dikunci.
* **Standardisasi Satuan**:
* Kuantitas `"1"` diautocomplete dengan kemasan luar master (`jenis_outer: "Karung"`) $\rightarrow$ **`"1 KRG"`**.
* Total `"20 PC"` distandardisasi menjadi **`"20 PC"`**.
#### Baris 3:
* **OCR SKU**: `"999999"`.
* **Validasi SKU**: Gagal karena panjang hanya 6 digit.
* **Hasil**: Baris dibuang sebagai noise.
#### Baris 4 (Baris Total):
* Terbaca `"Jumlah"` dan `"60 PC"`.
* Dideteksi sebagai watermark/kolom ringkasan melalui fungsi `isWatermark()`.
* **Hasil**: Baris dibuang.
---
## 3. Output: JSON Hasil Akhir & Struktur Database
Setelah melalui seluruh pipeline pemrosesan di atas, data yang dikembalikan ke aplikasi mobile Flutter dan disimpan ke database PostgreSQL berbentuk data bersih berikut:
### Objek JSON Metadata Akhir
```json
{
"id": "42",
"filePath": "1782870899198-sample_do.jpeg",
"createdAt": "2026-07-02T03:56:00.000Z",
"header": {
"tanggal": "23 June 2026",
"no_po": "PO/26/0000230828",
"no_so": "1601980321",
"no_do": "1659980277"
},
"shipment": {
"kepada_yth": "PT.PRIMAFOOD INTERNATIONAL",
"order_untuk": "PX HEAD OFFICE ANCOL",
"alamat": "JL. ANCOL BARAT VIII/1 KEL. ANCOL, KEC. PADEMANGAN JAKARTA UTARA, DKI JAKARTA",
"plat_truk": "B 9427 UXT",
"nama_driver": "Ahmad Supriadi",
"nama_penerima": ""
},
"items": [
{
"nomor_sku": "11310014",
"nama_barang": "AYAM SIZE 2 FROZEN (0.8-0.9)KG(*)",
"banyak": "2 KRG",
"jumlah": "40 PC"
},
{
"nomor_sku": "11310024",
"nama_barang": "AYAM SIZE A FROZEN (0.9-1)KG/PC(*)",
"banyak": "1 KRG",
"jumlah": "20 PC"
}
],
"latitude": -6.1284,
"longitude": 106.8427
}
```
### Penyimpanan ke Tabel PostgreSQL
#### 1. Baris Baru di Tabel `documents`
```sql
INSERT INTO documents (id, filename, upload_time, size, parsed, metadata, is_sample, file_hash, latitude, longitude)
VALUES (
42,
'1782870899198-sample_do.jpeg',
'2026-07-02 03:56:00',
151816,
true,
'{"header": {"tanggal": "23 June 2026", "no_po": "PO/26/0000230828", ...}, "shipment": {...}}',
false,
'd4a183...561fe99',
-6.1284,
106.8427
);
```
#### 2. Baris Baru di Tabel `ocr_items` (Bulk Insert)
```sql
INSERT INTO ocr_items (document_id, row_index, kode_barang_original, kode_barang, nama_barang, banyak_original, banyak, jumlah_original, jumlah, is_flagged, remark)
VALUES
(42, 0, '11310014', '11310014', 'AYAM SIZE 2 FROZEN (0.8-0.9)KG(*)', '2', '2 KRG', '40 PC', '40 PC', false, ''),
(42, 1, '1131OO24', '11310024', 'AYAM SIZE A FROZEN (0.9-1)KG/PC(*)', '1', '1 KRG', '20 PC', '20 PC', false, '');
```