> ## 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.

# Offline mode

<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: "Offline Mode"
description: "Sistem offline-first untuk POS dan inventory dengan auto-sync, conflict resolution, dan data persistence lokal."
------------------------------------------------------------------------------------------------------------------------------

# Offline Mode

<img src="https://mintcdn.com/quinnofspicy/e4f_upKhVWcjsUmM/docs/mintlify/screenshots/core/offline-mode.png?fit=max&auto=format&n=e4f_upKhVWcjsUmM&q=85&s=7cdfab9fa46daec142a3aa04f586bf4a" alt="Offline Mode" width="1920" height="1080" data-path="docs/mintlify/screenshots/core/offline-mode.png" />

**Offline Mode** adalah kemampuan SNISHOP ERP untuk tetap berfungsi meskipun tidak ada koneksi internet. Fitur ini menggunakan arsitektur **offline-first** — data disimpan secara lokal di perangkat melalui IndexedDB dan otomatis tersinkronisasi ke server ketika koneksi kembali tersedia.

Sistem ini dirancang untuk menjamin **zero data loss** pada kondisi jaringan tidak stabil, menjaga integritas transaksi keuangan, dan memastikan konsistensi data inventory melalui mekanisme conflict resolution yang deterministik.

## Kasus Penggunaan Kritis

Fitur ini sangat kritis untuk skenario berikut:

| Skenario | Deskripsi | Dampak jika Tidak Ada Offline Mode |
| - | - | - |
| **Kasir toko** | Jaringan intermittently di area komersial padat | Transaksi gagal, antrian pelanggan menumpuk |
| **Staf gudang** | Bekerja di basement, gudang tertutup, area minim sinyal | Stock movement tidak tercatat, selisih inventaris |
| **Sales lapangan** | Kunjungan ke area dengan coverage buruk | Order tidak masuk, kehilangan penjualan |
| **Backup server** | Server pusat mengalami downtime sementara | Operasional berhenti total |
| **Event/bazaar** | Lokasi sementara tanpa infrastruktur jaringan stabil | Tidak bisa bertransaksi selama acara |
| **Multi-cabang** | Sinkronisasi antar cabang tertunda | Data stok tidak akurat antar outlet |

## Arsitektur Offline-First

### Diagram Arsitektur Umum

```mermaid theme={null}
graph TD
    subgraph "Client (Browser/PWA)"
        A[User Action] --> B[Local Database - IndexedDB]
        B --> C[Sync Queue]
        C --> D[Connection Detector]
    end
    
    subgraph "Server"
        E[API Endpoint]
        F[Conflict Resolver]
        G[Central Database]
    end
    
    D -->|Online| E
    D -->|Offline| H[Queue & Retry]
    E --> F
    F --> G
    H -->|Reconnect| E
```

### Lapisan Data Persistence

```mermaid theme={null}
graph LR
    subgraph "Application Layer"
        UI[UI Components]
        SVC[Service Layer]
    end

    subgraph "Persistence Layer"
        IDB[(IndexedDB)]
        SQ[Sync Queue]
        CACHE[In-Memory Cache]
    end

    subgraph "Network Layer"
        API[REST API]
        WS[WebSocket]
        HB[Heartbeat]
    end

    UI --> SVC
    SVC --> IDB
    SVC --> CACHE
    SVC --> SQ
    SQ --> API
    HB --> SVC
    WS --> SVC
```

## Komponen Sistem

### 1. Local Storage Engine

Data disimpan menggunakan **IndexedDB** di browser dengan struktur object store terorganisasi:

| Komponen | Fungsi | Entitas Terkait | Kapasitas |
| - | - | - | - |
| **Transaction Store** | Menyimpan transaksi POS offline | `CompanyPOSTransaction`, `POSTransaction` | Unlimited (tergantung storage) |
| **Inventory Store** | Data stok & movement lokal | `Inventory`, `CompanyPOSInventory` | Mirror dari server |
| **Product Store** | Katalog produk & harga | `CompanyPOSProduct`, `CompanyPOSCategory` | Mirror dari server |
| **Stock Movement Store** | Pergerakan stok lokal | `StockMovement` | Unlimited |
| **Transfer Store** | Dokumen transfer stok | `StockTransfer` | Unlimited |
| **Opname Store** | Data stock opname | `StockOpname` | Unlimited |
| **Customer Store** | Data pelanggan/member | `POSMember` | Mirror dari server |
| **Alert Store** | Alert stok lokal | `StockAlert` | Mirror dari server |
| **Warehouse Store** | Data lokasi gudang | `WarehouseLocation` | Mirror dari server |
| **Sync Queue** | Antrian data yang belum tersync | Semua entitas | Unlimited |
| **Config Store** | Konfigurasi & settings | `CompanyPOSSettings` | Kecil |

**Storage Limits per Browser:**

| Browser | Default Limit | Kebijakan Eviction | Catatan |
| - | - | - | - |
| **Chrome** | 60% of disk | Auto-evict LRU (Least Recently Used) | Origin-based quota |
| **Firefox** | 50% of disk | Prompt jika melebihi limit | Persistent storage recommended |
| **Safari** | 1 GB | Hard limit, no prompt | Gunakan `navigator.storage.persist()` |
| **Edge** | 60% of disk | Same as Chrome (Chromium-based) | Same quota policy |

### 2. Connection Detector

Sistem deteksi koneksi multi-layer yang cerdas:

| Metode | Deskripsi | Akurasi | Latency |
| - | - | - | - |
| **Navigator.onLine** | Cek status koneksi browser | Basic | Instant |
| **Heartbeat Ping** | Ping server setiap 30 detik | High | \~30s detection |
| **API Response** | Deteksi dari HTTP response code | Definitive | On-request |
| **Event Listener** | Listen `online`/`offline` DOM events | Instant | Real-time |
| **WebSocket Health** | Monitor koneksi WebSocket aktif | Very High | Real-time |

**Status Indikator Visual:**

| Status | Ikon | Warna | Deskripsi | Tindakan User |
| - | - | - | - | - |
| **Online** | ☁️✅ | Hijau | Terkoneksi ke server | Operasional normal |
| **Syncing** | 🔄 | Biru | Sedang sinkronisasi data | Tunggu hingga selesai |
| **Offline** | ☁️❌ | Merah | Tidak ada koneksi | Lanjut bekerja, data di-queue |
| **Partial** | ⚠️ | Kuning | Koneksi tidak stabil | Hati-hati, mungkin ada delay sync |
| **Conflict** | 🔀 | Oranye | Ada konflik data | Review dan resolve manual |

### 3. Sync Queue & Engine

Antrian sinkronisasi yang mengelola data pending dengan mekanisme retry dan prioritas:

| Field | Tipe | Deskripsi |
| - | - | - |
| **Queue ID** | `string` | Identifier unik untuk setiap item (UUID v4) |
| **Entity Type** | `string` | Jenis data: `CompanyPOSTransaction`, `StockMovement`, `StockTransfer`, `StockOpname`, dll |
| **Operation** | `string` | Operasi: `create`, `update`, `delete` |
| **Payload** | `object` | Data lengkap yang akan di-sync |
| **Created At** | `date-time` | Kapan item ditambahkan ke queue |
| **Retry Count** | `number` | Berapa kali sudah dicoba sync |
| **Max Retries** | `number` | Batas percobaan (default: 5) |
| **Status** | `enum` | `pending`, `syncing`, `success`, `failed`, `conflict` |
| **Error Message** | `string` | Pesan error jika gagal |
| **Priority** | `enum` | `high`, `normal`, `low` |
| **Idempotency Key** | `string` | Kunci idempotensi untuk mencegah duplikasi |
| **Depends On** | `string[]` | Queue ID dependency (order guarantee) |

**Urutan Prioritas Sync:**

| Prioritas | Jenis Data | Alasan | SLA |
| - | - | - | - |
| **High** | `CompanyPOSTransaction` (payment\_method: cash/card/ewallet) | Mempengaruhi keuangan langsung | Immediate |
| **High** | `StockMovement` (movement\_type: out, sale) | Stok berkurang, perlu sinkron segera | Immediate |
| **Normal** | `StockMovement` (movement\_type: in, adjustment) | Stok bertambah, penting tapi tidak urgent | \< 5 menit |
| **Normal** | `StockTransfer` | Perpindahan antar lokasi | \< 5 menit |
| **Normal** | `StockOpname` | Hasil penghitungan fisik | \< 10 menit |
| **Low** | `StockAlert` (status: acknowledged/dismissed) | Status alert non-kritis | \< 30 menit |
| **Low** | `POSMember` (notes, discount updates) | Data pelengkap | Best effort |

### 4. Conflict Resolution

Ketika data diubah di kedua sisi (offline & server) selama periode offline, sistem menerapkan strategi resolusi konflik berdasarkan jenis entitas dan konteks bisnis:

