> ## Documentation Index
> Fetch the complete documentation index at: https://docs.quinnsambal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Expense management

<style>
  {`
    [class*="max-w-none"][class*="table"] {
      display: block !important;
      overflow-x: auto !important;
      max-width: 100% !important;
      width: 100% !important;
      flex-grow: 0 !important;
    }
    [class*="max-w-none"][class*="table"] > table {
      width: 100% !important;
      max-width: 100% !important;
      table-layout: fixed !important;
    }
    .mermaid {
      max-width: 100% !important;
      overflow-x: auto !important;
    }
    article svg[role="img"] {
      max-width: 100% !important;
      height: auto !important;
    }
    article img, .prose img {
      max-width: 100% !important;
      height: auto !important;
    }
    img[src*="LOGO"], img[src*="logo"] {
      max-width: 120px !important;
      max-height: 40px !important;
      width: auto !important;
      height: auto !important;
      object-fit: contain !important;
    }
    `}
</style>

***

title: "Expense Management"
description: "Pencatatan pengeluaran dengan 11 kategori, approval workflow berbasis role, auto-journaling ke GL, anti-double-counting untuk bahan baku inventori, dan budget tracking di SNISHOP ERP."
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

# Expense Management

<img src="https://mintcdn.com/quinnofspicy/e4f_upKhVWcjsUmM/docs/mintlify/screenshots/finance/expense-management.png?fit=max&auto=format&n=e4f_upKhVWcjsUmM&q=85&s=1e3e0ddd17a46aed2cabb402e8a23ae1" alt="Expense Management" width="1920" height="1080" data-path="docs/mintlify/screenshots/finance/expense-management.png" />

Expense Management menangani seluruh pencatatan pengeluaran perusahaan dengan **11 kategori expense**, **approval workflow berbasis role**, dan **auto-journaling ke General Ledger**. Sistem ini menerapkan mekanisme **anti-double-counting** melalui flag `is_inventory_material` untuk mencegah bahan baku inventori dihitung sebagai expense sekaligus HPP. Setiap expense yang disetujui otomatis membuat `GLJournalEntry` dan mengurangi `current_balance` di rekening terkait.

## Arsitektur Expense Management

```mermaid theme={null}
graph TB
    subgraph INPUT["Input Expense"]
        FORM["Form Expense<br/>11 kategori"]
        RECEIPT["Upload Receipt<br/>JPG/PNG/PDF"]
        ACCT["Pilih Akun<br/>Cash/Bank/E-Wallet"]
    end

    subgraph VALIDATION["Validasi Server"]
        IDEM["Idempotency Key<br/>Safe replay"]
        MEM["Membership Check<br/>canRecordExpense"]
        ACC["Account Validation<br/>Belongs to company"]
        INPUT_AMT["Amount Validation<br/>Positive number"]
    end

    subgraph APPROVAL["Approval Workflow"]
        AUTO["Auto-Approve<br/>Owner/Admin/Finance"]
        MANUAL["Manual Approval<br/>Manager/Owner"]
        REJECT["Reject<br/>Dengan alasan"]
    end

    subgraph POSTING["Auto-Posting"]
        FR["FinancialRecord<br/>type: expense"]
        GL["GLJournalEntry<br/>resolveExpenseGLMapping"]
        BAL["Account.current_balance<br/>-= amount"]
    end

    FORM --> VALIDATION
    RECEIPT --> VALIDATION
    ACCT --> VALIDATION

    VALIDATION --> APPROVAL
    IDEM --> APPROVAL
    MEM --> APPROVAL
    ACC --> APPROVAL
    INPUT_AMT --> APPROVAL

    APPROVAL --> POSTING
    AUTO --> FR
    MANUAL --> FR
    REJECT --> X["Stop"]

    FR --> GL
    FR --> BAL
```

## 11 Kategori Expense

| Kategori | Deskripsi | GL Mapping Default |
| - | - | - |
| **salary** | Gaji karyawan | Debit: Beban Gaji |
| **raw\_material** | Bahan baku produksi | Debit: Persediaan\* / Beban Bahan Baku |
| **operational** | Biaya operasional | Debit: Beban Operasional |
| **production\_cost** | Biaya produksi | Debit: Beban Produksi |
| **travel** | Biaya perjalanan | Debit: Beban Perjalanan |
| **meals** | Biaya makan | Debit: Beban Makan |
| **accommodation** | Biaya akomodasi | Debit: Beban Akomodasi |
| **equipment** | Pembelian peralatan | Debit: Beban Peralatan |
| **office\_supplies** | Perlengkapan kantor | Debit: Beban Kantor |
| **training** | Biaya pelatihan | Debit: Beban Pelatihan |
| **other** | Lainnya | Debit: Beban Lain-lain |

\*Kredit selalu ke akun kas/bank yang dipilih.

## Anti-Double-Counting: `is_inventory_material`

Flag `is_inventory_material` mencegah bahan baku yang sudah tercatat di inventori dihitung ulang sebagai expense.

```mermaid theme={null}
graph LR
    EXP["Expense Claim<br/>raw_material"] --> CHECK{is_inventory_material?}
    CHECK -->|true| INV["Debit: Persediaan (Asset)<br/>Bukan expense!"]
    CHECK -->|false| EXP2["Debit: Beban Bahan Baku<br/>Expense di P&L"]
    INV --> CR["Kredit: Kas/Bank"]
    EXP2 --> CR
```

### Kapan Gunakan `is_inventory_material = true`?

| Kondisi | Flag | Alasan |
| - | - | - |
| Beli bahan baku yang masuk inventori | true | Sudah tercatat sebagai asset (Persediaan) |
| Beli bahan habis pakai | false | Langsung jadi expense |
| Beli bahan untuk produksi khusus | false | Tidak masuk inventori |
| Restock bahan baku utama | true | Masuk persediaan |

## Approval Workflow

