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

# Customer membership

<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: "Customer Membership — Status Membership Pelanggan"
description: "Tampilan customer-facing untuk status membership: tier saat ini, progress bar upgrade, benefit aktif, riwayat poin dari CustomerLoyaltyLedger, dan integrasi real-time dengan POS."
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

# Customer Membership — Status Membership Pelanggan

<img src="https://mintcdn.com/quinnofspicy/e4f_upKhVWcjsUmM/docs/mintlify/screenshots/crm/customer-membership.png?fit=max&auto=format&n=e4f_upKhVWcjsUmM&q=85&s=8ac5cf54dd2b8f6232d39b959af3be5d" alt="Customer Membership" width="1920" height="1080" data-path="docs/mintlify/screenshots/crm/customer-membership.png" />

Halaman Customer Membership adalah tampilan **customer-facing** yang menunjukkan status loyalty pelanggan berdasarkan data `Customer` (40+ fields) dan `CustomerMembership` (tier definition). Pelanggan bisa melihat tier mereka saat ini, progress menuju tier berikutnya dengan **progress bar visual**, benefit yang aktif, dan riwayat lengkap perolehan serta penggunaan poin dari `CustomerLoyaltyLedger`.

Halaman ini tidak memiliki komponen React terpisah yang besar — ia mengkomposisikan data dari beberapa entitas dan menampilkannya dalam format yang mudah dipahami pelanggan.

## Entity Relationship Diagram

```mermaid theme={null}
erDiagram
    Customer ||--o{ CustomerLoyaltyLedger : "memiliki riwayat loyalty"
    Customer }o--|| CustomerMembership : "berada pada tier"
    CustomerMembership ||--o{ Customer : "mendefinisikan tier untuk"
    Customer ||--o{ CompanyPOSTransaction : "melakukan transaksi"
    CompanyPOSTransaction ||--o{ CustomerLoyaltyLedger : "memicu pencatatan loyalty"
    CustomerMembership ||--o| CustomerMembership : "tier hierarchy (order)"

    Customer {
        string id PK
        string company_id FK
        string name "nama pelanggan"
        string phone "nomor telepon"
        string email "email"
        string membership_level_id FK "ref ke CustomerMembership.id"
        string membership_level_name "nama tier saat ini"
        date membership_since "tanggal bergabung"
        number membership_points "saldo poin aktif"
        number lifetime_points "total poin sepanjang waktu"
        number stamps "saldo stamp aktif"
        number lifetime_value "total belanja (LTV)"
        number total_orders "jumlah transaksi"
        number average_order_value "rata-rata nilai transaksi"
        boolean has_negative_points "flag saldo negatif"
        number loyalty_deficit_points "defisit poin perlu rekonsiliasi"
        string loyalty_review_notes "catatan review deficit"
        date last_purchase_date "tanggal pembelian terakhir"
        date birthday "tanggal ulang tahun"
        string status "lead|prospect|customer|inactive"
        string customer_type "individual|business|retail|reseller"
        date last_tier_upgrade_date "waktu upgrade/downgrade terakhir"
        string last_tier_upgrade_reason "alasan perubahan tier"
        string last_tier_upgrade_by "operator yang mengubah tier"
    }

    CustomerMembership {
        string id PK
        string company_id FK
        string level_name "nama tier (Silver, Gold, Platinum)"
        string level_key UK "key unik (silver, gold, platinum)"
        string scheme_type "points|stamp|spending|visits|hybrid"
        number order "urutan hierarki tier"
        number discount_percentage "diskon % untuk tier ini"
        number points_multiplier "multiplier perolehan poin"
        number min_purchase "minimum pembelian untuk naik tier"
        number points_threshold "ambang belanja per poin"
        number points_per_threshold "poin per threshold"
        number stamps_required "stamp untuk naik tier / reward"
        number stamp_per_transaction "stamp per transaksi eligible"
        number min_redemption_points "minimum poin untuk tukar reward"
        string reward_type "none|discount_percentage|discount_amount|free_product"
        number reward_value "nilai reward"
        number expiry_days "masa kedaluwarsa poin"
        boolean is_active "status aktif tier"
        boolean priority_support "akses priority support"
        boolean free_delivery "gratis ongkir"
        number birthday_bonus "bonus poin ulang tahun"
        array benefits "daftar benefit kustom"
    }

    CustomerLoyaltyLedger {
        string id PK
        string company_id FK
        string customer_id FK "ref ke Customer.id"
        string idempotency_key UK "cegah double posting"
        string event_type "earn|redeem|return_reversal|tier_upgrade|manual_adjustment"
        string scheme_type "points|stamp|spending|visits|hybrid"
        number points_delta "perubahan poin (+/-)"
        number points_before "saldo poin sebelum event"
        number points_after "saldo poin setelah event"
        number stamps_delta "perubahan stamp (+/-)"
        number stamps_before "saldo stamp sebelum event"
        number stamps_after "saldo stamp setelah event"
        number eligible_amount "nilai transaksi eligible"
        string transaction_id "ref ke CompanyPOSTransaction"
        string transaction_number "nomor struk"
        object reward_details "detail reward jika redeem"
        number deficit_points "defisit jika void setelah redeem"
        boolean requires_manual_review "butuh review manual"
        string performed_by "operator yang memproses"
        string actor_role "role aktor"
        datetime created_at "waktu event"
    }

    CompanyPOSTransaction {
        string id PK
        string company_id FK
        string transaction_number "nomor transaksi"
        datetime transaction_date "tanggal transaksi"
        number total_amount "total nilai transaksi"
        string member_id FK "ref ke Customer.id"
        string payment_status "status pembayaran"
    }
```