| Strategi | Deskripsi | Kapan Digunakan | Entitas Terkait |
| - | - | - | - |
| **Server Wins** | Data server yang dipakai sebagai sumber kebenaran | Stok (`Inventory.quantity`), harga (`CompanyPOSProduct.price`) | `Inventory`, `CompanyPOSProduct` |
| **Client Wins** | Data lokal yang dipakai | Catatan personal, draft, preferensi user | `StockOpname.notes`, `StockMovement.notes` |
| **Append/Merge** | Gabungkan kedua perubahan (append entries) | Item independent dalam order, movement entries | `CompanyPOSTransaction.items`, `StockMovement` |
| **Idempotency Check** | Skip jika sudah ada (berdasarkan idempotency key) | Operasi dengan idempotency key | `StockMovement`, `StockTransfer` |
| **Manual Review** | User memilih mana yang dipakai | Konflik kompleks yang tidak bisa di-resolve otomatis | Semua entitas dengan konflik ganda |

**Flow Conflict Resolution:**

```mermaid theme={null}
flowchart TD
    A[Sync dimulai] --> B{Ada konflik?}
    B -->|Tidak| C[Apply semua perubahan]
    B -->|Ya| D{Jenis entitas?}
    D -->|Inventory / CompanyPOSProduct| E[Server wins — stok & harga dari server]
    D -->|CompanyPOSTransaction| F{Transaction number duplikat?}
    D -->|StockMovement| G{Idempotency key match?}
    D -->|StockTransfer| H{Transfer number duplikat?}
    D -->|StockOpname| I[Client wins — data fisik dari user]
    D -->|Kompleks / tidak terdefinisi| J[Flag untuk review manual]

    F -->|Ya| K[Assign new transaction number, keep payload]
    F -->|Tidak| L[Apply langsung]
    G -->|Ya| M[Skip — sudah diproses]
    G -->|Tidak| N[Apply sebagai movement baru]
    H -->|Ya| O[Update status, reconcile quantity]
    H -->|Tidak| P[Apply langsung]

    E --> Q[Log resolusi konflik]
    K --> Q
    L --> Q
    M --> Q
    N --> Q
    O --> Q
    P --> Q
    I --> Q
    J --> R[Notifikasi user untuk review]
    Q --> S[Sync selesai]
    R --> S
```

## Fitur Utama

### Transaksi POS Offline

Transaksi POS (`CompanyPOSTransaction`) adalah entitas inti yang mendukung operasi offline penuh:

| Kemampuan | Status Offline | Setelah Sync | Catatan Teknis |
| - | - | - | - |
| Buat transaksi baru | ✅ | Data terkirim ke server | Disimpan di IndexedDB, queue otomatis |
| Cetak receipt | ✅ | Tidak perlu sync | Template dari `CompanyPOSSettings.receipt_settings` |
| Hitung kembalian | ✅ | Kalkulasi lokal | `change_amount` dihitung di client |
| Scan barcode | ✅ | Lookup produk lokal | Cari di `CompanyPOSProduct` cache lokal |
| Apply diskon | ✅ | Validasi setelah sync | `discount_percentage` & `discount_amount` |
| Split payment | ✅ | Data terkirim setelah sync | Array `payments` dengan multi-tender |
| Lookup member | ✅ | Sync points setelah reconnect | Data `POSMember` dari cache lokal |
| Hitung poin | ✅ | Update `points_earned`/`points_used` | Kalkulasi lokal, validasi server |
| Cetak laporan shift | ⚠️ | Data terakhir yang di-sync | Snapshot last-synced data |
| Multi-channel pricing | ⚠️ | Harga terakhir dari server | `channel_pricing` cache lokal |

**Payment Methods yang Didukung Offline:**

| Metode | Enum Value | Offline Support | Catatan |
| - | - | - | - |
| Tunai | `cash` | ✅ Penuh | Kalkulasi kembalian lokal |
| Kartu | `card` | ✅ Penuh | Input manual, verifikasi nanti |
| Transfer | `transfer` | ✅ Penuh | `payment_reference` disimpan lokal |
| E-Wallet | `ewallet` | ✅ Penuh | Input manual |
| QRIS | `qris` | ✅ Penuh | `payment_reference` disimpan lokal |
| Saldo Member | `saldo` | ⚠️ Terbatas | Validasi saldo setelah sync |
| Mayar | `mayar` | ❌ Butuh online | Gateway verification required |
| Midtrans | `midtrans` | ❌ Butuh online | Payment gateway |
| Tripay | `tripay` | ❌ Butuh online | Payment gateway |
| Stripe | `stripe` | ❌ Butuh online | Payment gateway |
| PayPal | `paypal` | ❌ Butuh online | Payment gateway |

### Inventory Offline

Operasi inventory yang didukung dalam mode offline melibatkan beberapa entitas:

| Kemampuan | Entitas Terkait | Status Offline | Setelah Sync |
| - | - | - | - |
| Stock adjustment | `StockMovement` (type: `adjustment`) | ✅ | Movement tercatat di server |
| Stock opname input | `StockOpname` | ✅ | Hasil opname terkirim |
| Transfer stok | `StockTransfer` | ✅ | Movement terkirim, status `in_transit` |
| Terima barang | `StockMovement` (type: `in`) | ✅ | Movement tercatat |
| Barcode scan | `CompanyPOSProduct` | ✅ | Lookup produk lokal |
| Lihat stok real-time | `Inventory` | ⚠️ | Data terakhir sync (bisa stale) |
| Catat barang rusak | `StockMovement` (type: `damaged`) | ✅ | Movement tercatat |
| Hold stok untuk QC | `StockMovement` (type: `hold`) | ✅ | Informasional, tidak ubah on-hand |
| Release hold | `StockMovement` (type: `hold_release`) | ✅ | Informasional |

***

## Entity Relationship Diagram

Berikut adalah diagram relasi entitas yang terlibat dalam sistem Offline Mode. Setiap entitas memiliki peran penting dalam memastikan data tetap konsisten antara mode offline dan sinkronisasi ke server.