```mermaid theme={null}
sequenceDiagram
    participant U as User
    participant S as Server
    participant R as Role Check
    participant GL as GL Journal

    U->>S: Submit expense
    S->>S: Validate idempotency_key
    S->>S: Validate account belongs to company
    S->>R: Check canAutoApproveExpense

    alt Owner / Admin / Finance / Finance Admin
        R-->>S: Auto-approve
        S->>GL: Create GLJournalEntry
        S->>S: Create FinancialRecord
        S->>S: Update Account.current_balance
        S-->>U: Expense approved & posted
    else Other roles
        R-->>S: Pending approval
        S->>S: Create Expense (status: pending)
        S-->>U: Expense submitted, waiting approval
        Note over U: Manager/Owner approves later
    end
```

### Role-Based Auto-Approval

| Role | Auto-Approve | Alasan |
| - | - | - |
| Owner | ✓ | Full authority |
| Admin | ✓ | Delegated authority |
| Finance | ✓ | Financial authority |
| Finance Admin | ✓ | Financial authority |
| Production | ✗ | Perlu approval |
| Inventory | ✗ | Perlu approval |
| Kasir | ✗ | Perlu approval |

### Approval States

| Status | Deskripsi |
| - | - |
| pending | Menunggu approval |
| approved | Sudah disetujui, jurnal dibuat |
| rejected | Ditolak, tidak ada jurnal |

## Auto-Journaling ke GL

Setiap expense yang disetujui otomatis membuat `GLJournalEntry` melalui fungsi `resolveExpenseGLMapping()`.

### GL Mapping Logic

```mermaid theme={null}
graph TB
    EXP["Expense Approved"] --> MAP["resolveExpenseGLMapping()"]
    MAP --> CHECK{Category?}
    CHECK -->|raw_material + is_inventory_material| DEBIT1["Debit: Persediaan (Asset)"]
    CHECK -->|salary| DEBIT2["Debit: Beban Gaji (Expense)"]
    CHECK -->|operational| DEBIT3["Debit: Beban Operasional (Expense)"]
    CHECK -->|production_cost| DEBIT4["Debit: Beban Produksi (Expense)"]
    CHECK -->|other categories| DEBIT5["Debit: Beban sesuai kategori"]
    DEBIT1 --> CREDIT["Kredit: Kas/Bank (Account)"]
    DEBIT2 --> CREDIT
    DEBIT3 --> CREDIT
    DEBIT4 --> CREDIT
    DEBIT5 --> CREDIT
```

### Account Resolution

Sistem menggunakan **Indonesian name hints** untuk mencari akun COA yang sesuai:

| Hint | Akun yang Dicari |
| - | - |
| "persediaan", "inventory" | Akun Persediaan (Asset) |
| "gaji", "salary" | Akun Beban Gaji (Expense) |
| "operasional" | Akun Beban Operasional (Expense) |
| "kas", "cash" | Akun Kas (Asset) |

### Reconciliation Marker

Jika auto-journaling gagal (misalnya akun COA tidak ditemukan), sistem **tidak diam-diam gagal**. Sebaliknya:

* Expense tetap di-approve
* Catatan expense ditandai: `RECONCILIATION_REQUIRED`
* Admin harus membuat jurnal manual nanti

## Record Expense

### Step-by-Step

1. Klik **"Record Expense"**
2. Isi form:

| Field | Required | Deskripsi |
| - | - | - |
| Expense Date | ✓ | Tanggal pengeluaran (YYYY-MM-DD) |
| Amount | ✓ | Nominal (harus positif) |
| Category | ✓ | Pilih dari 11 kategori |
| Description | ✓ | Keterangan pengeluaran |
| Account | ✓ | Akun yang digunakan (kas/bank/e-wallet) |
| is\_inventory\_material | Opsional | Centang jika bahan baku inventori |
| Receipt | Opsional | Upload bukti (JPG/PNG/PDF) |
| Idempotency Key | Auto | Safe replay key |

3. Submit expense
4. Sistem validasi dan proses approval

### Idempotency

Setiap expense memiliki `idempotency_key` (max 128 chars) untuk mencegah double-posting. Jika submit dengan key yang sama, sistem akan return hasil yang sama tanpa membuat expense baru.

## Budget Tracking

### Budget per Kategori

Setiap kategori expense bisa dipasang budget:

| Field | Deskripsi |
| - | - |
| planned\_amount | Anggaran yang direncanakan |
| actual\_amount | Realisasi aktual |
| alert\_threshold | Threshold notifikasi (default 80%) |
| period | Periode: weekly, monthly, quarterly, yearly |
| start\_date | Tanggal mulai |
| end\_date | Tanggal selesai |

### Alert Levels

| Level | Threshold | Warna | Aksi |
| - | - | - | - |
| OK | ≤80% | Hijau | Lanjut |
| Warning | 80-100% | Kuning | Monitor |
| Over | >100% | Merah | Expense tetap bisa dicatat, ditandai over budget |

### Variance Analysis

```
Variance = actual_amount - planned_amount
Variance % = (actual_amount / planned_amount) × 100%
```

## Recurring Expense

Untuk pengeluaran rutin (sewa bulanan, gaji, asuransi tahunan):

| Setting | Opsi |
| - | - |
| Frequency | Daily, Weekly, Monthly, Yearly |
| Auto-create | Otomatis buat expense baru |
| Auto-approve | Jika sudah pernah di-approve sebelumnya |

### Contoh Recurring

| Expense | Frequency | Amount |
| - | - | - |
| Sewa gudang | Monthly | Rp 15.000.000 |
| Gaji karyawan | Monthly | Rp 25.000.000 |
| Asuransi | Yearly | Rp 12.000.000 |
| Supplies | Weekly | Rp 2.000.000 |

## Expense Reports

### By Category

| Report | Deskripsi |
| - | - |
| Total per Category | Jumlah expense per kategori |
| Trend Analysis | Tren pengeluaran dari waktu ke waktu |
| Budget Comparison | Planned vs actual per kategori |
| Top Expenses | Expense dengan nominal terbesar |

### By Period

