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

# Pos members

<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: "POS Members"
description: "Modul manajemen membership & loyalitas — POSMember entity, 5 skema loyalty (spending/stamp/points/visits/hybrid), auto-upgrade tier, dan integrasi real-time dengan transaksi kasir."
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

# POS Members

<img src="https://mintcdn.com/quinnofspicy/e4f_upKhVWcjsUmM/docs/mintlify/screenshots/pos/pos-members.png?fit=max&auto=format&n=e4f_upKhVWcjsUmM&q=85&s=0150cde7bf822b1c55b82794f1257772" alt="POS Members" width="1920" height="1080" data-path="docs/mintlify/screenshots/pos/pos-members.png" />

**POS Members** (`POSMembers.jsx` — 898 baris) adalah modul manajemen membership dan loyalitas pelanggan yang terintegrasi langsung dengan sistem POS. Modul ini mengelola **POSMember entity** dengan 40+ field, mendukung **5 skema loyalty** (spending, stamp, points, visits, hybrid), **auto-upgrade tier**, dan **real-time poin accumulation** yang langsung diproses saat transaksi kasir berlangsung.

Sistem membership ini dirancang untuk membangun retensi pelanggan melalui program loyalitas multi-level yang fleksibel — dari sekadar stamp card sederhana hingga program hybrid dengan poin + tier + benefit kustom.

## Arsitektur Sistem Membership

```mermaid theme={null}
graph TB
    subgraph "POSMembers.jsx — 898 lines"
        TAB1[Tab: Database<br/>CustomerAudienceList]
        TAB2[Tab: Tier & Levels<br/>MembershipLevelManager]
        TAB3[Tab: Poin & Reward<br/>LoyaltyLedger]
        TAB4[Tab: Import/Export<br/>CSV/VCF]
    end

    subgraph "POSMember Entity — 40+ fields"
        direction TB
        F1[Profile: nama, phone, email, address]
        F2[Loyalty: tier, points, stamps, total_spent]
        F3[Preferences: birthday, preferences, notes]
        F4[Metadata: created_at, last_visit, visit_count]
    end

    subgraph "Loyalty Schemes — 5 types"
        S1[spending<br/>Berdasarkan total belanja]
        S2[stamp<br/>Kumpulkan stamp per transaksi]
        S3[points<br/>Poin per Rupiah]
        S4[visits<br/>Reward berdasarkan frekuensi]
        S5[hybrid<br/>Kombinasi poin + stamp + visit]
    end

    subgraph "Server Functions"
        SRV1[manageCustomerLoyalty]
        SRV2[upgradeMembershipTier]
        SRV3[redeemLoyaltyReward]
    end

    TAB1 --> F1
    TAB2 --> S1 & S2 & S3 & S4 & S5
    TAB3 --> SRV1 & SRV2 & SRV3
    F2 --> S1 & S2 & S3 & S4 & S5
```

## POSMember Entity Schema

| Group | Field | Tipe | Deskripsi |
| - | - | - | - |
| **Profile** | `name` | String | Nama lengkap member |
| | `phone` | String | Nomor telepon (unique) |
| | `email` | String | Email address |
| | `address` | String | Alamat lengkap |
| | `photo_url` | String | URL foto profil |
| **Loyalty** | `tier` | Enum | bronze / silver / gold / platinum |
| | `total_points` | Integer | Poin aktif yang bisa ditukar |
| | `lifetime_points` | Integer | Total poin pernah dikumpulkan |
| | `stamps` | Integer | Stamp terkumpul (skema stamp) |
| | `total_spent` | Decimal | Total akumulasi belanja |
| | `visit_count` | Integer | Jumlah kunjungan |
| **Tier Progress** | `next_tier` | Enum | Tier berikutnya |
| | `tier_progress` | Decimal | Persentase progress ke tier berikut |
| | `spending_to_next_tier` | Decimal | Sisa belanja untuk upgrade |
| **Preferences** | `birthday` | Date | Tanggal ulang tahun |
| | `preferences` | JSON | Preferensi produk/rasa |
| | `notes` | String | Catatan dari staff |
| **Metadata** | `created_at` | Timestamp | Waktu pendaftaran |
| | `last_visit` | Timestamp | Kapan terakhir berkunjung |
| | `last_purchase` | Timestamp | Kapan terakhir bertransaksi |
| | `company_id` | UUID | Scoped ke company (multi-tenant) |

## 5 Skema Loyalty

```mermaid theme={null}
graph LR
    subgraph "spending"
        SP1[Total Belanja] --> SP2{≥ Threshold?}
        SP2 -->|Ya| SP3[Auto Upgrade Tier]
        SP2 -->|Tidak| SP4[Tetap Tier]
    end

    subgraph "stamp"
        ST1[Transaksi] --> ST2[+1 Stamp]
        ST2 --> ST3{≥ 10 Stamps?}
        ST3 -->|Ya| ST4[Free Item]
        ST3 -->|Tidak| ST5[Stamp Bertambah]
    end

    subgraph "points"
        PT1[Belanja Rp X] --> PT2[Poin = X / rate]
        PT2 --> PT3[Accumulate]
        PT3 --> PT4[Redeem untuk reward]
    end

    subgraph "visits"
        VT1[Kunjungan] --> VT2[visit_count++]
        VT2 --> VT3{≥ N visits?}
        VT3 -->|Ya| VT4[Free reward]
        VT3 -->|Tidak| VT5[Continue]
    end

    subgraph "hybrid"
        HY1[Transaksi] --> HY2[Poin + Stamp + Visit]
        HY2 --> HY3[Multilevel reward]
    end
```

