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

# 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: "Membership — Program Loyalty"
description: "Manajemen tier membership dengan 5 skema loyalty (spending, stamp, points, visits, hybrid), benefit konfigurasi, dan auto-upgrade di SNISHOP ERP."
----------------------------------------------------------------------------------------------------------------------------------------------------------------

# Membership — Program Loyalty

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

Halaman Membership mengelola tier membership dan program loyalty perusahaan. Sistem mendukung 5 skema loyalty dengan benefit yang bisa dikustomisasi per tier, termasuk diskon, poin multiplier, dan free delivery.

## Arsitektur

```mermaid theme={null}
flowchart TB
    subgraph MANAGER["MembershipLevelManager"]
        CRUD[CRUD Tier<br/>Create/Edit/Delete]
        DRAG[Drag to Reorder<br/>Priority]
        SCHEME[Scheme Type<br/>5 jenis]
        BEN[Benefits<br/>Configuration]
    end

    subgraph SCHEMES["5 Skema Loyalty"]
        SPD[Spending<br/>Total pembelian]
        STP[Stamp<br/>Stempel per transaksi]
        PNT[Points<br/>Akumulasi poin]
        VIS[Visits<br/>Jumlah kunjungan]
        HYB[Hybrid<br/>Kombinasi]
    end

    subgraph BENEFITS["Benefit per Tier"]
        DIS[Discount %]
        MUL[Points Multiplier]
        BIR[Birthday Bonus]
        PRI[Priority Support]
        FRD[Free Delivery]
        CUS[Custom Benefits]
    end

    subgraph ENTITIES["Entitas"]
        CM[CustomerMembership]
        CU[Customer]
    end

    MANAGER --> SCHEMES
    MANAGER --> BENEFITS
    MANAGER --> CM
    CU -->|Auto-upgrade| CM
```

## Akses Halaman

URL: `/crm` → tab **Loyalty**

## Diagram Relasi Entitas (ERD)

Berikut adalah relasi antar entitas yang terlibat dalam sistem membership SNISHOP ERP:

```mermaid theme={null}
erDiagram
    Company ||--o{ CustomerMembership : "memiliki banyak tier"
    Company ||--o{ Customer : "memiliki banyak pelanggan"
    Company ||--o{ CustomerLoyaltyLedger : "memiliki banyak mutasi"
    Company ||--o{ POSMember : "memiliki banyak member POS"

    CustomerMembership ||--o{ Customer : "dijadikan tier oleh"
    Customer ||--o{ CustomerLoyaltyLedger : "memiliki banyak mutasi loyalty"
    Customer ||--o{ POSMember : "dapat terhubung ke"

    POSMember ||--o{ CustomerLoyaltyLedger : "memiliki banyak mutasi"
    CompanyPOSTransaction ||--o{ CustomerLoyaltyLedger : "menghasilkan mutasi"

    CustomerMembership {
        string company_id PK
        string level_name "Nama tier (Silver, Gold, Platinum)"
        string level_key UK "Key unik (silver, gold, platinum)"
        string icon "Ikon emoji tier"
        string color "Warna representasi tier"
        string description "Penjelasan syarat & keuntungan (max 1000 char)"
        number discount_percentage "Diskon 0-100%"
        number points_multiplier "Pengali poin (1=normal, 2=double)"
        number min_purchase "Minimum pembelian (skema spending)"
        array benefits "Daftar benefit (array of string)"
        boolean priority_support "Akses priority WhatsApp"
        boolean free_delivery "Gratis ongkos kirim"
        number birthday_bonus "Bonus poin di hari ulang tahun"
        boolean is_active "Status aktif tier"
        number order "Urutan prioritas tier"
        enum scheme_type "points|stamp|spending|visits|hybrid"
        number points_threshold "Ambang batas belanja per poin"
        number points_per_threshold "Poin per threshold"
        number stamps_required "Stamp untuk naik level"
        number stamp_per_transaction "Stamp per transaksi eligible"
        number min_redemption_points "Minimum poin tukar reward"
        enum reward_type "none|discount_percentage|discount_amount|free_product"
        number reward_value "Nilai reward (persen/nominal)"
        string reward_product_id "ID produk reward (free_product)"
        string reward_product_name "Nama produk reward"
        number expiry_days "Masa kedaluwarsa poin (hari)"
    }

    Customer {
        string company_id PK
        string name "Nama pelanggan"
        string email "Email pelanggan"
        string phone "Nomor telepon"
        string membership_level_id FK "ID tier membership"
        string membership_level_name "Nama tier saat ini"
        date membership_since "Sejak menjadi member"
        number membership_points "Saldo poin aktif"
        number lifetime_points "Total poin sepanjang waktu"
        number stamps "Saldo stamp aktif"
        boolean has_negative_points "Flag saldo negatif"
        number loyalty_deficit_points "Defisit poin perlu rekonsiliasi"
        string loyalty_review_notes "Catatan review deficit"
        datetime 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"
        number lifetime_value "Total nilai transaksi"
        number total_orders "Jumlah transaksi"
    }

    CustomerLoyaltyLedger {
        string company_id PK
        string customer_id FK "ID pemilik mutasi"
        string customer_name "Nama customer saat transaksi"
        string customer_phone "Kontak customer"
        string transaction_id FK "ID transaksi POS"
        string transaction_number "Nomor struk"
        string idempotency_key UK "Pencegah double-posting"
        enum event_type "earn|redeem|return_reversal|tier_upgrade|manual_adjustment"
        enum 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"
        number stamps_after "Saldo stamp setelah"
        number eligible_amount "Nilai transaksi eligible"
        string reason "Alasan mutasi"
        number deficit_points "Defisit jika void setelah redeem"
        boolean requires_manual_review "Flag butuh review manual"
        object reward_details "Detail reward saat redeem"
        string performed_by "Operator yang memproses"
        string actor_role "Role aktor"
        datetime created_at "Waktu event di server"
        string finance_record_id FK "ID catatan keuangan terkait"
    }

    POSMember {
        string name "Nama member POS"
        string phone "Nomor telepon"
        string email "Email member"
        string membership_tier "regular|silver|gold|platinum"
        number points "Saldo poin saat ini"
        number lifetime_points "Total poin sepanjang waktu"
        number stamps "Saldo stamp"
        boolean has_negative_points "Flag saldo negatif"
        number loyalty_deficit_points "Defisit poin"
        number total_spent "Total belanja"
        number visit_count "Jumlah kunjungan"
        number discount_percentage "Diskon khusus member"
    }
```