```mermaid theme={null}
erDiagram
    CompanyPOSTransaction {
        string company_id PK
        string location_id FK
        string transaction_number UK
        string invoice_number
        datetime transaction_date
        array items
        number subtotal
        number discount_amount
        number discount_percentage
        number tax_amount
        number total
        string payment_method
        number payment_amount
        array payments
        string payment_status
        number paid_amount
        number remaining_amount
        number excess_amount
        number change_amount
        string payment_reference
        string payment_verified_by
        datetime payment_verified_at
        string customer_id FK
        string customer_name
        string customer_phone
        string customer_address
        number points_earned
        number points_used
        string cashier_id FK
        string cashier_name
        string assigned_to_id
        string assigned_to_name
        string notes
        string source
        string order_status
        string status
        string tracking_number
        string shipping_address
        string sales_channel
        object metadata
        datetime verified_at
        string verified_by
        datetime processed_at
        datetime shipped_at
        datetime delivered_at
        datetime cancelled_at
    }

    Inventory {
        string company_id PK
        string product_id FK
        string product_name
        string product_sku
        string category_key
        string category_name
        string variant_key
        string variant_label
        number size_grams
        string base_product_key
        string location_id FK
        string location_name
        string variant_id
        string variant_name
        object variant_attributes
        number quantity
        number reserved_quantity
        number available_quantity
        number in_transit_quantity
        number blocked_quantity
        number min_stock
        number max_stock
        number reorder_point
        number reorder_quantity
        boolean auto_reorder
        number unit_cost
        number total_value
        datetime last_stock_in
        string last_production_release_lot_id
        datetime last_stock_out
        string stock_status
        object channel_allocations
        object geographic_zones
        boolean alert_sent
        string notes
        string rack
        object metadata
    }

    StockMovement {
        string company_id PK
        string inventory_id FK
        string product_id FK
        string product_sku
        string product_type
        string identity_status
        string source_material_id
        string product_name
        string category_key
        string category_name
        string variant_key
        string variant_name
        string base_product_key
        string variant_label
        number size_grams
        string location_id FK
        string location_name
        string movement_type
        number quantity
        number stock_before
        number stock_after
        string reference_type
        string lot_id FK
        string lot_number
        string reference_id
        string from_location_id FK
        string to_location_id FK
        string reason
        string override_reason
        string allocation_strategy
        number unit_cost
        number total_value
        string sales_channel
        string channel_price_key
        number unit_price
        number total_revenue
        number total_cost
        number profit
        number profit_margin
        string performed_by
        string performed_by_name
        string notes
        string idempotency_key
        string created_by
        object metadata
    }

    CompanyPOSProduct {
        string company_id PK
        string name
        string sku UK
        string category
        string category_key
        string category_name
        string variant_key
        string variant_name
        string variant_label
        number size_grams
        string base_product_key
        string description
        number price
        number cost
        number stock
        number min_stock
        string image_url
        array gallery
        array variants
        boolean is_active
        datetime deactivated_at
        string deactivated_by
        number tax_rate
        string supplier
        string unit
        string variant
        string barcode
        string product_type
        string packaging_type
        number net_weight_grams
        number gross_weight_grams
        string storage_condition
        boolean is_bundle
        array bundle_components
        object channel_pricing
        boolean is_locked
        datetime locked_at
        string locked_by
        string lock_reason
        number sold_count
    }

    StockOpname {
        string company_id PK
        string warehouse_id FK
        string warehouse_name
        string opname_number UK
        date opname_date
        string status
        string conducted_by_user_id FK
        string conducted_by_name
        datetime cutoff_at
        string ledger_version
        string approved_by
        datetime approved_at
        string rejected_by
        datetime rejected_at
        string rejection_reason
        array items
        number total_items
        number total_variance_items
        number total_variance_value
        boolean adjustment_posted
        array finance_record_ids
        string notes
    }

    StockTransfer {
        string company_id PK
        string transfer_number UK
        string from_location_id FK
        string from_location_name
        string to_location_id FK
        string to_location_name
        string product_id FK
        string product_name
        string product_sku
        string category_key
        string category_name
        string variant_key
        string variant_name
        string variant_label
        number size_grams
        string base_product_key
        string lot_id FK
        string lot_number
        number quantity_sent
        number quantity_received
        number discrepancy_quantity
        string discrepancy_reason
        string transfer_mode
        string status
        number unit_cost
        number total_value
        string transfer_date
        datetime shipped_at
        string shipped_by
        datetime received_at
        string received_by
        datetime cancelled_at
        string cancelled_by
        string cancellation_reason
        string reason
        string notes
        string idempotency_key
        object metadata
        string cost_status
    }

    StockAlert {
        string company_id PK
        string inventory_id FK
        string product_id FK
        string product_name
        string location_id FK
        string location_name
        string alert_type
        number current_quantity
        number threshold_quantity
        string severity
        string status
        string title
        string description
        string recommended_action
        string fingerprint
        string source
        string entity_ref_id
        array movement_ids
        datetime detected_at
        string acknowledged_by
        datetime acknowledged_at
        datetime resolved_at
        boolean auto_reorder_triggered
        string notes
    }

    AuditLog {
        string company_id PK
        string user_id FK
        string user_email
        string action
        string entity_type
        string entity_id
        object old_value
        object new_value
        datetime timestamp
        string ip_address
        string device_info
        string description
        string status
    }

    WarehouseLocation {
        string company_id PK
        string location_name
        string location_code UK
        string location_type
        string description
        string address
        string city
        string manager_name
        string manager_contact
        number capacity
        number current_utilization
        boolean is_active
        object coordinates
    }

    POSProduct {
        string name
        string sku UK
        string category
        string description
        number price
        number cost
        number stock
        number min_stock
        string image_url
        array variants
        boolean is_active
        number tax_rate
        string supplier
        string unit
        string digital_product_id
        boolean is_synced_from_digital
        datetime last_synced
    }

    CompanyPOSTransaction ||--o{ StockMovement : "generates via sale"
    CompanyPOSTransaction }o--|| WarehouseLocation : "located at"
    CompanyPOSTransaction }o--|| CompanyPOSProduct : "contains items"
    Inventory }o--|| CompanyPOSProduct : "tracks product"
    Inventory }o--|| WarehouseLocation : "stored at"
    Inventory ||--o{ StockMovement : "has movements"
    Inventory ||--o{ StockAlert : "triggers alerts"
    StockMovement }o--|| WarehouseLocation : "from/to location"
    StockOpname }o--|| WarehouseLocation : "conducted at"
    StockOpname ||--o{ StockMovement : "posts adjustments"
    StockTransfer }o--|| WarehouseLocation : "between locations"
    StockTransfer }o--|| CompanyPOSProduct : "transfers product"
    StockTransfer ||--o{ StockMovement : "generates movements"
    CompanyPOSProduct }o--|| POSProduct : "synced from"
    AuditLog }o--|| CompanyPOSTransaction : "logs actions"
    AuditLog }o--|| StockMovement : "logs actions"
    AuditLog }o--|| Inventory : "logs actions"
```

***

## Entity Schema Reference

### CompanyPOSTransaction

Entitas utama untuk transaksi POS yang dapat dibuat saat mode offline dan di-sinkronisasi ketika koneksi tersedia.

| Field | Type | Required | Description |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan pemilik transaksi |
| `location_id` | string | Tidak | ID lokasi gudang atau toko tempat transaksi terjadi |
| `location_name` | string | Tidak | Nama lokasi gudang atau toko |
| `transaction_number` | string | Ya | Nomor transaksi unik sebagai identifier bisnis |
| `invoice_number` | string | Tidak | Nomor invoice yang dicetak untuk pelanggan |
| `transaction_date` | datetime | Tidak | Tanggal dan waktu transaksi dilakukan |
| `items` | array | Ya | Daftar produk yang dibeli dalam transaksi |
| `subtotal` | number | Tidak | Subtotal total sebelum diskon dan pajak diterapkan |
| `discount_amount` | number | Tidak | Nominal diskon dalam rupiah (default: 0) |
| `discount_percentage` | number | Tidak | Persentase diskon yang diterapkan (default: 0) |
| `tax_amount` | number | Tidak | PPN atas transaksi; mode inclusive sudah termasuk, exclusive tambahan |
| `total` | number | Ya | Total akhir transaksi yang harus dibayar |
| `total_amount` | number | Tidak | Total akhir (alias dari total) |
| `payment_method` | string | Tidak | Metode pembayaran: cash, card, transfer, ewallet, qris, saldo, mayar, debt, manual\_transfer, midtrans, tripay, stripe, paypal |
| `payment_amount` | number | Tidak | Jumlah uang yang dibayarkan oleh pelanggan |
| `payments` | array | Tidak | Detail pembayaran multi-tender untuk split payment |
| `payment_status` | string | Tidak | Status pembayaran: pending, pending\_verification, partially\_paid, paid, failed, refunded, rejected |
| `paid_amount` | number | Tidak | Nominal pembayaran aktual yang telah diverifikasi (default: 0) |
| `remaining_amount` | number | Tidak | Sisa kekurangan tagihan yang belum dibayar (default: 0) |
| `excess_amount` | number | Tidak | Kelebihan nominal transfer dari pelanggan (default: 0) |
| `change_amount` | number | Tidak | Kembalian yang diberikan kepada pelanggan (default: 0) |
| `payment_reference` | string | Tidak | Nomor referensi bukti transfer atau QRIS untuk pembayaran non-tunai |
| `payment_verified_by` | string | Tidak | Nama pengguna yang memverifikasi pembayaran non-tunai |
| `payment_verified_at` | datetime | Tidak | Waktu verifikasi pembayaran non-tunai |
| `customer_id` | string | Tidak | ID member atau pelanggan jika terdaftar |
| `customer_name` | string | Tidak | Nama pelanggan yang melakukan transaksi |
| `customer_phone` | string | Tidak | Nomor telepon pelanggan |
| `customer_address` | string | Tidak | Alamat lengkap pelanggan |
| `points_earned` | number | Tidak | Poin loyalitas yang didapat dari transaksi ini (default: 0) |
| `points_used` | number | Tidak | Poin loyalitas yang dipakai untuk membayar (default: 0) |
| `cashier_id` | string | Tidak | ID karyawan kasir yang memproses transaksi |
| `cashier_name` | string | Tidak | Nama kasir yang memproses transaksi |
| `assigned_to_id` | string | Tidak | ID pengguna yang ditugaskan menangani order |
| `assigned_to_name` | string | Tidak | Nama pengguna yang ditugaskan menangani order |
| `notes` | string | Tidak | Catatan tambahan terkait transaksi |
| `source` | string | Tidak | Sumber transaksi: pos, online\_catalog, website, marketplace, landing\_page, whatsapp, reseller, b2b, grab, social\_media |
| `order_status` | string | Tidak | Status pesanan: pending, processing, shipped, delivered, completed, cancelled, rejected |
| `status` | string | Tidak | Status transaksi: pending, processing, shipped, delivered, completed, failed, refunded, cancelled |
| `tracking_number` | string | Tidak | Nomor resi pengiriman untuk order yang dikirim |
| `shipping_address` | string | Tidak | Alamat pengiriman untuk order online |
| `sales_channel` | string | Tidak | Sales channel ternormalisasi: offline\_pos, offline, website, online\_catalog, marketplace, landing\_page, whatsapp, reseller, b2b, grab, social\_media |
| `metadata` | object | Tidak | Data tambahan seperti informasi online order |
| `verified_at` | datetime | Tidak | Waktu transaksi diverifikasi |
| `verified_by` | string | Tidak | Pengguna yang memverifikasi transaksi |
| `processed_at` | datetime | Tidak | Waktu transaksi mulai diproses |
| `shipped_at` | datetime | Tidak | Waktu transaksi dikirim |
| `delivered_at` | datetime | Tidak | Waktu transaksi diterima pelanggan |
| `cancelled_at` | datetime | Tidak | Waktu transaksi dibatalkan |