| Period | Deskripsi |
| - | - |
| Daily | Expense harian |
| Weekly | Expense mingguan |
| Monthly | Expense bulanan |
| Yearly | Expense tahunan |

### By Payment Method

| Method | Deskripsi |
| - | - |
| Cash | Expense tunai |
| Transfer | Expense transfer bank |
| E-Wallet | Expense via e-wallet |

## Export & Import

| Format | Use Case |
| - | - |
| CSV | Integrasi dengan sistem lain |
| Excel | Analisis lebih lanjut |
| PDF | Arsip, presentasi |

## Integration

### Bank Integration

* Auto-import bank transactions (jika tersedia)
* Match dengan expense
* Rekonsiliasi

### Accounting Integration

* Sync ke general ledger (otomatis)
* COA mapping
* Journal entries
* Financial reports

## Best Practices

### Documentation

* Always upload receipt untuk audit trail
* Clear description untuk setiap expense
* Proper categorization untuk laporan akurat
* Timely recording (sedekat mungkin dengan tanggal transaksi)

### Approval

* Follow approval workflow
* Document justification untuk expense besar
* Keep approval records
* Regular review oleh manager

### Budget Management

* Set realistic budgets berdasarkan historical data
* Monitor regularly (mingguan/bulanan)
* Adjust as needed (kuartalan)
* Review variances dan investigasi penyebab

***

## Entity Relationship Diagram

Berikut adalah diagram relasi antar-entitas yang terlibat dalam modul Expense Management. Diagram ini menunjukkan bagaimana data mengalir dari pengajuan expense hingga menjadi jurnal akuntansi di General Ledger.

```mermaid theme={null}
erDiagram
    Expense ||--o| ApprovalWorkflow : "mengikuti workflow"
    Expense ||--o| ApprovalRequest : "memiliki approval"
    Expense ||--o| FinancialRecord : "menghasilkan record keuangan"
    Expense ||--o| GLJournalEntry : "menghasilkan jurnal GL"
    ApprovalWorkflow ||--o{ ApprovalRequest : "mendefinisikan alur"
    FinancialRecord ||--o| GLJournalEntry : "direkonsiliasi melalui"

    Expense {
        string company_id PK
        string requester_id FK
        string expense_code UK
        date expense_date
        string category "11 enum values"
        string description
        number amount
        string currency "default: IDR"
        string receipt_url
        string status "draft|submitted|approved|rejected|paid"
        string approval_workflow_id FK
        string approval_request_id FK
        string approval_status "pending|approved|rejected"
        string payment_method "company_account|reimbursement|direct_payment"
        date payment_date
        string reference_number
        string notes
        string account_id FK
        string recipient_name
        boolean is_inventory_material "anti double-counting HPP"
        string approved_by
        string idempotency_key UK
    }

    ApprovalWorkflow {
        string company_id PK
        string workflow_name
        string description
        string document_type "expense_request|leave_request|purchase_order|discount_request|budget_adjustment"
        array approval_levels "multi-level approval config"
        object amount_limits "limit per level"
        boolean is_active
    }

    ApprovalRequest {
        string company_id PK
        string workflow_id FK
        string document_type
        string document_id FK
        string requester_id FK
        datetime submission_date
        number amount
        string description
        number current_approval_level
        array approval_history "array of approval actions"
        string overall_status "pending|approved|rejected|on_hold"
        datetime final_approval_date
    }

    FinancialRecord {
        string user_id PK
        string company_id FK
        string account_id FK
        string type "income|expense|transfer"
        number amount
        string category
        string description
        datetime date
        string attachment_url
        string source "manual|ai_text|ai_scan|pos|manufacturing|distribution|recall|stock_opname"
        string mode "personal|business"
        string transfer_to_account_id
        number transfer_fee
        string reference_id FK
        string reference_type "invoice_payment|pos_transaction|expense|manual|transfer|..."
        string idempotency_key UK
        boolean is_inventory_material
        number channel_fee
        number cogs_amount
        number tax_amount
    }

    GLJournalEntry {
        string company_id PK
        date entry_date
        string reference_number
        string reference_type "manual|invoice|purchase_order|transfer|expense"
        string reference_id FK
        string description
        array line_items "debit/credit entries"
        number total_debit
        number total_credit
        boolean is_balanced "debit == credit"
        string status "draft|posted|reversed"
        date posted_date
        string posted_by
        string notes
    }
```

### Penjelasan Relasi

| Relasi | Kardinalitas | Deskripsi |
| - | - | - |
| Expense → ApprovalWorkflow | Many-to-One | Satu workflow bisa digunakan oleh banyak expense. Workflow menentukan level dan urutan approval. |
| Expense → ApprovalRequest | One-to-One | Setiap expense yang memerlukan approval manual memiliki satu ApprovalRequest yang melacak progres approval. |
| Expense → FinancialRecord | One-to-One | Setiap expense yang disetujui menghasilkan satu FinancialRecord dengan `type: "expense"` dan `reference_type: "expense"`. |
| Expense → GLJournalEntry | One-to-One | Setiap expense yang disetujui menghasilkan satu GLJournalEntry melalui `resolveExpenseGLMapping()`. |
| ApprovalWorkflow → ApprovalRequest | One-to-Many | Satu workflow mendefinisikan aturan; banyak ApprovalRequest yang menjalankannya. |
| FinancialRecord → GLJournalEntry | One-to-One | FinancialRecord direkonsiliasi dengan GLJournalEntry melalui `reference_id`. |

***

## Complete Entity Schema Tables

### Expense Entity (22 Fields)

Entitas `Expense` adalah inti dari modul Expense Management. Setiap baris mewakili satu pengajuan pengeluaran.