## Entitas: CustomerMembership (Schema Lengkap)

Entitas `CustomerMembership` mendefinisikan setiap tier dalam program loyalty. Setiap tier memiliki konfigurasi skema, kriteria upgrade, benefit, dan reward yang independen. Berikut adalah schema lengkap seluruh field:

### Field Identitas & Visual

| Field | Tipe | Default | Wajib | Deskripsi |
| - | - | - | - | - |
| `company_id` | string | — | **Ya** | ID perusahaan pemilik tier ini. Setiap tier terisolasi per company. |
| `level_name` | string | — | **Ya** | Nama tampilan tier, misal "Silver", "Gold", "Platinum", "Diamond". |
| `level_key` | string | — | **Ya** | Key unik untuk referensi programatik, misal `silver`, `gold`, `platinum`. Harus unik dalam satu company. |
| `icon` | string | — | Tidak | Ikon emoji untuk tier (misal "🥈", "🥇", "💎"). Ditampilkan di POS dan dashboard. |
| `color` | string | — | Tidak | Kode warna hex/nama untuk tier (misal "#C0C0C0", "gold"). Digunakan di UI badge dan kartu member. |
| `description` | string | — | Tidak | Penjelasan lengkap mengenai keuntungan dan syarat keanggotaan pada level ini. Maksimal 1.000 karakter. |
| `is_active` | boolean | `true` | Tidak | Status aktif tier. Tier non-aktif tidak bisa dijadikan target upgrade namun tetap berlaku untuk member yang sudah berada di tier tersebut. |
| `order` | number | `0` | Tidak | Urutan tier dalam hierarki. Semakin tinggi nilainya, semakin premium tier tersebut. Digunakan untuk sorting dan penentuan arah upgrade/downgrade. |

### Field Benefit & Diskon

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `discount_percentage` | number | `0` | Persentase diskon otomatis di POS (range 0–100). Diterapkan ke subtotal transaksi sebelum pajak. |
| `points_multiplier` | number | `1` | Pengali perolehan poin per transaksi. Nilai 1 = normal, 2 = double points, 3 = triple points. |
| `benefits` | array\[string] | — | Daftar benefit dalam bentuk teks bebas. Ditampilkan di kartu member dan halaman profil pelanggan. |
| `priority_support` | boolean | `false` | Hak akses antrian prioritas via WhatsApp dedicated. |
| `free_delivery` | boolean | `false` | Gratis ongkos kirim untuk semua pesanan (delivery & marketplace). |
| `birthday_bonus` | number | `0` | Bonus poin tambahan yang diberikan di hari ulang tahun customer. Nilai 0 berarti tidak ada bonus. |

### Field Skema Spending

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `min_purchase` | number | `0` | Minimum akumulasi pembelian (`lifetime_value`) untuk mencapai tier ini. Digunakan pada skema `spending` dan sebagai salah satu kriteria `hybrid`. |