### Penjelasan Relasi

| Relasi | Kardinalitas | Deskripsi |
| - | - | - |
| `Customer` → `CustomerMembership` | Many-to-One | Banyak pelanggan bisa berada pada tier yang sama. Field `membership_level_id` di `Customer` merujuk ke `id` di `CustomerMembership`. |
| `Customer` → `CustomerLoyaltyLedger` | One-to-Many | Satu pelanggan memiliki banyak entri ledger (setiap kali earn, redeem, atau adjust). |
| `Customer` → `CompanyPOSTransaction` | One-to-Many | Satu pelanggan memiliki banyak transaksi POS. |
| `CompanyPOSTransaction` → `CustomerLoyaltyLedger` | One-to-Many | Satu transaksi POS bisa memicu satu atau lebih entri ledger (misal: earn + tier\_upgrade sekaligus). |

## Arsitektur Data

```mermaid theme={null}
graph TB
    subgraph "Data Sources"
        C[Customer Entity<br/>40+ fields<br/>membership_level_id<br/>membership_points<br/>lifetime_value]
        CM[CustomerMembership<br/>Tier definitions<br/>discount_percentage<br/>points_multiplier]
        LL[CustomerLoyaltyLedger<br/>Point history<br/>earn/redeem/expire]
    end

    subgraph "Customer Membership Page"
        TIER[Tier Card<br/>Current level + badge]
        PROG[Progress Bar<br/>Next tier progress]
        BEN[Benefit List<br/>Active perks]
        HIST[Point History<br/>Ledger timeline]
    end

    C --> TIER
    CM --> TIER
    C --> PROG
    CM --> PROG
    CM --> BEN
    LL --> HIST
```

## Entity Schema — CustomerMembership (Detail Lengkap)

Entitas `CustomerMembership` mendefinisikan **seluruh konfigurasi tier loyalty** — mulai dari skema perolehan, ambang batas, hingga jenis reward yang tersedia. Setiap perusahaan (`company_id`) bisa memiliki beberapa tier yang diurutkan berdasarkan field `order`.

### Field Identitas & Visual

| Field | Tipe | Default | Wajib | Deskripsi |
| - | - | - | - | - |
| `id` | UUID | auto | Ya | Primary key unik untuk setiap tier |
| `company_id` | UUID | — | Ya | Foreign key ke perusahaan pemilik program loyalty |
| `level_name` | String | — | Ya | Nama tampilan tier, contoh: "Silver", "Gold", "Platinum", "Diamond" |
| `level_key` | String | — | Ya | Key unik untuk referensi programatis (contoh: `silver`, `gold`, `platinum`). Bersifat unique per company. |
| `icon` | String | — | Tidak | Emoji ikon tier (contoh: 🥉🥈🥇💎) |
| `color` | String | — | Tidak | Kode warna hex untuk badge tier (contoh: `#FFD700`) |
| `description` | String | — | Tidak | Penjelasan lengkap mengenai keuntungan dan syarat keanggotaan pada level ini (maks. 1000 karakter) |
| `order` | Number | `0` | Tidak | Urutan level dalam hierarki — semakin tinggi nilainya, semakin premium tier tersebut |
| `is_active` | Boolean | `true` | Tidak | Status aktif tier. Tier non-aktif tidak bisa dijadikan tier baru pelanggan, tetapi pelanggan yang sudah berada di tier ini tetap mempertahankannya. |

### Field Skema Loyalty (Scheme Configuration)

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `scheme_type` | Enum | `"points"` | Skema loyalty yang berlaku pada tier ini. Pilihan: `points`, `stamp`, `spending`, `visits`, `hybrid` |
| `points_threshold` | Number | `10000` | Ambang batas belanja untuk memperoleh poin (dalam Rupiah). Contoh: Rp 10.000 = setiap kelipatan Rp 10.000 dapat poin. |
| `points_per_threshold` | Number | `1` | Jumlah poin yang diperoleh per threshold tercapai. |
| `stamps_required` | Number | `10` | Jumlah stamp yang dibutuhkan untuk reward atau naik level. |
| `stamp_per_transaction` | Number | `1` | Jumlah stamp yang diperoleh per transaksi eligible. |
| `min_purchase` | Number | `0` | Minimum total pembelian (lifetime\_value) untuk mencapai tier ini. Digunakan sebagai kriteria auto-upgrade. |

