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

# Company 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: "Company Membership — Konfigurasi Membership Perusahaan"
description: "MembershipLevelManager.jsx (821 baris) — CRUD CustomerMembership entity, 5 skema loyalty (spending/stamp/points/visits/hybrid), auto-upgrade tier, multi-company isolation, dan integrasi real-time dengan POS."
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

# Company Membership — Konfigurasi Membership Perusahaan

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

Halaman Company Membership (`MembershipLevelManager.jsx` — **821 baris**) mengelola konfigurasi membership level di tingkat perusahaan. Komponen ini merupakan interface untuk entitas `CustomerMembership` yang di-scope per `company_id`, memungkinkan setiap perusahaan memiliki program loyalty yang benar-benar independen dengan **5 skema loyalty**, **auto-upgrade tier**, dan **benefit kustom**.

Konfigurasi yang dibuat di halaman ini langsung berdampak ke seluruh transaksi POS — saat kasir memasukkan nomor member, sistem otomatis lookup tier dari `CustomerMembership` dan apply diskon + poin multiplier yang sesuai.

## Arsitektur Komponen

```mermaid theme={null}
graph TB
    subgraph "MembershipLevelManager.jsx — 821 lines"
        direction TB
        LIST[Tier List<br/>Sorted by order field]
        FORM[Tier Form Modal<br/>Create / Edit]
        PREVIEW[Benefit Preview<br/>Real-time calculation]
        SCHEME[Scheme Selector<br/>5 types]
        DELETE[Delete Confirmation<br/>+ cascade check]
    end

    subgraph "CustomerMembership Entity"
        direction TB
        F1[Identity: level_name, level_key, icon, color]
        F2[Benefit: discount_percentage, points_multiplier]
        F3[Criteria: min_purchase, stamps_required, points_required, visits_required]
        F4[Perks: priority_support, free_delivery, birthday_bonus]
        F5[Custom: benefits[] array]
    end

    subgraph "Consumers"
        C1[POS Cashier<br/>Apply discount at checkout]
        C2[Customer Portal<br/>Show tier & progress]
        C3[Loyalty Engine<br/>Auto-upgrade check]
    end

    LIST --> FORM
    FORM --> SCHEME
    FORM --> F1 & F2 & F3 & F4 & F5
    F2 --> C1
    F3 --> C3
    F1 --> C2
```

## Akses Halaman

| Metode | Detail |
| - | - |
| **URL** | `/company-membership` |
| **Sidebar** | Menu **CRM** → **Company Membership** |
| **CRM Tab** | Tab "Loyalty" di halaman `/crm` |

## Entitas: CustomerMembership