### Field Skema Points

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `points_threshold` | number | `10000` | Ambang batas nilai belanja (dalam Rupiah) untuk memperoleh poin. Misal Rp 10.000 = setiap kelipatan Rp 10.000 dari transaksi eligible menghasilkan poin. |
| `points_per_threshold` | number | `1` | Jumlah poin yang diperoleh per threshold tercapai. Misal 1 poin per Rp 10.000. |
| `min_redemption_points` | number | `0` | Minimum poin yang harus dimiliki customer sebelum bisa melakukan penukaran reward. Nilai 0 berarti tidak ada minimum. |

### Field Skema Stamp

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `stamps_required` | number | `10` | Jumlah stamp yang dibutuhkan untuk naik tier atau menukar reward pada skema stamp. |
| `stamp_per_transaction` | number | `1` | Jumlah stamp yang diperoleh customer per satu transaksi eligible. Bisa lebih dari 1 untuk transaksi dengan nilai tertentu. |

### Field Reward

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `reward_type` | enum | `"none"` | Jenis reward yang bisa ditukarkan: `none` (tidak ada), `discount_percentage` (diskon persen), `discount_amount` (diskon nominal), `free_product` (produk gratis). |
| `reward_value` | number | `0` | Nilai reward — berupa persentase (untuk `discount_percentage`) atau nominal Rupiah (untuk `discount_amount`). |
| `reward_product_id` | string | — | ID produk yang diberikan sebagai reward jika `reward_type = "free_product"`. Referensi ke entitas POSProduct. |
| `reward_product_name` | string | — | Nama produk reward gratis (disimpan sebagai denormalisasi agar tetap tersedia meski produk dihapus). |

### Field Skema Visits & Kedaluwarsa

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `scheme_type` | enum | `"points"` | Skema loyalty yang berlaku pada tier ini: `points`, `stamp`, `spending`, `visits`, atau `hybrid`. |
| `expiry_days` | number | — | Masa kedaluwarsa poin dalam hari. Jika diisi, poin yang tidak digunakan akan hangus setelah N hari sejak perolehan. Kosong = tidak ada kedaluwarsa. |

### Total: 25 Field

Entitas `CustomerMembership` memiliki **25 field** yang mencakup identitas, visual, benefit, 5 jenis skema loyalty, konfigurasi reward, dan kedaluwarsa poin.

## Entitas: Customer (Field Membership)

Entitas `Customer` memiliki beberapa field khusus yang terhubung dengan sistem membership:

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `membership_level_id` | string | — | Foreign key ke `CustomerMembership._id`. Menentukan tier aktif pelanggan. |
| `membership_level_name` | string | — | Denormalisasi nama tier untuk performa query (tidak perlu join). |
| `membership_since` | date | — | Tanggal pertama kali customer terdaftar sebagai member. |
| `membership_points` | number | `0` | Saldo poin aktif yang bisa digunakan untuk redeem reward. |
| `lifetime_points` | number | `0` | Total poin yang pernah diperoleh sepanjang waktu (tidak berkurang saat redeem). |
| `stamps` | number | `0` | Saldo stamp aktif untuk skema stamp. |
| `has_negative_points` | boolean | `false` | Flag yang menandakan saldo poin negatif akibat pembatalan/void transaksi setelah poin terpakai. |
| `loyalty_deficit_points` | number | `0` | Besaran defisit poin yang membutuhkan rekonsiliasi manual oleh admin. |
| `loyalty_review_notes` | string | — | Catatan review dari admin/operator terkait penanganan loyalty deficit. |
| `last_tier_upgrade_date` | datetime | — | Timestamp perubahan tier terakhir (upgrade atau downgrade). |
| `last_tier_upgrade_reason` | string | — | Alasan perubahan tier, misal "Auto-upgrade: spending threshold met" atau "Manual downgrade oleh admin". |
| `last_tier_upgrade_by` | string | — | Email operator atau admin yang melakukan perubahan tier. Untuk auto-upgrade, diisi oleh sistem. |
| `lifetime_value` | number | `0` | Total nilai transaksi sepanjang waktu — menjadi kriteria utama skema spending. |
| `total_orders` | number | `0` | Jumlah total transaksi — menjadi kriteria utama skema visits. |

## Entitas: CustomerLoyaltyLedger (Audit Trail)

Setiap mutasi loyalty (perolehan, penukaran, pembatalan, penyesuaian) dicatat di `CustomerLoyaltyLedger` sebagai audit trail lengkap. Entitas ini menjamin traceability penuh atas setiap perubahan saldo poin dan stamp.