| Skema | Cara Kerja | Contoh | Best For |
| - | - | - | - |
| **spending** | Upgrade tier berdasarkan total belanja | Bronze → Silver di Rp 1jt | Retail umum |
| **stamp** | Kumpulkan stamp, tukar untuk reward | 10 stamp = 1 free item | F\&B, coffee shop |
| **points** | Poin per Rupiah, redeem untuk diskon | 1 poin / Rp 10rb | Semua bisnis |
| **visits** | Reward berdasarkan frekuensi kunjungan | Visit ke-5 gratis dessert | Layanan berulang |
| **hybrid** | Kombinasi poin + stamp + visits | Poin + stamp + visit bonus | Enterprise loyalty |

## Sistem Tier

| Tier | Syarat | Benefit | Poin Multiplier |
| - | - | - | - |
| **Bronze** | Default (baru daftar) | Accumulate poin 1x | 1.0x |
| **Silver** | Total belanja ≥ Rp 1.000.000 | Poin 1.2x, diskon 2% | 1.2x |
| **Gold** | Total belanja ≥ Rp 5.000.000 | Poin 1.5x, diskon 5%, priority queue | 1.5x |
| **Platinum** | Total belanja ≥ Rp 15.000.000 | Poin 2x, diskon 10%, exclusive promo, free delivery | 2.0x |

### Auto-Upgrade Flow

```mermaid theme={null}
sequenceDiagram
    participant T as Transaksi POS
    participant L as manageCustomerLoyalty
    participant M as POSMember
    participant N as Notification

    T->>L: Update total_spent += transaction_total
    L->>M: Check: total_spent ≥ tier_threshold?
    alt Total ≥ Next Tier
        L->>M: Upgrade tier (Bronze → Silver)
        L->>M: Reset points/stamps (scheme-dependent)
        L->>N: Send upgrade notification
        N-->>M: "Selamat! Anda naik ke Silver"
    else Total < Next Tier
        L->>M: Update tier_progress percentage
        L->>M: Accumulate points normally
    end
```

## Sistem Poin

### Akumulasi Poin per Channel

| Channel | Poin per Rp 10.000 | Alasan |
| - | - | - |
| **Offline POS** | 1 poin | Standard rate |
| **Website** | 1 poin | Standard rate |
| **WhatsApp** | 1 poin | Standard rate |
| **Marketplace** | 0.5 poin | Commission already deducted |
| **B2B** | 0 poin | Sudah harga grosir |
| **Reseller** | 0 poin | Margin reseller |

### Penukaran Poin

| Reward | Poin Dibutuhkan | Nilai Ekuivalen |
| - | - | - |
| **Diskon Rp 10.000** | 100 poin | Rp 100/poin |
| **Diskon Rp 25.000** | 250 poin | Rp 100/poin |
| **Diskon Rp 50.000** | 500 poin | Rp 100/poin |
| **Produk Gratis (small)** | 300 poin | Value-based |
| **Produk Gratis (medium)** | 500 poin | Value-based |
| **Merchandise** | 1000 poin | Value-based |

## Cara Akses

| Metode | Detail |
| - | - |
| **URL** | `/pos/pos-members` |
| **Sidebar** | Menu **POS** → **Members** |

## Flow Penggunaan

```mermaid theme={null}
flowchart TD
    A[Buka halaman Members] --> B[Lihat daftar member]
    B --> C{Aksi?}
    C -->|Cari| D[Gunakan search bar]
    C -->|Tambah baru| E[Klik Tambah Member]
    C -->|Lihat detail| F[Klik nama member]
    C -->|Tukar poin| G[Buka profil → Redeem]
    C -->|Import| H[Upload CSV/VCF]

    E --> I[Isi nama, telepon, email]
    I --> J[Member terdaftar]

    F --> K[Lihat total poin, tier, riwayat]
    K --> L{Redeem?}
    L -->|Ya| M[Pilih reward]
    L -->|Upgrade?| N[Cek progress ke tier berikut]
    M --> O[Konfirmasi → poin berkurang]
```

## Integrasi dengan Transaksi