### Field Reward System

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `reward_type` | Enum | `"none"` | Jenis reward yang dapat ditukarkan: `none`, `discount_percentage`, `discount_amount`, `free_product` |
| `reward_value` | Number | `0` | Nilai reward — bisa berupa persentase diskon (jika `reward_type=discount_percentage`) atau nominal Rupiah (jika `discount_amount`) |
| `reward_product_id` | UUID | — | ID produk yang diberikan gratis jika `reward_type=free_product` |
| `reward_product_name` | String | — | Nama produk reward gratis (untuk tampilan) |
| `min_redemption_points` | Number | `0` | Minimal poin yang harus dimiliki pelanggan sebelum bisa melakukan penukaran reward |
| `expiry_days` | Number | — | Masa kedaluwarsa poin dalam hari. Jika diisi, poin yang tidak digunakan akan hangus setelah N hari. |

### Field Benefit & Privilege

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `discount_percentage` | Number | `0` | Persentase diskon otomatis (0-100) yang diterapkan saat transaksi POS |
| `points_multiplier` | Number | `1` | Multiplier perolehan poin. `1` = normal, `1.5` = 1.5x, `2` = double points |
| `free_delivery` | Boolean | `false` | Apakah tier ini mendapat gratis ongkir |
| `priority_support` | Boolean | `false` | Akses prioritas ke support via WhatsApp dedicated |
| `birthday_bonus` | Number | `0` | Jumlah poin bonus yang diberikan di hari ulang tahun pelanggan |
| `benefits` | Array\[String] | `[]` | Daftar benefit kustom (teks bebas) yang ditampilkan ke pelanggan |

### Contoh Konfigurasi Tier Lengkap

```json theme={null}
{
  "level_name": "Gold",
  "level_key": "gold",
  "icon": "🥇",
  "color": "#FFD700",
  "scheme_type": "points",
  "order": 2,
  "min_purchase": 5000000,
  "discount_percentage": 10,
  "points_multiplier": 1.5,
  "points_threshold": 10000,
  "points_per_threshold": 1,
  "min_redemption_points": 500,
  "reward_type": "discount_amount",
  "reward_value": 50000,
  "free_delivery": true,
  "priority_support": true,
  "birthday_bonus": 200,
  "benefits": ["Akses menu eksklusif", "Undangan event khusus member"],
  "expiry_days": 365,
  "is_active": true
}
```

## Entity Schema — CustomerLoyaltyLedger (Detail Lengkap)

Entitas `CustomerLoyaltyLedger` adalah **buku besar (ledger) mutasi loyalty** yang mencatat setiap perubahan poin dan stamp pelanggan. Setiap baris merepresentasikan satu event loyalty dengan snapshot saldo sebelum dan sesudahnya, sehingga memungkinkan audit trail lengkap.

### Field Identitas & Referensi

| Field | Tipe | Wajib | Deskripsi |
| - | - | - | - |
| `id` | UUID | Ya | Primary key entri ledger |
| `company_id` | UUID | Ya | Foreign key ke perusahaan |
| `customer_id` | UUID | Ya | Foreign key ke `Customer` — pemilik transaksi loyalty |
| `customer_name` | String | Tidak | Nama customer **saat transaksi** (denormalized untuk performa reporting) |
| `customer_phone` | String | Tidak | Nomor telepon customer saat transaksi (denormalized) |
| `transaction_id` | UUID | Tidak | Referensi ke `CompanyPOSTransaction.id` — transaksi POS yang memicu event |
| `transaction_number` | String | Tidak | Nomor struk / receipt number (denormalized dari transaksi POS) |
| `idempotency_key` | UUID | Ya | Key unik untuk mencegah **double crediting / double posting**. Jika event yang sama dikirim dua kali (misal retry network), hanya satu yang diproses. |

### Field Event & Skema

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `event_type` | Enum | — | Tipe event mutasi loyalty (lihat tabel enum di bawah) |
| `scheme_type` | Enum | `"points"` | Skema loyalty yang berlaku saat event ini terjadi |

**Enum `event_type`:**

| Nilai | Deskripsi | Kapan Terjadi |
| - | - | - |
| `earn` | Perolehan poin/stamp dari transaksi | Setelah transaksi POS selesai dan terverifikasi |
| `redeem` | Penukaran poin/stamp untuk reward | Pelanggan menukarkan poin untuk diskon atau produk gratis |
| `return_reversal` | Pembatalan poin akibat retur/void | Transaksi asli di-void atau diretur setelah poin sudah dicatat |
| `tier_upgrade` | Pencatatan perubahan tier | Pelanggan naik atau turun tier |
| `manual_adjustment` | Penyesuaian manual oleh admin | Admin/operator mengoreksi poin secara manual (misal: komplain pelanggan) |

### Field Snapshot Saldo (Points)

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `points_delta` | Number | `0` | Perubahan poin: positif (+) untuk earn, negatif (-) untuk redeem/reversal |
| `points_before` | Number | `0` | Saldo poin **sebelum** event ini diproses |
| `points_after` | Number | `0` | Saldo poin **setelah** event ini diproses. Harus konsisten: `points_after = points_before + points_delta` |