### Field Utama

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `company_id` | string | — | ID perusahaan (multi-tenant isolation). |
| `customer_id` | string | — | ID customer pemilik mutasi loyalty. |
| `customer_name` | string | — | Nama customer saat transaksi (denormalisasi untuk audit). |
| `customer_phone` | string | — | Nomor telepon customer saat transaksi. |
| `transaction_id` | string | — | ID transaksi POS yang menghasilkan mutasi ini. |
| `transaction_number` | string | — | Nomor struk / receipt number transaksi. |
| `idempotency_key` | string | — | **Wajib.** Key unik untuk mencegah double-crediting atau double-posting. Setiap mutasi harus memiliki key yang unik. |
| `event_type` | enum | — | **Wajib.** Tipe event mutasi: `earn`, `redeem`, `return_reversal`, `tier_upgrade`, `manual_adjustment`. |
| `scheme_type` | enum | `"points"` | Skema loyalty yang berlaku saat event terjadi. |

### Field Saldo (Before/After Snapshot)

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `points_delta` | number | `0` | Perubahan poin: positif = bertambah, negatif = berkurang. |
| `points_before` | number | `0` | Saldo poin sebelum event terjadi. |
| `points_after` | number | `0` | Saldo poin setelah event terjadi. |
| `stamps_delta` | number | `0` | Perubahan stamp: positif = bertambah, negatif = berkurang. |
| `stamps_before` | number | `0` | Saldo stamp sebelum event. |
| `stamps_after` | number | `0` | Saldo stamp setelah event. |
| `eligible_amount` | number | `0` | Nilai transaksi yang sah (eligible) untuk perhitungan perolehan poin. |

### Field Reward & Deficit

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `reward_details` | object | — | Detail reward saat event = `redeem`. Berisi `reward_type`, `discount_amount`, `discount_percentage`, `product_id`, `product_name`, `cost_points`, `cost_stamps`. |
| `deficit_points` | number | `0` | Defisit poin yang terjadi jika transaksi di-void setelah poin sudah terpakai. |
| `requires_manual_review` | boolean | `false` | Flag yang menandakan transaksi reversal menghasilkan saldo negatif dan butuh review manual oleh admin. |
| `reason` | string | — | Alasan atau keterangan mutasi (wajib untuk `manual_adjustment`). |

### Field Audit

| Field | Tipe | Deskripsi |
| - | - | - |
| `performed_by` | string | Email atau ID operator yang memproses mutasi. |
| `actor_role` | string | Role aktor: `owner`, `admin`, `cashier`. |
| `created_at` | datetime | Waktu event tercatat di server. |
| `finance_record_id` | string | ID FinancialRecord terkait (untuk event `redeem` yang mem-posting biaya COGS reward ke modul keuangan). |

## Entitas: POSMember

Entitas `POSMember` adalah representasi member yang terdaftar langsung melalui modul POS (kasir). Memiliki field yang mirip dengan `Customer` namun lebih sederhana dan terfokus pada operasional kasir.

| Field | Tipe | Default | Deskripsi |
| - | - | - | - |
| `name` | string | — | **Wajib.** Nama member POS. |
| `phone` | string | — | **Wajib.** Nomor telepon member. |
| `email` | string | — | Alamat email member. |
| `address` | string | — | Alamat lengkap. |
| `birth_date` | date | — | Tanggal lahir (untuk birthday bonus). |
| `membership_tier` | enum | `"regular"` | Tier member: `regular`, `silver`, `gold`, `platinum`. |
| `points` | number | `0` | Saldo poin aktif. |
| `lifetime_points` | number | `0` | Total poin sepanjang waktu. |
| `stamps` | number | `0` | Saldo stamp aktif. |
| `has_negative_points` | boolean | `false` | Flag saldo negatif. |
| `loyalty_deficit_points` | number | `0` | Defisit poin setelah retur/void. |
| `loyalty_review_notes` | string | — | Catatan review deficit. |
| `last_tier_upgrade_date` | datetime | — | Waktu perubahan tier terakhir. |
| `last_tier_upgrade_reason` | string | — | Alasan perubahan tier. |
| `last_tier_upgrade_by` | string | — | Email operator yang mengubah tier. |
| `total_spent` | number | `0` | Total nilai belanja. |
| `visit_count` | number | `0` | Jumlah kunjungan/transaksi. |
| `last_visit` | datetime | — | Waktu kunjungan terakhir. |
| `discount_percentage` | number | `0` | Diskon khusus member (di-set berdasarkan tier). |
| `notes` | string | — | Catatan tambahan tentang member. |
| `is_active` | boolean | `true` | Status keaktifan member. |

## State Machine: Tier Progression

Berikut adalah diagram state machine untuk progresi tier membership customer:

```mermaid theme={null}
stateDiagram-v2
    [*] --> NonMember: Customer baru terdaftar

    state MemberProgram {
        NonMember --> Regular: Registrasi member\n(poin = 0, stamps = 0)
        Regular --> Silver: Kriteria tier Silver terpenuhi
        Silver --> Gold: Kriteria tier Gold terpenuhi
        Gold --> Platinum: Kriteria tier Platinum terpenuhi
        Platinum --> Diamond: Kriteria tier Diamond terpenuhi

        Diamond --> Gold: Downgrade manual oleh admin\n(spending turun / inactive)
        Platinum --> Silver: Downgrade manual oleh admin
        Gold --> Regular: Downgrade manual oleh admin
        Silver --> Regular: Downgrade manual oleh admin

        Regular --> Regular: Transaksi POS\n(earn poin/stamp)
        Silver --> Silver: Transaksi POS\n(earn poin/stamp)
        Gold --> Gold: Transaksi POS\n(earn poin/stamp)
        Platinum --> Platinum: Transaksi POS\n(earn poin/stamp)
        Diamond --> Diamond: Transaksi POS\n(earn poin/stamp)
    }

    MemberProgram --> Inactive: Tidak aktif > 365 hari\natau dinonaktifkan admin
    Inactive --> Regular: Re-aktivasi oleh admin
    Inactive --> [*]
```

### Transisi State — Detail

| Dari | Ke | Trigger | Kondisi |
| - | - | - | - |
| `[*]` | NonMember | Registrasi customer baru | Customer dibuat dengan `membership_level_id = null` |
| NonMember | Regular | Registrasi member | Customer diberi `membership_level_id` ke tier terendah |
| Regular | Silver+ | Auto-upgrade | Kriteria tier berikutnya terpenuhi (lihat skema di bawah) |
| Silver+ | Gold+ | Auto-upgrade | Kriteria tier berikutnya terpenuhi |
| Tier tinggi | Tier rendah | Manual downgrade | Admin melakukan downgrade dengan alasan yang dicatat |
| Active | Inactive | Timeout / manual | Tidak aktif > 365 hari atau dinonaktifkan oleh admin |
| Inactive | Regular | Re-aktivasi | Admin mengaktifkan kembali customer |

## 5 Skema Loyalty — Perbandingan Lengkap

| Aspek | Spending | Stamp | Points | Visits | Hybrid |
| - | - | - | - | - | - |
| **Kriteria Upgrade** | `lifetime_value >= min_purchase` | `stamps >= stamps_required` | `membership_points >= points_required` | `total_orders >= visits_required` | Salah satu kriteria terpenuhi |
| **Field Kunci** | `min_purchase` | `stamps_required`, `stamp_per_transaction` | `points_threshold`, `points_per_threshold` | `visits_required` (via `total_orders`) | Kombinasi semua field |
| **Satuan Metrik** | Rupiah (Rp) | Jumlah stempel | Jumlah poin | Jumlah kunjungan | Multi-metrik |
| **Perolehan** | Otomatis dari total transaksi | +`stamp_per_transaction` per trx | `eligible_amount / points_threshold * points_per_threshold * multiplier` | +1 per transaksi | Tergantung skema tier |
| **Cocok Untuk** | F\&B premium, restoran fine dining | Coffee shop, bakery, quick service | Retail, supermarket, F\&B general | Fast food, warung, kantin | Bisnis dengan multi-channel |
| **Kelebihan** | Mencerminkan nilai pelanggan | Sederhana, mudah dipahami | Fleksibel, bisa untuk redeem | Mendorong frekuensi kunjungan | Fleksibel, customer punya banyak jalur |
| **Kekurangan** | Customer kecil sulit upgrade | Tidak mencerminkan nilai transaksi | Perlu perhitungan lebih kompleks | Tidak mencerminkan nilai per trx | Konfigurasi lebih rumit |

### Skema Spending

```
Upgrade jika: customer.lifetime_value >= tier.min_purchase
```

| Tier | min\_purchase |
| - | - |
| Silver | Rp 1.000.000 |
| Gold | Rp 5.000.000 |
| Platinum | Rp 20.000.000 |

### Skema Stamp

```
Upgrade jika: customer.membership_points >= tier.stamps_required
Setiap transaksi: customer.membership_points += tier.stamp_per_transaction
```

| Tier | stamps\_required | stamp\_per\_transaction |
| - | - | - |
| Silver | 10 | 1 |
| Gold | 25 | 2 |
| Platinum | 50 | 3 |

### Skema Points

```
Upgrade jika: customer.membership_points >= tier.points_required
```

Poin diperoleh dari transaksi dengan multiplier sesuai tier.

### Skema Visits

```
Upgrade jika: customer.total_orders >= tier.visits_required
```

### Skema Hybrid

```
Upgrade jika: SALAH SATU kriteria terpenuhi
  - lifetime_value >= min_purchase
  - membership_points >= points_required
  - total_orders >= visits_required
```