### Inventory

Entitas inventory yang di-cache secara lokal saat offline dan di-sinkronisasi ketika koneksi tersedia.

| Field | Type | Required | Description |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan pemilik inventory |
| `product_id` | string | Ya | ID produk dari CompanyPOSProduct yang dilacak stoknya |
| `product_name` | string | Tidak | Nama produk untuk tampilan tanpa lookup |
| `product_sku` | string | Tidak | SKU produk untuk pencarian cepat |
| `category_key` | string | Tidak | Kunci kategori canonical produk |
| `category_name` | string | Tidak | Nama label kategori canonical |
| `variant_key` | string | Tidak | Kunci varian canonical produk |
| `variant_label` | string | Tidak | Label gabungan kategori dan varian untuk POS/inventory |
| `size_grams` | number | Tidak | Ukuran canonical produk dalam gram |
| `base_product_key` | string | Tidak | Kunci produk utama sebelum pemecahan kategori/varian |
| `location_id` | string | Ya | ID lokasi gudang atau toko tempat stok disimpan |
| `location_name` | string | Tidak | Nama lokasi gudang atau toko |
| `variant_id` | string | Tidak | ID varian produk jika tersedia |
| `variant_name` | string | Tidak | Nama varian seperti Merah-L, Biru-XL |
| `variant_attributes` | object | Tidak | Atribut varian: color, size, material |
| `quantity` | number | Tidak | Jumlah stok saat ini (default: 0) |
| `reserved_quantity` | number | Tidak | Stok yang sudah direserve untuk order yang belum selesai (default: 0) |
| `available_quantity` | number | Tidak | Stok tersedia: quantity - reserved\_quantity - blocked\_quantity (default: 0) |
| `in_transit_quantity` | number | Tidak | Kuantitas stok yang sedang dalam perjalanan transfer (default: 0) |
| `blocked_quantity` | number | Tidak | Kuantitas stok ditahan untuk QC, karantina, atau recall (default: 0) |
| `min_stock` | number | Tidak | Minimum stok untuk memicu alert (default: 5) |
| `max_stock` | number | Tidak | Maksimum kapasitas stok (default: 1000) |
| `reorder_point` | number | Tidak | Titik reorder otomatis untuk replenishment (default: 10) |
| `reorder_quantity` | number | Tidak | Jumlah reorder saat titik tercapai (default: 50) |
| `auto_reorder` | boolean | Tidak | Auto create purchase order saat low stock (default: false) |
| `unit_cost` | number | Tidak | Harga modal per unit untuk kalkulasi HPP (default: 0) |
| `total_value` | number | Tidak | Total nilai stok: quantity × unit\_cost (default: 0) |
| `last_stock_in` | datetime | Tidak | Waktu terakhir stok masuk ke lokasi ini |
| `last_production_release_lot_id` | string | Tidak | Marker artefak rilis produksi terakhir untuk verifikasi replay idempotent |
| `last_stock_out` | datetime | Tidak | Waktu terakhir stok keluar dari lokasi ini |
| `stock_status` | string | Tidak | Status stok: in\_stock, low\_stock, out\_of\_stock, overstock |
| `channel_allocations` | object | Tidak | Alokasi stok per channel: offline, online, marketplace, b2b |
| `geographic_zones` | object | Tidak | Distribusi geografis: same\_city, other\_cities, export |
| `alert_sent` | boolean | Tidak | Apakah alert stok sudah dikirim (default: false) |
| `notes` | string | Tidak | Catatan tambahan terkait inventory |
| `rack` | string | Tidak | Lokasi rak penyimpanan seperti A1, B2 |
| `metadata` | object | Tidak | Metadata teknis non-otoritatif seperti correlation/idempotency key |

### StockMovement

Entitas pergerakan stok yang dicatat saat offline dan di-sinkronisasi dengan validasi idempotency key.

| Field | Type | Required | Description |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan pemilik pergerakan stok |
| `inventory_id` | string | Ya | ID inventory yang mengalami perubahan stok |
| `product_id` | string | Tidak | ID produk yang bergerak |
| `product_sku` | string | Tidak | SKU produk untuk identifikasi cepat |
| `product_type` | string | Tidak | Tipe produk: finished\_good, raw\_material, dll |
| `identity_status` | string | Tidak | Status identitas: linked atau legacy\_review |
| `source_material_id` | string | Tidak | ID material sumber jika applicable |
| `product_name` | string | Tidak | Nama produk untuk tampilan |
| `category_key` | string | Tidak | Kunci kategori canonical |
| `category_name` | string | Tidak | Nama label kategori |
| `variant_key` | string | Tidak | Kunci varian canonical |
| `variant_name` | string | Tidak | Nama label varian |
| `base_product_key` | string | Tidak | Kunci produk utama |
| `variant_label` | string | Tidak | Label gabungan kategori dan varian |
| `size_grams` | number | Tidak | Ukuran produk dalam gram |
| `location_id` | string | Tidak | ID lokasi gudang tempat pergerakan terjadi |
| `location_name` | string | Tidak | Nama lokasi gudang |
| `movement_type` | string | Ya | Tipe pergerakan: in, out, transfer, adjustment, return, damaged, hold, hold\_release |
| `quantity` | number | Ya | Jumlah perubahan stok |
| `stock_before` | number | Tidak | Jumlah stok sebelum pergerakan |
| `stock_after` | number | Tidak | Jumlah stok setelah pergerakan |
| `reference_type` | string | Tidak | Sumber perubahan: purchase, sale, cashier\_sale, production, raw\_material\_outbound, qc\_release, transfer, adjustment, opname\_adjustment, return, manual, distribution\_shipment, quality\_hold |
| `lot_id` | string | Tidak | ID Lot/Batch terkait untuk traceability |
| `lot_number` | string | Tidak | Nomor label Lot/Batch fisik |
| `reference_id` | string | Tidak | ID transaksi terkait seperti PO, SO, Transfer |
| `from_location_id` | string | Tidak | ID lokasi asal untuk transfer |
| `to_location_id` | string | Tidak | ID lokasi tujuan untuk transfer |
| `reason` | string | Tidak | Alasan perubahan stok |
| `override_reason` | string | Tidak | Alasan jika pilihan lot menyimpang dari FIFO standar |
| `allocation_strategy` | string | Tidak | Strategi alokasi: fifo, fefo, atau manual |
| `unit_cost` | number | Tidak | Harga modal per unit saat transaksi (default: 0) |
| `total_value` | number | Tidak | Nilai modal total: quantity × unit\_cost (default: 0) |
| `sales_channel` | string | Tidak | Nama channel penjualan: Shopee, TikTok, WhatsApp, Indomaret, B2B |
| `channel_price_key` | string | Tidak | Key pricing channel dari CompanyPOSProduct.channel\_pricing |
| `unit_price` | number | Tidak | Harga jual per unit untuk transaksi penjualan (default: 0) |
| `total_revenue` | number | Tidak | Pendapatan penjualan: quantity × unit\_price (default: 0) |
| `total_cost` | number | Tidak | Total modal: quantity × unit\_cost (default: 0) |
| `profit` | number | Tidak | Laba atau rugi: total\_revenue - total\_cost (default: 0) |
| `profit_margin` | number | Tidak | Margin laba persentase: profit / total\_revenue × 100 (default: 0) |
| `performed_by` | string | Tidak | Email user yang melakukan pergerakan |
| `performed_by_name` | string | Tidak | Nama lengkap user yang melakukan pergerakan |
| `notes` | string | Tidak | Catatan tambahan terkait pergerakan |
| `idempotency_key` | string | Tidak | Kunci idempotensi untuk retry operasi yang aman |
| `created_by` | string | Tidak | ID aktor terautentikasi yang membuat record |
| `metadata` | object | Tidak | Metadata korelasi command untuk audit |

### StockOpname

Entitas sesi stock opname yang dapat diinput secara offline dan disubmit setelah sinkronisasi.

