2026-10-01 22:26:44 +07:00
|
|
|
|
# Task Breakdown: EnakPoint & EnakCoin
|
|
|
|
|
|
|
|
|
|
|
|
**Sumber:** [PRD EnakPoint & EnakCoin](prd-point-coin.md)
|
|
|
|
|
|
**Tanggal:** 2026-09-29
|
|
|
|
|
|
|
2026-10-07 13:48:29 +07:00
|
|
|
|
> **Catatan 2026-10-07:** fase 3 (Pembayaran EnakPoint) dihapus sesuai
|
|
|
|
|
|
> [`enakgame-prd.md`](./enakgame-prd.md) §3.2: EnakPoint hanya bisa ditukar ke voucher,
|
|
|
|
|
|
> bukan alat bayar. Payment method EnakPoint, kode bayar, bayar di kasir & app, refund
|
|
|
|
|
|
> EnakPoint, dan EnakPoint di laporan (PC-303 – PC-308) sudah dihapus dari backend
|
|
|
|
|
|
> (migrasi `000102`). PIN customer (PC-301) dan pengaturan loyalitas organisasi (PC-302)
|
|
|
|
|
|
> tetap dipakai. Sisa dokumen ini dibiarkan sebagai riwayat.
|
|
|
|
|
|
|
2026-10-01 22:26:44 +07:00
|
|
|
|
Setiap task menyebut bagian PRD yang dikerjakan, lapisan kode yang disentuh, task yang
|
|
|
|
|
|
harus selesai lebih dulu, dan kriteria selesai. Ukuran: **S** ≤ 1 hari, **M** 2–3 hari,
|
|
|
|
|
|
**L** 4–5 hari.
|
|
|
|
|
|
|
|
|
|
|
|
Konvensi kode mengikuti yang sudah ada: `migrations/` (lanjut dari `000089`),
|
|
|
|
|
|
`entities` → `repository` → `processor` → `service` → `handler` / `validator` →
|
|
|
|
|
|
`router`, dan wiring di `internal/app/app.go`.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Ringkasan
|
|
|
|
|
|
|
|
|
|
|
|
| Fase | Task | Terblokir oleh catatan PRD |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| 1. Fondasi | PC-101 – PC-109 | – |
|
|
|
|
|
|
| 2. Earning | PC-201 – PC-205 | – |
|
|
|
|
|
|
| 3. Pembayaran EnakPoint | PC-301 – PC-308 | N2 (keuangan), N3 (legal) sebelum **rilis** |
|
|
|
|
|
|
| 4. Pergerakan saldo | PC-401 – PC-404 | N3 (legal) sebelum **rilis** |
|
|
|
|
|
|
| 5. Kedaluwarsa | PC-501 – PC-504 | N4 (model kedaluwarsa) sebelum **dikerjakan** |
|
|
|
|
|
|
| 6. Bersih-bersih | PC-601 – PC-602 | – |
|
|
|
|
|
|
|
|
|
|
|
|
Catatan N2 dan N3 hanya memblokir **rilis** fase 3–4, bukan pengerjaannya. N4
|
|
|
|
|
|
memblokir pengerjaan fase 5, karena rumus kedaluwarsanya belum ditentukan.
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
PC-101 ─┬─ PC-103 ── PC-104 ─┬─ PC-105 ── PC-106
|
|
|
|
|
|
│ ├─ PC-107
|
|
|
|
|
|
│ ├─ PC-108
|
|
|
|
|
|
│ ├─ PC-203 ── PC-204 ── PC-205
|
|
|
|
|
|
│ ├─ PC-305 ── PC-306 / PC-307
|
|
|
|
|
|
│ ├─ PC-401 / PC-402 / PC-403 / PC-404
|
|
|
|
|
|
│ └─ PC-502 ── PC-503
|
|
|
|
|
|
PC-102 ── PC-109 ─┬─ PC-201 ── PC-202 ── PC-203
|
|
|
|
|
|
└─ PC-302
|
|
|
|
|
|
PC-301 ── PC-304 ── PC-305
|
|
|
|
|
|
PC-303 ── PC-305
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Fase 1 — Fondasi
|
|
|
|
|
|
|
|
|
|
|
|
Semua fase lain bergantung pada fase ini. **PC-104 (wallet engine) adalah inti.** Tidak
|
|
|
|
|
|
ada kode lain yang boleh mengubah saldo tanpa lewat engine ini.
|
|
|
|
|
|
|
|
|
|
|
|
### PC-101 · Migrasi tabel wallet, ledger, dan lot · M
|
|
|
|
|
|
- **PRD:** §8 (`customer_wallets`, `wallet_transactions`, `wallet_lots`,
|
|
|
|
|
|
`wallet_lot_allocations`), K5, K9
|
|
|
|
|
|
- **Kerjakan:** migrasi `000090_create_wallet_tables` (up & down) dengan semua
|
|
|
|
|
|
`CHECK` constraint dan index persis seperti di §8.
|
|
|
|
|
|
- **Selesai jika:**
|
|
|
|
|
|
- Up dan down berjalan bersih di database kosong dan di salinan staging.
|
|
|
|
|
|
- Uji constraint langsung di SQL, masing-masing harus ditolak:
|
|
|
|
|
|
`PAYMENT` bercurrency `COIN`; `TRANSFER_IN` tanpa `counterparty_customer_id`;
|
|
|
|
|
|
`EARN_REVERSAL` tanpa `reverses_transaction_id`; `ADJUSTMENT` tanpa `reason`;
|
|
|
|
|
|
`EXPIRE` dengan `reference_type` selain `LOT`; saldo negatif; lot dengan
|
|
|
|
|
|
`remaining_amount > original_amount`.
|
|
|
|
|
|
- **Bergantung pada:** –
|
|
|
|
|
|
|
|
|
|
|
|
### PC-102 · Migrasi pengaturan organisasi dan riwayat perubahan setting · S
|
|
|
|
|
|
- **PRD:** F2, `loyalty_setting_changes` di §8
|
|
|
|
|
|
- **Kerjakan:** tabel `organization_settings` (key–value, pola sama dengan
|
|
|
|
|
|
`outlet_settings`, `UNIQUE(organization_id, key)`) dan `loyalty_setting_changes`.
|
|
|
|
|
|
Saat ini belum ada tempat menyimpan setting per organisasi.
|
|
|
|
|
|
- **Selesai jika:** up/down bersih.
|
|
|
|
|
|
- **Bergantung pada:** –
|
|
|
|
|
|
|
|
|
|
|
|
### PC-103 · Entities dan repository wallet · M
|
|
|
|
|
|
- **PRD:** §7.1–§7.4
|
|
|
|
|
|
- **Kerjakan:**
|
|
|
|
|
|
- Entities untuk empat tabel PC-101.
|
|
|
|
|
|
- Repository yang **selalu** memakai `DBFromContext` (berbeda dari repository
|
|
|
|
|
|
gamification yang sekarang memakai `r.db` langsung).
|
|
|
|
|
|
- `LockWallet(ctx, customerID)` dengan `SELECT … FOR UPDATE`. Membuat baris wallet
|
|
|
|
|
|
jika belum ada (`INSERT … ON CONFLICT DO NOTHING`, lalu lock).
|
|
|
|
|
|
- `LockWallets(ctx, a, b)` yang selalu mengunci berurutan berdasarkan `customer_id`.
|
|
|
|
|
|
- Update saldo bersyarat yang **mengembalikan error jika 0 baris ter-update**.
|
|
|
|
|
|
- Query lot aktif sesuai urutan K9: `expires_at NULLS LAST, created_at`.
|
|
|
|
|
|
- **Selesai jika:** test repository menunjukkan update bersyarat gagal saat saldo
|
|
|
|
|
|
kurang, dan dua goroutine yang mengunci wallet yang sama berjalan bergantian.
|
|
|
|
|
|
- **Bergantung pada:** PC-101
|
|
|
|
|
|
|
|
|
|
|
|
### PC-104 · Wallet engine (credit / debit / lot / idempotensi) · L
|
|
|
|
|
|
- **PRD:** K5, K6, K9, §7, §8.1
|
|
|
|
|
|
- **Kerjakan:** processor `WalletProcessor` sebagai **satu-satunya pintu** perubahan
|
|
|
|
|
|
saldo:
|
|
|
|
|
|
- `Credit(ctx, CreditInput)`: menulis ledger, membuat lot (dengan `expires_at` dan
|
|
|
|
|
|
`origin_lot_id` dari input), dan menambah saldo.
|
|
|
|
|
|
- `Debit(ctx, DebitInput)`: mengambil dari lot sesuai urutan K9 (atau dari lot
|
|
|
|
|
|
tertentu lebih dulu, untuk reversal di F10), menulis alokasi dan ledger, lalu
|
|
|
|
|
|
mengurangi saldo. Mengembalikan daftar alokasi, supaya transfer/exchange bisa
|
|
|
|
|
|
membuat lot penerima dengan `expires_at` yang sama.
|
|
|
|
|
|
- `DebitUpTo`: mengambil sebanyak yang tersedia, untuk reversal dengan shortfall
|
|
|
|
|
|
(F10, Q3).
|
|
|
|
|
|
- Idempotensi: jika `idempotency_key` sudah ada, kembalikan hasil pertama tanpa
|
|
|
|
|
|
mutasi baru.
|
|
|
|
|
|
- Validasi di level kode untuk aturan §8.1 (tipe ↔ currency ↔ referensi wajib),
|
|
|
|
|
|
sebagai lapisan kedua di atas constraint database.
|
|
|
|
|
|
- Semua method mewajibkan caller sudah berada di dalam transaksi dan wallet sudah
|
|
|
|
|
|
dikunci.
|
|
|
|
|
|
- **Selesai jika:** unit test mencakup debit yang melewati beberapa lot, urutan lot
|
|
|
|
|
|
dengan dan tanpa `expires_at`, debit melebihi saldo (ditolak), `DebitUpTo` dengan
|
|
|
|
|
|
shortfall, idempotency key ganda, dan saldo = `SUM(ledger)` = `SUM(lot.remaining)`
|
|
|
|
|
|
setelah setiap skenario.
|
|
|
|
|
|
- **Bergantung pada:** PC-103
|
|
|
|
|
|
|
|
|
|
|
|
### PC-105 · Migrasi data Point & Token lama · M
|
|
|
|
|
|
- **PRD:** §10, Q6
|
|
|
|
|
|
- **Kerjakan:** migrasi data (atau command satu kali) yang:
|
|
|
|
|
|
- Membuat wallet dari `customer_points` dan penjumlahan semua jenis
|
|
|
|
|
|
`customer_tokens`.
|
|
|
|
|
|
- Menulis ledger `MIGRATION` dan lot tanpa `expires_at` per customer per currency,
|
|
|
|
|
|
dengan rincian per jenis token di `metadata`.
|
|
|
|
|
|
- Mengganti `TOKENS` menjadi `COINS` di `campaigns.type` dan
|
|
|
|
|
|
`campaign_rules.reward_type`.
|
|
|
|
|
|
- **Selesai jika:** jumlah total Point dan Token sebelum = jumlah total saldo wallet
|
|
|
|
|
|
sesudah (diuji pada salinan data staging), dan migrasi aman dijalankan dua kali
|
|
|
|
|
|
(tidak menggandakan).
|
|
|
|
|
|
- **Bergantung pada:** PC-104
|
|
|
|
|
|
|
|
|
|
|
|
### PC-106 · Endpoint saldo & riwayat customer · M
|
|
|
|
|
|
- **PRD:** F6, §9 (customer app)
|
|
|
|
|
|
- **Kerjakan:** `GET /customer/wallet` dan `GET /customer/wallet/transactions`
|
|
|
|
|
|
(pagination, filter currency/tipe/tanggal). Ganti isi `/customer/points` dan
|
|
|
|
|
|
`/customer/tokens` menjadi alias yang membaca `customer_wallets`. Hapus pemakaian
|
|
|
|
|
|
`customer_points_repository` untuk saldo.
|
|
|
|
|
|
- **Selesai jika:** response menampilkan asal/tujuan setiap mutasi sesuai §8.1, dan
|
|
|
|
|
|
aplikasi lama yang memanggil `/points` / `/tokens` tetap mendapat angka yang benar.
|
|
|
|
|
|
- **Bergantung pada:** PC-105
|
|
|
|
|
|
|
|
|
|
|
|
### PC-107 · Wallet customer dan adjustment di dashboard · M
|
|
|
|
|
|
- **PRD:** F7
|
|
|
|
|
|
- **Kerjakan:** `GET /marketing/customers/:id/wallet` (saldo, lot aktif, mutasi) dan
|
|
|
|
|
|
`POST /marketing/customers/:id/wallet/adjust` (alasan wajib, tidak boleh membuat saldo
|
|
|
|
|
|
negatif, tercatat `created_by_user`).
|
|
|
|
|
|
- **Selesai jika:** adjustment muncul di riwayat customer dengan nama admin dan alasan.
|
|
|
|
|
|
Adjustment yang melebihi saldo ditolak.
|
|
|
|
|
|
- **Bergantung pada:** PC-104
|
|
|
|
|
|
|
|
|
|
|
|
### PC-108 · Job rekonsiliasi · S
|
|
|
|
|
|
- **PRD:** §7.5
|
|
|
|
|
|
- **Kerjakan:** query dan job terjadwal yang memeriksa semua invarian §7.5 dan
|
|
|
|
|
|
melaporkan selisih ke log dan notifikasi admin.
|
|
|
|
|
|
- **Selesai jika:** job menemukan selisih yang sengaja dibuat di data test, dan diam
|
|
|
|
|
|
saat data konsisten.
|
|
|
|
|
|
- **Bergantung pada:** PC-104
|
|
|
|
|
|
|
|
|
|
|
|
### PC-109 · Pembaca setting loyalitas dan riwayat perubahan · M
|
|
|
|
|
|
- **PRD:** F1, F2 (bagian mekanisme, bukan UI), §8 `loyalty_setting_changes`
|
|
|
|
|
|
- **Kerjakan:** service yang membaca setting outlet dan organisasi dengan nilai default
|
|
|
|
|
|
PRD bila key belum diisi, mengembalikan struct bertipe (bukan string mentah), dan
|
|
|
|
|
|
menulis `loyalty_setting_changes` setiap kali setting loyalitas diubah.
|
|
|
|
|
|
- **Selesai jika:** outlet tanpa setting mendapat semua default PRD, dan setiap
|
|
|
|
|
|
perubahan tercatat nilai lama, nilai baru, dan pelakunya.
|
|
|
|
|
|
- **Bergantung pada:** PC-102
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Fase 2 — Earning
|
|
|
|
|
|
|
|
|
|
|
|
### PC-201 · API pengaturan loyalitas outlet · M
|
|
|
|
|
|
- **PRD:** F1
|
|
|
|
|
|
- **Kerjakan:** `GET/PUT /outlets/:id/loyalty-settings` dengan validasi F1. Response
|
|
|
|
|
|
menyertakan **persentase cashback efektif** (`earn_value × nilai EnakPoint /
|
|
|
|
|
|
earn_per_amount`).
|
|
|
|
|
|
- **Selesai jika:** validasi menolak nilai di luar batas, dan hanya Admin/Manager yang
|
|
|
|
|
|
bisa mengubah.
|
|
|
|
|
|
- **Bergantung pada:** PC-109
|
|
|
|
|
|
|
|
|
|
|
|
### PC-202 · Kalkulator earning · S
|
|
|
|
|
|
- **PRD:** F1 (rumus), Q1, Q10
|
|
|
|
|
|
- **Kerjakan:** fungsi murni `CalculateEarning(order, pointPaidAmount, settings)` yang
|
|
|
|
|
|
mengembalikan Point, Coin, basis, dan snapshot setting.
|
|
|
|
|
|
- **Selesai jika:** unit test mencakup contoh di PRD (Rp 87.500 → 875 Point, 3 Coin;
|
|
|
|
|
|
dengan Rp 20.000 dibayar EnakPoint → 675 Point, 2 Coin), basis di bawah minimum,
|
|
|
|
|
|
`max_per_order`, setting nonaktif, dan pajak tidak ikut dihitung.
|
|
|
|
|
|
- **Bergantung pada:** PC-201
|
|
|
|
|
|
|
|
|
|
|
|
### PC-203 · Earning saat order lunas · L
|
|
|
|
|
|
- **PRD:** F3
|
|
|
|
|
|
- **Kerjakan:**
|
|
|
|
|
|
- **Satukan titik "order menjadi lunas".** Saat ini `payment_status = completed`
|
|
|
|
|
|
di-set di empat tempat: `OrderProcessorImpl.UpdateOrder`,
|
|
|
|
|
|
`OrderProcessorImpl.updateOrderStatus`, dan dua tempat di `split_bill_processor.go`.
|
|
|
|
|
|
Buat satu hook `onOrderPaid(orderID)` yang dipanggil dari keempatnya.
|
|
|
|
|
|
- Hook menjalankan earning **setelah** transaksi pembayaran commit, sehingga
|
|
|
|
|
|
kegagalan earning tidak menggagalkan pembayaran.
|
|
|
|
|
|
- Idempotency key `earn:{order_id}:{currency}`.
|
|
|
|
|
|
- Lewati order tanpa customer, customer default, atau customer nonaktif.
|
|
|
|
|
|
- **Jaring pengaman:** job yang mencari order lunas beberapa hari terakhir yang belum
|
|
|
|
|
|
punya `EARN` (dan seharusnya punya), lalu menjalankan ulang earning.
|
|
|
|
|
|
- **Selesai jika:** test untuk pembayaran penuh, split bill (lunas di pembayaran
|
|
|
|
|
|
terakhir), self-order, dan pemanggilan ganda (hasilnya tetap satu earning). Earning
|
|
|
|
|
|
yang sengaja digagalkan tidak menggagalkan pembayaran dan terambil oleh job.
|
|
|
|
|
|
- **Bergantung pada:** PC-104, PC-202
|
|
|
|
|
|
|
|
|
|
|
|
### PC-204 · Reversal earning saat void / refund · M
|
|
|
|
|
|
- **PRD:** F10, Q3
|
|
|
|
|
|
- **Kerjakan:** panggil reversal dari `VoidOrder`, `RefundOrder`, dan `RefundPayment`.
|
|
|
|
|
|
Ambil pertama dari lot `EARN` order tersebut, lalu lot lain. Gunakan `DebitUpTo` dan
|
|
|
|
|
|
catat `shortfall`. Refund tidak pernah diblokir.
|
|
|
|
|
|
- **Selesai jika:** test untuk void penuh, refund sebagian (proporsional, akumulasi tidak
|
|
|
|
|
|
melebihi earning), dan saldo yang sudah terpakai (shortfall tercatat, saldo 0).
|
|
|
|
|
|
- **Bergantung pada:** PC-203
|
|
|
|
|
|
|
|
|
|
|
|
### PC-205 · Tampilkan earning di order dan struk · S
|
|
|
|
|
|
- **PRD:** F3
|
|
|
|
|
|
- **Kerjakan:** tambah `points_earned` dan `coins_earned` ke response order dan data
|
|
|
|
|
|
struk.
|
|
|
|
|
|
- **Selesai jika:** nilai sama dengan baris `EARN` di ledger, dan bernilai 0 untuk order
|
|
|
|
|
|
tanpa earning.
|
|
|
|
|
|
- **Bergantung pada:** PC-203
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Fase 3 — Pembayaran EnakPoint
|
|
|
|
|
|
|
|
|
|
|
|
> Boleh dikerjakan sekarang. **Tidak boleh dirilis** sebelum catatan N2 (keuangan) dan
|
|
|
|
|
|
> N3 (legal) ditutup.
|
|
|
|
|
|
|
|
|
|
|
|
### PC-301 · PIN customer · L
|
|
|
|
|
|
- **PRD:** K8, F11, Q16, Q17
|
|
|
|
|
|
- **Kerjakan:**
|
|
|
|
|
|
- Migrasi kolom PIN di `customers` dan tabel `customer_security_events`.
|
|
|
|
|
|
- Purpose OTP baru `pin_setup` dan `pin_reset` di `OtpProcessor`.
|
|
|
|
|
|
- Endpoint `/customer/pin/*` (status, OTP, buat, ganti, reset).
|
|
|
|
|
|
- Hash bcrypt, tolak PIN lemah (digit sama, berurutan, tanggal lahir).
|
|
|
|
|
|
- Kunci 30 menit setelah 5 kali salah, dengan penghitung di database.
|
|
|
|
|
|
- Tahan transfer keluar 24 jam setelah reset.
|
|
|
|
|
|
- `VerifyPin(ctx, customerID, pin)` untuk dipakai task lain, dengan error
|
|
|
|
|
|
`PIN_NOT_SET`, `PIN_INVALID`, `PIN_LOCKED`, `TRANSFER_BLOCKED`.
|
|
|
|
|
|
- Hapus PIN oleh admin: `DELETE /marketing/customers/:id/pin`, dan
|
|
|
|
|
|
`GET /marketing/customers/:id/security-events`.
|
|
|
|
|
|
- PIN tidak pernah muncul di log (termasuk log request body).
|
|
|
|
|
|
- **Selesai jika:** test untuk semua aturan di atas, termasuk 5 kali salah lalu PIN
|
|
|
|
|
|
benar tetap ditolak selama terkunci, dan reset OTP membuka kunci.
|
|
|
|
|
|
- **Bergantung pada:** –
|
|
|
|
|
|
|
|
|
|
|
|
### PC-302 · API pengaturan loyalitas organisasi · S
|
|
|
|
|
|
- **PRD:** F2
|
|
|
|
|
|
- **Kerjakan:** `GET/PUT /marketing/loyalty-settings` untuk nilai EnakPoint, kurs
|
|
|
|
|
|
exchange, dan batas transfer, serta `GET /marketing/loyalty-settings/history`.
|
|
|
|
|
|
Sebelum menyimpan perubahan nilai EnakPoint atau kurs, response preview menampilkan
|
|
|
|
|
|
total saldo beredar dan nilai rupiahnya sebelum/sesudah.
|
|
|
|
|
|
- **Selesai jika:** perubahan tercatat di riwayat, dan transaksi lama tetap memakai nilai
|
|
|
|
|
|
yang dibekukan.
|
|
|
|
|
|
- **Bergantung pada:** PC-109
|
|
|
|
|
|
|
|
|
|
|
|
### PC-303 · Payment method EnakPoint · M
|
|
|
|
|
|
- **PRD:** F9 (payment method), §8 (perubahan `payments`), §10.5
|
|
|
|
|
|
- **Kerjakan:**
|
|
|
|
|
|
- Tambah tipe `point` ke `PaymentMethodType` dan validator (`oneof=cash card
|
|
|
|
|
|
digital_wallet point`).
|
|
|
|
|
|
- Migrasi kolom `points_used` dan `point_value` di `payments`.
|
|
|
|
|
|
- Buat payment method sistem "EnakPoint" untuk setiap organisasi yang sudah ada, dan
|
|
|
|
|
|
otomatis untuk organisasi baru.
|
|
|
|
|
|
- Tolak hapus / ubah tipe method sistem.
|
|
|
|
|
|
- Sembunyikan method ini di kasir jika outlet tidak mengaktifkan `accept_payment`.
|
|
|
|
|
|
- **Selesai jika:** setiap organisasi punya tepat satu method EnakPoint yang tidak bisa
|
|
|
|
|
|
dihapus.
|
|
|
|
|
|
- **Bergantung pada:** –
|
|
|
|
|
|
|
|
|
|
|
|
### PC-304 · Kode bayar sekali pakai · M
|
|
|
|
|
|
- **PRD:** F9 (persetujuan customer), K8
|
|
|
|
|
|
- **Kerjakan:** `POST /customer/wallet/payment-code` (butuh PIN) menghasilkan kode 6
|
|
|
|
|
|
digit dan QR yang berlaku 2 menit, disimpan di Redis dengan TTL dan diambil
|
|
|
|
|
|
sekali-pakai (`GETDEL`). Kode terikat ke `customer_id`.
|
|
|
|
|
|
- **Selesai jika:** kode kedaluwarsa, kode yang sudah dipakai, dan kode milik customer
|
|
|
|
|
|
lain semuanya ditolak.
|
|
|
|
|
|
- **Bergantung pada:** PC-301
|
|
|
|
|
|
|
|
|
|
|
|
### PC-305 · Bayar EnakPoint di kasir · L
|
|
|
|
|
|
- **PRD:** F9 (perhitungan, pencatatan), K7
|
|
|
|
|
|
- **Kerjakan:**
|
|
|
|
|
|
- `GET /orders/:id/point-payment/preview`.
|
|
|
|
|
|
- Cabang method `point` di `CreatePayment`: validasi kode bayar, cocokkan customer
|
|
|
|
|
|
order, hitung `maks_point`, lalu lakukan langkah 1–6 F9 dalam **satu transaksi**
|
|
|
|
|
|
bersama pembuatan baris `payments`.
|
|
|
|
|
|
- Idempotency `payment:{payment_id}`.
|
|
|
|
|
|
- Tolak jika `nominal_rupiah > remaining_amount` (tidak ada kembalian).
|
|
|
|
|
|
- **Selesai jika:** test untuk pembayaran penuh, sebagian + tunai (split), batas
|
|
|
|
|
|
`max_payment_percent`, order walk-in (ditolak), kode salah (ditolak), dan dua
|
|
|
|
|
|
pembayaran bersamaan untuk customer yang sama (saldo tidak terpakai dua kali).
|
|
|
|
|
|
Earning (PC-203) menghitung basis tanpa bagian EnakPoint.
|
|
|
|
|
|
- **Bergantung pada:** PC-104, PC-303, PC-304
|
|
|
|
|
|
|
|
|
|
|
|
### PC-306 · Bayar EnakPoint dari app / self-order · M
|
|
|
|
|
|
- **PRD:** F9
|
|
|
|
|
|
- **Kerjakan:** `POST /customer/orders/:id/pay-with-points` (butuh PIN, hanya untuk
|
|
|
|
|
|
order milik customer itu sendiri), memakai logika yang sama dengan PC-305.
|
|
|
|
|
|
- **Selesai jika:** customer tidak bisa membayar order milik customer lain.
|
|
|
|
|
|
- **Bergantung pada:** PC-305
|
|
|
|
|
|
|
|
|
|
|
|
### PC-307 · Refund pembayaran EnakPoint · M
|
|
|
|
|
|
- **PRD:** F9 (void/refund), K7, Q13
|
|
|
|
|
|
- **Kerjakan:** `PAYMENT_REFUND` di jalur void dan refund memakai `point_value` yang
|
|
|
|
|
|
dibekukan, dengan pembulatan ke bawah. Kembalikan ke lot dengan `expires_at` asal
|
|
|
|
|
|
(aturan perpanjangan 7 hari mengikuti N4, jadi untuk sekarang cukup pakai tanggal
|
|
|
|
|
|
asal). **Tolak** permintaan refund bagian EnakPoint lewat method lain.
|
|
|
|
|
|
- **Selesai jika:** test untuk void penuh, refund sebagian, perubahan nilai EnakPoint di
|
|
|
|
|
|
antara bayar dan refund (jumlah EnakPoint yang kembali tetap sama), dan percobaan
|
|
|
|
|
|
refund tunai atas pembayaran EnakPoint (ditolak).
|
|
|
|
|
|
- **Bergantung pada:** PC-305
|
|
|
|
|
|
|
|
|
|
|
|
### PC-308 · EnakPoint di laporan · S
|
|
|
|
|
|
- **PRD:** F9 (laporan)
|
|
|
|
|
|
- **Kerjakan:** laporan per payment method menampilkan EnakPoint terpisah dan tidak
|
|
|
|
|
|
menghitungnya sebagai kas masuk. Cek laporan analytics yang menjumlahkan pembayaran.
|
|
|
|
|
|
- **Selesai jika:** total kas masuk di laporan tidak berubah saat sebagian order dibayar
|
|
|
|
|
|
EnakPoint. Perlakuan akuntansi lanjutan menunggu N2.
|
|
|
|
|
|
- **Bergantung pada:** PC-305
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Fase 4 — Pergerakan Saldo
|
|
|
|
|
|
|
|
|
|
|
|
> Boleh dikerjakan sekarang. Transfer (PC-402) **tidak boleh dirilis** sebelum N3
|
|
|
|
|
|
> (legal) ditutup.
|
|
|
|
|
|
|
|
|
|
|
|
### PC-401 · Exchange EnakCoin → EnakPoint · M
|
|
|
|
|
|
- **PRD:** F4, K3
|
|
|
|
|
|
- **Kerjakan:** `GET /customer/wallet/exchange/preview` dan
|
|
|
|
|
|
`POST /customer/wallet/exchange` (butuh PIN, `Idempotency-Key`). Jumlah harus
|
|
|
|
|
|
kelipatan `coin_amount`. Kurs dibekukan di metadata. Lot EnakPoint kedaluwarsa pada
|
|
|
|
|
|
`min(lot EnakCoin asal, sekarang + masa berlaku EnakPoint)`.
|
|
|
|
|
|
- **Selesai jika:** test untuk kurs default 1:1, kurs 10:3, jumlah bukan kelipatan
|
|
|
|
|
|
(ditolak), dan `expires_at` lot hasil exchange tidak pernah lebih lama dari lot asal.
|
|
|
|
|
|
- **Bergantung pada:** PC-104, PC-301, PC-302
|
|
|
|
|
|
|
|
|
|
|
|
### PC-402 · Transfer antar customer · L
|
|
|
|
|
|
- **PRD:** F5, Q4, Q16
|
|
|
|
|
|
- **Kerjakan:** `GET /customer/wallet/transfer/recipient` (nama disamarkan) dan
|
|
|
|
|
|
`POST /customer/wallet/transfer` (butuh PIN, `Idempotency-Key`). Kunci dua wallet
|
|
|
|
|
|
berurutan, cek batas organisasi (per transaksi dan harian), cek
|
|
|
|
|
|
`transfer_blocked_until`. Lot penerima mewarisi `expires_at` dan `origin_lot_id` dari
|
|
|
|
|
|
lot pengirim. Kirim notifikasi push ke penerima.
|
|
|
|
|
|
- **Selesai jika:** test untuk transfer ke diri sendiri / customer lain organisasi /
|
|
|
|
|
|
customer default (semua ditolak), batas harian, transfer dua arah bersamaan (tidak
|
|
|
|
|
|
deadlock), dan `expires_at` di penerima sama persis dengan pengirim.
|
|
|
|
|
|
- **Bergantung pada:** PC-104, PC-301, PC-302
|
|
|
|
|
|
|
|
|
|
|
|
### PC-403 · Semua game memakai EnakCoin · M
|
|
|
|
|
|
- **PRD:** F8, K1
|
|
|
|
|
|
- **Kerjakan:**
|
|
|
|
|
|
- `GamePlayProcessor.PlayGame` memotong EnakCoin lewat wallet engine sesuai
|
|
|
|
|
|
`games.metadata.coin_cost`.
|
|
|
|
|
|
- Seluruh permainan dalam satu transaksi: ubah repository game, game play, dan game
|
|
|
|
|
|
prize ke `DBFromContext`. Gagal mengurangi stok hadiah membatalkan permainan (saat
|
|
|
|
|
|
ini hanya di-`Printf`), dan rollback manual `AddTokens` dihapus.
|
|
|
|
|
|
- Rename `game_plays.token_used` menjadi `coins_used`.
|
|
|
|
|
|
- **Selesai jika:** test untuk saldo kurang (ditolak, tidak ada `game_plays` yang
|
|
|
|
|
|
tercatat), stok hadiah gagal (semua dibatalkan), dan `GAME_SPEND` menunjuk
|
|
|
|
|
|
`game_plays.id`.
|
|
|
|
|
|
- **Bergantung pada:** PC-104
|
|
|
|
|
|
|
|
|
|
|
|
### PC-404 · Telusuri per butir di dashboard · S
|
|
|
|
|
|
- **PRD:** F7, §8.1 (per butir)
|
|
|
|
|
|
- **Kerjakan:** `GET /marketing/wallet-transactions/:id/trace` yang mengembalikan
|
|
|
|
|
|
alokasi lot dan rantai `origin_lot_id` sampai ke lot pertama.
|
|
|
|
|
|
- **Selesai jika:** contoh di §8 (A transfer 120 ke B, B bayar 30) menelusuri ke order
|
|
|
|
|
|
#ORD-1 milik A.
|
|
|
|
|
|
- **Bergantung pada:** PC-402
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Fase 5 — Kedaluwarsa
|
|
|
|
|
|
|
|
|
|
|
|
> **Jangan dikerjakan sebelum catatan N4 (model kedaluwarsa) diputuskan.** Struktur lot
|
|
|
|
|
|
> sudah ada sejak PC-101, jadi yang tersisa hanya rumus, job, dan tampilan.
|
|
|
|
|
|
|
|
|
|
|
|
### PC-501 · Setting kedaluwarsa · M
|
|
|
|
|
|
- **PRD:** F12 (pengaturan), N4
|
|
|
|
|
|
- **Kerjakan:** key setting sesuai model yang dipilih di N4, dengan validasi dan preview
|
|
|
|
|
|
"saldo yang didapat hari ini kedaluwarsa pada …".
|
|
|
|
|
|
- **Bergantung pada:** PC-302, **N4**
|
|
|
|
|
|
|
|
|
|
|
|
### PC-502 · Hitung `expires_at` saat lot dibuat · M
|
|
|
|
|
|
- **PRD:** F12 (tabel kedaluwarsa per lot), N4
|
|
|
|
|
|
- **Kerjakan:** satu fungsi `ComputeExpiry(currency, receivedAt, settings)` yang dipakai
|
|
|
|
|
|
oleh `EARN` dan `ADJUSTMENT`, serta aturan aktivasi pertama untuk lot lama (termasuk
|
|
|
|
|
|
lot `MIGRATION`).
|
|
|
|
|
|
- **Bergantung pada:** PC-104, PC-501
|
|
|
|
|
|
|
|
|
|
|
|
### PC-503 · Job kedaluwarsa · M
|
|
|
|
|
|
- **PRD:** F12 (proses kedaluwarsa)
|
|
|
|
|
|
- **Kerjakan:** job per jam yang memproses lot lewat tanggal dengan `EXPIRE`
|
|
|
|
|
|
(idempotency `expire:{lot_id}`) di bawah lock wallet.
|
|
|
|
|
|
- **Jangan menyalin pola `OmsetMilestoneScheduler`.** Scheduler itu menyimpan state di
|
|
|
|
|
|
memori, dan menurut komentarnya sendiri bisa mengirim ulang notifikasi setelah
|
|
|
|
|
|
restart. Job ini harus aman dijalankan di banyak instance sekaligus: pilih lot
|
|
|
|
|
|
dengan `FOR UPDATE SKIP LOCKED`, dan andalkan idempotency key.
|
|
|
|
|
|
- **Selesai jika:** dua instance yang berjalan bersamaan tidak menghanguskan lot yang
|
|
|
|
|
|
sama dua kali, dan tidak ada lot yang lewat tanggal lebih dari 1 jam tanpa diproses.
|
|
|
|
|
|
- **Bergantung pada:** PC-502
|
|
|
|
|
|
|
|
|
|
|
|
### PC-504 · Pengingat dan tampilan saldo yang akan kedaluwarsa · M
|
|
|
|
|
|
- **PRD:** F6 (`/wallet/expiring`), F12 (pengingat)
|
|
|
|
|
|
- **Kerjakan:** endpoint `GET /customer/wallet/expiring`, field saldo kedaluwarsa
|
|
|
|
|
|
terdekat di `/wallet`, dan notifikasi pengingat yang dikelompokkan per tanggal.
|
|
|
|
|
|
Jadwal pengingat mengikuti N4.
|
|
|
|
|
|
- **Bergantung pada:** PC-503
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Fase 6 — Bersih-bersih
|
|
|
|
|
|
|
|
|
|
|
|
### PC-601 · Hapus tabel dan kode lama · S
|
|
|
|
|
|
- **PRD:** §10.7
|
|
|
|
|
|
- **Kerjakan:** satu rilis setelah PC-105 berjalan di production, drop
|
|
|
|
|
|
`customer_points` dan `customer_tokens`. Hapus `CustomerPointsProcessor` dan
|
|
|
|
|
|
`CustomerTokensProcessor` beserta stub `not implemented`, rute yang di-comment di
|
|
|
|
|
|
`router.go`, dan alias `/customer/points` / `/customer/tokens` setelah aplikasi
|
|
|
|
|
|
diperbarui.
|
|
|
|
|
|
- **Bergantung pada:** PC-106, PC-403, dan konfirmasi bahwa aplikasi sudah tidak
|
|
|
|
|
|
memanggil endpoint lama.
|
|
|
|
|
|
|
|
|
|
|
|
### PC-602 · Dokumentasi integrasi · S
|
|
|
|
|
|
- **Kerjakan:** panduan integrasi untuk tim aplikasi dan POS (seperti
|
|
|
|
|
|
`integration-weight-based-products.md`): endpoint, kode error PIN, alur kode bayar, dan
|
|
|
|
|
|
contoh request/response.
|
|
|
|
|
|
- **Bergantung pada:** PC-305, PC-402
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Yang Bisa Dimulai Sekarang
|
|
|
|
|
|
|
|
|
|
|
|
Bisa dikerjakan paralel tanpa menunggu apa pun:
|
|
|
|
|
|
- **PC-101** (migrasi wallet) → langsung lanjut **PC-103**, lalu **PC-104**
|
|
|
|
|
|
- **PC-102** (setting organisasi) → **PC-109**
|
|
|
|
|
|
- **PC-301** (PIN customer)
|
|
|
|
|
|
- **PC-303** (payment method EnakPoint)
|
|
|
|
|
|
|
|
|
|
|
|
PC-104 adalah jalur kritis: hampir semua task lain menunggunya.
|