## Sistem Reward — 4 Jenis Reward

Sistem membership SNISHOP ERP mendukung 4 jenis reward yang bisa dikonfigurasi per tier melalui field `reward_type`:

### 1. Discount Percentage (`discount_percentage`)

Reward berupa potongan harga dalam persentase dari subtotal transaksi.

| Aspek | Detail |
| - | - |
| **Field** | `reward_type = "discount_percentage"`, `reward_value = 10` (untuk 10%) |
| **Mekanisme** | Customer menukarkan poin → mendapat voucher diskon persentase |
| **Berlaku untuk** | Satu transaksi atau periode tertentu |
| **Contoh** | Tukar 500 poin → diskon 10% untuk transaksi berikutnya |

### 2. Discount Amount (`discount_amount`)

Reward berupa potongan harga dalam nominal Rupiah tetap.

| Aspek | Detail |
| - | - |
| **Field** | `reward_type = "discount_amount"`, `reward_value = 25000` (untuk Rp 25.000) |
| **Mekanisme** | Customer menukarkan poin → mendapat voucher diskon nominal tetap |
| **Berlaku untuk** | Satu transaksi dengan minimum pembelian tertentu |
| **Contoh** | Tukar 300 poin → diskon Rp 25.000 untuk min. pembelian Rp 100.000 |

### 3. Free Product (`free_product`)

Reward berupa produk gratis yang sudah ditentukan.

| Aspek | Detail |
| - | - |
| **Field** | `reward_type = "free_product"`, `reward_product_id`, `reward_product_name` |
| **Mekanisme** | Customer menukarkan poin → mendapat produk spesifik secara gratis |
| **Berlaku untuk** | Satu produk yang sudah dikonfigurasi |
| **Contoh** | Tukar 200 poin → gratis 1x Es Teh Manis (reward\_product\_id = "prod\_xxx") |

### 4. None (`none`)

Tidak ada reward yang bisa ditukarkan — tier ini hanya memberikan benefit pasif (diskon otomatis, multiplier, dll).

| Aspek | Detail |
| - | - |
| **Field** | `reward_type = "none"` |
| **Mekanisme** | Customer tidak bisa menukarkan poin, namun tetap mendapat benefit tier |
| **Berlaku untuk** | Tier yang hanya mengandalkan benefit pasif |
| **Contoh** | Tier Silver: diskon 5% otomatis, tanpa opsi redeem |

### Perbandingan 4 Jenis Reward

| Kriteria | Discount % | Discount Amount | Free Product | None |
| - | - | - | - | - |
| **Fleksibilitas** | Tinggi (semua produk) | Tinggi (semua produk) | Rendah (1 produk) | N/A |
| **Prediksi Biaya** | Variabel (% dari trx) | Tetap (nominal) | Tetap (HPP produk) | Nol |
| **Daya Tarik** | Tinggi | Tinggi | Sangat tinggi (konkret) | Rendah |
| **Kompleksitas** | Sedang | Sedang | Tinggi (butuh stock) | Nol |
| **COGS Tracking** | Via `finance_record_id` | Via `finance_record_id` | Via `finance_record_id` | Tidak ada |

## Benefit Konfigurasi

Setiap tier bisa memiliki benefit yang berbeda:

| Benefit | Tipe | Deskripsi |
| - | - | - |
| **Discount %** | number | Persentase diskon di POS |
| **Points Multiplier** | number | Pengali perolehan poin |
| **Birthday Bonus** | boolean | Poin ganda di bulan ulang tahun |
| **Priority Support** | boolean | Akses antrian prioritas |
| **Free Delivery** | boolean | Gratis ongkos kirim |
| **Custom Benefits** | array | Benefit kustom (text) |

### Contoh Tier

| Tier | Discount | Multiplier | Free Delivery | Priority |
| - | - | - | - | - |
| Silver | 5% | 1x | — | — |
| Gold | 10% | 1.5x | ✓ | — |
| Platinum | 15% | 2x | ✓ | ✓ |
| Diamond | 20% | 3x | ✓ | ✓ |

## Sequence Diagram: Alur Upgrade Tier

Berikut adalah sequence diagram lengkap untuk proses auto-upgrade dan manual downgrade tier membership:

### Auto-Upgrade Flow