| Field | Type | Required | Description |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan pemilik sesi opname |
| `warehouse_id` | string | Tidak | Referensi ke WarehouseLocation tempat opname |
| `warehouse_name` | string | Tidak | Nama gudang tempat opname dilakukan |
| `opname_number` | string | Tidak | Nomor sesi opname yang unik |
| `opname_date` | date | Ya | Tanggal pelaksanaan stock opname |
| `status` | string | Tidak | Status sesi: draft, in\_progress, counting, submitted, pending\_approval, completed, approved, posted, rejected |
| `conducted_by_user_id` | string | Ya | ID pengguna yang melaksanakan opname |
| `conducted_by_name` | string | Tidak | Nama pengguna yang melaksanakan opname |
| `cutoff_at` | datetime | Tidak | Waktu pembekuan baseline stok sistem |
| `ledger_version` | string | Tidak | Penanda versi snapshot buku besar saat cutoff |
| `approved_by` | string | Tidak | Email atau ID approver yang menyetujui |
| `approved_at` | datetime | Tidak | Waktu persetujuan diberikan |
| `rejected_by` | string | Tidak | Email atau ID approver yang menolak |
| `rejected_at` | datetime | Tidak | Waktu penolakan diberikan |
| `rejection_reason` | string | Tidak | Alasan penolakan sesi stock opname |
| `items` | array | Tidak | Daftar item yang diopname dengan system\_quantity vs physical\_quantity |
| `total_items` | number | Tidak | Total item yang dihitung (default: 0) |
| `total_variance_items` | number | Tidak | Total item yang memiliki selisih (default: 0) |
| `total_variance_value` | number | Tidak | Total nilai rupiah selisih HPP (default: 0) |
| `adjustment_posted` | boolean | Tidak | Apakah penyesuaian sudah diposting ke inventory (default: false) |
| `finance_record_ids` | array | Tidak | ID FinancialRecord hasil posting selisih opname ke modul keuangan |
| `notes` | string | Tidak | Catatan tambahan sesi opname |

### StockTransfer

Entitas transfer stok antar lokasi yang dapat dibuat saat offline.

| Field | Type | Required | Description |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan pemilik transfer |
| `transfer_number` | string | Tidak | Nomor dokumen transfer unik (TRF-YYYYMMDD-XXXX) |
| `from_location_id` | string | Ya | ID lokasi gudang atau toko asal |
| `from_location_name` | string | Tidak | Nama lokasi asal |
| `to_location_id` | string | Ya | ID lokasi gudang atau toko tujuan |
| `to_location_name` | string | Tidak | Nama lokasi tujuan |
| `product_id` | string | Ya | ID produk CompanyPOSProduct yang ditransfer |
| `product_name` | string | Tidak | Nama produk yang ditransfer |
| `product_sku` | string | Tidak | SKU produk |
| `category_key` | string | Tidak | Kunci kategori canonical |
| `category_name` | string | Tidak | Nama label kategori |
| `variant_key` | string | Tidak | Kunci varian canonical |
| `variant_name` | string | Tidak | Nama label varian |
| `variant_label` | string | Tidak | Label gabungan kategori dan varian |
| `size_grams` | number | Tidak | Ukuran produk dalam gram |
| `base_product_key` | string | Tidak | Kunci produk utama |
| `lot_id` | string | Tidak | ID lot batch asal untuk traceability |
| `lot_number` | string | Tidak | Nomor label lot batch fisik |
| `quantity_sent` | number | Ya | Kuantitas yang dikirim dari lokasi asal |
| `quantity_received` | number | Tidak | Kuantitas riil yang diterima di lokasi tujuan (default: 0) |
| `discrepancy_quantity` | number | Tidak | Selisih kuantitas: quantity\_sent - quantity\_received (default: 0) |
| `discrepancy_reason` | string | Tidak | Alasan selisih jika barang rusak atau hilang di jalan |
| `transfer_mode` | string | Tidak | Mode transfer: in\_transit (2-tahap) atau direct (instan) |
| `status` | string | Tidak | Status siklus hidup: draft, in\_transit, completed, cancelled |
| `unit_cost` | number | Tidak | Harga modal atau HPP per unit produk (default: 0) |
| `total_value` | number | Tidak | Nilai total transfer: quantity\_sent × unit\_cost (default: 0) |
| `transfer_date` | string | Tidak | Tanggal transfer dalam format YYYY-MM-DD |
| `shipped_at` | datetime | Tidak | Waktu pengiriman berangkat |
| `shipped_by` | string | Tidak | Email atau identitas petugas yang mengirim |
| `received_at` | datetime | Tidak | Waktu barang diterima di tujuan |
| `received_by` | string | Tidak | Email atau identitas petugas yang menerima |
| `cancelled_at` | datetime | Tidak | Waktu transfer dibatalkan |
| `cancelled_by` | string | Tidak | Pengguna yang membatalkan transfer |
| `cancellation_reason` | string | Tidak | Alasan pembatalan transfer |
| `reason` | string | Tidak | Alasan pengiriman seperti restock atau pemerataan outlet |
| `notes` | string | Tidak | Catatan tambahan transfer |
| `idempotency_key` | string | Tidak | Kunci idempotensi operasi |
| `metadata` | object | Tidak | Metadata teknis audit dan korelasi |
| `cost_status` | string | Tidak | Status kelengkapan biaya: complete, provisional\_zero\_cost, unassigned |

### StockAlert

Entitas alert stok yang dapat terdeteksi saat offline dan di-sinkronisasi bersama data lainnya.

| Field | Type | Required | Description |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan pemilik alert |
| `inventory_id` | string | Tidak | ID inventory yang memicu alert |
| `product_id` | string | Tidak | ID produk yang terdampak |
| `product_name` | string | Tidak | Nama produk untuk tampilan |
| `location_id` | string | Tidak | ID lokasi gudang terdampak |
| `location_name` | string | Tidak | Nama lokasi gudang |
| `alert_type` | string | Ya | Tipe alert: low\_stock, out\_of\_stock, overstock, expiring\_soon, reconciliation\_needed, negative\_stock, rapid\_depletion, orphaned\_movement, duplicate\_movement, futuristic\_date, lot\_negative |
| `current_quantity` | number | Tidak | Kuantitas stok saat ini ketika alert terpicu |
| `threshold_quantity` | number | Tidak | Kuantitas ambang batas yang memicu alert |
| `severity` | string | Tidak | Tingkat keparahan: low, medium, high, critical (default: medium) |
| `status` | string | Tidak | Status alert: active, acknowledged, dismissed, resolved |
| `title` | string | Tidak | Ringkasan alert untuk tampilan daftar |
| `description` | string | Tidak | Penjelasan detail anomali atau kondisi |
| `recommended_action` | string | Tidak | Tindakan yang disarankan oleh sistem |
| `fingerprint` | string | Tidak | Key stabil per anomali untuk mencegah notifikasi duplikat antar perangkat |
| `source` | string | Tidak | Asal deteksi alert, misal stock\_anomaly\_detector |
| `entity_ref_id` | string | Tidak | ID entitas terkait seperti LotBatch atau StockMovement |
| `movement_ids` | array | Tidak | ID StockMovement yang memicu anomali |
| `detected_at` | datetime | Tidak | Waktu anomali terdeteksi oleh scanner |
| `acknowledged_by` | string | Tidak | Pengguna yang mengakui alert |
| `acknowledged_at` | datetime | Tidak | Waktu alert diakui |
| `resolved_at` | datetime | Tidak | Waktu alert diselesaikan |
| `auto_reorder_triggered` | boolean | Tidak | Apakah reorder otomatis sudah dipicu (default: false) |
| `notes` | string | Tidak | Catatan tambahan terkait alert |

### AuditLog

Entitas audit trail yang mencatat setiap operasi sinkronisasi dan perubahan data.

| Field | Type | Required | Description |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan tempat aksi dilakukan |
| `user_id` | string | Ya | ID pengguna yang melakukan aksi |
| `user_email` | string | Tidak | Email pengguna untuk identifikasi |
| `action` | string | Ya | Aksi yang dilakukan: create, read, update, delete, approve, reject, lock, unlock, unlock\_request |
| `entity_type` | string | Ya | Tipe entitas yang diakses seperti CompanyPOSTransaction, Inventory |
| `entity_id` | string | Tidak | ID entitas spesifik yang diakses |
| `old_value` | object | Tidak | Nilai lama sebelum perubahan (untuk update) |
| `new_value` | object | Tidak | Nilai baru setelah perubahan (untuk update) |
| `timestamp` | datetime | Ya | Waktu aksi dilakukan |
| `ip_address` | string | Tidak | Alamat IP dari mana aksi dilakukan |
| `device_info` | string | Tidak | User Agent atau informasi perangkat |
| `description` | string | Tidak | Deskripsi detail aksi yang dilakukan |
| `status` | string | Tidak | Status eksekusi: success atau failed (default: success) |

### WarehouseLocation

Entitas lokasi gudang yang di-cache secara lokal untuk referensi saat offline.