```mermaid theme={null}
sequenceDiagram
    participant K as Kasir
    participant POS as CompanyPOSCashier
    participant L as manageCustomerLoyalty
    participant M as POSMember

    K->>POS: Input nomor member / scan kartu
    POS->>M: Lookup by phone/member_id
    M-->>POS: Return: tier, points, stamps, discounts
    POS->>POS: Apply tier discount (2-10%)
    POS->>POS: Calculate points from this transaction
    K->>POS: Proses transaksi → finalize
    POS->>L: Post-transaction loyalty update
    L->>M: Accumulate points += earned
    L->>M: stamps += 1 (if stamp scheme)
    L->>M: total_spent += transaction_total
    L->>M: Check auto-upgrade eligibility
    L-->>POS: Return: new_points, new_tier
    POS-->>K: Tampilkan: "Poin baru: 1.250 (+50)"
```

## CustomerLoyaltyLedger

Setiap transaksi loyalty dicatat di `CustomerLoyaltyLedger` untuk audit trail:

| Field | Tipe | Deskripsi |
| - | - | - |
| `member_id` | UUID | Reference ke POSMember |
| `transaction_type` | Enum | `earn` / `redeem` / `expire` / `adjust` |
| `points_change` | Integer | +50 (earn) atau -100 (redeem) |
| `stamps_change` | Integer | +1 (earn) atau -10 (redeem) |
| `reference_transaction_id` | UUID | Link ke CompanyPOSTransaction |
| `description` | String | "Earned 50 points from INV-20260110-0042" |
| `created_at` | Timestamp | Waktu pencatatan |

## Tips

* **Manfaatkan data member** untuk membuat program promo eksklusif, misalnya diskon khusus untuk tier platinum
* **Ingatkan pelanggan** untuk selalu menyebutkan nomor member atau scan kartu member saat bertransaksi agar poin tercatat
* **Export data member secara berkala** sebagai backup
* **Review tier threshold** secara periodik untuk memastikan program membership tetap menarik dan profitable
* **Gunakan skema stamp** untuk bisnis F\&B — pelanggan lebih termotivasi oleh visual stamp yang terisi
* **Monitor redemption rate** — jika terlalu rendah, pertimbangkan menurunkan threshold reward

***

## Entity Relationship Diagram

```mermaid theme={null}
erDiagram
    CompanyPOSTransaction ||--o| Customer : "customer_id → customer lookup"
    CompanyPOSTransaction ||--o| POSMember : "customer_id → member lookup"
    CompanyPOSTransaction ||--o{ CustomerLoyaltyLedger : "transaction_id → loyalty events"

    Customer ||--o| CustomerMembership : "membership_level_id → level config"
    Customer ||--o{ CustomerLoyaltyLedger : "customer_id → audit trail"
    Customer ||--o{ CompanyPOSTransaction : "customer_id → riwayat transaksi"

    CustomerMembership ||--o{ Customer : "level_key → banyak customer"
    POSMember ||--o{ CustomerLoyaltyLedger : "phone lookup → loyalty events"

    CompanyPOSTransaction {
        string company_id PK
        string transaction_number UK
        string customer_id FK
        string customer_name
        string customer_phone
        number points_earned
        number points_used
        number total
        enum payment_method
        enum sales_channel
        enum status
    }

    Customer {
        string company_id PK
        string name
        string phone
        string email
        enum customer_type
        string membership_level_id FK
        string membership_level_name
        number membership_points
        number lifetime_points
        number stamps
        boolean has_negative_points
        number loyalty_deficit_points
        enum status
        number lifetime_value
        number total_orders
    }

    POSMember {
        string name
        string phone UK
        string email
        enum membership_tier
        number points
        number lifetime_points
        number stamps
        boolean has_negative_points
        number loyalty_deficit_points
        number total_spent
        number visit_count
        number discount_percentage
        boolean is_active
    }

    CustomerMembership {
        string company_id PK
        string level_name
        string level_key UK
        enum scheme_type
        number discount_percentage
        number points_multiplier
        number min_purchase
        number points_threshold
        number stamps_required
        enum reward_type
        number reward_value
        boolean is_active
        number order
    }

    CustomerLoyaltyLedger {
        string company_id PK
        string customer_id FK
        string transaction_id FK
        string idempotency_key UK
        enum event_type
        enum scheme_type
        number points_delta
        number points_before
        number points_after
        number stamps_delta
        number stamps_before
        number stamps_after
        number eligible_amount
        boolean requires_manual_review
    }
```

## Tabel Schema Lengkap

### POSMember

| Field | Tipe | Default | Required | Deskripsi |
| - | - | - | - | - |
| `name` | String | — | ✅ | Nama member |
| `phone` | String | — | ✅ | Nomor telepon (unique identifier) |
| `email` | String | — | — | Alamat email |
| `address` | String | — | — | Alamat lengkap |
| `birth_date` | Date | — | — | Tanggal lahir |
| `membership_tier` | Enum | `regular` | — | Tier keanggotaan: `regular`, `silver`, `gold`, `platinum` |
| `points` | Number | `0` | — | Total poin aktif saat ini |
| `lifetime_points` | Number | `0` | — | Total poin pernah dikumpulkan sepanjang waktu |
| `stamps` | Number | `0` | — | Total stamp yang dimiliki saat ini |
| `has_negative_points` | Boolean | `false` | — | Flag saldo negatif akibat pembatalan/void transaksi |
| `loyalty_deficit_points` | Number | `0` | — | Defisit poin loyalty setelah retur/void, butuh rekonsiliasi manual |
| `loyalty_review_notes` | String | — | — | Catatan review penanganan loyalty deficit |
| `last_tier_upgrade_date` | DateTime | — | — | Waktu perubahan level membership terakhir |
| `last_tier_upgrade_reason` | String | — | — | Alasan perubahan level membership |
| `last_tier_upgrade_by` | String | — | — | Email operator yang mengubah level |
| `total_spent` | Number | `0` | — | Total akumulasi belanja |
| `visit_count` | Number | `0` | — | Jumlah kunjungan/transaksi |
| `last_visit` | DateTime | — | — | Waktu kunjungan terakhir |
| `discount_percentage` | Number | `0` | — | Diskon khusus member (%) |
| `notes` | String | — | — | Catatan dari staff |
| `is_active` | Boolean | `true` | — | Status keaktifan member |