```mermaid theme={null}
sequenceDiagram
    participant Cashier as Kasir (POS)
    participant POS as Modul POS
    participant Engine as Loyalty Engine
    participant CM as CustomerMembership
    participant Cust as Customer
    participant Ledger as CustomerLoyaltyLedger

    Cashier->>POS: Input transaksi + pilih customer
    POS->>Cust: Lookup customer by phone/name
    Cust-->>POS: Return customer data (tier, points, lifetime_value)
    POS->>CM: Get tier config (scheme_type, min_purchase, multiplier)
    CM-->>POS: Return tier configuration

    POS->>Engine: Calculate eligible_amount & points
    Engine->>Engine: eligible_amount = subtotal - excluded_items
    Engine->>Engine: points = (eligible_amount / points_threshold) * points_per_threshold * multiplier
    Engine-->>POS: Return calculated points

    POS->>Cust: Update membership_points += points
    POS->>Cust: Update lifetime_value += eligible_amount
    POS->>Cust: Update total_orders += 1
    POS->>Ledger: Create ledger entry (event_type: "earn")
    Note over Ledger: points_before → points_after<br/>idempotency_key = trx_id + "earn"

    POS->>Engine: Check upgrade criteria
    Engine->>CM: Get next tier criteria
    CM-->>Engine: Return next tier (min_purchase, points_required, etc.)
    Engine->>Engine: Evaluate: lifetime_value >= min_purchase?

    alt Kriteria terpenuhi & auto_upgrade = true
        Engine->>Cust: Update membership_level_id = next_tier._id
        Engine->>Cust: Update membership_level_name = next_tier.level_name
        Engine->>Cust: Update last_tier_upgrade_date = now()
        Engine->>Cust: Update last_tier_upgrade_reason = "Auto-upgrade"
        Engine->>Ledger: Create ledger entry (event_type: "tier_upgrade")
        Engine-->>POS: Return upgrade result
        POS-->>Cashier: Tampilkan notifikasi "Customer naik ke tier [Gold]!"
    else Kriteria belum terpenuhi
        Engine-->>POS: Return no upgrade
        POS-->>Cashier: Transaksi selesai (normal)
    end
```

### Manual Downgrade Flow

```mermaid theme={null}
sequenceDiagram
    participant Admin as Admin / Owner
    participant CRM as Modul CRM
    participant Cust as Customer
    participant Ledger as CustomerLoyaltyLedger

    Admin->>CRM: Buka profil customer
    CRM->>Cust: Fetch customer + current tier
    Cust-->>CRM: Return customer data
    CRM-->>Admin: Tampilkan detail tier & riwayat loyalty

    Admin->>CRM: Klik "Ubah Tier" → pilih tier baru (downgrade)
    CRM-->>Admin: Form konfirmasi: tier lama → tier baru + alasan
    Admin->>CRM: Input alasan downgrade + konfirmasi

    CRM->>Cust: Update membership_level_id = new_tier._id
    CRM->>Cust: Update membership_level_name = new_tier.level_name
    CRM->>Cust: Update last_tier_upgrade_date = now()
    CRM->>Cust: Update last_tier_upgrade_reason = alasan yang diinput
    CRM->>Cust: Update last_tier_upgrade_by = admin.email
    CRM->>Ledger: Create ledger entry (event_type: "tier_upgrade", reason: downgrade)
    Note over Ledger: Snapshot points_before/after<br/>performed_by = admin.email<br/>actor_role = "admin"

    CRM-->>Admin: Konfirmasi "Tier berhasil diubah ke [Silver]"
```

### Reward Redemption Flow

```mermaid theme={null}
sequenceDiagram
    participant Cashier as Kasir (POS)
    participant POS as Modul POS
    participant Cust as Customer
    participant CM as CustomerMembership
    participant Ledger as CustomerLoyaltyLedger
    participant Finance as Modul Keuangan

    Cashier->>POS: Customer ingin redeem reward
    POS->>Cust: Fetch membership data
    Cust-->>POS: Return points, stamps, tier info
    POS->>CM: Get reward config (reward_type, reward_value, min_redemption)
    CM-->>POS: Return reward configuration

    alt reward_type = "discount_percentage"
        POS-->>Cashier: Tampilkan opsi: "Tukar [X] poin → Diskon [Y]%"
        Cashier->>POS: Konfirmasi redeem
        POS->>Cust: Update membership_points -= cost_points
        POS->>POS: Apply discount_percentage ke transaksi ini
        POS->>Ledger: Create entry (event_type: "redeem", reward_details: {type, percentage})
    else reward_type = "discount_amount"
        POS-->>Cashier: Tampilkan opsi: "Tukar [X] poin → Diskon Rp [Y]"
        Cashier->>POS: Konfirmasi redeem
        POS->>Cust: Update membership_points -= cost_points
        POS->>POS: Apply discount_amount ke transaksi ini
        POS->>Ledger: Create entry (event_type: "redeem", reward_details: {type, amount})
    else reward_type = "free_product"
        POS-->>Cashier: Tampilkan opsi: "Tukar [X] poin → Gratis [Product Name]"
        Cashier->>POS: Konfirmasi redeem
        POS->>Cust: Update membership_points -= cost_points
        POS->>POS: Add free product (reward_product_id) ke transaksi
        POS->>Ledger: Create entry (event_type: "redeem", reward_details: {product_id, product_name})
        POS->>Finance: Post COGS reward cost (finance_record_id)
    end

    POS->>Cust: Check has_negative_points
    alt Saldo menjadi negatif
        POS->>Cust: Set has_negative_points = true
        POS->>Cust: Set loyalty_deficit_points = abs(negative_value)
        POS->>Ledger: Flag requires_manual_review = true
    end

    POS-->>Cashier: Transaksi selesai dengan reward diterapkan
```