| # | Field | Tipe | Required | Default | Enum / Format | Deskripsi |
| - | - | - | - | - | - | - |
| 1 | `company_id` | string | ✓ | — | — | ID perusahaan pemilik expense. Digunakan untuk RLS dan multi-tenancy. |
| 2 | `requester_id` | string | ✓ | — | — | ID karyawan yang mengajukan expense. Terkait dengan entitas User. |
| 3 | `expense_code` | string | — | Auto-generated | Unique | Kode pengeluaran unik (misal: `EXP-2026-0001`). Di-generate otomatis oleh sistem. |
| 4 | `expense_date` | string | ✓ | — | date (YYYY-MM-DD) | Tanggal pengeluaran terjadi. Harus sedekat mungkin dengan tanggal transaksi aktual. |
| 5 | `category` | string | ✓ | — | 11 enum values | Kategori pengeluaran. Menentukan GL mapping dan budget tracking. Lihat tabel kategori di bawah. |
| 6 | `description` | string | — | — | — | Keterangan detail mengenai pengeluaran. Harus jelas untuk keperluan audit. |
| 7 | `amount` | number | ✓ | — | Positive | Nominal pengeluaran dalam satuan mata uang. Harus bernilai positif. |
| 8 | `currency` | string | — | `"IDR"` | ISO 4217 | Mata uang yang digunakan. Default IDR untuk seluruh transaksi di PT Selera Pedas Nusantara. |
| 9 | `receipt_url` | string | — | — | URL | URL bukti pengeluaran/invoice yang di-upload (JPG/PNG/PDF). Penting untuk audit trail. |
| 10 | `status` | string | — | `"draft"` | `draft`, `submitted`, `approved`, `rejected`, `paid` | Status lifecycle expense. Mengontrol state machine dan aksi yang tersedia. |
| 11 | `approval_workflow_id` | string | — | — | FK → ApprovalWorkflow | ID workflow approval yang digunakan. Menentukan berapa level approval yang diperlukan. |
| 12 | `approval_request_id` | string | — | — | FK → ApprovalRequest | ID ApprovalRequest yang terkait. Hanya terisi jika expense memerlukan approval manual. |
| 13 | `approval_status` | string | — | `"pending"` | `pending`, `approved`, `rejected` | Status approval saat ini. Sinkron dengan `ApprovalRequest.overall_status`. |
| 14 | `payment_method` | string | — | — | `company_account`, `reimbursement`, `direct_payment` | Metode pembayaran: dari akun perusahaan, reimbursement ke karyawan, atau pembayaran langsung. |
| 15 | `payment_date` | string | — | — | date (YYYY-MM-DD) | Tanggal pembayaran direalisasikan. Diisi saat status berubah ke `paid`. |
| 16 | `reference_number` | string | — | — | — | Nomor referensi pembayaran (misal: nomor transfer, nomor cek). Untuk rekonsiliasi bank. |
| 17 | `notes` | string | — | — | — | Catatan tambahan dari approver atau admin. Bisa berisi alasan rejection atau instruksi khusus. |
| 18 | `account_id` | string | — | — | FK → GLAccount | ID rekening sumber dana (kas/bank/e-wallet). Digunakan untuk kredit di jurnal GL dan pengurangan saldo. |
| 19 | `recipient_name` | string | — | — | — | Nama penerima atau supplier. Berguna untuk expense kategori vendor/supplier. |
| 20 | `is_inventory_material` | boolean | — | `false` | true/false | **Flag anti-double-counting.** Jika `true`, expense untuk bahan baku tercatat sebagai aset persediaan, bukan beban. Mencegah bahan baku inventori dihitung ganda sebagai expense dan HPP. |
| 21 | `approved_by` | string | — | — | — | User ID atau email approver yang menyetujui expense. Untuk audit trail. |
| 22 | `idempotency_key` | string | — | — | Max 128 chars | Kunci idempoten untuk mencegah double submission. Jika submit dengan key yang sama, sistem return hasil yang sama tanpa membuat expense baru. |

### ApprovalWorkflow Entity (7 Fields + Nested Array)

Entitas `ApprovalWorkflow` mendefinisikan aturan dan alur approval untuk berbagai jenis dokumen (expense, leave, purchase order, dll).

| # | Field | Tipe | Required | Default | Enum / Format | Deskripsi |
| - | - | - | - | - | - | - |
| 1 | `company_id` | string | ✓ | — | — | ID perusahaan. Setiap perusahaan bisa memiliki workflow yang berbeda. |
| 2 | `workflow_name` | string | ✓ | — | — | Nama alur persetujuan (misal: "Expense > 5 Juta", "Purchase Order Approval"). |
| 3 | `description` | string | — | — | Max 1000 chars | Penjelasan mengenai proses dan aturan alur persetujuan yang diterapkan dalam workflow ini. |
| 4 | `document_type` | string | ✓ | — | `expense_request`, `leave_request`, `purchase_order`, `discount_request`, `budget_adjustment` | Tipe dokumen yang memerlukan persetujuan. Menentukan kapan workflow ini dipicu. |
| 5 | `approval_levels` | array | ✓ | — | Nested object\[] | **Konfigurasi multi-level approval.** Setiap level mendefinisikan siapa yang boleh approve, berapa approval diperlukan, dan apakah bisa parallel. Lihat detail di bawah. |
| 6 | `amount_limits` | object | — | — | `{ level_1, level_2, level_3 }` | Limit nominal untuk setiap level approval. Expense di atas limit level 1 akan naik ke level 2, dst. |
| 7 | `is_active` | boolean | — | `true` | true/false | Apakah workflow ini aktif. Workflow non-aktif tidak bisa digunakan untuk expense baru. |

#### Struktur `approval_levels[]` (Nested Array)

Field `approval_levels` adalah array of objects yang mendefinisikan setiap tingkat approval:

| Field dalam Item | Tipe | Default | Deskripsi |
| - | - | - | - |
| `level` | number | — | Urutan tingkat persetujuan (1 = pertama, 2 = kedua, dst). Level yang lebih tinggi biasanya untuk nominal lebih besar. |
| `approver_role` | string | — | Role yang dapat melakukan persetujuan di level ini (misal: `"manager"`, `"owner"`, `"finance"`). |
| `approver_ids` | string\[] | — | User ID spesifik sebagai approver. Jika diisi, hanya user dalam list yang bisa approve di level ini (opsional, override `approver_role`). |
| `required_approvals` | number | `1` | Jumlah persetujuan yang diperlukan di level ini. Jika > 1, maka beberapa approver harus menyetujui. |
| `parallel_approval` | boolean | `false` | Jika `true`, approval di level ini bisa dilakukan secara parallel (tidak harus berurutan). Jika `false`, harus sequential. |

**Contoh konfigurasi `approval_levels` untuk expense:**

```jsonc theme={null}
"approval_levels": [
  {
    "level": 1,
    "approver_role": "manager",
    "approver_ids": [],
    "required_approvals": 1,
    "parallel_approval": false
  },
  {
    "level": 2,
    "approver_role": "owner",
    "approver_ids": [],
    "required_approvals": 1,
    "parallel_approval": false
  }
]
```

### ApprovalRequest Entity (12 Fields + Nested Array)

Entitas `ApprovalRequest` melacak progres approval untuk satu dokumen spesifik (expense, leave request, dll).

| # | Field | Tipe | Required | Default | Enum / Format | Deskripsi |
| - | - | - | - | - | - | - |
| 1 | `company_id` | string | ✓ | — | — | ID perusahaan. |
| 2 | `workflow_id` | string | ✓ | — | FK → ApprovalWorkflow | ID workflow yang digunakan sebagai acuan approval. |
| 3 | `document_type` | string | ✓ | — | — | Tipe dokumen yang diminta approval (misal: `"expense_request"`). |
| 4 | `document_id` | string | ✓ | — | FK → Expense | ID dokumen spesifik yang sedang di-approve. |
| 5 | `requester_id` | string | ✓ | — | FK → User | User yang mengajukan permintaan approval. |
| 6 | `submission_date` | string | — | — | date-time | Tanggal dan waktu pengajuan approval. |
| 7 | `amount` | number | — | — | — | Jumlah nominal yang terkait (untuk referensi dan amount\_limits check). |
| 8 | `description` | string | — | — | — | Deskripsi singkat mengenai permintaan approval. |
| 9 | `current_approval_level` | number | — | `1` | — | Level approval yang sedang aktif. Dimulai dari 1 dan naik setiap kali satu level selesai. |
| 10 | `approval_history` | array | — | `[]` | Nested object\[] | Riwayat semua aksi approval. Setiap item mencatat siapa, kapan, dan apa keputusannya. |
| 11 | `overall_status` | string | — | `"pending"` | `pending`, `approved`, `rejected`, `on_hold` | Status keseluruhan approval. `approved` hanya jika semua level terpenuhi. |
| 12 | `final_approval_date` | string | — | — | date-time | Tanggal dan waktu approval terakhir (saat overall\_status berubah ke `approved`). |

#### Struktur `approval_history[]` (Nested Array)

| Field dalam Item | Tipe | Deskripsi |
| - | - | - |
| `level` | number | Level approval yang di-respons. |
| `approver_id` | string | User ID approver yang mengambil keputusan. |
| `approval_date` | string (date-time) | Tanggal dan waktu keputusan diambil. |
| `status` | string | `approved`, `rejected`, atau `pending`. |
| `comments` | string | Komentar atau catatan dari approver (wajib diisi jika reject). |

### FinancialRecord Entity (20 Fields)

Entitas `FinancialRecord` adalah ledger universal yang mencatat semua arus kas (income, expense, transfer). Expense yang disetujui otomatis membuat FinancialRecord baru.

| # | Field | Tipe | Required | Default | Enum / Format | Deskripsi |
| - | - | - | - | - | - | - |
| 1 | `user_id` | string | ✓ | — | FK → User | ID pengguna yang mencatat atau terkait transaksi. |
| 2 | `company_id` | string | — | — | FK → Company | ID perusahaan (null untuk personal). Expense ERP selalu memiliki company\_id. |
| 3 | `account_id` | string | — | — | FK → GLAccount | ID rekening/kantong sumber dana. Untuk expense, ini adalah akun yang dikredit. |
| 4 | `type` | string | ✓ | — | `income`, `expense`, `transfer` | Jenis transaksi. Expense selalu `type: "expense"`. |
| 5 | `amount` | number | ✓ | — | — | Jumlah transaksi. Selalu positif. |
| 6 | `category` | string | — | — | — | Kategori transaksi (sama dengan category di Expense). |
| 7 | `description` | string | — | — | — | Deskripsi singkat transaksi. |
| 8 | `date` | string | ✓ | — | date-time | Tanggal dan waktu transaksi dicatat. |
| 9 | `attachment_url` | string | — | — | URL | URL bukti transaksi (sama dengan receipt\_url di Expense). |
| 10 | `source` | string | — | `"manual"` | `manual`, `ai_text`, `ai_scan`, `pos`, `manufacturing`, `distribution`, `recall`, `stock_opname` | Sumber pencatatan. Expense dari ERP biasanya `source: "manual"`. |
| 11 | `mode` | string | — | `"personal"` | `personal`, `business` | Mode pencatatan. Expense ERP selalu `mode: "business"`. |
| 12 | `transfer_to_account_id` | string | — | — | FK → GLAccount | ID rekening tujuan (hanya untuk `type: "transfer"`). |
| 13 | `transfer_fee` | number | — | `0` | — | Biaya transfer (jika ada). |
| 14 | `reference_id` | string | — | — | FK | ID referensi transaksi sumber. Untuk expense, ini adalah `Expense._id`. |
| 15 | `reference_type` | string | — | — | `invoice_payment`, `pos_transaction`, `expense`, `manual`, `transfer`, `production_order`, `distribution_shipment`, `distribution_return`, `batch_recall`, `stock_opname` | Tipe referensi. Expense selalu `reference_type: "expense"`. |
| 16 | `idempotency_key` | string | — | — | — | Kunci idempoten untuk mencegah double-posting. |
| 17 | `is_inventory_material` | boolean | — | `false` | true/false | Flag anti-double-counting. Diturunkan dari Expense.is\_inventory\_material. |
| 18 | `channel_fee` | number | — | `0` | — | Potongan biaya marketplace / platform fee. |
| 19 | `cogs_amount` | number | — | `0` | — | Biaya pokok penjualan (HPP/COGS). Digunakan oleh P\&L untuk menghitung laba kotor. |
| 20 | `tax_amount` | number | — | `0` | — | Jumlah pajak yang termasuk dalam amount (untuk revenue: PPN keluaran). |