| Field | Type | Required | Description |
| - | - | - | - |
| `company_id` | string | Ya | ID perusahaan pemilik lokasi |
| `location_name` | string | Ya | Nama lokasi seperti Gudang Utama atau Toko Cabang A |
| `location_code` | string | Ya | Kode unik lokasi untuk identifikasi |
| `location_type` | string | Tidak | Tipe lokasi: warehouse, store, transit, virtual (default: warehouse) |
| `description` | string | Tidak | Penjelasan mengenai lokasi gudang, kapasitas, dan jenis barang |
| `address` | string | Tidak | Alamat lengkap lokasi |
| `city` | string | Tidak | Kota lokasi gudang |
| `manager_name` | string | Tidak | Nama penanggung jawab lokasi |
| `manager_contact` | string | Tidak | Kontak penanggung jawab lokasi |
| `capacity` | number | Tidak | Kapasitas maksimal dalam unit atau meter persegi |
| `current_utilization` | number | Tidak | Utilisasi saat ini dalam persentase (default: 0) |
| `is_active` | boolean | Tidak | Status keaktifan lokasi (default: true) |
| `coordinates` | object | Tidak | Koordinat GPS: latitude dan longitude |

### POSProduct

Entitas katalog produk global yang di-sinkronisasi ke cache lokal untuk akses offline.

| Field | Type | Required | Description |
| - | - | - | - |
| `name` | string | Ya | Nama produk |
| `sku` | string | Tidak | SKU atau Barcode produk |
| `category` | string | Tidak | Kategori produk |
| `description` | string | Tidak | Deskripsi lengkap produk |
| `price` | number | Ya | Harga jual produk |
| `cost` | number | Tidak | Harga modal atau beli produk |
| `stock` | number | Tidak | Stok saat ini (default: 0) |
| `min_stock` | number | Tidak | Minimum stok untuk alert (default: 5) |
| `image_url` | string | Tidak | URL gambar utama produk |
| `variants` | array | Tidak | Daftar varian produk: nama, sku, price, stock |
| `is_active` | boolean | Tidak | Status keaktifan produk (default: true) |
| `tax_rate` | number | Tidak | Persentase pajak 0-100 (default: 0) |
| `supplier` | string | Tidak | Nama supplier produk |
| `unit` | string | Tidak | Satuan produk (default: pcs) |
| `digital_product_id` | string | Tidak | ID dari DigitalProduct jika sinkronisasi |
| `is_synced_from_digital` | boolean | Tidak | Apakah produk disinkronkan dari DigitalProduct (default: false) |
| `last_synced` | datetime | Tidak | Waktu terakhir sinkronisasi dilakukan |

***

## Sinkronisasi Status Lifecycle

Diagram state berikut menggambarkan siklus hidup status sinkronisasi data dari pembuatan di mode offline hingga berhasil tersinkronisasi atau gagal.

```mermaid theme={null}
stateDiagram-v2
    [*] --> pending: Data dibuat saat offline
    pending --> syncing: Sync engine mulai mengirim
    syncing --> synced: Server konfirmasi diterima
    syncing --> conflict: Deteksi konflik data
    syncing --> failed: Server menolak atau timeout
    
    conflict --> manual_review: Memerlukan intervensi user
    manual_review --> syncing: User pilih resolusi, retry sync
    manual_review --> discarded: User buang perubahan lokal
    
    failed --> pending: Retry otomatis dijadwalkan
    failed --> discarded: Max retries tercapai, data dibuang
    failed --> manual_review: Gagal berulang, perlu review
    
    synced --> [*]: Data tersinkronisasi sempurna
    
    note right of pending
        Antrian prioritas:
        1. High — Transaksi POS
        2. Normal — Stock movement
        3. Low — Catatan, preferensi
    end note
    
    note right of conflict
        Strategi resolusi:
        - Server wins (stok, harga)
        - Client wins (catatan, draft)
        - Merge (item independen)
        - Manual review (kompleks)
    end note
    
    note right of failed
        Retry policy:
        - Max 5 percobaan
        - Exponential backoff
        - Prioritas tinggi dicoba duluan
    end note
```

### Status Sync Queue — Detail Transisi

| Status Asal | Status Tujuan | Pemicu | Deskripsi |
| - | - | - | - |
| `[*]` | `pending` | User membuat data baru saat offline | Data masuk ke antrian sinkronisasi lokal |
| `pending` | `syncing` | Connection detector mendeteksi online | Sync engine mulai memproses antrian |
| `syncing` | `synced` | Server mengembalikan 2xx | Data berhasil diterima dan divalidasi server |
| `syncing` | `conflict` | Server mengembalikan 409 Conflict | Data bentrok dengan versi di server |
| `syncing` | `failed` | Timeout atau error jaringan | Pengiriman gagal, akan di-retry |
| `conflict` | `manual_review` | Konflik memerlukan keputusan user | Ditampilkan di panel konflik untuk resolusi |
| `conflict` | `syncing` | Auto-resolve berhasil | Sistem otomatis memilih resolusi berdasarkan strategi |
| `manual_review` | `syncing` | User memilih resolusi | Data dikirim ulang dengan resolusi yang dipilih |
| `manual_review` | `discarded` | User memutuskan buang perubahan | Data lokal dihapus dari antrian |
| `failed` | `pending` | Retry timer habis | Antrian dijadwalkan ulang untuk percobaan berikutnya |
| `failed` | `discarded` | Max retries (5x) tercapai | Data ditandai gagal permanen |
| `failed` | `manual_review` | Gagal berulang tanpa perbaikan | Diperlukan investigasi manual oleh admin |

***

## Sequence Diagram — Flow Data Offline

### 1. Pembuatan Transaksi POS Offline

Diagram ini menggambarkan flow lengkap saat kasir membuat transaksi POS tanpa koneksi internet.

```mermaid theme={null}
sequenceDiagram
    participant Kasir as Kasir (Browser/PWA)
    participant IDB as IndexedDB (Local Store)
    participant SQ as Sync Queue
    participant CD as Connection Detector
    participant SE as Sync Engine
    participant API as Server API
    participant DB as Central Database

    Note over Kasir,DB: Status: OFFLINE — Tidak ada koneksi internet

    Kasir->>Kasir: Scan barcode produk
    Kasir->>IDB: Lookup produk di Product Store lokal
    IDB-->>Kasir: Return data produk & harga lokal
    
    Kasir->>Kasir: Tambah item ke keranjang
    Kasir->>Kasir: Hitung total, diskon, pajak
    Kasir->>Kasir: Pilih metode pembayaran (cash)
    
    Kasir->>IDB: Simpan transaksi ke Transaction Store
    IDB-->>Kasir: Konfirmasi penyimpanan lokal
    
    Kasir->>SQ: Enqueue transaksi ke sync queue
    Note over SQ: Priority: HIGH<br/>Entity: CompanyPOSTransaction<br/>Status: pending
    
    SQ-->>Kasir: Tampilkan badge "1 item pending"
    
    CD->>CD: Heartbeat ping gagal (offline)
    CD-->>Kasir: Update indikator: Offline (merah)
    
    Note over Kasir,DB: Koneksi TIDAK tersedia — data aman di IndexedDB
```

### 2. Proses Sinkronisasi Saat Koneksi Kembali

Diagram ini menggambarkan flow saat koneksi internet kembali dan sync engine memproses antrian.

```mermaid theme={null}
sequenceDiagram
    participant CD as Connection Detector
    participant SE as Sync Engine
    participant SQ as Sync Queue
    participant IDB as IndexedDB (Local Store)
    participant API as Server API
    participant DB as Central Database
    participant Kasir as Kasir (Browser/PWA)

    Note over CD,DB: Koneksi internet kembali

    CD->>API: Heartbeat ping ke /health
    API-->>CD: Response 200 OK
    CD-->>SE: Event: "online" terdeteksi
    CD-->>Kasir: Update indikator: Syncing (biru)
    
    SE->>SQ: Ambil item dengan status pending
    Note over SQ: Urutan: HIGH → NORMAL → LOW
    
    SQ-->>SE: Return daftar item pending (sorted by priority)
    
    loop Untuk setiap item di queue
        SE->>SE: Set status item → "syncing"
        SE->>API: POST payload ke endpoint terkait
        API->>DB: Validasi & simpan ke database
        DB-->>API: Konfirmasi penyimpanan
        
        alt Sukses (2xx)
            API-->>SE: Response sukses + server ID
            SE->>SQ: Update status → "synced"
            SE->>IDB: Update local record dengan server ID
        else Konflik (409)
            API-->>SE: Response conflict + server version
            SE->>SQ: Update status → "conflict"
            SE->>SE: Jalankan conflict resolution strategy
        else Gagal (5xx / timeout)
            API-->>SE: Error response
            SE->>SQ: Increment retry_count
            SE->>SQ: Update status → "failed" atau kembali "pending"
        end
    end
    
    SE-->>Kasir: Update indikator: Online + Synced (hijau)
    SE-->>Kasir: Notifikasi "Semua data tersinkronisasi"
```