### Field Snapshot Saldo (Stamps)

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `stamps_delta` | Number | `0` | Perubahan stamp: positif (+) untuk earn, negatif (-) untuk redeem |
| `stamps_before` | Number | `0` | Saldo stamp sebelum event |
| `stamps_after` | Number | `0` | Saldo stamp setelah event |

### Field Transaksi & Audit

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `eligible_amount` | Number | `0` | Nilai transaksi yang sah (eligible) untuk perhitungan poin. Bisa berbeda dari total transaksi jika ada produk yang tidak termasuk program loyalty. |
| `reason` | String | — | Alasan atau keterangan mutasi (contoh: "Earned from INV-20260110-0042", "Manual adjustment — komplain poin hilang") |
| `deficit_points` | Number | `0` | Defisit poin yang terjadi jika poin sudah terpakai (redeem) sebelum transaksi asli di-void. Jika > 0, berarti pelanggan memiliki "utang poin". |
| `requires_manual_review` | Boolean | `false` | Flag yang di-set `true` jika transaksi reversal menghasilkan saldo negatif dan butuh review manual oleh admin. |
| `reward_details` | Object | — | Detail reward jika event adalah `redeem` (lihat sub-table di bawah) |
| `performed_by` | String | — | Email atau ID operator yang memproses event ini |
| `actor_role` | String | — | Role aktor: `owner`, `admin`, `cashier` |
| `finance_record_id` | UUID | — | ID `FinancialRecord` yang terhubung — diisi saat event redeem mem-posting biaya reward (COGS) ke modul keuangan |
| `created_at` | DateTime | auto | Waktu event tercatat di server |

**Sub-field `reward_details`:**

| Sub-field | Tipe | Deskripsi |
| - | - | - |
| `reward_type` | String | Jenis reward yang ditukarkan |
| `discount_amount` | Number | Nominal diskon yang diberikan |
| `discount_percentage` | Number | Persentase diskon yang diberikan |
| `product_id` | UUID | ID produk gratis (jika `reward_type=free_product`) |
| `product_name` | String | Nama produk gratis |
| `cost_points` | Number | Jumlah poin yang diperlukan untuk reward ini |
| `cost_stamps` | Number | Jumlah stamp yang diperlukan untuk reward ini |

## State Machine — Tier Progression

```mermaid theme={null}
stateDiagram-v2
    [*] --> Lead : Registrasi pelanggan baru

    state "Tanpa Tier (Lead)" as Lead
    state "Silver" as Silver
    state "Gold" as Gold
    state "Platinum" as Platinum
    state "Diamond" as Diamond

    Lead --> Silver : min_purchase tercapai\nATAU points >= threshold\nATAU stamps >= stamps_required

    Silver --> Gold : min_purchase Gold tercapai\nATAU lifetime_points >= Gold threshold\nATAU stamps >= Gold stamps_required
    Gold --> Platinum : min_purchase Platinum tercapai\nATAU lifetime_points >= Platinum threshold
    Platinum --> Diamond : min_purchase Diamond tercapai\nATAU lifetime_points >= Diamond threshold

    Gold --> Silver : Penurunan LTV / poin tidak mencukupi\n(review manual oleh admin)
    Platinum --> Gold : Penurunan LTV / poin tidak mencukupi
    Diamond --> Platinum : Penurunan LTV / poin tidak mencukupi

    Silver --> [*] : Customer inactive > 12 bulan
    Gold --> [*] : Customer inactive > 12 bulan

    state "Tier Upgrade Trigger" as UpgradeTrigger {
        [*] --> CheckCriteria
        CheckCriteria --> EvaluateScheme : scheme_type?
        EvaluateScheme --> PointsCheck : points
        EvaluateScheme --> StampCheck : stamp
        EvaluateScheme --> SpendingCheck : spending
        EvaluateScheme --> VisitsCheck : visits
        EvaluateScheme --> HybridCheck : hybrid (all)
        PointsCheck --> [*]
        StampCheck --> [*]
        SpendingCheck --> [*]
        VisitsCheck --> [*]
        HybridCheck --> [*]
    }
```

### Aturan Transisi Tier

| Transisi | Trigger | Kondisi |
| - | - | - |
| **No Tier → Silver** | Transaksi POS pertama yang eligible | `lifetime_value >= Silver.min_purchase` ATAU `membership_points >= Silver.points_threshold` ATAU `stamps >= Silver.stamps_required` |
| **Silver → Gold** | Akumulasi belanja/poin | `lifetime_value >= Gold.min_purchase` ATAU `membership_points >= Gold.points_threshold` |
| **Gold → Platinum** | Akumulasi belanja/poin | `lifetime_value >= Platinum.min_purchase` ATAU `membership_points >= Platinum.points_threshold` |
| **Platinum → Diamond** | Akumulasi belanja/poin | `lifetime_value >= Diamond.min_purchase` ATAU `membership_points >= Diamond.points_threshold` |
| **Downgrade (misal Gold → Silver)** | Review manual admin | Admin melakukan downgrade berdasarkan `loyalty_review_notes` dan evaluasi kebijakan |
| **Upgrade dicatat** | Setiap transisi tier | Buat entri `CustomerLoyaltyLedger` dengan `event_type=tier_upgrade`, update `Customer.last_tier_upgrade_date`, `last_tier_upgrade_reason`, `last_tier_upgrade_by` |