### GLJournalEntry Entity (14 Fields + Nested Array)

Entitas `GLJournalEntry` mencatat entri jurnal akuntansi ke General Ledger. Setiap expense yang disetujui menghasilkan satu GLJournalEntry melalui `resolveExpenseGLMapping()`.

| # | Field | Tipe | Required | Default | Enum / Format | Deskripsi |
| - | - | - | - | - | - | - |
| 1 | `company_id` | string | ✓ | — | FK → Company | ID perusahaan. |
| 2 | `entry_date` | string | ✓ | — | date (YYYY-MM-DD) | Tanggal entri jurnal. Biasanya sama dengan expense\_date atau payment\_date. |
| 3 | `reference_number` | string | — | — | — | Nomor referensi (misal: `EXP-2026-0001`). |
| 4 | `reference_type` | string | — | — | `manual`, `invoice`, `purchase_order`, `transfer`, `expense` | Tipe referensi sumber. Expense menghasilkan `reference_type: "expense"`. |
| 5 | `reference_id` | string | — | — | FK | ID dokumen sumber (Expense.\_id). |
| 6 | `description` | string | ✓ | — | — | Deskripsi jurnal (misal: "Expense: Beli cabai 50kg - raw\_material"). |
| 7 | `line_items` | array | ✓ | — | Nested object\[] | **Baris-baris jurnal dengan debit/credit.** Minimal 2 baris (satu debit, satu kredit) agar balanced. |
| 8 | `total_debit` | number | — | `0` | — | Total debit dari seluruh line items. Harus sama dengan total\_credit. |
| 9 | `total_credit` | number | — | `0` | — | Total kredit dari seluruh line items. Harus sama dengan total\_debit. |
| 10 | `is_balanced` | boolean | — | `false` | true/false | Apakah debit = credit. Jurnal yang tidak balanced tidak bisa di-post. |
| 11 | `status` | string | — | `"draft"` | `draft`, `posted`, `reversed` | Status jurnal. `posted` = sudah masuk buku besar. `reversed` = dibatalkan dengan jurnal kebalikan. |
| 12 | `posted_date` | string | — | — | date (YYYY-MM-DD) | Tanggal jurnal di-post ke buku besar. |
| 13 | `posted_by` | string | — | — | — | User yang mem-post jurnal. |
| 14 | `notes` | string | — | — | — | Catatan tambahan (misal: `RECONCILIATION_REQUIRED` jika auto-journaling gagal). |

#### Struktur `line_items[]` (Nested Array)

| Field dalam Item | Tipe | Default | Deskripsi |
| - | - | - | - |
| `account_id` | string | — | ID GLAccount yang didebit atau dikredit. |
| `account_code` | string | — | Kode akun COA (misal: `1-1001` untuk Kas, `5-2001` untuk Beban Gaji). |
| `debit` | number | `0` | Nominal debit. Hanya salah satu dari debit/kredit yang boleh > 0. |
| `credit` | number | `0` | Nominal kredit. Hanya salah satu dari debit/kredit yang boleh > 0. |
| `description` | string | — | Deskripsi untuk baris jurnal ini. |

***

## Expense Lifecycle State Machine

Berikut adalah diagram state machine yang menggambarkan seluruh lifecycle expense dari pembuatan hingga pembayaran. State machine ini mengontrol transisi yang valid dan mencegah transisi ilegal.

```mermaid theme={null}
stateDiagram-v2
    [*] --> draft : User klik "Record Expense"

    draft --> submitted : Submit expense\n(validate idempotency_key,\namount > 0, account valid)
    draft --> [*] : Delete (hanya draft)

    submitted --> approved : Auto-approve\n(Owner/Admin/Finance/FinanceAdmin)\nATAU manual approve\nsetelah semua level selesai
    submitted --> rejected : Reject oleh approver\n(dengan alasan wajib)

    approved --> paid : Pembayaran direalisasikan\n(payment_date diisi,\nreference_number dicatat)

    rejected --> submitted : Re-submit setelah revisi\n(mengubah amount/description)
    rejected --> [*] : Dibatalkan permanen

    paid --> [*] : Selesai

    note right of draft
        Expense baru dimulai dari state draft.
        Di state ini, expense bisa diedit atau dihapus.
        Belum memicu approval workflow.
    end note

    note right of submitted
        Expense sedang menunggu approval.
        Jika requester memiliki role
        Owner/Admin/Finance/FinanceAdmin,
        langsung auto-approve tanpa
        menunggu ApprovalRequest.
    end note

    note right of approved
        Semua level approval telah selesai.
        Sistem otomatis membuat:
        1. FinancialRecord (type: expense)
        2. GLJournalEntry (via resolveExpenseGLMapping)
        3. Update Account.current_balance -= amount
    end note

    note right of paid
        Pembayaran telah direalisasikan.
        payment_date dan reference_number
        telah dicatat untuk rekonsiliasi bank.
    end note
```

### Tabel Transisi State