### Customer

| Field | Tipe | Default | Required | Deskripsi |
| - | - | - | - | - |
| `company_id` | String | — | ✅ | ID perusahaan (multi-tenant) |
| `name` | String | — | ✅ | Nama customer |
| `phone` | String | — | ✅ | Nomor telepon |
| `email` | String | — | — | Alamat email |
| `whatsapp_number` | String | — | — | Nomor WhatsApp untuk follow up |
| `company` | String | — | — | Nama perusahaan (untuk B2B) |
| `address` | String | — | — | Alamat lengkap |
| `customer_type` | Enum | `individual` | — | Tipe customer: `individual`, `business`, `retail`, `reseller`, `distributor`, `modern_market` |
| `membership_level_id` | String | — | — | ID level membership (referensi ke CustomerMembership) |
| `membership_level_name` | String | — | — | Nama level membership (denormalized) |
| `membership_since` | Date | — | — | Sejak kapan menjadi member |
| `membership_points` | Number | `0` | — | Total poin member saat ini |
| `lifetime_points` | Number | `0` | — | Total poin sepanjang waktu |
| `stamps` | Number | `0` | — | Total stamp yang dimiliki saat ini |
| `has_negative_points` | Boolean | `false` | — | Flag saldo negatif akibat pembatalan/void |
| `loyalty_deficit_points` | Number | `0` | — | Defisit poin loyalty yang butuh rekonsiliasi manual |
| `loyalty_review_notes` | String | — | — | Catatan review penanganan loyalty deficit |
| `last_tier_upgrade_date` | DateTime | — | — | Waktu perubahan level membership terakhir |
| `last_tier_upgrade_reason` | String | — | — | Alasan perubahan level membership |
| `last_tier_upgrade_by` | String | — | — | Email operator/admin yang mengubah tier |
| `status` | Enum | `lead` | — | Status: `lead`, `prospect`, `customer`, `inactive` |
| `source` | String | — | — | Sumber customer (ads, referral, walk-in, dll) |
| `tags` | Array\[String] | — | — | Tag/label untuk segmentasi |
| `lifetime_value` | Number | `0` | — | Total nilai transaksi sepanjang waktu |
| `total_orders` | Number | `0` | — | Jumlah transaksi |
| `average_order_value` | Number | `0` | — | Rata-rata nilai transaksi |
| `last_purchase_date` | Date | — | — | Tanggal pembelian terakhir |
| `last_contact_date` | Date | — | — | Tanggal terakhir di-follow up |
| `last_contact_method` | Enum | — | — | Metode follow up terakhir: `phone`, `whatsapp`, `email`, `meeting` |
| `last_contact_notes` | String | — | — | Catatan follow up terakhir |
| `birthday` | Date | — | — | Tanggal ulang tahun |
| `preferences` | Object | — | — | Preferensi: `preferred_contact`, `preferred_payment`, `language` |
| `notes` | String | — | — | Catatan umum |
| `auth_user_id` | String | — | — | ID autentikasi dari Auth provider |
| `default_address_id` | String | — | — | ID alamat default di buku alamat |
| `addresses` | Array\[Object] | — | — | Buku alamat tersimpan customer |
| `billing_address` | String | — | — | Alamat penagihan resmi (B2B) |
| `shipping_address` | String | — | — | Alamat pengiriman default (B2B) |
| `parent_customer_id` | String | — | — | ID perusahaan induk (untuk outlet/cabang) |
| `outlet_name` | String | — | — | Nama cabang/outlet |
| `tax_id` | String | — | — | NPWP / identitas administrasi pajak |
| `payment_terms` | Enum | `net_30` | — | Ketentuan pembayaran B2B: `cash`, `net_7`, `net_14`, `net_30`, `net_60`, `custom` |
| `payment_terms_days` | Number | `30` | — | Jumlah hari jatuh tempo pembayaran |
| `credit_limit` | Number | `0` | — | Batas plafon riutang kredit B2B |
| `is_active` | Boolean | `true` | — | Status aktif customer |

### CustomerMembership