### Penanganan Loyalty Deficit

Ketika transaksi di-void **setelah** poin dari transaksi tersebut sudah terpakai (diredeem), sistem mencatat defisit:

```mermaid theme={null}
stateDiagram-v2
    [*] --> Normal : Poin positif
    Normal --> Deficit : Void transaksi setelah\npoin sudah di-redeem
    Deficit --> Review : has_negative_points = true\nloyalty_deficit_points > 0\nrequires_manual_review = true
    Review --> Resolved : Admin melakukan\nmanual_adjustment\nmelalui CustomerLoyaltyLedger
    Resolved --> Normal : Saldo kembali positif\natau dinolkan
```

## Perbandingan Skema Loyalty

Sistem SNISHOP ERP mendukung **5 jenis skema loyalty** yang dikonfigurasi per tier melalui field `scheme_type` di `CustomerMembership`. Setiap skema memiliki karakteristik, formula perolehan, dan kasus penggunaan yang berbeda.

| Aspek | Points | Stamp | Spending | Visits | Hybrid |
| - | - | - | - | - | - |
| **Konsep** | Kumpulkan poin dari belanja | Kumpulkan stempel per transaksi | Akumulasi nominal belanja | Hitung frekuensi kunjungan | Kombinasi beberapa skema |
| **Field utama** | `points_threshold`, `points_per_threshold` | `stamps_required`, `stamp_per_transaction` | `min_purchase` | `min_purchase` (sebagai visit count proxy) | Semua field di atas |
| **Formula earn** | `floor(eligible_amount / points_threshold) × points_per_threshold × points_multiplier` | `stamp_per_transaction` per transaksi eligible | Langsung tambah `eligible_amount` ke `lifetime_value` | +1 per transaksi | Gabungan semua |
| **Formula progress** | `membership_points / next_tier.points_threshold × 100%` | `stamps / next_tier.stamps_required × 100%` | `lifetime_value / next_tier.min_purchase × 100%` | `total_orders / next_tier.min_purchase × 100%` | `max(all_progress)` |
| **Cocok untuk** | Restoran casual, F\&B umum | Coffee shop, warung, bakery | Fine dining, restoran premium | Salon, barbershop, laundry | Enterprise F\&B multi-konsep |
| **Keuntungan** | Fleksibel, mudah dipahami | Sederhana, visual (seperti kartu stempel) | Mendorong spending lebih tinggi | Mendorong frekuensi kunjungan | Komprehensif, adil |
| **Kelemahan** | Perlu hitung konversi poin | Tidak membedakan nominal belanja | Tidak mendorong frekuensi | Tidak membedakan nominal | Kompleks, perlu monitoring lebih |
| **Contoh** | Belanja Rp 10.000 = 1 poin | Setiap beli kopi = 1 stamp, 10 stamp = free coffee | Belanja Rp 5jt = Silver, Rp 15jt = Gold | 10 kunjungan = Silver, 25 = Gold | Gabungan poin + visit count |

### Formula Perhitungan Poin per Skema

```
# Skema Points
earned_points = floor(eligible_amount / points_threshold) × points_per_threshold × points_multiplier

# Contoh: Belanja Rp 85.000, threshold Rp 10.000, per_threshold 1 poin, multiplier 1.5x
earned_points = floor(85000 / 10000) × 1 × 1.5 = 8 × 1.5 = 12 poin

# Skema Stamp
earned_stamps = stamp_per_transaction (jika eligible_amount > 0)

# Skema Spending
# Tidak ada poin/stamp — langsung akumulasi ke lifetime_value
# Progress = lifetime_value / next_tier.min_purchase × 100%

# Skema Visits
visit_count += 1 (per transaksi eligible)
# Progress = visit_count / next_tier.min_purchase × 100%

# Skema Hybrid
# Hitung semua skema di atas, progress = max(all_percentages)
```

## Sequence Diagram — Loyalty Point Earn Flow

