Sistem saat ini punya dua saldo customer: **Point** (`customer_points`) dan **Token**
(`customer_tokens`, per jenis `SPIN` / `RAFFLE` / `MINIGAME`). Keduanya baru sebatas
tabel dan endpoint baca:
- Tidak ada jalur yang menambah saldo. Order selesai tidak menghasilkan apa pun.
-`AddPoints` / `DeductPoints` masih `not implemented`.
- Tidak ada riwayat transaksi. "History" di `/customer/points` sebenarnya adalah baris
saldo itu sendiri.
- Token hanya dipakai untuk spin game, dan pemotongannya tidak atomik (repository tidak
memakai `DBFromContext`, jumlah baris ter-update tidak dicek).
- Point belum bisa dipakai untuk apa pun, termasuk membayar.
PRD ini mendefinisikan ulang saldo customer menjadi **EnakPoint** dan **EnakCoin**,
lengkap dengan cara mendapatkannya, memakainya, memindahkannya, masa berlakunya, dan
jejak auditnya.
---
## 2. Tujuan
1. Customer mendapat EnakPoint dan EnakCoin otomatis dari order yang lunas, dengan
besaran yang diatur **per outlet**.
2. Customer bisa **membayar order dengan EnakPoint**, penuh atau sebagian. EnakCoin
tidak bisa dipakai membayar.
3. Customer bisa menukar EnakCoin ke EnakPoint, satu arah, dengan kurs yang bisa diatur
(default **1 EnakCoin = 1 EnakPoint**).
4. Customer bisa mentransfer EnakPoint dan EnakCoin ke customer lain.
5. EnakPoint dan EnakCoin bisa **kedaluwarsa**, dengan masa berlaku yang diatur sendiri
oleh owner.
6. Setiap perubahan saldo tercatat di ledger, lengkap dengan asal dan tujuannya
**sampai ke tiap butir**, dan bisa diaudit serta direkonsiliasi dengan saldo.
### Bukan tujuan
- Menukar EnakPoint ke EnakCoin. Exchange hanya satu arah.
- Membayar dengan EnakCoin.
- Menunaikan EnakPoint/EnakCoin dalam bentuk apa pun (lihat K7). Ini larangan, bukan
fitur yang ditunda.
---
## 3. Istilah
| Istilah | Kode | Arti |
|---|---|---|
| **EnakPoint** | `POINT` | Saldo yang bernilai rupiah. Satu-satunya saldo yang bisa dipakai membayar order. |
| **EnakCoin** | `COIN` | Pengganti Token. **Mata uang untuk bermain game** (spin, ferris wheel, raffle, minigame, dan game berikutnya). Bisa ditukar ke EnakPoint. **Tidak bisa** dipakai membayar. |
| **Wallet** | – | Saldo EnakPoint dan EnakCoin milik satu customer. |
| **Ledger** | – | Catatan setiap mutasi saldo. Saldo wallet = jumlah seluruh mutasi di ledger. |
| **Lot** | `wallet_lots` | Satu "paket" saldo yang masuk bersamaan, dengan asal dan tanggal kedaluwarsa sendiri. Saldo wallet = jumlah sisa semua lot. |
| **Nilai EnakPoint** | `point_value` | Nilai rupiah dari 1 EnakPoint saat dipakai membayar. Default Rp 1. |
Ledger bersifat **append-only**. Baris tidak pernah di-`UPDATE` atau di-`DELETE`.
Koreksi dilakukan dengan baris baru (`EARN_REVERSAL`, `PAYMENT_REFUND`, atau
`ADJUSTMENT`) yang menunjuk baris yang dikoreksi. Customer yang punya riwayat tidak bisa
dihapus permanen, cukup dinonaktifkan.
### `wallet_lots` dan `wallet_lot_allocations`: saldo per butir (K9)
```sql
CREATE TABLE wallet_lots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL,
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE RESTRICT,
currency VARCHAR(10) NOT NULL CHECK (currency IN ('POINT','COIN')),
source_transaction_id UUID NOT NULL REFERENCES wallet_transactions(id), -- mutasi masuk pembuatnya
origin_lot_id UUID REFERENCES wallet_lots(id), -- lot asal: transfer / exchange / refund
original_amount BIGINT NOT NULL CHECK (original_amount > 0),
remaining_amount BIGINT NOT NULL CHECK (remaining_amount >= 0
AND remaining_amount <= original_amount),
expires_at TIMESTAMPTZ, -- NULL = tidak kedaluwarsa
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- urutan pemakaian (K9): paling cepat kedaluwarsa dulu, yang tanpa tanggal paling akhir
CREATE INDEX idx_wallet_lots_consume ON wallet_lots
(customer_id, currency, expires_at NULLS LAST, created_at) WHERE remaining_amount > 0;
CREATE INDEX idx_wallet_lots_expiry ON wallet_lots (expires_at) WHERE remaining_amount > 0;
-- Setiap mutasi keluar mencatat lot mana yang dipakai dan berapa banyak
CREATE TABLE wallet_lot_allocations (
transaction_id UUID NOT NULL REFERENCES wallet_transactions(id), -- mutasi keluar
lot_id UUID NOT NULL REFERENCES wallet_lots(id),
amount BIGINT NOT NULL CHECK (amount > 0),
PRIMARY KEY (transaction_id, lot_id)
);
```
`wallet_lots.remaining_amount` adalah satu-satunya kolom yang di-`UPDATE`, sebagai
ringkasan untuk mempercepat pemakaian. Nilainya selalu bisa dihitung ulang dari
`original_amount − SUM(wallet_lot_allocations.amount)` (§7.5). Ledger dan alokasi tetap
append-only.
**Contoh.** Customer A punya lot 100 EnakPoint (dari order #ORD-1, kedaluwarsa 31 Des)
dan lot 50 EnakPoint (dari order #ORD-2, kedaluwarsa 31 Jan). A mentransfer 120 ke B.
- `TRANSFER_OUT` A dialokasikan 100 dari lot #ORD-1 dan 20 dari lot #ORD-2.
- B mendapat dua lot: 100 (kedaluwarsa 31 Des, `origin_lot_id` = lot #ORD-1) dan 20
(kedaluwarsa 31 Jan, `origin_lot_id` = lot #ORD-2).
- Kalau B lalu membayar dengan 30 EnakPoint, alokasinya menunjukkan bahwa 30 EnakPoint
itu berasal dari order #ORD-1 milik A.
### Perubahan tabel yang sudah ada
```sql
-- payment_methods.type: tambah nilai 'point'
-- (validator saat ini: oneof=cash card digital_wallet)
-- PIN customer (F11)
ALTER TABLE customers
ADD COLUMN pin_hash VARCHAR(255),
ADD COLUMN pin_set_at TIMESTAMPTZ,
ADD COLUMN pin_failed_attempts INT NOT NULL DEFAULT 0,
ADD COLUMN pin_locked_until TIMESTAMPTZ,
ADD COLUMN transfer_blocked_until TIMESTAMPTZ; -- 24 jam setelah reset PIN
CREATE TABLE customer_security_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE RESTRICT,
event VARCHAR(30) NOT NULL, -- PIN_SET, PIN_CHANGED, PIN_RESET, PIN_FAILED,
-- PIN_LOCKED, PIN_REMOVED_BY_ADMIN
actor_user UUID, -- diisi untuk PIN_REMOVED_BY_ADMIN
reason VARCHAR(255),
ip_address VARCHAR(45),
user_agent VARCHAR(255),
created_at TIMESTAMPTZ DEFAULT NOW()
);
ALTER TABLE payments
ADD COLUMN points_used BIGINT, -- diisi hanya untuk method EnakPoint
ADD COLUMN point_value DECIMAL(10,2), -- nilai 1 EnakPoint saat dibayar (beku)
ADD CONSTRAINT chk_payments_point_pair CHECK (
(points_used IS NULL AND point_value IS NULL)
OR (points_used > 0 AND point_value > 0));
-- Riwayat perubahan setting loyalitas (F2, F12)
CREATE TABLE loyalty_setting_changes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL,
outlet_id UUID, -- NULL untuk setting organisasi
key VARCHAR(100) NOT NULL,
old_value TEXT,
new_value TEXT,
changed_by UUID NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
```
### 8.1 Jejak Asal & Tujuan per Tipe
| Tipe | Currency | Arah | Dari mana / ke mana | `reference_type` → `reference_id` | Kolom wajib tambahan | Contoh `description` |
|---|---|---|---|---|---|---|
| `EARN` | POINT / COIN | masuk | Order yang lunas | `ORDER` → `orders.id` | `outlet_id` | "Belanja #ORD-0123 di Outlet Kemang" |
| `EARN_REVERSAL` | POINT / COIN | keluar | Ditarik karena order di-void/refund | `ORDER` → `orders.id` | `reverses_transaction_id` (baris `EARN` asal), `outlet_id` | "Batal #ORD-0123 di Outlet Kemang" |
| `PAYMENT` | POINT | keluar | Dipakai membayar order | `PAYMENT` → `payments.id` | `outlet_id`, `created_by_user` (kasir, jika via POS) | "Bayar #ORD-0123 di Outlet Kemang (Rp 50.000)" |
| `PAYMENT_REFUND` | POINT | masuk | Dikembalikan karena pembayaran di-void/refund | `PAYMENT` → `payments.id` | `reverses_transaction_id` (baris `PAYMENT` asal), `outlet_id` | "Pengembalian #ORD-0123 di Outlet Kemang" |
| `EXCHANGE_OUT` | COIN | keluar | Ditukar menjadi EnakPoint | `WALLET_TX` → baris `EXCHANGE_IN` pasangannya | `group_id` | "Tukar 50 EnakCoin ke EnakPoint" |
| `EXCHANGE_IN` | POINT | masuk | Hasil tukar EnakCoin | `WALLET_TX` → baris `EXCHANGE_OUT` pasangannya | `group_id` | "Dari tukar 50 EnakCoin" |
| `TRANSFER_OUT` | POINT / COIN | keluar | Dikirim ke customer lain | `WALLET_TX` → baris `TRANSFER_IN` penerima | `counterparty_customer_id`, `group_id` | "Transfer ke Bu*** Sa*** (08**-****-1234)" |
| `TRANSFER_IN` | POINT / COIN | masuk | Diterima dari customer lain | `WALLET_TX` → baris `TRANSFER_OUT` pengirim | `counterparty_customer_id`, `group_id` | "Transfer dari An*** (08**-****-5678)" |
| `GAME_SPEND` | COIN | keluar | Dipakai bermain game | `GAME_PLAY` → `game_plays.id` | – | "Main Spin Wheel: dapat Voucher 10rb" |
| `EXPIRE` | POINT / COIN | keluar | Hangus karena masa berlaku habis | `LOT` → `wallet_lots.id` | – | "Kedaluwarsa: 150 EnakPoint dari Belanja #ORD-0098" |
| `ADJUSTMENT` | POINT / COIN | masuk/keluar | Koreksi manual oleh admin | `USER` → `users.id` admin | `created_by_user`, `reason` | "Koreksi oleh admin: komplain #45" |
| `MIGRATION` | POINT / COIN | masuk | Saldo lama sebelum sistem ini | `LEGACY_POINTS` / `LEGACY_TOKENS` → id baris lama | – | "Saldo awal dari sistem lama" |
| `REWARD_REDEEM` | POINT | keluar | Ditukar reward (fase berikut) | `REWARD_REDEMPTION` → id penukaran | – | "Tukar reward: Tumbler" |
Setiap mutasi **masuk** membuat satu atau lebih lot. Setiap mutasi **keluar** mencatat
alokasi ke lot yang dipakai. Dengan begitu jejak bisa ditelusuri di dua tingkat:
**Per mutasi** (lewat `reference_*`):
- **EnakPoint yang dipakai bayar:** `PAYMENT` → `payments` → order, outlet, kasir, dan
nominal rupiah yang ditutup.
- **EnakPoint yang kembali:** `PAYMENT_REFUND` → `PAYMENT` asal → order.
- **Saldo yang masuk lewat transfer:** `TRANSFER_IN` → baris `TRANSFER_OUT` pengirim.
- **EnakPoint hasil tukar:** `EXCHANGE_IN` → `EXCHANGE_OUT` (EnakCoin).
- **Earning yang ditarik:** `EARN_REVERSAL` → `EARN` asal → order.
- **Saldo yang hangus:** `EXPIRE` → lot → mutasi masuk yang membuat lot tersebut.
**Per butir** (lewat `wallet_lot_allocations` dan `origin_lot_id`): dari pengurangan
mana pun, lihat lot yang terpakai, lalu ikuti `origin_lot_id` ke belakang melewati
transfer, exchange, atau refund, sampai ke lot pertama yang dibuat oleh `EARN`,
`ADJUSTMENT`, atau `MIGRATION`.
**`description` dibekukan saat dibuat.** Nama outlet, nomor order, atau nama penerima
yang berubah belakangan tidak mengubah riwayat. Prinsipnya sama seperti snapshot harga
di `order_items`. Nama penerima/pengirim disamarkan di `description`. Nama lengkap hanya
terlihat oleh admin lewat `counterparty_customer_id`.
`locked_until`), dan `TRANSFER_BLOCKED` (beserta `transfer_blocked_until`).
Endpoint `/points` dan `/tokens` dipertahankan sementara sebagai alias yang membaca
dari `customer_wallets`, lalu dihapus setelah aplikasi diperbarui.
### POS / Dashboard (`/api/v1`)
| Method | Path | Role | Keterangan |
|---|---|---|---|
| GET | `/orders/:id/point-payment/preview` | Kasir | `maks_point`, nilai EnakPoint, nominal rupiah untuk customer order |
| POST | `/orders/:id/payments` | Kasir | Endpoint pembayaran yang sudah ada. Untuk method EnakPoint, body membawa `{ "points": 50000, "payment_code": "482913" }` |
| GET/PUT | `/outlets/:id/loyalty-settings` | Admin/Manager | F1 |
8 EnakCoin. Rincian saldo per jenis disimpan di `metadata` baris ledger
`MIGRATION`, supaya asal saldo awal tetap bisa ditelusuri.
3. Tulis satu baris ledger `MIGRATION` dan satu lot (tanpa tanggal kedaluwarsa) per
customer per currency yang saldonya > 0, supaya rekonsiliasi (§7.5) langsung
berlaku.
4. `campaigns.type` / `campaign_rules.reward_type`: nilai `TOKENS` diganti `COINS`.
5. Tambah tipe `point` ke `payment_methods`, lalu buat payment method sistem
"EnakPoint" untuk setiap organisasi. Tambah kolom `points_used` / `point_value` ke
`payments`.
6. Tambah kolom PIN ke `customers` dan tabel `customer_security_events`. Semua customer
yang sudah ada mulai tanpa PIN, dan akan diminta membuatnya lewat OTP saat pertama
kali transfer, membayar, atau exchange.
7. `customer_points` dan `customer_tokens` dibiarkan read-only selama satu rilis, lalu
di-drop di migrasi berikutnya.
---
## 11. Di Luar Scope
- **Penukaran reward dengan EnakPoint.** Katalog reward sudah ada. Alurnya akan dibahas
di PRD terpisah dan memakai tipe ledger `REWARD_REDEEM`.
- **Tier otomatis** berdasarkan EnakPoint. Dibahas di PRD terpisah (lihat Q8 untuk
arahannya).
- **Eksekusi campaign rules** (bonus/multiplier) di atas earning dasar outlet.
- **Earning untuk order tanpa customer terdaftar** (klaim belakangan lewat struk/QR).
---
## 12. Pertanyaan & Catatan
### 12.1 Sudah Diputuskan
| # | Pertanyaan | Keputusan | Tercermin di |
|---|---|---|---|
| Q1 | Basis earning: sebelum atau sesudah pajak/service charge? | **Sebelum pajak** (`subtotal − discount`) | F1 |
| Q2 | Apakah earning dari self-order (QR meja) diperlakukan sama? | **Ya**, semua kanal sama selama order punya customer | F3 |
| Q3 | Saat reversal earning dan saldo tidak cukup: tarik sampai 0, izinkan saldo negatif, atau blokir refund? | **Tarik sampai 0 dan catat shortfall.** Refund tidak diblokir | F10 |
| Q4 | Batas transfer diatur per organisasi atau global? Perlu limit harian? | **Per organisasi**, default tanpa batas | F2, F5 |
| Q5 | Konfirmasi transfer pakai password, PIN khusus, atau OTP? | **PIN customer 6 digit**, juga untuk pembayaran dan exchange | K8, F11 |
| Q6 | Saldo Token `RAFFLE`/`MINIGAME` yang ada ikut dikonversi ke EnakCoin? | **Ya**, semua jenis dijumlahkan menjadi EnakCoin. EnakCoin adalah mata uang untuk semua game | K1, F8, §10 |
| Q7 | Customer app melayani satu organisasi atau banyak? | **Satu nomor telepon = satu customer di satu organisasi**, sesuai `customers.phone_number` yang unik secara global | K4 |
| Q8 | Bagaimana tier (level keanggotaan, mis. Silver/Gold; tabel `tiers` sudah ada tapi belum terhubung ke customer) berhubungan dengan EnakPoint? | **Dibahas di PRD terpisah.** Arahan untuk PRD itu: tier dihitung dari **total `EARN` dalam 12 bulan terakhir**, bukan dari saldo, supaya customer tidak turun tier karena memakai, mentransfer, atau kehilangan saldo karena kedaluwarsa, dan tier tidak bisa "dibeli" lewat transfer. Ledger di PRD ini sudah mencatat `EARN`, jadi tidak ada yang perlu diubah di sini | §11 |
| Q9 | Jejak cukup per mutasi, atau harus per butir? | **Per butir**, lewat lot. EnakPoint dan EnakCoin **bisa kedaluwarsa**, dengan masa berlaku yang diatur sendiri | K9, F12, §8 |
| Q10 | Apakah bagian order yang dibayar EnakPoint tetap menghasilkan earning? | **Tidak.** Basis earning dikurangi nominal EnakPoint | F1 |
| Q11 | Berapa nilai rupiah 1 EnakPoint, dan kurs EnakCoin → EnakPoint? | **1 EnakPoint = Rp 1** dan **1 EnakCoin = 1 EnakPoint** sebagai default. Keduanya bisa diubah di setting | K3, F2, F4 |
| Q13 | Refund sebagian yang tidak habis dibagi nilai EnakPoint: sisa rupiahnya ke mana? | **Dibulatkan ke bawah**, sisanya hangus | F9 |
| Q16 | Setelah reset PIN, berapa lama transfer keluar ditahan? | **24 jam, hanya transfer.** Pembayaran dan exchange tetap bisa | F11 |
Tidak ada. Semua hal yang belum diputuskan sudah dipindahkan ke catatan N1–N4 di
bawah, masing-masing dengan batas waktu.
### 12.3 Ditunda (Catatan agar Tidak Lupa)
Hal-hal berikut sengaja belum diputuskan. Masing-masing punya batas waktu, yaitu fase
yang tidak boleh dirilis sebelum catatan ini ditutup.
**N1 — Pembayaran EnakPoint di kasir untuk customer tanpa aplikasi** (sebelumnya Q12)
- **Situasi:** persetujuan pembayaran di kasir saat ini hanya lewat kode bayar dari
aplikasi (F9). Customer yang tidak punya aplikasi, HP-nya mati, atau tidak ada
internet belum bisa membayar dengan EnakPoint di kasir.
- **Batasan yang harus tetap dijaga:** PIN tidak boleh diketik di layar kasir (K8).
- **Opsi yang sudah terpikir:** PIN pad atau layar yang menghadap customer; OTP ke
nomor telepon (butuh HP tapi tidak butuh aplikasi); atau memang tidak didukung.
- **Batas waktu:** tidak memblokir fase 3. Fase 3 bisa rilis tanpa jalur ini, tetapi
kasir perlu tahu apa yang harus dikatakan ke customer tanpa aplikasi.
- **Pemilik keputusan:** product owner.
**N2 — Perlakuan akuntansi EnakPoint dan EnakCoin** (sebelumnya Q14)
- **Situasi:** EnakPoint yang dipakai membayar bukan kas masuk. Saldo yang beredar
berpotensi menjadi kewajiban. Saldo yang kedaluwarsa (F12) menjadi "breakage" yang
juga perlu dicatat.
- **Yang perlu diputuskan:** apakah EnakPoint yang dipakai dicatat sebagai beban
promosi atau pengurang liabilitas loyalitas; apakah saldo beredar dicatat sebagai
liabilitas; bagaimana breakage dari kedaluwarsa dicatat; apakah EnakCoin (yang bisa
ditukar ke EnakPoint, K3) ikut dihitung; dan apakah perlu jurnal otomatis ke modul
chart of account yang sudah ada.
- **Dampak ke sistem:** laporan payment method, laporan penjualan (penjualan kotor vs
kas masuk), dan kemungkinan jurnal otomatis.
- **Batas waktu:** sebelum fase 3 (pembayaran EnakPoint) dirilis ke outlet pertama.
- **Pemilik keputusan:** tim keuangan.
**N3 — Tinjauan regulasi uang elektronik** (sebelumnya Q15)
- **Situasi:** EnakPoint bernilai rupiah, bisa dipakai membayar, dan bisa ditransfer
antar customer. Kombinasi ini mirip dengan uang elektronik yang diatur Bank
Indonesia.
- **Mitigasi yang sudah ada di desain:** tidak bisa ditunaikan (K7), hanya berlaku di
outlet dalam organisasi yang sama (K4), tampilan "setara potongan", bukan "saldo
rupiah", dan bisa kedaluwarsa (F12).
- **Yang perlu dicek ke legal:** apakah fitur transfer (F5) masih aman; apakah perlu
batas transfer wajib (F2); dan apa yang harus ada di Syarat & Ketentuan.
- **Batas waktu:** sebelum fase 3 (pembayaran) dan fase 4 (transfer) dirilis.
- **Pemilik keputusan:** legal.
**N4 — Model kedaluwarsa saldo**
- **Situasi:** sudah diputuskan bahwa EnakPoint dan EnakCoin bisa kedaluwarsa dan
owner bisa mengatur sendiri (Q9). Yang belum diputuskan adalah **modelnya**.
- **Pilihan:**
| | A. Tanggal tetap (gaya Telkomsel POIN / XL Poin) | B. Per saldo masuk (draft F12 saat ini) |
|---|---|---|
| Cara kerja | Semua saldo hangus di tanggal yang sama, mis. tiap 31 Des (1× setahun) atau tiap 30 Jun & 31 Des (2× setahun) | Tiap saldo punya tanggal sendiri, mis. 12 bulan sejak didapat |
| Mudah dipahami customer | Sangat mudah: "semua hangus 31 Desember" | Lebih rumit: "150 hangus 3 Okt, 200 hangus 18 Nov" |
| Pengingat | Satu kampanye besar menjelang tanggal hangus | Banyak pengingat kecil |
| Adil | Kurang. Saldo yang didapat sehari sebelum tanggal hangus langsung hilang. Bisa ditutup dengan **periode tanggung**, mis. saldo yang didapat < 3 bulan sebelum tanggal hangus ikut ke tanggal hangus berikutnya | Adil. Semua saldo punya umur yang sama |
| Efek bisnis | Lonjakan belanja menjelang tanggal hangus | Lebih rata |
- **Pilihan ketiga:** keduanya didukung sebagai mode di setting, dan owner memilih.
Ini tidak mengubah struktur data. Lot, alokasi, dan `EXPIRE` tetap sama. Yang berbeda
hanya rumus `expires_at` saat lot dibuat:
- A: tanggal hangus berikutnya setelah (tanggal didapat + periode tanggung)
- B: tanggal didapat + masa berlaku
- **Usulan sementara (belum disetujui):** dukung keduanya, dengan default model A
setahun sekali tiap 31 Desember dan periode tanggung 3 bulan, karena model ini sudah
familiar bagi customer di Indonesia.
- **Pertanyaan turunan yang ikut diputuskan bersama N4:**
- Saat kedaluwarsa pertama kali diaktifkan, bagaimana dengan saldo lama yang belum
punya tanggal kedaluwarsa? Usulan: diberi masa berlaku penuh sejak tanggal aktivasi
(model B), atau ikut tanggal hangus kedua berikutnya (model A).
- EnakPoint yang dikembalikan karena refund, padahal lot asalnya sudah atau hampir
kedaluwarsa? Usulan: diberi masa berlaku minimal 7 hari sejak refund.
- Saat kedaluwarsa dinonaktifkan, apakah saldo yang sudah terjadwal kedaluwarsa ikut
dibatalkan? Usulan: tidak, hanya saldo baru yang tidak kedaluwarsa.
- Pengingat dikirim berapa hari sebelum tanggal hangus, dan berapa kali?
- **Dampak ke sistem:** tabel pengaturan di F12, rumus kedaluwarsa per lot, isi
pengingat, dan tampilan "saldo yang akan kedaluwarsa" di aplikasi.
- **Batas waktu:** sebelum fase 5 (kedaluwarsa) dikerjakan. Fase 1–4 tidak terblokir,
karena lot sudah dibuat sejak fase 1 dan dipakai oleh kedua model.