| Field | Tipe | Default | Required | Deskripsi |
| - | - | - | - | - |
| `company_id` | String | — | ✅ | ID perusahaan |
| `level_name` | String | — | ✅ | Nama level (Silver, Gold, Platinum, dll) |
| `level_key` | String | — | ✅ | Key unik untuk level (`silver`, `gold`, `platinum`) |
| `icon` | String | — | — | Icon emoji untuk level |
| `color` | String | — | — | Warna representasi level |
| `description` | String | — | — | Penjelasan keuntungan dan syarat keanggotaan (max 1000 karakter) |
| `discount_percentage` | Number | `0` | — | Diskon % untuk member level ini (0–100) |
| `points_multiplier` | Number | `1` | — | Multiplier poin (1 = normal, 2 = double points) |
| `min_purchase` | Number | `0` | — | Minimum pembelian untuk mencapai level ini |
| `benefits` | Array\[String] | — | — | Daftar benefit yang didapat |
| `priority_support` | Boolean | `false` | — | Akses priority support via WhatsApp |
| `free_delivery` | Boolean | `false` | — | Gratis ongkir |
| `birthday_bonus` | Number | `0` | — | Bonus poin di hari ulang tahun |
| `is_active` | Boolean | `true` | — | Status keaktifan level |
| `order` | Number | `0` | — | Urutan level (semakin tinggi = semakin premium) |
| `scheme_type` | Enum | `points` | — | Skema loyalty: `points`, `stamp`, `spending`, `visits`, `hybrid` |
| `points_threshold` | Number | `10000` | — | Ambang batas belanja per perolehan poin (misal Rp 10.000) |
| `points_per_threshold` | Number | `1` | — | Jumlah poin yang diperoleh per threshold |
| `stamps_required` | Number | `10` | — | Jumlah stamp yang dibutuhkan untuk reward/naik level |
| `stamp_per_transaction` | Number | `1` | — | Jumlah stamp yang diperoleh per transaksi eligible |
| `min_redemption_points` | Number | `0` | — | Minimal poin untuk melakukan penukaran reward |
| `reward_type` | Enum | `none` | — | Jenis reward: `none`, `discount_percentage`, `discount_amount`, `free_product` |
| `reward_value` | Number | `0` | — | Nilai reward (persen diskon atau nominal) |
| `reward_product_id` | String | — | — | ID produk reward gratis (jika `reward_type = free_product`) |
| `reward_product_name` | String | — | — | Nama produk reward gratis |
| `expiry_days` | Number | — | — | Masa kedaluwarsa poin dalam hari (opsional) |

### CustomerLoyaltyLedger

| Field | Tipe | Default | Required | Deskripsi |
| - | - | - | - | - |
| `company_id` | String | — | ✅ | ID perusahaan |
| `customer_id` | String | — | ✅ | ID customer pemilik transaksi loyalty |
| `customer_name` | String | — | — | Nama customer saat transaksi (denormalized) |
| `customer_phone` | String | — | — | Nomor telepon customer (denormalized) |
| `transaction_id` | String | — | — | ID transaksi POS / referensi sale asal |
| `transaction_number` | String | — | — | Nomor struk / receipt number |
| `idempotency_key` | String | — | ✅ | Key untuk mencegah double crediting/posting |
| `event_type` | Enum | — | ✅ | Tipe event: `earn`, `redeem`, `return_reversal`, `tier_upgrade`, `manual_adjustment` |
| `scheme_type` | Enum | `points` | — | Skema loyalty: `points`, `stamp`, `spending`, `visits`, `hybrid` |
| `points_delta` | Number | `0` | — | Perubahan poin (+ bertambah, - berkurang) |
| `points_before` | Number | `0` | — | Saldo poin sebelum event |
| `points_after` | Number | `0` | — | Saldo poin setelah event |
| `stamps_delta` | Number | `0` | — | Perubahan stamp (+ bertambah, - berkurang) |
| `stamps_before` | Number | `0` | — | Saldo stamp sebelum event |
| `stamps_after` | Number | `0` | — | Saldo stamp setelah event |
| `eligible_amount` | Number | `0` | — | Nilai transaksi eligible untuk perolehan poin |
| `reason` | String | — | — | Alasan atau keterangan mutasi |
| `deficit_points` | Number | `0` | — | Defisit poin jika poin sudah terpakai sebelum void/return |
| `requires_manual_review` | Boolean | `false` | — | Flag jika reversal menghasilkan saldo negatif |
| `reward_details` | Object | — | — | Detail reward saat redeem: `reward_type`, `discount_amount`, `discount_percentage`, `product_id`, `product_name`, `cost_points`, `cost_stamps` |
| `performed_by` | String | — | — | Email/ID operator yang memproses |
| `actor_role` | String | — | — | Role aktor: `owner`, `admin`, `cashier` |
| `created_at` | DateTime | — | — | Waktu event tercatat di server |
| `finance_record_id` | String | — | — | ID FinancialRecord (COGS reward) yang terhubung |

## State Machine — Tier Progression