```mermaid theme={null}
sequenceDiagram
    participant K as Kasir (POS)
    participant POS as CompanyPOSTransaction
    participant LE as Loyalty Engine
    participant CM as CustomerMembership
    participant C as Customer
    participant LL as CustomerLoyaltyLedger

    K->>POS: Input transaksi + pilih member
    POS->>C: Lookup customer by phone/ID
    C-->>POS: membership_level_id, membership_points, stamps

    POS->>CM: Lookup tier config by membership_level_id
    CM-->>POS: scheme_type, points_threshold, points_multiplier, discount_percentage

    POS->>POS: Hitung eligible_amount (total - produk non-loyalty)
    POS->>POS: Apply discount_percentage ke cart

    K->>POS: Finalize transaksi → payment success

    POS->>LE: Post-transaction: trigger loyalty earn
    LE->>LE: Cek idempotency_key (cegah duplikat)

    alt scheme_type = "points"
        LE->>LE: earned = floor(eligible / threshold) × per_threshold × multiplier
    else scheme_type = "stamp"
        LE->>LE: earned_stamps = stamp_per_transaction
    else scheme_type = "spending"
        LE->>LE: eligible_amount → lifetime_value
    else scheme_type = "visits"
        LE->>LE: visit_count += 1
    else scheme_type = "hybrid"
        LE->>LE: Hitung semua skema sekaligus
    end

    LE->>LL: Create ledger entry: event_type=earn
    Note over LL: points_before → points_after<br/>stamps_before → stamps_after<br/>eligible_amount, transaction_id

    LE->>C: Update membership_points += earned
    LE->>C: Update lifetime_value += eligible_amount
    LE->>C: Update total_orders += 1
    LE->>C: Update last_purchase_date = now()

    LE->>LE: Cek auto-upgrade criteria
    alt Criteria upgrade terpenuhi
        LE->>C: Update membership_level_id = next_tier.id
        LE->>C: Update membership_level_name = next_tier.level_name
        LE->>C: Update last_tier_upgrade_date, reason, by
        LE->>LL: Create ledger entry: event_type=tier_upgrade
    end

    LE-->>POS: Return: new balance, tier status
    POS-->>K: "Poin baru: 2.500 (+12)"
```

## Sequence Diagram — Loyalty Point Redeem Flow

```mermaid theme={null}
sequenceDiagram
    participant K as Kasir (POS)
    participant POS as CompanyPOSTransaction
    participant LE as Loyalty Engine
    participant CM as CustomerMembership
    participant C as Customer
    participant LL as CustomerLoyaltyLedger
    participant FIN as FinancialRecord

    K->>POS: Pilih menu "Tukar Poin" untuk member
    POS->>C: Lookup customer, cek membership_points
    C-->>POS: membership_level_id, membership_points

    POS->>CM: Lookup reward config by membership_level_id
    CM-->>POS: reward_type, reward_value, min_redemption_points, reward_product_id

    alt reward_type = "discount_percentage"
        POS->>POS: Hitung diskon: subtotal × reward_value / 100
    else reward_type = "discount_amount"
        POS->>POS: Diskon = reward_value (nominal tetap)
    else reward_type = "free_product"
        POS->>POS: Tambahkan produk gratis (reward_product_id) ke cart
    end

    POS->>POS: Cek: membership_points >= min_redemption_points?

    alt Poin tidak mencukupi
        POS-->>K: "Poin tidak cukup untuk redeem reward ini"
    else Poin mencukupi
        K->>POS: Konfirmasi redeem → finalize transaksi

        POS->>LE: Trigger loyalty redeem
        LE->>LE: Cek idempotency_key

        LE->>LL: Create ledger entry: event_type=redeem
        Note over LL: points_before → points_after (delta negatif)<br/>reward_details: {reward_type, cost_points, ...}

        LE->>C: Update membership_points -= cost_points

        alt reward menghasilkan biaya COGS
            LE->>FIN: Create FinancialRecord (biaya reward)
            FIN-->>LE: finance_record_id
            LE->>LL: Update ledger.finance_record_id
        end

        LE-->>POS: Return: new balance
        POS-->>K: "Poin baru: 2.400 (-100). Reward diterapkan."
    end
```

## Sequence Diagram — Return/Reversal Flow

```mermaid theme={null}
sequenceDiagram
    participant ADM as Admin/Owner
    participant POS as CompanyPOSTransaction
    participant LE as Loyalty Engine
    participant C as Customer
    participant LL as CustomerLoyaltyLedger

    ADM->>POS: Void/Return transaksi original
    POS->>LE: Trigger return_reversal

    LE->>LL: Lookup ledger entry by transaction_id
    LL-->>LE: Original earn entry (points_delta, points_after)

    alt Poin dari transaksi MASIH ada (belum di-redeem)
        LE->>LL: Create: event_type=return_reversal, points_delta = -original_earned
        LE->>C: membership_points -= original_earned
        LE-->>ADM: "Poin berhasil dikurangi"
    else Poin SUDAH terpakai (sudah di-redeem sebagian/seluruhnya)
        LE->>LE: Hitung deficit = original_earned - current_points (jika current < original)
        LE->>LL: Create: event_type=return_reversal, deficit_points > 0
        LE->>C: membership_points -= original_earned (bisa jadi negatif)
        LE->>C: has_negative_points = true
        LE->>C: loyalty_deficit_points = deficit

        alt deficit > 0
            LE->>LL: Set requires_manual_review = true
            LE-->>ADM: "PERHATIAN: Saldo poin negatif. Defisit [N] poin perlu rekonsiliasi manual."
        end
    end
```

## Akses Halaman

| Metode | Detail |
| - | - |
| **URL** | `/customer-membership` |
| **Sidebar** | Menu **CRM** → **Customer Membership** |
| **Customer Portal** | Via self-service dashboard |

## Informasi yang Ditampilkan

### Status Tier Saat Ini