### 3. Penanganan Konflik Data (Conflict Resolution)

Diagram ini menggambarkan flow saat terjadi konflik data antara versi offline dan server.

```mermaid theme={null}
sequenceDiagram
    participant SE as Sync Engine
    participant CR as Conflict Resolver
    participant API as Server API
    participant IDB as IndexedDB (Local Store)
    participant SQ as Sync Queue
    participant Admin as Admin / Kasir

    SE->>API: POST CompanyPOSTransaction
    API-->>SE: 409 Conflict — transaction_number sudah ada
    
    SE->>CR: Forward konflik ke resolver
    CR->>CR: Identifikasi jenis data
    
    alt Stok atau Harga — Server Wins
        CR->>CR: Ambil versi server sebagai sumber kebenaran
        CR->>IDB: Replace local data dengan server version
        CR->>SQ: Update status → "synced" (resolved)
        CR-->>SE: Resolusi: server_wins
    else Catatan atau Draft — Client Wins
        CR->>API: PUT overwrite dengan data lokal
        API-->>CR: Konfirmasi overwrite
        CR->>SQ: Update status → "synced"
        CR-->>SE: Resolusi: client_wins
    else Transaksi dengan item independen — Merge
        CR->>CR: Bandingkan item per item
        CR->>CR: Gabungkan item yang tidak bentrok
        CR->>API: PUT merged payload
        API-->>CR: Konfirmasi merge
        CR->>SQ: Update status → "synced"
        CR-->>SE: Resolusi: merged
    else Konflik kompleks — Manual Review
        CR->>SQ: Update status → "manual_review"
        CR-->>Admin: Notifikasi: "Konflik terdeteksi, perlu review"
        
        Admin->>CR: Review kedua versi data
        Admin->>CR: Pilih versi yang benar atau edit manual
        CR->>API: PUT resolusi final
        API-->>CR: Konfirmasi
        CR->>SQ: Update status → "synced"
        CR-->>SE: Resolusi: manual_resolved
    end
    
    SE->>IDB: Update local record sesuai resolusi
    SE-->>Admin: Notifikasi: "Konflik terselesaikan"
```

### 4. Stock Opname Offline — Input dan Sinkronisasi

Diagram ini menggambarkan flow saat staf gudang melakukan stock opname di area tanpa koneksi.

```mermaid theme={null}
sequenceDiagram
    participant Staff as Staff Gudang
    participant PWA as PWA (Offline Mode)
    participant IDB as IndexedDB (Local Store)
    participant SQ as Sync Queue
    participant SE as Sync Engine
    participant API as Server API
    participant DB as Central Database

    Note over Staff,DB: Status: OFFLINE — Gudang tanpa sinyal

    Staff->>PWA: Buka modul Stock Opname
    PWA->>IDB: Load data produk & stok terakhir dari cache
    IDB-->>PWA: Return Inventory + CompanyPOSProduct lokal
    
    PWA-->>Staff: Tampilkan daftar produk untuk dihitung
    
    loop Hitung setiap produk
        Staff->>PWA: Input physical_quantity per produk
        PWA->>PWA: Hitung variance = physical - system_quantity
        PWA->>IDB: Simpan hasil hitung ke local store
    end
    
    Staff->>PWA: Submit sesi opname (status: submitted)
    PWA->>IDB: Simpan StockOpname ke Transaction Store
    PWA->>SQ: Enqueue ke sync queue (priority: NORMAL)
    Note over SQ: Entity: StockOpname<br/>Status: pending<br/>Items: semua hitungan
    
    SQ-->>Staff: Badge: "1 opname pending sync"
    
    Note over Staff,DB: --- Koneksi kembali ---
    
    SE->>SQ: Ambil item pending
    SE->>API: POST StockOpname + items
    API->>DB: Simpan sesi opname
    API->>DB: Hitung total_variance_items & total_variance_value
    DB-->>API: Konfirmasi
    
    API-->>SE: 201 Created
    SE->>SQ: Update status → "synced"
    SE->>IDB: Update local record dengan server ID
    
    SE-->>Staff: Notifikasi: "Stock opname berhasil tersinkronisasi"
    
    Note over API,DB: Server memicu approval workflow jika variance > threshold
```

***

## Referensi Enum & Status

### payment\_method — Metode Pembayaran Transaksi POS

| Nilai | Deskripsi |
| - | - |
| `cash` | Pembayaran tunai langsung di kasir |
| `card` | Pembayaran menggunakan kartu debit atau kredit |
| `transfer` | Transfer bank manual atau otomatis |
| `ewallet` | Pembayaran via dompet elektronik (GoPay, OVO, Dana, dll) |
| `qris` | Pembayaran via QRIS (QR Code Indonesian Standard) |
| `saldo` | Pembayaran menggunakan saldo akun pelanggan |
| `mayar` | Pembayaran via gateway Mayar |
| `debt` | Transaksi hutang atau tempo |
| `manual_transfer` | Transfer bank manual yang perlu verifikasi |
| `midtrans` | Pembayaran via gateway Midtrans |
| `tripay` | Pembayaran via gateway Tripay |
| `stripe` | Pembayaran via gateway Stripe |
| `paypal` | Pembayaran via PayPal |

### payment\_status — Status Pembayaran

| Nilai | Deskripsi |
| - | - |
| `pending` | Pembayaran belum diterima atau dikonfirmasi |
| `pending_verification` | Pembayaran diterima, menunggu verifikasi manual |
| `partially_paid` | Pembayaran baru diterima sebagian dari total tagihan |
| `paid` | Pembayaran lunas dan terverifikasi penuh |
| `failed` | Pembayaran gagal diproses |
| `refunded` | Pembayaran sudah dikembalikan ke pelanggan |
| `rejected` | Pembayaran ditolak setelah review |

### order\_status — Status Pesanan

| Nilai | Deskripsi |
| - | - |
| `pending` | Pesanan baru diterima, belum diproses |
| `processing` | Pesanan sedang diproses atau dikemas |
| `shipped` | Pesanan sudah dikirim ke kurir |
| `delivered` | Pesanan sudah sampai ke pelanggan |
| `completed` | Pesanan selesai dan dikonfirmasi pelanggan |
| `cancelled` | Pesanan dibatalkan oleh pelanggan atau sistem |
| `rejected` | Pesanan ditolak oleh merchant |

### status (CompanyPOSTransaction) — Status Transaksi

| Nilai | Deskripsi |
| - | - |
| `pending` | Transaksi baru dibuat, menunggu pemrosesan |
| `processing` | Transaksi sedang diproses lebih lanjut |
| `shipped` | Barang terkait transaksi sudah dikirim |
| `delivered` | Barang sudah diterima pelanggan |
| `completed` | Transaksi selesai sepenuhnya |
| `failed` | Transaksi gagal diproses |
| `refunded` | Dana transaksi sudah dikembalikan |
| `cancelled` | Transaksi dibatalkan |

### sales\_channel — Channel Penjualan

| Nilai | Deskripsi |
| - | - |
| `offline_pos` | Transaksi langsung di kasir menggunakan POS |
| `offline` | Transaksi offline non-POS |
| `website` | Pesanan dari website resmi |
| `online_catalog` | Pesanan dari katalog online |
| `marketplace` | Pesanan dari marketplace (Tokopedia, Shopee, dll) |
| `landing_page` | Pesanan dari landing page kampanye |
| `whatsapp` | Pesanan melalui WhatsApp |
| `reseller` | Pesanan dari channel reseller |
| `b2b` | Pesanan bisnis ke bisnis |
| `grab` | Pesanan dari platform Grab |
| `social_media` | Pesanan dari media sosial |

### source — Sumber Transaksi (Legacy)

| Nilai | Deskripsi |
| - | - |
| `pos` | Transaksi dari sistem POS |
| `online_catalog` | Transaksi dari katalog online |
| `website` | Transaksi dari website |
| `marketplace` | Transaksi dari marketplace |
| `landing_page` | Transaksi dari landing page |
| `whatsapp` | Transaksi dari WhatsApp |
| `reseller` | Transaksi dari reseller |
| `b2b` | Transaksi B2B |
| `grab` | Transaksi dari Grab |
| `social_media` | Transaksi dari media sosial |

### movement\_type — Tipe Pergerakan Stok

| Nilai | Deskripsi |
| - | - |
| `in` | Stok masuk ke lokasi (dari pembelian, produksi, dll) |
| `out` | Stok keluar dari lokasi (penjualan, produksi, dll) |
| `transfer` | Perpindahan stok antar lokasi |
| `adjustment` | Penyesuaian stok manual atau dari stock opname |
| `return` | Pengembalian barang ke stok |
| `damaged` | Stok rusak dan dikeluarkan dari inventaris |
| `hold` | Stok ditahan untuk QC atau karantina (tidak mengubah on-hand) |
| `hold_release` | Stok dilepaskan dari status hold (tidak mengubah on-hand) |