| Group | Field | Tipe | Keterangan |
| - | - | - | - |
| **Identitas** | `level_name` | String | Nama tier (contoh: "Gold") |
| | `level_key` | String | Key unik (contoh: "gold") |
| | `icon` | String | Emoji/icon tier (🥇, 🥈, 🥉) |
| | `color` | String | Warna hex badge (#FFD700) |
| **Benefit** | `discount_percentage` | Decimal | Diskon otomatis (0-100) |
| | `points_multiplier` | Decimal | Pengali poin (1.0, 1.5, 2.0) |
| **Kriteria** | `min_purchase` | Decimal | Minimum total belanja untuk upgrade |
| | `stamps_required` | Integer | Minimum stamp untuk upgrade |
| | `points_required` | Integer | Minimum poin untuk upgrade |
| | `visits_required` | Integer | Minimum kunjungan untuk upgrade |
| **Skema** | `scheme_type` | Enum | spending / stamp / points / visits / hybrid |
| **Perks** | `priority_support` | Boolean | Akses support prioritas |
| | `free_delivery` | Boolean | Gratis ongkir |
| | `birthday_bonus` | Boolean | Bonus poin saat ulang tahun |
| **Custom** | `benefits` | Array | Daftar benefit kustom (string\[]) |
| **Status** | `is_active` | Boolean | Aktif/nonaktif |
| | `order` | Integer | Urutan hierarki (1 = terendah) |
| **Auto** | `auto_upgrade` | Boolean | Auto-upgrade saat kriteria terpenuhi |
| **Scope** | `company_id` | UUID | Multi-tenant isolation |

## 5 Skema Loyalty

```mermaid theme={null}
graph TB
    subgraph "1. spending"
        SP[Total Belanja] --> SP1{≥ min_purchase?}
        SP1 -->|Ya| SP2[Upgrade Tier]
        SP1 -->|Tidak| SP3[Tetap]
    end

    subgraph "2. stamp"
        ST[Stamp per Transaksi] --> ST1{≥ stamps_required?}
        ST1 -->|Ya| ST2[Upgrade Tier]
        ST1 -->|Tidak| ST3[Stamp Bertambah]
    end

    subgraph "3. points"
        PT[Poin per Rupiah] --> PT1{≥ points_required?}
        PT1 -->|Ya| PT2[Upgrade Tier]
        PT1 -->|Tidak| PT3[Poin Bertambah]
    end

    subgraph "4. visits"
        VT[Frek. Kunjungan] --> VT1{≥ visits_required?}
        VT1 -->|Ya| VT2[Upgrade Tier]
        VT1 -->|Tidak| VT3[Visit Bertambah]
    end

    subgraph "5. hybrid"
        HY[Salah Satu Kriteria] --> HY1{Any met?}
        HY1 -->|Ya| HY2[Upgrade Tier]
        HY1 -->|Tidak| HY3[Tetap]
    end
```

| Skema | Kriteria Utama | Gatekeeper | Contoh Use Case |
| - | - | - | - |
| **spending** | `total_spent ≥ min_purchase` | Rupiah threshold | Retail: Silver (1jt), Gold (5jt), Platinum (15jt) |
| **stamp** | `stamps ≥ stamps_required` | Stamp count | F\&B: 10 stamp = free item |
| **points** | `total_points ≥ points_required` | Point balance | Universal: 1000 poin = Gold |
| **visits** | `visit_count ≥ visits_required` | Visit frequency | Layanan: 20 visits = VIP |
| **hybrid** | Any of the above | Flexible | Enterprise: kombinasi semua |

## Multi-Company Isolation

```mermaid theme={null}
graph LR
    subgraph "Company A — Retail"
        A1[Bronze<br/>Default, 1x poin]
        A2[Silver<br/>5%, 1.2x, min 1jt]
        A3[Gold<br/>10%, 1.5x, min 5jt]
        A4[Platinum<br/>15%, 2x, min 15jt]
    end

    subgraph "Company B — Coffee Shop"
        B1[Regular<br/>Default, stamp card]
        B2[Silver<br/>7%, 10 stamps]
        B3[Gold<br/>12%, 25 stamps]
        B4[Diamond<br/>20%, 50 stamps]
    end

    subgraph "RLS Policy"
        RLS[WHERE company_id = current_company_id]
    end

    A1 & A2 & A3 & A4 -.-> RLS
    B1 & B2 & B3 & B4 -.-> RLS
```

Setiap record `CustomerMembership` terikat pada `company_id` melalui Row-Level Security (RLS), sehingga perusahaan yang berbeda bisa memiliki program loyalty yang benar-benar independen.

## Auto-Upgrade Flow

```mermaid theme={null}
sequenceDiagram
    participant POS as Transaksi POS
    participant L as Loyalty Engine
    participant CM as CustomerMembership
    participant C as Customer
    participant N as Notification

    POS->>L: Post-transaction: total_spent updated
    L->>CM: Get next tier (order = current + 1)
    CM-->>L: Next tier criteria
    L->>L: Check: scheme_type criteria met?

    alt spending: total_spent ≥ min_purchase
        L->>C: Update membership_level_id
        L->>C: Reset points/stamps (scheme-dependent)
        L->>N: "Selamat! Anda naik ke {tier_name}"
    else stamp: stamps ≥ stamps_required
        L->>C: Update membership_level_id
        L->>C: Reset stamps to 0
        L->>N: "Selamat! Anda naik ke {tier_name}"
    else Not met
        L->>C: Update progress percentage
        L->>L: No action
    end
```

## Integrasi dengan Modul Lain

| Modul | Komponen | Integrasi |
| - | - | - |
| **CRM** | `MembershipLevelManager` | Tab "Loyalty" — CRUD tier |
| **POS** | `CompanyPOSCashier` | Apply `discount_percentage` saat checkout |
| **POS** | `usePOSCart` | Apply `points_multiplier` saat accumulate |
| **Customer** | `Customer` entity | Field `membership_level_id` link |
| **Finance** | GL Journal | Discount impact on revenue |
| **AI** | AI Agent | Filter customers by tier |

## Perbedaan dengan Customer Membership

| Aspek | Company Membership | Customer Membership |
| - | - | - |
| **Scope** | Konfigurasi tier (admin) | Data membership pelanggan |
| **Komponen** | `MembershipLevelManager` — 821 baris | Customer-facing view |
| **User** | Admin/Owner | Pelanggan |
| **Fungsi** | Setup program loyalty | Lihat status membership sendiri |
| **Entity** | `CustomerMembership` (definition) | `Customer` (instance) |
| **URL** | `/company-membership` | `/customer-membership` |
| **Akses** | Admin only | Public (customer-scoped) |

## Tips

* **Mulai dari skema sederhana** (spending) sebelum beralih ke hybrid — lebih mudah dipahami pelanggan
* **Set `auto_upgrade = true`** untuk semua tier agar pelanggan otomatis naik tanpa intervensi manual
* **Review threshold** secara berkala — jika terlalu banyak pelanggan di tier tertinggi, pertimbangkan menaikkan kriteria
* **Gunakan warna tier yang berbeda jelas** agar kasir bisa langsung identifikasi tier pelanggan dari badge
* **Custom benefits** bisa digunakan untuk benefit non-sistem seperti "free parking" atau "early access"

***

## Entity Relationship Diagram

```mermaid theme={null}
erDiagram
    Customer ||--o| CustomerMembership : "membership_level_id → level"
    Customer ||--o{ CustomerLoyaltyLedger : "customer_id"
    CustomerMembership ||--o{ CustomerLoyaltyLedger : "scheme_type"
    POSMember }o--o| CustomerMembership : "membership_tier"
    POSMember ||--o{ CustomerLoyaltyLedger : "loyalty transactions"

    Customer {
        uuid company_id PK
        string name
        string phone
        string email
        string membership_level_id FK
        string membership_level_name
        date membership_since
        number membership_points
        number lifetime_points
        number stamps
        boolean has_negative_points
        number loyalty_deficit_points
    }

    CustomerMembership {
        uuid company_id PK
        string level_name
        string level_key UK
        string icon
        string color
        string description
        number discount_percentage
        number points_multiplier
        number min_purchase
        array benefits
        boolean priority_support
        boolean free_delivery
        number birthday_bonus
        boolean is_active
        number order
        enum scheme_type
        number points_threshold
        number points_per_threshold
        number stamps_required
        number stamp_per_transaction
        number min_redemption_points
        enum reward_type
        number reward_value
        string reward_product_id
        string reward_product_name
        number expiry_days
    }

    CustomerLoyaltyLedger {
        uuid company_id PK
        uuid customer_id FK
        string customer_name
        string customer_phone
        string transaction_id
        string transaction_number
        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
        string reason
        number deficit_points
        boolean requires_manual_review
        object reward_details
        string performed_by
        string actor_role
        datetime created_at
        string finance_record_id FK
    }

    POSMember {
        string name
        string phone
        string email
        string address
        date birth_date
        enum membership_tier
        number points
        number lifetime_points
        number stamps
        boolean has_negative_points
        number loyalty_deficit_points
        number total_spent
        number visit_count
        datetime last_visit
        number discount_percentage
        boolean is_active
    }
```

## Tabel Schema — CustomerMembership

Entitas `CustomerMembership` mendefinisikan tier membership per perusahaan. Total **26 field**.

| # | Field | Tipe | Default | Required | Deskripsi |
| - | - | - | - | - | - |
| 1 | `company_id` | UUID | — | **Ya** | ID perusahaan (multi-tenant isolation) |
| 2 | `level_name` | String | — | **Ya** | Nama level (Silver, Gold, Platinum, dll) |
| 3 | `level_key` | String | — | **Ya** | Key unik untuk level (silver, gold, platinum) |
| 4 | `icon` | String | — | Tidak | Icon emoji untuk level (🥇, 🥈, 🥉) |
| 5 | `color` | String | — | Tidak | Warna hex badge (#FFD700) |
| 6 | `description` | String | — | Tidak | Penjelasan lengkap keuntungan dan syarat keanggotaan (max 1000 char) |
| 7 | `discount_percentage` | Number | `0` | Tidak | Diskon % untuk member level ini (0–100) |
| 8 | `points_multiplier` | Number | `1` | Tidak | Multiplier poin yang didapat (1 = normal, 2 = double) |
| 9 | `min_purchase` | Number | `0` | Tidak | Minimum pembelian untuk mendapat level ini |
| 10 | `benefits` | Array\<String> | — | Tidak | List benefit yang didapat (string\[]) |
| 11 | `priority_support` | Boolean | `false` | Tidak | Akses priority support via WhatsApp |
| 12 | `free_delivery` | Boolean | `false` | Tidak | Gratis ongkir |
| 13 | `birthday_bonus` | Number | `0` | Tidak | Bonus poin di hari ulang tahun |
| 14 | `is_active` | Boolean | `true` | Tidak | Status aktif/nonaktif tier |
| 15 | `order` | Number | `0` | Tidak | Urutan level (semakin tinggi semakin premium) |
| 16 | `scheme_type` | Enum | `"points"` | Tidak | Skema loyalty: `points`, `stamp`, `spending`, `visits`, `hybrid` |
| 17 | `points_threshold` | Number | `10000` | Tidak | Ambang batas belanja per perolehan poin (misal Rp 10.000) |
| 18 | `points_per_threshold` | Number | `1` | Tidak | Jumlah poin yang diperoleh per threshold |
| 19 | `stamps_required` | Number | `10` | Tidak | Jumlah stamp yang dibutuhkan untuk reward atau naik level |
| 20 | `stamp_per_transaction` | Number | `1` | Tidak | Jumlah stamp yang diperoleh per transaksi eligible |
| 21 | `min_redemption_points` | Number | `0` | Tidak | Minimal poin untuk dapat melakukan penukaran reward |
| 22 | `reward_type` | Enum | `"none"` | Tidak | Jenis reward: `none`, `discount_percentage`, `discount_amount`, `free_product` |
| 23 | `reward_value` | Number | `0` | Tidak | Nilai reward (persen diskon atau nominal diskon) |
| 24 | `reward_product_id` | String | — | Tidak | ID produk reward gratis jika `reward_type = free_product` |
| 25 | `reward_product_name` | String | — | Tidak | Nama produk reward gratis |
| 26 | `expiry_days` | Number | — | Tidak | Masa kedaluwarsa poin dalam hari (opsional) |

## Tabel Schema — CustomerLoyaltyLedger

Entitas `CustomerLoyaltyLedger` mencatat setiap mutasi loyalty (earn, redeem, reversal, upgrade, adjustment). Total **24 field**.

| # | Field | Tipe | Default | Required | Deskripsi |
| - | - | - | - | - | - |
| 1 | `company_id` | UUID | — | **Ya** | ID perusahaan |
| 2 | `customer_id` | UUID | — | **Ya** | ID customer penerima / pemilik transaksi loyalty |
| 3 | `customer_name` | String | — | Tidak | Nama customer saat transaksi (denormalized) |
| 4 | `customer_phone` | String | — | Tidak | Nomor telepon / kontak customer (denormalized) |
| 5 | `transaction_id` | String | — | Tidak | ID transaksi POS atau referensi sale asal |
| 6 | `transaction_number` | String | — | Tidak | Nomor struk / receipt number transaksi |
| 7 | `idempotency_key` | String | — | **Ya** | Idempotency key untuk mencegah double crediting / posting |
| 8 | `event_type` | Enum | — | **Ya** | Tipe event mutasi: `earn`, `redeem`, `return_reversal`, `tier_upgrade`, `manual_adjustment` |
| 9 | `scheme_type` | Enum | `"points"` | Tidak | Skema loyalty: `points`, `stamp`, `spending`, `visits`, `hybrid` |
| 10 | `points_delta` | Number | `0` | Tidak | Perubahan poin (+ bertambah, − berkurang) |
| 11 | `points_before` | Number | `0` | Tidak | Saldo poin sebelum event |
| 12 | `points_after` | Number | `0` | Tidak | Saldo poin setelah event |
| 13 | `stamps_delta` | Number | `0` | Tidak | Perubahan stamp (+ bertambah, − berkurang) |
| 14 | `stamps_before` | Number | `0` | Tidak | Saldo stamp sebelum event |
| 15 | `stamps_after` | Number | `0` | Tidak | Saldo stamp setelah event |
| 16 | `eligible_amount` | Number | `0` | Tidak | Nilai transaksi yang sah / eligible untuk perolehan poin |
| 17 | `reason` | String | — | Tidak | Alasan atau keterangan mutasi loyalty |
| 18 | `deficit_points` | Number | `0` | Tidak | Defisit poin jika poin telah terpakai sebelum void/return |
| 19 | `requires_manual_review` | Boolean | `false` | Tidak | Flag jika transaksi reversal menghasilkan saldo negatif dan butuh review manual |
| 20 | `reward_details` | Object | — | Tidak | Detail reward jika event = `redeem` (reward\_type, discount\_amount, discount\_percentage, product\_id, product\_name, cost\_points, cost\_stamps) |
| 21 | `performed_by` | String | — | Tidak | Email atau ID operator yang memproses |
| 22 | `actor_role` | String | — | Tidak | Role aktor (owner, admin, cashier) |
| 23 | `created_at` | DateTime | — | Tidak | Waktu event tercatat di server |
| 24 | `finance_record_id` | String | — | Tidak | ID FinancialRecord (biaya COGS reward) yang terhubung, diisi saat event redeem |

## State Machine — Membership Tier Progression

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

    Regular --> Silver : spending ≥ min_purchase\nATAU stamps ≥ stamps_required\nATAU points ≥ points_required\nATAU visits ≥ visits_required
    Silver --> Gold : spending ≥ min_purchase\nATAU stamps ≥ stamps_required\nATAU points ≥ points_required\nATAU visits ≥ visits_required
    Gold --> Platinum : spending ≥ min_purchase\nATAU stamps ≥ stamps_required\nATAU points ≥ points_required\nATAU visits ≥ visits_required

    Platinum --> Gold : Downgrade manual\n(admin override)
    Gold --> Silver : Downgrade manual\n(admin override)
    Silver --> Regular : Downgrade manual\n(admin override)

    Regular --> Regular : earn points/stamps\nbelum cukup kriteria
    Silver --> Silver : earn points/stamps\nbelum cukup kriteria
    Gold --> Gold : earn points/stamps\nbelum cukup kriteria
    Platinum --> Platinum : earn points/stamps\ntier tertinggi

    state "Tier Upgrade Conditions" as UC {
        [*] --> CheckScheme
        CheckScheme --> SpendingCheck : scheme_type = spending
        CheckScheme --> StampCheck : scheme_type = stamp
        CheckScheme --> PointsCheck : scheme_type = points
        CheckScheme --> VisitsCheck : scheme_type = visits
        CheckScheme --> HybridCheck : scheme_type = hybrid
        SpendingCheck --> Upgrade : total_spent ≥ min_purchase
        StampCheck --> Upgrade : stamps ≥ stamps_required
        PointsCheck --> Upgrade : membership_points ≥ points_required
        VisitsCheck --> Upgrade : visit_count ≥ visits_required
        HybridCheck --> Upgrade : ANY criteria met
        Upgrade --> [*]
    }
```

## Sequence Diagram — Tier Upgrade

```mermaid theme={null}
sequenceDiagram
    participant POS as Transaksi POS
    participant LE as Loyalty Engine
    participant CM as CustomerMembership
    participant C as Customer
    participant LL as CustomerLoyaltyLedger
    participant N as Notification

    POS->>LE: Post-transaction hook (eligible_amount)
    LE->>C: Read current membership_level_id
    C-->>LE: Current tier + scheme_type
    LE->>CM: Get next tier (order = current_order + 1)
    CM-->>LE: Next tier criteria (min_purchase / stamps_required / points_required / visits_required)

    alt auto_upgrade = true
        LE->>LE: Evaluate scheme_type criteria

        alt Criteria MET
            LE->>C: UPDATE membership_level_id = next_tier.id
            LE->>C: UPDATE last_tier_upgrade_date = NOW()
            LE->>C: UPDATE last_tier_upgrade_reason = "Auto-upgrade"
            LE->>LL: INSERT event_type = "tier_upgrade"
            LE->>LL: SET points_before, points_after, reason
            LE->>N: "Selamat! Anda naik ke {level_name} 🎉"
        else Criteria NOT MET
            LE->>LL: INSERT event_type = "earn" (points/stamps delta)
            LE->>C: UPDATE membership_points / stamps
            LE->>LE: Calculate progress percentage
        end
    else auto_upgrade = false
        LE->>LL: INSERT event_type = "earn" (points/stamps delta)
        LE->>C: UPDATE membership_points / stamps
        LE->>LE: No auto-upgrade, admin must manually upgrade
    end
```

## Sequence Diagram — Points Expiry

```mermaid theme={null}
sequenceDiagram
    participant CRON as Cron Job\n(Harian)
    participant LE as Loyalty Engine
    participant CM as CustomerMembership
    participant C as Customer
    participant LL as CustomerLoyaltyLedger
    participant N as Notification

    CRON->>LE: Trigger: daily points expiry check
    LE->>CM: SELECT tiers WHERE expiry_days IS NOT NULL
    CM-->>LE: List tiers with expiry config

    loop For each tier with expiry_days
        LE->>C: SELECT customers WHERE membership_level_id = tier.id\nAND last_earn_date < NOW() - expiry_days
        C-->>LE: List customers with expired points

        loop For each affected customer
            LE->>C: Read current membership_points
            LE->>LE: expired_points = points older than expiry_days
            LE->>C: UPDATE membership_points = membership_points - expired_points
            LE->>C: UPDATE lifetime_points (unchanged)

            alt expired_points > 0
                LE->>LL: INSERT event_type = "manual_adjustment"
                LE->>LL: SET points_delta = -expired_points
                LE->>LL: SET points_before, points_after
                LE->>LL: SET reason = "Kedaluwarsa otomatis ({expiry_days} hari)"
                LE->>N: "Poin Anda berkurang {expired_points} poin karena kedaluwarsa"
            end

            alt membership_points < 0
                LE->>C: UPDATE has_negative_points = true
                LE->>C: UPDATE loyalty_deficit_points = abs(membership_points)
                LE->>LL: UPDATE requires_manual_review = true
                LE->>N: "Perhatian: Saldo poin negatif, diperlukan review manual"
            end
        end
    end
```

## Sequence Diagram — Member Benefits Application

```mermaid theme={null}
sequenceDiagram
    participant Cashier as Kasir POS
    participant POS as CompanyPOSCashier
    participant C as Customer
    participant CM as CustomerMembership
    participant CART as usePOSCart
    participant LL as CustomerLoyaltyLedger

    Cashier->>POS: Input nomor telepon member
    POS->>C: Lookup customer by phone
    C-->>POS: Customer data + membership_level_id
    POS->>CM: Lookup tier by membership_level_id
    CM-->>POS: Tier config (discount_percentage, points_multiplier, benefits)

    POS->>CART: Apply discount_percentage ke subtotal
    CART->>CART: discounted_total = subtotal × (1 - discount_percentage/100)

    Cashier->>POS: Proses pembayaran → finalize
    POS->>CART: Calculate points earned
    CART->>CART: eligible_amount = subtotal (after discount)
    CART->>CART: points = floor(eligible_amount / points_threshold) × points_per_threshold
    CART->>CART: adjusted_points = points × points_multiplier

    POS->>C: UPDATE membership_points += adjusted_points
    POS->>C: UPDATE stamps += stamp_per_transaction (if scheme = stamp)
    POS->>C: UPDATE total_spent += eligible_amount
    POS->>C: UPDATE visit_count += 1
    POS->>C: UPDATE last_visit = NOW()

    POS->>LL: INSERT event_type = "earn"
    POS->>LL: SET points_delta = adjusted_points
    POS->>LL: SET stamps_delta = stamp_per_transaction
    POS->>LL: SET eligible_amount, transaction_id, transaction_number

    POS-->>Cashier: Tampilkan total poin didapat + diskon applied

    alt birthday_bonus > 0 AND today = customer birthday
        POS->>C: UPDATE membership_points += birthday_bonus
        POS->>LL: INSERT event_type = "earn", reason = "Bonus ulang tahun"
        POS-->>Cashier: "🎂 Bonus ulang tahun: +{birthday_bonus} poin!"
    end
```

## Enum Tables

### `scheme_type` — Skema Loyalty

| Nilai | Deskripsi | Kriteria Upgrade |
| - | - | - |
| `points` | Akumulasi poin dari setiap transaksi | `membership_points ≥ points_required` |
| `stamp` | Stamp per transaksi eligible | `stamps ≥ stamps_required` |
| `spending` | Total nominal belanja (rupiah) | `total_spent ≥ min_purchase` |
| `visits` | Frekuensi kunjungan / transaksi | `visit_count ≥ visits_required` |
| `hybrid` | Kombinasi fleksibel — salah satu kriteria terpenuhi | ANY of the above |

### `reward_type` — Jenis Reward

| Nilai | Deskripsi | Field Terkait |
| - | - | - |
| `none` | Tidak ada reward yang dapat ditukar | — |
| `discount_percentage` | Tukar poin untuk diskon persentase | `reward_value` = persentase diskon |
| `discount_amount` | Tukar poin untuk diskon nominal (Rupiah) | `reward_value` = nominal diskon |
| `free_product` | Tukar poin untuk produk gratis | `reward_product_id`, `reward_product_name` |

### `event_type` — Tipe Event Loyalty Ledger

| Nilai | Deskripsi | Impact pada Saldo |
| - | - | - |
| `earn` | Perolehan poin/stamp dari transaksi | `points_delta > 0` atau `stamps_delta > 0` |
| `redeem` | Penukaran reward | `points_delta < 0` atau `stamps_delta < 0` |
| `return_reversal` | Pembatalan/void → reversal poin | `points_delta < 0` (reversal) |
| `tier_upgrade` | Kenaikan tier (tidak mengubah saldo) | `points_delta = 0` |
| `manual_adjustment` | Penyesuaian manual oleh admin | Bisa positif atau negatif |

### `membership_tier` (POSMember) — Tier Legacy

| Nilai | Deskripsi |
| - | - |
| `regular` | Tier default / belum naik |
| `silver` | Tier perak |
| `gold` | Tier emas |
| `platinum` | Tier tertinggi (legacy) |

## RBAC — Hak Akses Company Membership

| Role | Create | Read | Update | Delete | Keterangan |
| - | - | - | - | - | - |
| **Super Admin** | ✅ | ✅ | ✅ | ✅ | Akses penuh semua perusahaan |
| **Owner** | ✅ | ✅ | ✅ | ✅ | Akses penuh dalam company\_id sendiri |
| **Admin** | ✅ | ✅ | ✅ | ✅ | Kelola tier loyalty per perusahaan |
| **Manager** | ❌ | ✅ | ❌ | ❌ | Hanya lihat konfigurasi tier |
| **Cashier** | ❌ | ✅ | ❌ | ❌ | Lihat tier untuk apply diskon di POS |
| **Customer** | ❌ | ✅ (own) | ❌ | ❌ | Lihat tier membership sendiri saja |

> **Catatan RLS:** Semua operasi pada `CustomerMembership` dan `CustomerLoyaltyLedger` di-filter oleh `company_id = user.data.active_company_id` melalui Row-Level Security, memastikan isolasi data antar perusahaan.


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