```mermaid theme={null}
graph LR
    subgraph "Tier Card"
        ICON[🥇 Icon]
        NAME[Gold Member]
        SINCE[Sejak: 15 Mar 2025]
        POINTS[Poin: 2.450]
        LTV[Total Belanja: Rp 7.250.000]
    end
```

| Info | Sumber Data | Format |
| - | - | - |
| **Nama Tier** | `Customer.membership_level_name` | Text (contoh: "Gold") |
| **Ikon Tier** | `CustomerMembership.icon` | Emoji (🥉🥈🥇💎) |
| **Warna Badge** | `CustomerMembership.color` | Hex (#FFD700) |
| **Sejak** | `Customer.membership_since` | Date (15 Mar 2025) |
| **Poin Saat Ini** | `Customer.membership_points` | Number (2.450) |
| **Lifetime Points** | `Customer.lifetime_points` | Number (total pernah dikumpulkan) |
| **Total Belanja** | `Customer.lifetime_value` | Rupiah (Rp 7.250.000) |

### Progress Upgrade

```mermaid theme={null}
graph LR
    subgraph "Progress Bar — Spending Scheme"
        CUR[Gold<br/>Rp 7.250.000] --> BAR[████████████░░░░░]
        BAR --> NEXT[Platinum<br/>Rp 15.000.000]
        PCT[48.3% — sisa Rp 7.750.000]
    end
```

Formula progress per skema:

| Skema | Formula | Contoh |
| - | - | - |
| **spending** | `lifetime_value / next_tier.min_purchase × 100%` | 7.25jt / 15jt = 48.3% |
| **stamp** | `stamps / next_tier.stamps_required × 100%` | 7 / 10 = 70% |
| **points** | `membership_points / next_tier.points_required × 100%` | 2450 / 5000 = 49% |
| **visits** | `visit_count / next_tier.visits_required × 100%` | 15 / 20 = 75% |
| **hybrid** | `max(all_progress_percentages)` | Max dari semua skema |

### Benefit Aktif

| Benefit | Indikator | Sumber |
| - | - | - |
| **Discount %** | ✓ 10% | `CustomerMembership.discount_percentage` |
| **Points Multiplier** | ✓ 1.5x | `CustomerMembership.points_multiplier` |
| **Free Delivery** | ✓ Aktif | `CustomerMembership.free_delivery` |
| **Priority Support** | ✓ Aktif | `CustomerMembership.priority_support` |
| **Birthday Bonus** | ✓ Aktif | `CustomerMembership.birthday_bonus` |
| **Custom Benefits** | Daftar kustom | `CustomerMembership.benefits[]` |

### Riwayat Poin — CustomerLoyaltyLedger

```mermaid theme={null}
sequenceDiagram
    participant T as Transaksi POS
    participant L as Loyalty Engine
    participant LL as CustomerLoyaltyLedger
    participant P as Point History

    T->>L: Transaksi selesai
    L->>LL: Create: type=earn, points=+50
    Note over LL: "Earned 50 points from INV-20260110-0042"

    T->>L: Redeem reward
    L->>LL: Create: type=redeem, points=-100
    Note over LL: "Redeemed: Diskon Rp 10.000"

    L->>P: Render ledger entries
    P->>P: Timeline: earn → redeem → earn → earn → ...
```

| Field | Tipe | Deskripsi |
| - | - | - |
| `customer_id` | UUID | Reference ke Customer |
| `event_type` | Enum | `earn` / `redeem` / `return_reversal` / `tier_upgrade` / `manual_adjustment` |
| `scheme_type` | Enum | `points` / `stamp` / `spending` / `visits` / `hybrid` |
| `points_delta` | Number | +50 (earn) atau -100 (redeem) |
| `points_before` | Number | Saldo poin sebelum event (audit trail) |
| `points_after` | Number | Saldo poin setelah event (audit trail) |
| `stamps_delta` | Number | +1 (earn stamp) atau -10 (redeem stamp) |
| `stamps_before` | Number | Saldo stamp sebelum event |
| `stamps_after` | Number | Saldo stamp setelah event |
| `eligible_amount` | Number | Nilai transaksi yang sah untuk perhitungan poin |
| `transaction_id` | UUID | Link ke CompanyPOSTransaction |
| `transaction_number` | String | Nomor struk / receipt number |
| `idempotency_key` | UUID | Pencegah double-posting |
| `reward_details` | Object | Detail reward jika event\_type = redeem |
| `deficit_points` | Number | Defisit jika void setelah poin terpakai |
| `requires_manual_review` | Boolean | Flag butuh review admin |
| `performed_by` | String | Operator yang memproses |
| `actor_role` | String | Role: owner, admin, cashier |
| `finance_record_id` | UUID | Link ke FinancialRecord (COGS reward) |
| `reason` | String | Deskripsi/alasan mutasi |
| `created_at` | DateTime | Waktu pencatatan |

## Integrasi dengan POS

```mermaid theme={null}
sequenceDiagram
    participant K as Kasir
    participant POS as CompanyPOSCashier
    participant C as Customer
    participant CM as CustomerMembership
    participant L as Loyalty Engine

    K->>POS: Input nomor member
    POS->>C: Lookup customer by phone
    C-->>POS: membership_level_id, membership_points
    POS->>CM: Lookup tier by membership_level_id
    CM-->>POS: discount_percentage=10%, points_multiplier=1.5

    POS->>POS: Apply 10% discount to cart
    K->>POS: Process payment → finalize

    POS->>L: Post-transaction loyalty update
    L->>L: earned_points = transaction_total / 10000 × 1.5
    L->>C: membership_points += earned_points
    L->>C: total_spent += transaction_total
    L->>L: Check auto-upgrade criteria
    L-->>POS: Return: new balance, tier status
    POS-->>K: "Poin baru: 2.500 (+50)"
```

### Alur Data Integrasi POS → CRM

```mermaid theme={null}
graph LR
    subgraph "POS (Point of Sale)"
        T[Transaksi Selesai]
        V[Void/Return]
    end

    subgraph "Loyalty Engine"
        E[Earn Calculator]
        R[Redeem Processor]
        RV[Reversal Handler]
        U[Upgrade Checker]
    end

    subgraph "CRM Data"
        C[Customer]
        CM[CustomerMembership]
        LL[CustomerLoyaltyLedger]
    end

    T --> E
    T --> U
    V --> RV
    E --> LL
    R --> LL
    RV --> LL
    U --> LL
    E --> C
    R --> C
    RV --> C
    CM --> E
    CM --> R
    CM --> U
```

## Perbedaan dengan Company Membership

| Aspek | Company Membership | Customer Membership |
| - | - | - |
| **Scope** | Konfigurasi tier (admin) | Status pelanggan (customer-facing) |
| **Komponen** | `MembershipLevelManager` — 821 baris | Composited view |
| **User** | Admin/Owner | Pelanggan / Kasir (lookup) |
| **Fungsi** | Setup program loyalty | Lihat status dan progress |
| **Data** | `CustomerMembership` entity (definition) | `Customer` + `CustomerMembership` (instance) |
| **URL** | `/company-membership` | `/customer-membership` |
| **Akses** | Admin only (RLS) | Customer-scoped |
| **Write** | CRUD tier definitions | Read-only (data derived from transactions) |

## Entitas Terkait

| Entitas | Peran | Fields Relevan |
| - | - | - |
| `Customer` | Data pelanggan (tier, poin, LTV) | `membership_level_id`, `membership_points`, `lifetime_value`, `membership_since`, `stamps`, `lifetime_points`, `has_negative_points`, `loyalty_deficit_points`, `last_tier_upgrade_date` |
| `CustomerMembership` | Definisi tier dan benefit | `level_name`, `level_key`, `scheme_type`, `discount_percentage`, `points_multiplier`, `min_purchase`, `points_threshold`, `points_per_threshold`, `stamps_required`, `stamp_per_transaction`, `reward_type`, `reward_value`, `min_redemption_points`, `expiry_days`, `free_delivery`, `priority_support`, `birthday_bonus`, `benefits` |
| `CustomerLoyaltyLedger` | Riwayat transaksi poin/stamp | `event_type`, `scheme_type`, `points_delta`, `points_before`, `points_after`, `stamps_delta`, `eligible_amount`, `transaction_id`, `reward_details`, `deficit_points`, `requires_manual_review`, `idempotency_key`, `performed_by`, `finance_record_id` |
| `CompanyPOSTransaction` | Sumber transaksi yang trigger loyalty | `total_amount`, `member_id`, `payment_status`, `transaction_number`, `transaction_date` |
| `POSMember` | Data member di konteks POS (mirror Customer untuk loyalty) | `membership_tier`, `points`, `stamps`, `total_spent`, `visit_count`, `discount_percentage`, `has_negative_points`, `loyalty_deficit_points` |

## Tips

* **Progress bar adalah motivator utama** — pastikan pelanggan selalu bisa melihat seberapa dekat mereka ke tier berikutnya
* **Riwayat poin memberikan transparansi** — pelanggan bisa verifikasi setiap earn dan redeem
* **Birthday bonus** meningkatkan engagement — pastikan `Customer.birthday` diisi saat registrasi
* **Export riwayat poin** berguna untuk customer service saat ada komplain tentang poin yang hilang
* **Integrasi POS → CRM** harus real-time — pelanggan yang baru transaksi harus langsung lihat poin bertambah
* **Idempotency key** mencegah double-credit saat ada retry network — setiap event loyalty memiliki key unik
* **Loyalty deficit** perlu ditangani segera — flag `requires_manual_review` di ledger menandai transaksi yang butuh rekonsiliasi admin
* **Skema hybrid** memberikan fleksibilitas tertinggi tetapi memerlukan monitoring lebih — pastikan semua sub-skema terkonfigurasi dengan benar
* **Audit trail** di `CustomerLoyaltyLedger` (points\_before, points\_after) memungkinkan rekonstruksi penuh riwayat loyalty pelanggan kapan saja
* **Finance integration** — setiap redeem yang menghasilkan reward tercatat di `FinancialRecord` melalui `finance_record_id`, memastikan COGS reward terakuntansi dengan benar


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