```mermaid theme={null}
stateDiagram-v2
    [*] --> Regular : Pendaftaran member baru

    Regular --> Silver : total_spent ≥ min_purchase(Silver)
    Silver --> Gold : total_spent ≥ min_purchase(Gold)
    Gold --> Platinum : total_spent ≥ min_purchase(Platinum)

    Platinum --> Gold : downgrade (total_spent < threshold & review period)
    Gold --> Silver : downgrade (total_spent < threshold & review period)
    Silver --> Regular : downgrade (total_spent < threshold & review period)

    Regular --> Regular : transaksi bertambah, progress update
    Silver --> Silver : accumulate poin/stamp
    Gold --> Gold : accumulate poin/stamp
    Platinum --> Platinum : accumulate poin/stamp

    state Regular {
        [*] --> Aktif
        Aktif --> Nonaktif : is_active = false
        Nonaktif --> Aktif : reaktivasi
    }

    state Silver {
        [*] --> Aktif
        Aktif --> Nonaktif : is_active = false
        Nonaktif --> Aktif : reaktivasi
    }

    state Gold {
        [*] --> Aktif
        Aktif --> Nonaktif : is_active = false
        Nonaktif --> Aktif : reaktivasi
    }

    state Platinum {
        [*] --> Aktif
        Aktif --> Nonaktif : is_active = false
        Nonaktif --> Aktif : reaktivasi
    }
```

### Kondisi Defisit Poin (Negative Points)

```mermaid theme={null}
stateDiagram-v2
    [*] --> Normal : points ≥ 0

    Normal --> Defisit : void/return → points < 0
    Defisit --> Review : requires_manual_review = true
    Review --> Normal : admin adjustment (loyalty_deficit_points → 0)
    Review --> Defisit : pending review (loyalty_review_notes dicatat)

    Normal --> Normal : earn points dari transaksi
    Defisit --> Normal : earn points menutupi defisit
```

## Sequence Diagrams

### Member Lookup di Kasir POS

```mermaid theme={null}
sequenceDiagram
    participant K as Kasir
    participant POS as CompanyPOSCashier
    participant C as Customer Entity
    participant M as POSMember Entity
    participant CM as CustomerMembership

    K->>POS: Input nomor telepon / scan kartu member
    POS->>C: Lookup by phone
    alt Customer ditemukan
        C-->>POS: Return: customer data + membership_level_id
        POS->>CM: Get level config by membership_level_id
        CM-->>POS: Return: discount_%, points_multiplier, scheme_type
        POS->>M: Lookup POSMember by phone (jika ada)
        M-->>POS: Return: points, stamps, tier, total_spent
        POS-->>K: Tampilkan profil member + benefit aktif
    else Customer tidak ditemukan
        POS-->>K: Tampilkan opsi: "Daftar member baru?"
        K->>POS: Input data member baru (nama, telepon)
        POS->>C: Create Customer record
        POS->>M: Create POSMember record (default tier: regular)
        C-->>POS: Return: customer_id
        POS-->>K: Member baru terdaftar
    end
```

### Points Earn — Setelah Transaksi

```mermaid theme={null}
sequenceDiagram
    participant POS as CompanyPOSCashier
    participant L as Loyalty Service
    participant CL as CustomerLoyaltyLedger
    participant C as Customer
    participant M as POSMember

    POS->>L: Post-transaction: customer_id, eligible_amount, transaction_id
    L->>C: Get customer → membership_level_id, membership_points
    L->>M: Get POSMember → points, stamps, total_spent

    L->>L: Hitung poin = eligible_amount / points_threshold × points_multiplier
    L->>L: Hitung stamp += stamp_per_transaction (jika skema stamp/hybrid)

    L->>CL: Create ledger entry (event_type: earn)
    Note over CL: points_before → points_after<br/>stamps_before → stamps_after<br/>idempotency_key = transaction_id + event

    L->>C: Update membership_points += points_delta
    L->>C: Update stamps += stamps_delta
    L->>C: Update lifetime_points += points_delta
    L->>C: Update total_spent += eligible_amount
    L->>C: Update visit_count += 1

    L->>M: Update points += points_delta
    L->>M: Update stamps += stamps_delta
    L->>M: Update total_spent += eligible_amount

    L->>L: Check: total_spent ≥ min_purchase(next tier)?
    alt Meets threshold
        L->>C: Upgrade membership_level_id → next level
        L->>C: Update membership_level_name
        L->>CL: Create ledger entry (event_type: tier_upgrade)
        L-->>POS: Return: "Upgrade ke [Level]! Poin baru: X"
    else Belum mencapai threshold
        L-->>POS: Return: "Poin baru: X (+Y poin)"
    end
```

### Points Redeem — Penukaran Poin/Reward