### reference\_type — Sumber Perubahan Stok

| Nilai | Deskripsi |
| - | - |
| `purchase` | Pergerakan dari pembelian barang |
| `sale` | Pergerakan dari penjualan umum |
| `cashier_sale` | Pergerakan dari transaksi kasir POS |
| `production` | Pergerakan dari hasil produksi |
| `raw_material_outbound` | Pengeluaran bahan baku untuk produksi |
| `qc_release` | Pelepasan stok setelah lolos quality control |
| `transfer` | Pergerakan dari transfer antar lokasi |
| `adjustment` | Penyesuaian stok manual |
| `opname_adjustment` | Penyesuaian dari hasil stock opname |
| `return` | Pergerakan dari pengembalian barang |
| `manual` | Pergerakan manual tanpa referensi transaksi |
| `distribution_shipment` | Pergerakan dari pengiriman distribusi |
| `quality_hold` | Penahanan stok untuk quality check |

### stock\_status — Status Ketersediaan Stok

| Nilai | Deskripsi |
| - | - |
| `in_stock` | Stok tersedia dalam jumlah memadai |
| `low_stock` | Stok mendekati batas minimum, perlu reorder |
| `out_of_stock` | Stok habis, tidak tersedia |
| `overstock` | Stok melebihi kapasitas maksimum |

### status (StockOpname) — Status Sesi Stock Opname

| Nilai | Deskripsi |
| - | - |
| `draft` | Sesi opname baru dibuat, belum dimulai |
| `in_progress` | Sesi opname sedang berjalan |
| `counting` | Proses penghitungan fisik sedang berlangsung |
| `submitted` | Hasil opname sudah disubmit untuk review |
| `pending_approval` | Menunggu persetujuan dari approver |
| `completed` | Sesi opname selesai diproses |
| `approved` | Sesi opname disetujui oleh approver |
| `posted` | Penyesuaian stok sudah diposting ke inventory |
| `rejected` | Sesi opname ditolak oleh approver |

### status (StockTransfer) — Status Transfer Stok

| Nilai | Deskripsi |
| - | - |
| `draft` | Transfer baru dibuat, belum dikirim |
| `in_transit` | Barang sedang dalam perjalanan antar lokasi |
| `completed` | Transfer selesai, barang sudah diterima |
| `cancelled` | Transfer dibatalkan sebelum selesai |

### transfer\_mode — Mode Transfer

| Nilai | Deskripsi |
| - | - |
| `in_transit` | Alur 2-tahap: kirim → dalam perjalanan → terima |
| `direct` | Transfer instan: langsung tercatat di kedua lokasi |

### cost\_status — Status Kelengkapan Biaya Transfer

| Nilai | Deskripsi |
| - | - |
| `complete` | Biaya transfer sudah lengkap dan terverifikasi |
| `provisional_zero_cost` | Biaya sementara di-set nol, perlu konfirmasi |
| `unassigned` | Biaya belum diassign ke transfer ini |

### alert\_type — Tipe Alert Stok

| Nilai | Deskripsi |
| - | - |
| `low_stock` | Stok mendekati batas minimum |
| `out_of_stock` | Stok sudah habis |
| `overstock` | Stok melebihi batas maksimum |
| `expiring_soon` | Produk mendekati tanggal kedaluwarsa |
| `reconciliation_needed` | Diperlukan rekonsiliasi data stok |
| `negative_stock` | Stok bernilai negatif (anomali) |
| `rapid_depletion` | Stok berkurang sangat cepat secara tidak wajar |
| `orphaned_movement` | Pergerakan stok tanpa referensi yang valid |
| `duplicate_movement` | Pergerakan stok terdeteksi duplikat |
| `futuristic_date` | Tanggal pergerakan stok di masa depan (anomali) |
| `lot_negative` | Lot batch menunjukkan kuantitas negatif |

### severity — Tingkat Keparahan Alert

| Nilai | Deskripsi |
| - | - |
| `low` | Keparahan rendah, tidak memerlukan tindakan segera |
| `medium` | Keparahan sedang, perlu diperhatikan dalam waktu dekat |
| `high` | Keparahan tinggi, perlu tindakan segera |
| `critical` | Keparahan kritis, memerlukan tindakan darurat |

### status (StockAlert) — Status Alert

| Nilai | Deskripsi |
| - | - |
| `active` | Alert aktif dan belum ditindaklanjuti |
| `acknowledged` | Alert sudah diakui oleh pengguna |
| `dismissed` | Alert diabaikan karena tidak relevan |
| `resolved` | Alert sudah diselesaikan dan tidak lagi aktif |

### action (AuditLog) — Tipe Aksi Audit

| Nilai | Deskripsi |
| - | - |
| `create` | Pengguna membuat record baru |
| `read` | Pengguna membaca atau mengakses record |
| `update` | Pengguna memperbarui data record |
| `delete` | Pengguna menghapus record |
| `approve` | Pengguna menyetujui approval request |
| `reject` | Pengguna menolak approval request |
| `lock` | Pengguna mengunci record dari perubahan |
| `unlock` | Pengguna membuka kunci record |
| `unlock_request` | Pengguna meminta pembukaan kunci record |

### status (AuditLog) — Status Eksekusi Audit

| Nilai | Deskripsi |
| - | - |
| `success` | Aksi berhasil dieksekusi |
| `failed` | Aksi gagal dieksekusi |

### location\_type — Tipe Lokasi Gudang

| Nilai | Deskripsi |
| - | - |
| `warehouse` | Gudang utama untuk penyimpanan stok |
| `store` | Toko atau outlet penjualan |
| `transit` | Lokasi transit untuk perpindahan barang |
| `virtual` | Lokasi virtual untuk tracking stok non-fisik |

### product\_type — Tipe Produk

| Nilai | Deskripsi |
| - | - |
| `finished_good` | Produk jadi siap dijual |
| `raw_material` | Bahan baku untuk produksi |
| `semi_finished` | Produk setengah jadi |
| `packaging_material` | Material kemasan untuk produk |
| `bundle` | Paket bundle virtual dari beberapa SKU |

### storage\_condition — Kondisi Penyimpanan

| Nilai | Deskripsi |
| - | - |
| `room_temperature` | Penyimpanan pada suhu ruangan |
| `chilled` | Penyimpanan dalam kondisi dingin |
| `frozen` | Penyimpanan dalam kondisi beku |

### identity\_status — Status Identitas Produk

| Nilai | Deskripsi |
| - | - |
| `linked` | Identitas produk terhubung ke referensi canonical |
| `legacy_review` | Identitas produk lama perlu review manual |

### allocation\_strategy — Strategi Alokasi Stok

| Nilai | Deskripsi | |
| - | - | - |
| `fifo` | First In First Out — stok tertua yang keluar duluan | |
| `fefo` | First Expired First Out — stok yang segera kedaluwarsa keluar duluan | |
| `manual` | Alokasi manual dipilih oleh pengguna | . |

### Teknis

* **Gunakan idempotency key** pada setiap operasi stock mutation untuk mencegah duplikasi saat retry.
* **Selalu enqueue StockMovement bersama CompanyPOSTransaction** untuk menjaga konsistensi stok antara client dan server.
* **Prioritaskan transaksi keuangan** (payment\_method: cash/card/ewallet) di atas operasi inventory non-kritis.
* **Implementasikan exponential backoff** pada retry mechanism untuk menghindari membanjiri server saat reconnect.
* **Validasi data lokal sebelum enqueue** — pastikan required fields (`company_id`, `transaction_number`, `items`, `total`) terisi.
* **Gunakan `navigator.storage.persist()`** untuk meminta browser agar tidak meng-evict data IndexedDB secara otomatis.
* **Log semua sync events** untuk debugging dan audit trail — termasuk timestamp, entity type, operation, dan result.

### Rekomendasi Infrastruktur

| Aspek | Rekomendasi | Alasan |
| - | - | - |
| **Heartbeat interval** | 30 detik | Balance antara deteksi cepat dan overhead network |
| **Max retries** | 5 kali | Cukup untuk gangguan sementara, tidak blocking terlalu lama |
| **Retry backoff** | Exponential (1s, 2s, 4s, 8s, 16s) | Hindari server overload saat reconnect |
| **Queue batch size** | 20 items per batch | Balance antara throughput dan memory |
| **IndexedDB versioning** | Semantic versioning | Deteksi schema mismatch client-server |
| **Data retention (local)** | 30 hari setelah sync | Balance antara debuggability dan storage |
| **Conflict window** | Last-write-wins untuk stok | Stok harus konsisten, bukan akurat secara historis |


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