| Dari State | Ke State | Trigger | Kondisi | Aksi Otomatis |
| - | - | - | - | - |
| `[*]` | `draft` | User klik "Record Expense" | Form valid | Buat Expense record |
| `draft` | `submitted` | User klik "Submit" | Idempotency key unik, amount > 0, account valid | Cek auto-approve eligibility |
| `draft` | `[*]` | User klik "Delete" | Hanya draft yang bisa dihapus | Hapus record |
| `submitted` | `approved` | Approver klik "Approve" | Semua level approval terpenuhi | Buat FinancialRecord + GLJournalEntry + update saldo |
| `submitted` | `rejected` | Approver klik "Reject" | Alasan rejection wajib diisi | Catat rejection di approval\_history |
| `rejected` | `submitted` | Requester klik "Re-submit" | Expense telah direvisi | Reset approval, buat ApprovalRequest baru |
| `approved` | `paid` | Finance klik "Mark as Paid" | payment\_date dan reference\_number diisi | Catat tanggal pembayaran |

***

## Approval Workflow — Detailed Sequence Diagram

Diagram berikut menggambarkan alur approval secara detail, termasuk interaksi antara user, server, approval workflow engine, dan GL posting.

```mermaid theme={null}
sequenceDiagram
    participant U as User (Requester)
    participant S as Server (API)
    participant V as Validation Layer
    participant AW as ApprovalWorkflow Engine
    participant AR as ApprovalRequest
    participant FR as FinancialRecord
    participant GL as GLJournalEntry
    participant ACC as Account (GLAccount)

    U->>S: POST /expenses (submit expense)
    S->>V: Validate idempotency_key
    V-->>S: Key valid (belum pernah digunakan)
    S->>V: Validate amount > 0
    V-->>S: Amount valid
    S->>V: Validate account_id belongs to company
    V-->>S: Account valid

    S->>AW: Load ApprovalWorkflow by expense.approval_workflow_id
    AW-->>S: Workflow config (approval_levels, amount_limits)

    S->>S: Check canAutoApproveExpense(user.role)

    alt Role = Owner / Admin / Finance / FinanceAdmin
        S->>S: Auto-approve (skip ApprovalRequest)
        S->>FR: Create FinancialRecord (type: "expense", reference_type: "expense")
        FR-->>S: FinancialRecord created
        S->>GL: Create GLJournalEntry via resolveExpenseGLMapping()
        Note over GL: Debit: Beban sesuai category<br/>Kredit: Kas/Bank (account_id)<br/>is_balanced: true
        GL-->>S: GLJournalEntry created (status: "posted")
        S->>ACC: Update current_balance -= amount
        ACC-->>S: Balance updated
        S->>S: Update Expense.status = "approved"
        S-->>U: 200 OK — Expense approved & posted
    else Role lain (Production, Inventory, Kasir, dll)
        S->>AR: Create ApprovalRequest (overall_status: "pending", current_approval_level: 1)
        AR-->>S: ApprovalRequest created
        S->>S: Update Expense.status = "submitted", approval_status = "pending"
        S-->>U: 202 Accepted — Menunggu approval

        Note over U: Manager/Owner membuka dashboard approval

        U->>S: POST /approval-requests/:id/approve (level 1)
        S->>AR: Push ke approval_history (level: 1, status: "approved")
        AR->>AR: Check: semua level selesai?

        alt Ada level selanjutnya
            AR->>AR: current_approval_level += 1
            AR-->>S: Masih ada level berikutnya
            S-->>U: Level 1 approved, menunggu level 2
        else Semua level selesai
            AR->>AR: overall_status = "approved", final_approval_date = now()
            AR-->>S: All levels approved
            S->>FR: Create FinancialRecord
            S->>GL: Create GLJournalEntry
            S->>ACC: Update current_balance -= amount
            S->>S: Update Expense.status = "approved"
            S-->>U: Expense fully approved & posted
        end
    end
```

### Detail Mekanisme Auto-Approval

Auto-approval terjadi **tanpa membuat ApprovalRequest** jika requester memiliki role tertentu. Ini menghemat satu round-trip ke database dan mempercepat proses untuk user yang memang memiliki otoritas penuh.

| Role | Auto-Approve | Alasan Bisnis |
| - | - | - |
| Owner | ✓ | Pemilik perusahaan memiliki otoritas penuh atas seluruh pengeluaran |
| Admin | ✓ | Delegasi otoritas dari Owner untuk operasional sehari-hari |
| Finance | ✓ | Otoritas finansial — bertanggung jawab atas kebenaran pencatatan |
| Finance Admin | ✓ | Otoritas finansial — asisten finance dengan akses pencatatan |
| Production | ✗ | Perlu approval Manager/Owner karena bukan otoritas finansial |
| Inventory | ✗ | Perlu approval karena hanya mengelola stok, bukan keuangan |
| Kasir | ✗ | Perlu approval karena hanya mengelola transaksi POS |

***

## Expense Category — Detail Lengkap

Berikut adalah penjelasan detail untuk ke-11 kategori expense beserta GL mapping, contoh transaksi, dan perlakuan khusus untuk `is_inventory_material`.

| # | Kategori | Label | GL Debit Default | GL Kredit | is\_inventory\_material | Contoh Transaksi |
| - | - | - | - | - | - | - |
| 1 | `salary` | Gaji Karyawan | Beban Gaji (5-2001) | Kas/Bank | Tidak relevan | Gaji bulanan staf dapur Rp 5.000.000 |
| 2 | `raw_material` | Bahan Baku | Persediaan (1-1200) atau Beban Bahan Baku (5-3001) | Kas/Bank | **Bisa true atau false** | Beli cabai 50kg, beli minyak goreng 20L |
| 3 | `operational` | Operasional | Beban Operasional (5-4001) | Kas/Bank | false | Biaya listrik, air, internet gudang |
| 4 | `production_cost` | Biaya Produksi | Beban Produksi (5-5001) | Kas/Bank | false | Biaya gas LPG, biaya packaging khusus |
| 5 | `travel` | Perjalanan | Beban Perjalanan (5-6001) | Kas/Bank | false | Transportasi supplier visit, tiket perjalanan |
| 6 | `meals` | Makan | Beban Makan (5-6101) | Kas/Bank | false | Makan tim lembur, konsumsi rapat |
| 7 | `accommodation` | Akomodasi | Beban Akomodasi (5-6201) | Kas/Bank | false | Hotel untuk training luar kota |
| 8 | `equipment` | Peralatan | Beban Peralatan (5-7001) | Kas/Bank | false | Beli blender industri, timbangan digital |
| 9 | `office_supplies` | Perlengkapan Kantor | Beban Kantor (5-7101) | Kas/Bank | false | Tinta printer, kertas HVS, ATK |
| 10 | `training` | Pelatihan | Beban Pelatihan (5-7201) | Kas/Bank | false | Biaya training HACCP, workshop karyawan |
| 11 | `other` | Lainnya | Beban Lain-lain (5-9001) | Kas/Bank | false | Biaya tak terkategori |