```mermaid theme={null}
sequenceDiagram
    participant K as Kasir
    participant POS as CompanyPOSCashier
    participant L as Loyalty Service
    participant CL as CustomerLoyaltyLedger
    participant C as Customer
    participant M as POSMember
    participant F as FinancialRecord

    K->>POS: Pilih reward → redeem points
    POS->>L: Redeem request: customer_id, reward_type, cost_points

    L->>C: Get customer → membership_points, min_redemption_points
    L->>M: Get POSMember → points

    alt Points ≥ cost_points AND points ≥ min_redemption_points
        L->>L: Calculate points_delta = -cost_points

        L->>CL: Create ledger entry (event_type: redeem)
        Note over CL: reward_details: {reward_type, discount_amount, cost_points}<br/>points_before → points_after

        L->>C: Update membership_points -= cost_points
        L->>M: Update points -= cost_points

        alt reward_type = free_product
            L->>F: Post COGS reward expense
            F-->>L: finance_record_id
            L->>CL: Update finance_record_id
        end

        L-->>POS: Return: "Reward ditukar! Sisa poin: X"
        POS-->>K: Tampilkan konfirmasi redeem
    else Points tidak cukup
        L-->>POS: Return: "Poin tidak cukup (butuh: X, tersedia: Y)"
        POS-->>K: Tampilkan error
    end
```

### Return Reversal — Pembatalan dengan Poin Terpakai

```mermaid theme={null}
sequenceDiagram
    participant K as Kasir
    participant POS as CompanyPOSCashier
    participant L as Loyalty Service
    participant CL as CustomerLoyaltyLedger
    participant C as Customer

    K->>POS: Void/return transaksi original
    POS->>L: Reversal: transaction_id, original_points_earned, original_points_used

    L->>CL: Lookup original earn ledger by transaction_id
    L->>C: Get current membership_points

    alt Poin earn masih ada (belum dipakai)
        L->>CL: Create ledger (event_type: return_reversal, points_delta: -earned)
        L->>C: Update membership_points -= original_points_earned
        L-->>POS: Return: "Poin dikembalikan"
    else Poin sudah terpakai sebagian
        L->>L: deficit = original_points_earned - current_points
        L->>CL: Create ledger (event_type: return_reversal, deficit_points: deficit)
        L->>C: Update membership_points = 0
        L->>C: Update has_negative_points = true
        L->>C: Update loyalty_deficit_points = deficit
        L->>CL: Set requires_manual_review = true
        L-->>POS: Return: "Defisit poin tercatat, butuh review admin"
    end
```

## Tabel Enum

### membership\_tier (POSMember)

| Nilai | Deskripsi | Poin Multiplier | Diskon Default |
| - | - | - | - |
| `regular` | Tier dasar, baru daftar | 1.0x | 0% |
| `silver` | Total belanja mencapai threshold Silver | 1.2x | 2% |
| `gold` | Total belanja mencapai threshold Gold | 1.5x | 5% |
| `platinum` | Total belanja mencapai threshold Platinum | 2.0x | 10% |

### customer\_type (Customer)

| Nilai | Deskripsi |
| - | - |
| `individual` | Perorangan / konsumen akhir |
| `business` | Perusahaan / korporasi |
| `retail` | Toko retail |
| `reseller` | Reseller / agen |
| `distributor` | Distributor |
| `modern_market` | Pasar modern / minimarket |

### status (Customer)

| Nilai | Deskripsi |
| - | - |
| `lead` | Calon customer baru, belum transaksi |
| `prospect` | Sudah menunjukkan minat, dalam pipeline |
| `customer` | Customer aktif, sudah bertransaksi |
| `inactive` | Tidak aktif dalam periode tertentu |

### scheme\_type (CustomerMembership & CustomerLoyaltyLedger)

| Nilai | Deskripsi | Cara Perolehan |
| - | - | - |
| `points` | Poin per Rupiah belanja | `eligible_amount / points_threshold × points_multiplier` |
| `stamp` | Stamp per transaksi | `stamp_per_transaction` per transaksi eligible |
| `spending` | Akumulasi belanja untuk upgrade tier | `total_spent += eligible_amount`, check `min_purchase` |
| `visits` | Reward berdasarkan frekuensi kunjungan | `visit_count += 1`, check threshold kunjungan |
| `hybrid` | Kombinasi poin + stamp + visits | Semua mekanisme berjalan bersamaan |

### event\_type (CustomerLoyaltyLedger)

| Nilai | Deskripsi | Impact Poin | Impact Stamp |
| - | - | - | - |
| `earn` | Perolehan poin dari transaksi | `+points_delta` | `+stamps_delta` |
| `redeem` | Penukaran poin untuk reward | `-points_delta` | `-stamps_delta` |
| `return_reversal` | Pembatalan/void transaksi asal | `-points_delta` (reversal) | `-stamps_delta` (reversal) |
| `tier_upgrade` | Kenaikan tier keanggotaan | Tidak berubah | Tidak berubah |
| `manual_adjustment` | Penyesuaian manual oleh admin | `+/-points_delta` | `+/-stamps_delta` |

### reward\_type (CustomerMembership)

| Nilai | Deskripsi | Cara Klaim |
| - | - | - |
| `none` | Tidak ada reward yang dapat ditukar | — |
| `discount_percentage` | Diskon persentase dari total belanja | Tukar poin → apply diskon % |
| `discount_amount` | Diskon nominal tetap | Tukar poin → potongan nominal |
| `free_product` | Produk gratis tertentu | Tukar poin → ambil produk (`reward_product_id`) |