## Fitur Manager

### CRUD Tier

| Operasi | Deskripsi |
| - | - |
| **Create** | Tambah tier baru dengan form |
| **Edit** | Ubah konfigurasi tier |
| **Delete** | Hapus tier (dengan konfirmasi) |

### Drag to Reorder

Tier bisa di-drag untuk mengubah urutan prioritas. Urutan ini menentukan level mana yang lebih tinggi dalam hierarki.

### Auto-Upgrade

Jika `auto_upgrade = true`, sistem otomatis menaikkan tier pelanggan saat kriteria terpenuhi:

```mermaid theme={null}
flowchart LR
    TX[Transaksi POS] --> CHK{Check Criteria}
    CHK -->|Met| UP[Auto Upgrade]
    UP --> UPD[Update Customer<br/>membership_level]
    CHK -->|Belum Met| WAIT[Tetap di Tier Lama]
```

## Integrasi dengan POS

Saat pelanggan melakukan transaksi di POS:

1. POS lookup customer berdasarkan nama/telepon
2. Baca `membership_level_id` dan `discount_percentage`
3. Apply diskon otomatis ke total transaksi
4. Tambah poin/stamp berdasarkan `points_multiplier`
5. Cek apakah kriteria upgrade terpenuhi → auto-upgrade jika ya

## Stamp Scheme Persistence

Untuk skema stamp, tag `SCHEME:STAMP` di-inject ke array `benefits` sebagai mekanisme persistence untuk identifikasi skema.

## Penanganan Loyalty Deficit

Ketika transaksi di-void atau diretur setelah poin/stamp sudah terpakai (redeem), sistem mencatat defisit:

```mermaid theme={null}
flowchart TD
    A[Transaksi di-void/retur] --> B{Poin sudah terpakai?}
    B -->|Belum| C[Reverse poin: points_delta negatif]
    B -->|Sudah| D[Hitung deficit = poin terpakai - poin yang di-reverse]
    D --> E{Saldo menjadi negatif?}
    E -->|Ya| F[Set has_negative_points = true<br/>Set loyalty_deficit_points]
    E -->|Tidak| G[Update saldo normal]
    F --> H[Set requires_manual_review = true]
    H --> I[Admin review & rekonsiliasi manual]
    C --> J[Update ledger: event_type = return_reversal]
    G --> J
```

### Skenario Deficit

| Skenario | Kondisi | Penanganan |
| - | - | - |
| Void sebelum redeem | Poin masih utuh | Reverse otomatis, saldo kembali normal |
| Void setelah redeem sebagian | Poin sudah dipakai sebagian | Reverse poin yang tersisa, catat deficit |
| Void setelah redeem penuh | Poin sudah habis dipakai | Saldo jadi negatif, butuh review manual |
| Retur produk | Produk dikembalikan | Hitung ulang poin eligible, reverse selisih |

## Tips Penggunaan

* Mulai dengan 3-4 tier agar program loyalty tidak terlalu kompleks
* Tetapkan `min_purchase` yang realistis berdasarkan data historis
* Berikan benefit yang berbeda signifikan antar tier untuk motivasi upgrade
* Aktifkan `auto_upgrade` agar pelanggan otomatis naik tier
* Review dan sesuaikan kriteria tier setiap kuartal
* Gunakan birthday bonus untuk meningkatkan engagement
* Manfaatkan `CustomerLoyaltyLedger` untuk audit trail — setiap mutasi tercatat dengan `points_before` dan `points_after` sehingga bisa dilacak sepenuhnya
* Untuk skema stamp, pastikan `stamp_per_transaction` sesuai dengan rata-rata nilai transaksi agar program terasa adil
* Gunakan `expiry_days` untuk mendorong customer aktif menukarkan poin sebelum hangus
* Selalu monitor `loyalty_deficit_points` dan segera lakukan rekonsiliasi manual agar saldo member tetap akurat
* Pertimbangkan skema `hybrid` jika bisnis memiliki multi-channel (dine-in, delivery, marketplace) agar customer punya banyak jalur untuk naik tier


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