### Aturan GL Mapping untuk `raw_material`

Kategori `raw_material` adalah satu-satunya kategori yang GL mapping-nya **berubah tergantung flag `is_inventory_material`**:

```
JIKA category = "raw_material":
    JIKA is_inventory_material = true:
        DEBIT → Persediaan (1-1200) — Aset di Neraca
        KREDIT → Kas/Bank
        → TIDAK masuk P&L sebagai expense
        → Nanti saat bahan dipakai produksi, baru jadi HPP via inventory movement

    JIKA is_inventory_material = false:
        DEBIT → Beban Bahan Baku (5-3001) — Expense di P&L
        KREDIT → Kas/Bank
        → LANGSUNG masuk P&L sebagai expense
```

***

## Deep Dive: `is_inventory_material` Anti-Double-Counting

### Masalah yang Dipecahkan

Tanpa flag `is_inventory_material`, sistem akan mengalami **double-counting** pada bahan baku inventori:

1. **Saat beli bahan baku**: Expense tercatat → masuk P\&L sebagai beban → mengurangi laba
2. **Saat bahan dipakai produksi**: HPP (Harga Pokok Produksi) tercatat → masuk P\&L sebagai beban lagi

Akibatnya, beban tercatat **dua kali** untuk bahan yang sama: sekali sebagai expense, sekali sebagai HPP. Ini membuat laba kotor terlihat lebih kecil dari kenyataan dan laporan keuangan tidak akurat.

### Solusi: Flag `is_inventory_material`

```mermaid theme={null}
graph TB
    subgraph TANPA_FLAG["❌ TANPA is_inventory_material (SALAH)"]
        B1["Beli Cabai 50kg<br/>Rp 500.000"] --> E1["Expense: Beban Bahan Baku<br/>(P&L: -500.000)"]
        E1 --> P1["Produksi Sambal<br/>Gunakan cabai"]
        P1 --> H1["HPP: Beban Produksi<br/>(P&L: -500.000)"]
        H1 --> DOUBLE["⚠️ DOUBLE COUNT<br/>Beban total: Rp 1.000.000<br/>(Seharusnya Rp 500.000)"]
    end

    subgraph DENGAN_FLAG["✅ DENGAN is_inventory_material = true (BENAR)"]
        B2["Beli Cabai 50kg<br/>Rp 500.000"] --> A2["Debit: Persediaan (Aset)<br/>(Neraca: +500.000)<br/>BUKAN expense"]
        A2 --> P2["Produksi Sambal<br/>Gunakan cabai"]
        P2 --> H2["HPP: Persediaan → Beban<br/>(P&L: -500.000)"]
        H2 --> CORRECT["✅ BENAR<br/>Beban total: Rp 500.000<br/>Laba akurat"]
    end
```

### Panduan Penggunaan Flag

| Skenario | `is_inventory_material` | Penjelasan |
| - | - | - |
| Beli bahan baku yang **masuk gudang/inventori** (cabai, minyak, bumbu stok) | `true` | Bahan tercatat sebagai aset persediaan. Expense TIDAK masuk P\&L. Nanti saat dipakai produksi, inventory movement yang menghitung HPP. |
| Beli bahan **habis pakai** yang langsung dipakai (minyak goreng untuk 1 kali pakai, bumbu sekali masak) | `false` | Langsung jadi expense karena tidak masuk inventori. |
| Beli bahan untuk **produksi khusus** (bahan untuk pesanan catering tertentu) | `false` | Tidak masuk inventori karena langsung dipakai untuk order spesifik. |
| **Restock** bahan baku utama (cabai kering, bawang, minyak curah) | `true` | Masuk persediaan gudang. |
| Beli **perlengkapan non-inventori** (sarung tangan, masker, cleaning supplies) | `false` | Bukan bahan baku, langsung jadi expense operasional. |

### Dampak ke Laporan Keuangan

| Laporan | `is_inventory_material = true` | `is_inventory_material = false` |
| - | - | - |
| **Neraca (Balance Sheet)** | Persediaan (Aset) bertambah | Tidak terpengaruh |
| **Laba Rugi (P\&L)** | Belum terpengaruh saat beli; terpengaruh saat bahan dipakai (HPP) | Beban bertambah langsung saat beli |
| **Arus Kas (Cash Flow)** | Kas keluar sama | Kas keluar sama |
| **Laba Kotor** | Akurat (HPP hanya dihitung saat bahan dipakai) | Berpotensi double-count jika bahan juga dihitung HPP |

***

## Referensi Silang Modul

| Modul Terkait | Hubungan |
| - | - |
| [General Ledger](./general-ledger.mdx) | GLJournalEntry yang dihasilkan oleh expense masuk ke buku besar |
| [Chart of Accounts](./chart-of-accounts.mdx) | resolveExpenseGLMapping() menggunakan COA untuk menentukan akun debit/kredit |
| [Inventory](../inventory/inventory-management.mdx) | `is_inventory_material = true` berinteraksi dengan persediaan inventori |
| [Approval Workflow](../settings/approval-workflow.mdx) | Konfigurasi ApprovalWorkflow dan ApprovalRequest |
| [Budget](./budget-management.mdx) | Expense membandingkan actual\_amount terhadap planned\_amount |
| [Financial Reports](./financial-reports.mdx) | Expense masuk ke Laporan Laba Rugi dan Laporan Arus Kas |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.