### payment\_method (CompanyPOSTransaction)

| Nilai | Deskripsi |
| - | - |
| `cash` | Tunai |
| `card` | Kartu kredit/debit |
| `transfer` | Transfer bank |
| `ewallet` | Dompet digital (GoPay, OVO, Dana, dll) |
| `qris` | QRIS |
| `saldo` | Potong saldo member |
| `mayar` | Mayar payment gateway |
| `debt` | Hutang / piutang B2B |
| `manual_transfer` | Transfer manual (butuh verifikasi) |
| `midtrans` | Midtrans payment gateway |
| `tripay` | Tripay payment gateway |
| `stripe` | Stripe payment gateway |
| `paypal` | PayPal |

### sales\_channel (CompanyPOSTransaction)

| Nilai | Deskripsi |
| - | - |
| `offline_pos` | Transaksi langsung di kasir POS |
| `offline` | Transaksi offline non-POS |
| `website` | Pesanan dari website |
| `online_catalog` | Katalog online |
| `marketplace` | Marketplace (Tokopedia, Shopee, dll) |
| `landing_page` | Landing page |
| `whatsapp` | Pesanan via WhatsApp |
| `reseller` | Pesanan dari reseller |
| `b2b` | Pesanan B2B |
| `grab` | Pesanan via Grab |
| `social_media` | Pesanan dari media sosial |

### payment\_status (CompanyPOSTransaction)

| Nilai | Deskripsi |
| - | - |
| `pending` | Menunggu pembayaran |
| `pending_verification` | Menunggu verifikasi transfer manual |
| `partially_paid` | Baru dibayar sebagian (split payment) |
| `paid` | Lunas |
| `failed` | Pembayaran gagal |
| `refunded` | Dana dikembalikan |
| `rejected` | Pembayaran ditolak |

### order\_status (CompanyPOSTransaction)

| Nilai | Deskripsi |
| - | - |
| `pending` | Pesanan baru masuk |
| `processing` | Sedang diproses/disiapkan |
| `shipped` | Sedang dikirim |
| `delivered` | Sampai di tujuan |
| `completed` | Transaksi selesai |
| `cancelled` | Pesanan dibatalkan |
| `rejected` | Pesanan ditolak |

### last\_contact\_method (Customer)

| Nilai | Deskripsi |
| - | - |
| `phone` | Kontak terakhir via telepon |
| `whatsapp` | Kontak terakhir via WhatsApp |
| `email` | Kontak terakhir via email |
| `meeting` | Kontak terakhir via pertemuan langsung |

### payment\_terms (Customer — B2B)

| Nilai | Deskripsi | Hari Jatuh Tempo |
| - | - | - |
| `cash` | Bayar tunai | 0 |
| `net_7` | Net 7 hari | 7 |
| `net_14` | Net 14 hari | 14 |
| `net_30` | Net 30 hari | 30 |
| `net_60` | Net 60 hari | 60 |
| `custom` | Jatuh tempo kustom | Sesuai `payment_terms_days` |

### preferred\_contact (Customer.preferences)

| Nilai | Deskripsi |
| - | - |
| `phone` | Preferensi kontak via telepon |
| `whatsapp` | Preferensi kontak via WhatsApp |
| `email` | Preferensi kontak via email |

## RBAC — Hak Akses Modul Members

| Aksi | Owner | Admin | Manager | Kasir | Viewer |
| - | - | - | - | - | - |
| **Lihat daftar member** | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Lihat detail member** | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Tambah member baru** | ✅ | ✅ | ✅ | ✅ | ❌ |
| **Edit profil member** | ✅ | ✅ | ✅ | ❌ | ❌ |
| **Hapus member** | ✅ | ✅ | ❌ | ❌ | ❌ |
| **Import member (CSV/VCF)** | ✅ | ✅ | ✅ | ❌ | ❌ |
| **Export member** | ✅ | ✅ | ✅ | ❌ | ❌ |
| **Redeem poin/reward** | ✅ | ✅ | ✅ | ✅ | ❌ |
| **Adjust poin manual** | ✅ | ✅ | ❌ | ❌ | ❌ |
| **Upgrade/downgrade tier** | ✅ | ✅ | ❌ | ❌ | ❌ |
| **Review loyalty deficit** | ✅ | ✅ | ❌ | ❌ | ❌ |
| **Konfigurasi level membership** | ✅ | ✅ | ❌ | ❌ | ❌ |
| **Hapus level membership** | ✅ | ✅ | ❌ | ❌ | ❌ |
| **Lihat loyalty ledger** | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Multi-tenant scoping** | ✅ `company_id` | ✅ `company_id` | ✅ `company_id` | ✅ `company_id` | ✅ `company_id` |

> **Catatan RBAC**: Semua operasi di-scope ke `company_id` berdasarkan `active_company_id` user. Data member dari satu perusahaan tidak dapat diakses oleh user dari perusahaan lain. Role `admin` memiliki akses penuh dalam scope perusahaannya sendiri.


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