504 lines
23 KiB
Markdown
504 lines
23 KiB
Markdown
# Task Breakdown: EnakGame
|
||||
|
|
|
|||
|
|
**Sumber:** [RFC EnakGame](rfc-enakgame.md), [PRD EnakGame](enakgame-prd.md)
|
|||
|
|
**Tanggal:** 2026-10-07
|
|||
|
|
|
|||
|
|
Setiap task menyebut bagian RFC 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 `000101`),
|
|||
|
|
`entities` → `repository` → `processor` → `service` → `handler` / `validator` →
|
|||
|
|
`router`, dan wiring di `internal/app/app.go`. Repository **selalu** memakai
|
|||
|
|
`DBFromContext`. Semua mutasi saldo **hanya** lewat `WalletProcessor`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Ringkasan
|
|||
|
|
|
|||
|
|
| Fase | Task | Terblokir oleh |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 0. Matikan bayar dengan EnakPoint | EG-001 – EG-002 | – |
|
|||
|
|
| 1. Fondasi | EG-101 – EG-106 | – |
|
|||
|
|
| 2. Admin game & session | EG-201 – EG-206 | – |
|
|||
|
|
| 3. Reward Engine & complete | EG-301 – EG-303 | Pembulatan (RFC §19.2 #1) sebelum **rilis** |
|
|||
|
|
| 4. Economy Guard | EG-401 – EG-402 | – |
|
|||
|
|
| 5. Voucher & redemption | EG-501 – EG-507 | – |
|
|||
|
|
| 6. Budget metrics | EG-601 – EG-602 | Exhaustion policy & threshold (#3, #4) sebelum **rilis** |
|
|||
|
|
| 7. Event / campaign | EG-701 – EG-704 | Event stacking (#2) sebelum **rilis** |
|
|||
|
|
| 8. Voucher eksternal | EG-801 – EG-803 | Provider pertama yang konkret sebelum **dikerjakan** |
|
|||
|
|
| 9. Budget Controller & analytics | EG-901 – EG-903 | Threshold (#4) sebelum **dikerjakan** |
|
|||
|
|
| 10. Spin & bersih-bersih | EG-1001 – EG-1003 | – |
|
|||
|
|
|
|||
|
|
Fase 0 independen dan bisa jalan paralel dengan semua fase lain, tetapi **harus selesai
|
|||
|
|
sebelum EnakGame rilis** (PRD §3.2).
|
|||
|
|
|
|||
|
|
Fase 1–3 membuat game bisa dimainkan end-to-end dengan Coin. Fase 5 membuat Point bisa
|
|||
|
|
ditukar. Fase 6 membuat Finance bisa melihat biaya.
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Fase 0 EG-001 ── EG-002
|
|||
|
|
|
|||
|
|
Fondasi EG-101 ── EG-102 ── EG-105
|
|||
|
|
EG-103, EG-104, EG-106, EG-301 (independen)
|
|||
|
|
|
|||
|
|
Game EG-104 + EG-105 ─────────────────── EG-201, EG-203
|
|||
|
|
EG-104 + EG-105 + EG-301 ────────── EG-202
|
|||
|
|
EG-103 + EG-201 + EG-202 + EG-203 ─ EG-204 ── EG-205
|
|||
|
|
EG-204 + EG-205 + EG-301 + EG-302 ─ EG-303
|
|||
|
|
EG-106 ── EG-401
|
|||
|
|
EG-303 + EG-401 ─────────────────── EG-402
|
|||
|
|
|
|||
|
|
Voucher EG-501 ── EG-502
|
|||
|
|
EG-103 + EG-502 + EG-503 ────────── EG-504
|
|||
|
|
EG-303 + EG-504 ─────────────────── EG-505 ── EG-601 ─┬─ EG-602
|
|||
|
|
└─ EG-901 ── EG-902
|
|||
|
|
EG-504 ── EG-801
|
|||
|
|
EG-505 + EG-801 ─────────────────── EG-802 ── EG-803
|
|||
|
|
|
|||
|
|
Event EG-102 ── EG-701
|
|||
|
|
EG-203 + EG-701 ─────────────────── EG-702
|
|||
|
|
EG-402 + EG-702 ─────────────────── EG-703
|
|||
|
|
|
|||
|
|
Akhir EG-402 ── EG-1001 ── EG-1002 ── EG-1003
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Task kecil yang tidak digambar: EG-206, EG-302 (setelah EG-105), EG-506 (EG-504), EG-507
|
|||
|
|
(EG-501), EG-704 (EG-206 + EG-702), EG-903 (EG-303 + EG-505).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Fase 0 — Matikan Bayar dengan EnakPoint
|
|||
|
|
|
|||
|
|
PRD §3.2: Point hanya bisa ditukar ke voucher. Belum ada order yang dibayar dengan Point,
|
|||
|
|
jadi tidak ada data yang dimigrasi.
|
|||
|
|
|
|||
|
|
### EG-001 · Tutup semua jalur pembayaran EnakPoint · M
|
|||
|
|
- **RFC:** §14
|
|||
|
|
- **Kerjakan:**
|
|||
|
|
- Sudah dikonfirmasi belum ada pembayaran dengan EnakPoint di production (2026-10-07).
|
|||
|
|
- Migrasi: drop trigger yang membuat payment method `point` untuk organisasi baru
|
|||
|
|
(`000094`), lalu nonaktifkan / hapus baris `payment_methods` tipe `point`.
|
|||
|
|
- Tolak pembayaran bertipe `point` di jalur POS (`order_handler`, `order_processor`,
|
|||
|
|
`payment_method_processor`) dan customer app (`customer_order_payment_service`).
|
|||
|
|
- Hapus route `GET /orders/:id/point-payment/preview` dan
|
|||
|
|
`POST /customer/wallet/payment-code`.
|
|||
|
|
- Hapus `PAYMENT` dan `PAYMENT_REFUND` dari `walletTypeRules`, supaya engine menolak
|
|||
|
|
keduanya. CHECK database dibiarkan.
|
|||
|
|
- Hapus setting `loyalty.point.accept_payment` dari descriptor
|
|||
|
|
`LoyaltySettingsProcessor` dan dari response outlet customer.
|
|||
|
|
- **Selesai jika:** pembayaran order dengan method `point` ditolak di POS dan customer app,
|
|||
|
|
outlet baru tidak lagi mendapat payment method EnakPoint, dan test yang ada diperbarui.
|
|||
|
|
- **Bergantung pada:** –
|
|||
|
|
|
|||
|
|
### EG-002 · Hapus kode point payment · S
|
|||
|
|
- **Kerjakan:** hapus `PointPaymentProcessor`, `point_payment_refund.go`,
|
|||
|
|
`PointPaymentRepository`, `PointPaymentService`, `PointPaymentHandler`, wiring di
|
|||
|
|
`app.go` / `router.go`, dan test-nya (`point_payment_db_test.go`,
|
|||
|
|
`point_payment_method_db_test.go`). Pindahkan dulu helper yang ternyata dipakai di
|
|||
|
|
tempat lain (mis. pola pembuatan lot refund, yang dipakai ulang di EG-205).
|
|||
|
|
- **Selesai jika:** `go build ./...` dan seluruh test lulus, dan
|
|||
|
|
`grep -rn "PointPayment" internal/` kosong.
|
|||
|
|
- **Bergantung pada:** EG-001
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Fase 1 — Fondasi
|
|||
|
|
|
|||
|
|
### EG-101 · Migrasi extend `games` + arsipkan game lama · S
|
|||
|
|
- **RFC:** §5.1, §14
|
|||
|
|
- **Kerjakan:** migrasi up & down persis seperti §5.1. Semua baris lama menjadi
|
|||
|
|
`ARCHIVED`. **Tidak ada `DELETE`** (FK `game_plays` CASCADE).
|
|||
|
|
- **Selesai jika:**
|
|||
|
|
- Up/down bersih di database kosong dan di salinan staging.
|
|||
|
|
- Jumlah baris `game_plays` sebelum = sesudah.
|
|||
|
|
- Ditolak database: game `ACTIVE` tanpa `organization_id` atau `slug`;
|
|||
|
|
`entry_cost = 0`; dua game dengan slug sama di satu org.
|
|||
|
|
- **Bergantung pada:** –
|
|||
|
|
|
|||
|
|
### EG-102 · Migrasi budget, reward config, session · M
|
|||
|
|
- **RFC:** §5.2, §5.3, §5.4, §5.6
|
|||
|
|
- **Kerjakan:** `game_budgets`, `game_reward_configs`, `game_sessions`,
|
|||
|
|
`game_session_rewards`, dalam urutan foreign key.
|
|||
|
|
- **Selesai jika:** up/down bersih. Ditolak database: dua config `ACTIVE` untuk satu game;
|
|||
|
|
session `REFUNDED` tanpa `refund_transaction_id`; dua budget `GLOBAL` dengan
|
|||
|
|
`period_start` sama di satu org; `game_session_rewards` dengan
|
|||
|
|
`wallet_transaction_id` yang sama dua kali.
|
|||
|
|
- **Bergantung pada:** EG-101
|
|||
|
|
|
|||
|
|
### EG-103 · Tipe ledger baru · M
|
|||
|
|
- **RFC:** §6
|
|||
|
|
- **Kerjakan:**
|
|||
|
|
- Konstanta `GAME_SPEND_REFUND`, `GAME_REWARD`, `REWARD_REDEEM_REFUND`, ref
|
|||
|
|
`GAME_SESSION` di `constants/wallet.go`.
|
|||
|
|
- `walletTypeRules`: tambah tiga tipe, dan izinkan ref `GAME_SESSION` pada
|
|||
|
|
`GAME_SPEND`.
|
|||
|
|
- Migrasi: drop + create ulang tiga CHECK di `wallet_transactions` (§6.2).
|
|||
|
|
- Periksa invariant di `wallet_reconciliation_repository.go` terhadap tipe baru.
|
|||
|
|
- **Selesai jika:** test validasi di `wallet_processor_test.go` per tipe baru (currency
|
|||
|
|
salah, ref salah, reversal tanpa `reverses_transaction_id` → ditolak), dan test DB yang
|
|||
|
|
benar-benar insert ke `wallet_transactions` untuk setiap tipe (lolos engine **dan**
|
|||
|
|
CHECK).
|
|||
|
|
- **Bergantung pada:** –
|
|||
|
|
|
|||
|
|
### EG-104 · `audit_logs` + helper · S
|
|||
|
|
- **RFC:** §5.9, §13
|
|||
|
|
- **Kerjakan:** migrasi, entity, dan `AuditLogger.Record(ctx, entry)` yang menulis lewat
|
|||
|
|
`DBFromContext`, sehingga audit ikut commit/rollback bersama perubahannya.
|
|||
|
|
- **Selesai jika:** perubahan yang di-rollback tidak meninggalkan baris audit.
|
|||
|
|
- **Bergantung pada:** –
|
|||
|
|
|
|||
|
|
### EG-105 · Entities & repository EnakGame · M
|
|||
|
|
- **RFC:** §5.1–§5.6
|
|||
|
|
- **Kerjakan:** entities dan repository untuk kolom baru `games`, `game_reward_configs`,
|
|||
|
|
`game_sessions`, `game_session_rewards`, `game_budgets`. Transisi status session
|
|||
|
|
sebagai `UPDATE ... WHERE status = 'STARTED'` yang mengembalikan jumlah baris
|
|||
|
|
ter-update (D4). Semua query game/budget memfilter `organization_id`.
|
|||
|
|
- **Selesai jika:** test repository: dua transisi bersamaan pada session yang sama, hanya
|
|||
|
|
satu yang mendapat 1 baris.
|
|||
|
|
- **Bergantung pada:** EG-101, EG-102
|
|||
|
|
|
|||
|
|
### EG-106 · Setting limit EnakGame · S
|
|||
|
|
- **RFC:** §5.10
|
|||
|
|
- **Kerjakan:** key `enakgame.limit.user_daily` dan `enakgame.limit.global_daily` sebagai
|
|||
|
|
field descriptor di `LoyaltySettingsProcessor` (default 0 = tanpa batas, min 0).
|
|||
|
|
- **Selesai jika:** setting terbaca dengan default, bisa diubah lewat endpoint loyalty
|
|||
|
|
settings yang ada, dan perubahan tercatat di `loyalty_setting_changes`.
|
|||
|
|
- **Bergantung pada:** –
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Fase 2 — Admin Game & Session
|
|||
|
|
|
|||
|
|
### EG-201 · Admin game CRUD · M
|
|||
|
|
- **RFC:** §11 (admin)
|
|||
|
|
- **Kerjakan:** `/marketing/enakgame/games` CRUD + `PUT /:id/status`, org dari user admin.
|
|||
|
|
Validasi `slug`, `entry_cost ≥ 1`, `result_rules`. Game `ARCHIVED` tidak bisa diubah.
|
|||
|
|
Perubahan status masuk `audit_logs`.
|
|||
|
|
- **Selesai jika:** admin org A tidak bisa melihat atau mengubah game org B; game lama
|
|||
|
|
(arsip) tidak muncul.
|
|||
|
|
- **Bergantung pada:** EG-104, EG-105
|
|||
|
|
|
|||
|
|
### EG-202 · Admin reward config versioning · M
|
|||
|
|
- **RFC:** §5.2, D7
|
|||
|
|
- **Kerjakan:** `POST /games/:id/reward-configs` (versi baru, `rules` divalidasi
|
|||
|
|
`RewardCalculator.Validate`), `POST /reward-configs/:id/activate` (dalam satu transaksi:
|
|||
|
|
config lama → `RETIRED`, yang baru → `ACTIVE`), `GET` daftar versi. Tidak ada endpoint
|
|||
|
|
update atau delete. Wajib `RequireLoyaltyManager`. Semua aksi masuk `audit_logs`.
|
|||
|
|
- **Selesai jika:** config yang sudah dipakai session tidak bisa diubah lewat jalur apa
|
|||
|
|
pun; aktivasi bersamaan dua versi berakhir dengan tepat satu `ACTIVE`.
|
|||
|
|
- **Bergantung pada:** EG-104, EG-105, EG-301
|
|||
|
|
|
|||
|
|
### EG-203 · Admin budget + `GameBudgetPeriodJob` · M
|
|||
|
|
- **RFC:** §5.6, §12
|
|||
|
|
- **Kerjakan:** CRUD `/marketing/enakgame/budgets` (scope `GLOBAL` / `EVENT`), wajib
|
|||
|
|
`RequireLoyaltyManager`, audit. Job harian yang membuat budget global bulan berikutnya
|
|||
|
|
dari bulan berjalan bila belum ada.
|
|||
|
|
- **Selesai jika:** job aman dijalankan berulang (unique index mencegah duplikat), dan
|
|||
|
|
perubahan `amount` tercatat sebelum/sesudah di audit.
|
|||
|
|
- **Bergantung pada:** EG-104, EG-105
|
|||
|
|
|
|||
|
|
### EG-204 · Start Game · M
|
|||
|
|
- **RFC:** §7.1
|
|||
|
|
- **Kerjakan:** `POST /customer/enakgame/sessions` dengan `Idempotency-Key` wajib (≤ 50
|
|||
|
|
karakter, pola `customer_wallet_handler.go`). Langkah persis §7.1.
|
|||
|
|
- **Selesai jika:** test untuk:
|
|||
|
|
- Key yang sama dua kali → satu debit, session yang sama dikembalikan.
|
|||
|
|
- Coin kurang → ditolak, tidak ada ledger maupun session.
|
|||
|
|
- Game org lain, game tidak `ACTIVE`, tanpa config aktif, tanpa budget global → ditolak.
|
|||
|
|
- `entry_cost` dan `reward_config_id` tersimpan sebagai snapshot: mengubah game setelah
|
|||
|
|
start tidak mengubah session.
|
|||
|
|
- **Bergantung pada:** EG-103, EG-105, EG-201, EG-202, EG-203
|
|||
|
|
|
|||
|
|
### EG-205 · Refund otomatis + `GameSessionJob` · M
|
|||
|
|
- **RFC:** §7.3, §6.3
|
|||
|
|
- **Kerjakan:**
|
|||
|
|
- `RefundSession(ctx, sessionID, reason)`: lock wallet → transisi bersyarat ke
|
|||
|
|
`REFUNDED` → `Credit` `GAME_SPEND_REFUND` dengan lot per alokasi asal
|
|||
|
|
(`RefundExpiry`, `origin_lot_id`) → audit `SYSTEM`.
|
|||
|
|
- Job tiap 1 menit, per batch, satu transaksi per session, sesuai tabel §7.3.
|
|||
|
|
- Wiring start/stop di `app.go` seperti `WalletExpiryJob`.
|
|||
|
|
- **Selesai jika:** test untuk:
|
|||
|
|
- Game dinonaktifkan → session `STARTED` di-refund tanpa menunggu kedaluwarsa.
|
|||
|
|
- Expired dengan `completion_failed_at` → refund. Expired tanpa → `EXPIRED`, saldo tetap.
|
|||
|
|
- Lot refund punya `origin_lot_id` ke lot asal dan `expires_at` minimal 7 hari.
|
|||
|
|
- Complete dan refund bersamaan pada session yang sama → tepat satu yang berhasil.
|
|||
|
|
- Job dijalankan dua kali → tidak ada refund ganda.
|
|||
|
|
- **Bergantung pada:** EG-204
|
|||
|
|
|
|||
|
|
### EG-206 · Customer: daftar game & riwayat session · S
|
|||
|
|
- **RFC:** §11 (customer)
|
|||
|
|
- **Kerjakan:** `GET /customer/enakgame/games`, `GET /sessions`, `GET /sessions/:id`. Hanya
|
|||
|
|
game `ACTIVE` milik org customer. Rincian internal (`reward_breakdown` lengkap, angka
|
|||
|
|
RNG) tidak ditampilkan.
|
|||
|
|
- **Bergantung pada:** EG-105
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Fase 3 — Reward Engine & Complete
|
|||
|
|
|
|||
|
|
### EG-301 · `RewardCalculator` + empat tipe · M
|
|||
|
|
- **RFC:** §8
|
|||
|
|
- **Kerjakan:** interface `Validate` / `Calculate`, implementasi `FIXED`, `SCORE_BASED`,
|
|||
|
|
`OUTCOME_BASED`, `PROBABILITY`. RNG lewat interface yang diisi `crypto/rand` di
|
|||
|
|
production dan RNG tetap di test. Pembulatan ke bawah.
|
|||
|
|
- **Selesai jika:** unit test: contoh tabel PRD §12 untuk setiap tipe; band skor
|
|||
|
|
tumpang tindih / berlubang ditolak `Validate`; bobot `PROBABILITY` nol atau negatif
|
|||
|
|
ditolak; distribusi `PROBABILITY` pada 100.000 undian berada dalam toleransi bobotnya.
|
|||
|
|
- **Bergantung pada:** –
|
|||
|
|
|
|||
|
|
### EG-302 · Result Validator · S
|
|||
|
|
- **RFC:** §7.2 langkah 5
|
|||
|
|
- **Kerjakan:** validasi `result` terhadap `games.result_rules`: durasi minimal, skor
|
|||
|
|
maksimum, skor per detik, outcome yang dikenal. Mengembalikan alasan, bukan error.
|
|||
|
|
- **Selesai jika:** unit test per aturan, termasuk game tanpa `result_rules` (semua lolos).
|
|||
|
|
- **Bergantung pada:** EG-105
|
|||
|
|
|
|||
|
|
### EG-303 · Complete Game · L
|
|||
|
|
- **RFC:** §7.2, §7.3 (bagian `completion_failed_at`)
|
|||
|
|
- **Kerjakan:**
|
|||
|
|
- `POST /customer/enakgame/sessions/:id/complete`, langkah §7.2 dengan base reward dari
|
|||
|
|
budget global saja. Event menyusul di EG-703, guard di EG-402.
|
|||
|
|
- Satu baris `GAME_REWARD` + `game_session_rewards` per budget, key
|
|||
|
|
`game-reward:{session}:{budget}`. Lot dengan `ComputeExpiry(CoinExpiry)`.
|
|||
|
|
- Game tidak `ACTIVE` saat complete → `RefundSession(GAME_DEACTIVATED)`.
|
|||
|
|
- Error non-bisnis → tulis `completion_failed_at` di transaksi terpisah
|
|||
|
|
(`DetachTransaction`), lalu kembalikan 5xx.
|
|||
|
|
- **Selesai jika:** test untuk:
|
|||
|
|
- Complete dua kali → satu reward, response kedua sama dengan yang pertama.
|
|||
|
|
- Hasil tidak valid → reward 0, `flagged`, session `COMPLETED`, tanpa refund.
|
|||
|
|
- Session milik customer lain → 404. Session expired → ditolak.
|
|||
|
|
- Error yang disuntikkan setelah kredit → rollback total, `completion_failed_at` terisi.
|
|||
|
|
- Request yang membawa field `reward` diabaikan (P1).
|
|||
|
|
- **Bergantung pada:** EG-204, EG-205, EG-301, EG-302
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Fase 4 — Economy Guard
|
|||
|
|
|
|||
|
|
### EG-401 · Counter reward + increment bersyarat · M
|
|||
|
|
- **RFC:** §5.8, §9
|
|||
|
|
- **Kerjakan:** migrasi `game_reward_counters`; repository `Consume(scope, id, day, x,
|
|||
|
|
limit)` yang mengembalikan jumlah yang **benar-benar** diterima (dipotong ke sisa
|
|||
|
|
limit, 0 bila habis). Hari dihitung di Asia/Jakarta.
|
|||
|
|
- **Selesai jika:** test DB: 20 goroutine menambah counter yang sama dengan limit 100 →
|
|||
|
|
total tepat 100, tidak pernah lewat.
|
|||
|
|
- **Bergantung pada:** EG-106
|
|||
|
|
|
|||
|
|
### EG-402 · Guard di complete · S
|
|||
|
|
- **RFC:** §9
|
|||
|
|
- **Kerjakan:** panggil `Consume` untuk `USER` dan `GLOBAL` harian (dari setting) dan
|
|||
|
|
`GAME` harian (dari `result_rules`) sebelum kredit. Batas yang memotong dicatat di
|
|||
|
|
`reward_breakdown` dan dikembalikan di response.
|
|||
|
|
- **Selesai jika:** reward 30 dengan sisa limit user 10 → kredit 10, breakdown menyebut
|
|||
|
|
limit user. Sisa 0 → session `COMPLETED` dengan reward 0.
|
|||
|
|
- **Bergantung pada:** EG-303, EG-401
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Fase 5 — Voucher & Redemption
|
|||
|
|
|
|||
|
|
### EG-501 · Migrasi voucher · M
|
|||
|
|
- **RFC:** §5.7
|
|||
|
|
- **Kerjakan:** `vouchers`, `voucher_codes`, `voucher_redemptions`,
|
|||
|
|
`voucher_redemption_costs`.
|
|||
|
|
- **Selesai jika:** up/down bersih. Ditolak database: voucher `STATIC` tanpa `stock`;
|
|||
|
|
`EXTERNAL` tanpa `provider`; kode `REDEEMED` tanpa `redemption_id`; dua redemption dengan
|
|||
|
|
`(customer_id, idempotency_key)` yang sama.
|
|||
|
|
- **Bergantung pada:** –
|
|||
|
|
|
|||
|
|
### EG-502 · Admin voucher + impor kode · M
|
|||
|
|
- **RFC:** §11 (admin)
|
|||
|
|
- **Kerjakan:** CRUD `/marketing/enakgame/vouchers`, `POST /:id/codes` (CSV, kode duplikat
|
|||
|
|
dilewati dan dilaporkan), `GET /:id/codes` dengan jumlah per status. `RequireLoyaltyManager`
|
|||
|
|
dan audit.
|
|||
|
|
- **Selesai jika:** impor CSV yang sama dua kali tidak menggandakan kode.
|
|||
|
|
- **Bergantung pada:** EG-104, EG-501
|
|||
|
|
|
|||
|
|
### EG-503 · Aksi PIN `REDEEM` · S
|
|||
|
|
- **RFC:** §7.4 langkah 2
|
|||
|
|
- **Kerjakan:** `PinActionRedeem` di `customer_pin_processor.go`, ikut aturan kunci yang
|
|||
|
|
ada (5 salah, 30 menit).
|
|||
|
|
- **Bergantung pada:** –
|
|||
|
|
|
|||
|
|
### EG-504 · Redemption internal · L
|
|||
|
|
- **RFC:** §7.4
|
|||
|
|
- **Kerjakan:** `POST /customer/enakgame/vouchers/:id/redeem` untuk `STATIC` dan
|
|||
|
|
`CODE_POOL`, satu transaksi, langkah persis §7.4. Debit `REWARD_REDEEM` dengan key
|
|||
|
|
`redeem:{redemption}`.
|
|||
|
|
- **Selesai jika:** test untuk:
|
|||
|
|
- Stok tersisa 1, dua customer menukar bersamaan → tepat satu berhasil.
|
|||
|
|
- `CODE_POOL`: dua redemption bersamaan mendapat kode berbeda (`SKIP LOCKED`).
|
|||
|
|
- Key yang sama dua kali → satu debit, satu kode.
|
|||
|
|
- Point kurang, PIN salah, `max_per_customer` tercapai, voucher di luar masa berlaku →
|
|||
|
|
ditolak tanpa apa pun tercatat.
|
|||
|
|
- **Bergantung pada:** EG-103, EG-501, EG-502, EG-503
|
|||
|
|
|
|||
|
|
### EG-505 · Atribusi realized cost · M
|
|||
|
|
- **RFC:** §7.6, D5
|
|||
|
|
- **Kerjakan:** query rekursif §7.6 di dalam transaksi redemption, pembagian
|
|||
|
|
`face_value` proporsional dengan sisa pembulatan ke bagian terbesar, insert
|
|||
|
|
`voucher_redemption_costs`.
|
|||
|
|
- **Selesai jika:** test DB untuk rantai: `GAME_REWARD` → exchange → redeem;
|
|||
|
|
`GAME_REWARD` → transfer → exchange → redeem; campuran `GAME_REWARD` + `EARN` (contoh
|
|||
|
|
§7.6: Rp7.500 ke budget, Rp2.500 tanpa budget); `Σ cost = face_value` untuk kombinasi
|
|||
|
|
angka yang tidak habis dibagi.
|
|||
|
|
- **Bergantung pada:** EG-303, EG-504
|
|||
|
|
|
|||
|
|
### EG-506 · Customer: katalog & voucher saya · S
|
|||
|
|
- **RFC:** §11 (customer)
|
|||
|
|
- **Kerjakan:** `GET /customer/enakgame/vouchers` (stok tersedia, tanpa membocorkan
|
|||
|
|
jumlah kode per status) dan `GET /redemptions` (dengan kode voucher).
|
|||
|
|
- **Bergantung pada:** EG-504
|
|||
|
|
|
|||
|
|
### EG-507 · `VoucherCodeExpiryJob` · S
|
|||
|
|
- **RFC:** §12
|
|||
|
|
- **Kerjakan:** `AVAILABLE → EXPIRED` untuk kode lewat `expires_at`, per batch.
|
|||
|
|
- **Bergantung pada:** EG-501
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Fase 6 — Budget Metrics
|
|||
|
|
|
|||
|
|
### EG-601 · Perhitungan metrik & status budget · M
|
|||
|
|
- **RFC:** §10
|
|||
|
|
- **Kerjakan:** realized cost, Coin issued, remaining, utilization, forecast, exposure,
|
|||
|
|
dan status per budget. Global dibatasi periode; event tanpa batas waktu. Threshold dari
|
|||
|
|
`game_budgets.thresholds`.
|
|||
|
|
- **Selesai jika:** test dengan data tetap menghasilkan angka contoh PRD §30 (budget
|
|||
|
|
Rp100M, realized Rp60M, sisa 10 hari); Point dari `EARN` tidak ikut dihitung.
|
|||
|
|
- **Bergantung pada:** EG-505
|
|||
|
|
|
|||
|
|
### EG-602 · Endpoint metrik budget · S
|
|||
|
|
- **Kerjakan:** `GET /marketing/enakgame/budgets/:id/metrics`.
|
|||
|
|
- **Bergantung pada:** EG-601
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Fase 7 — Event / Campaign
|
|||
|
|
|
|||
|
|
### EG-701 · Migrasi event · S
|
|||
|
|
- **RFC:** §5.5
|
|||
|
|
- **Kerjakan:** `game_events`, `game_event_games`.
|
|||
|
|
- **Selesai jika:** up/down bersih; event tanpa `budget_id` atau dengan
|
|||
|
|
`end_at ≤ start_at` ditolak.
|
|||
|
|
- **Bergantung pada:** EG-102
|
|||
|
|
|
|||
|
|
### EG-702 · Admin event · M
|
|||
|
|
- **RFC:** §5.5, §11
|
|||
|
|
- **Kerjakan:** CRUD + status. Budget yang dipasang harus milik org yang sama dan ber-scope
|
|||
|
|
`EVENT`. Audit.
|
|||
|
|
- **Bergantung pada:** EG-203, EG-701
|
|||
|
|
|
|||
|
|
### EG-703 · Modifier event di complete · M
|
|||
|
|
- **RFC:** §7.2 langkah 7, §8 (modifier), D6
|
|||
|
|
- **Kerjakan:** cari event aktif untuk game, hitung tambahan dari multiplier dan bonus,
|
|||
|
|
pisahkan menjadi komponen per budget, terapkan `reward_limit` dan `user_daily_limit`
|
|||
|
|
event lewat counter `EVENT`, cap `max_reward`. Aturan tumpuk mengikuti default PRD §16.
|
|||
|
|
- **Selesai jika:** contoh PRD §7 (10 Coin + Ramadan 2x) menghasilkan dua baris
|
|||
|
|
`GAME_REWARD`: 10 ke budget global, 10 ke budget event; event di luar jam aktif tidak
|
|||
|
|
berpengaruh.
|
|||
|
|
- **Bergantung pada:** EG-402, EG-702
|
|||
|
|
|
|||
|
|
### EG-704 · Event di daftar game customer · S
|
|||
|
|
- **Kerjakan:** `GET /customer/enakgame/games` menyertakan event aktif per game (nama,
|
|||
|
|
banner, multiplier/bonus, berakhir kapan).
|
|||
|
|
- **Bergantung pada:** EG-206, EG-702
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Fase 8 — Voucher Eksternal
|
|||
|
|
|
|||
|
|
Terblokir sampai provider pertama ditentukan: API-nya menentukan bentuk adapter dan
|
|||
|
|
apakah provider menerima idempotency key (RFC §18).
|
|||
|
|
|
|||
|
|
### EG-801 · Interface provider + adapter pertama · M
|
|||
|
|
- **Kerjakan:** `VoucherProvider` (`Issue(ctx, redemptionID, voucher)`,
|
|||
|
|
`Lookup(ctx, redemptionID)`), adapter provider pertama, dan klasifikasi hasil: sukses,
|
|||
|
|
gagal pasti, tidak jelas.
|
|||
|
|
- **Bergantung pada:** EG-504
|
|||
|
|
|
|||
|
|
### EG-802 · Redemption dua tahap · M
|
|||
|
|
- **RFC:** §7.5
|
|||
|
|
- **Kerjakan:** jalur `EXTERNAL` di endpoint redeem: transaksi 1 (`PENDING` + debit),
|
|||
|
|
panggil provider di luar transaksi, transaksi 2 (`COMPLETED` + atribusi, atau `FAILED` +
|
|||
|
|
`REWARD_REDEEM_REFUND`).
|
|||
|
|
- **Selesai jika:** test dengan provider palsu untuk ketiga hasil; timeout meninggalkan
|
|||
|
|
`PENDING` dengan Point terpotong dan response "sedang diproses".
|
|||
|
|
- **Bergantung pada:** EG-505, EG-801
|
|||
|
|
|
|||
|
|
### EG-803 · `VoucherRedemptionRecoveryJob` · M
|
|||
|
|
- **RFC:** §7.5 langkah 4, §12
|
|||
|
|
- **Kerjakan:** ambil `PENDING` lewat N menit, `Lookup` ke provider, selesaikan transaksi 2.
|
|||
|
|
Setelah batas percobaan → refund + `FAILED`.
|
|||
|
|
- **Selesai jika:** tidak ada redemption yang tertinggal `PENDING` melewati batas
|
|||
|
|
percobaan; job berjalan dua kali tidak merefund dua kali.
|
|||
|
|
- **Bergantung pada:** EG-802
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Fase 9 — Budget Controller & Analytics
|
|||
|
|
|
|||
|
|
### EG-901 · Rekomendasi multiplier · M
|
|||
|
|
- **RFC:** §10 (rekomendasi), PRD §30–§31
|
|||
|
|
- **Kerjakan:** hitung multiplier yang membuat forecast = budget, dibatasi step maksimum dan
|
|||
|
|
min/max multiplier. `GET /budgets/:id/recommendation`.
|
|||
|
|
- **Bergantung pada:** EG-601
|
|||
|
|
|
|||
|
|
### EG-902 · Terima rekomendasi · S
|
|||
|
|
- **Kerjakan:** admin menerima rekomendasi → reward config versi baru dibuat dari config
|
|||
|
|
aktif dengan angka yang disesuaikan, tercatat di audit dengan `source = budget_controller`.
|
|||
|
|
- **Bergantung pada:** EG-202, EG-901
|
|||
|
|
|
|||
|
|
### EG-903 · Analytics · M
|
|||
|
|
- **RFC:** PRD §36
|
|||
|
|
- **Kerjakan:** endpoint dashboard game (plays, completed, rata-rata skor & reward, Coin
|
|||
|
|
issued, entry cost dibayar, Coin di-refund) dan economy (Coin generated / spent /
|
|||
|
|
expired / outstanding, Point redeemed), per org dan rentang tanggal.
|
|||
|
|
- **Bergantung pada:** EG-303, EG-505
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Fase 10 — Spin & Bersih-bersih
|
|||
|
|
|
|||
|
|
### EG-1001 · Spin sebagai game EnakGame · S
|
|||
|
|
- **RFC:** §14
|
|||
|
|
- **Kerjakan:** seeder / langkah admin untuk game spin baru dengan config `PROBABILITY` dan
|
|||
|
|
reward Coin. Client Phaser di luar repo ini.
|
|||
|
|
- **Selesai jika:** spin bisa dimainkan lewat `/customer/enakgame/sessions` end-to-end.
|
|||
|
|
- **Bergantung pada:** EG-402
|
|||
|
|
|
|||
|
|
### EG-1002 · Hapus alur game lama · M
|
|||
|
|
- **RFC:** §14, §15
|
|||
|
|
- **Kerjakan:** hapus route `POST /customer/spin`, `GET /customer/games`,
|
|||
|
|
`GET /customer/ferris-wheel`, admin `/marketing/games`, `/marketing/game-prizes`, dan
|
|||
|
|
`/marketing/rewards`, beserta handler, service, processor, dan test-nya
|
|||
|
|
(`GamePlayProcessor`, `SpinGameService`, dan seterusnya). **Tabel `games`,
|
|||
|
|
`game_prizes`, `game_plays` tetap ada** untuk riwayat ledger.
|
|||
|
|
- **Selesai jika:** build dan test lulus; aplikasi customer sudah tidak memanggil endpoint
|
|||
|
|
lama (konfirmasi tim aplikasi).
|
|||
|
|
- **Bergantung pada:** EG-1001. Tabel `rewards` tidak punya data produksi (dikonfirmasi
|
|||
|
|
2026-10-07), jadi tidak ada yang dipindah ke `vouchers`.
|
|||
|
|
- **Catatan:** endpoint customer lama boleh dimatikan **lebih awal**, kapan pun, untuk
|
|||
|
|
menutup temuan RFC §15 nomor 1 dan 2.
|
|||
|
|
|
|||
|
|
### EG-1003 · Bersihkan kolom lama `games` · S
|
|||
|
|
- **Kerjakan:** drop `games.is_active` dan berhenti membaca `metadata.coin_cost`.
|
|||
|
|
- **Bergantung pada:** EG-1002
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Yang Bisa Dimulai Sekarang
|
|||
|
|
|
|||
|
|
Bisa dikerjakan paralel tanpa menunggu apa pun:
|
|||
|
|
|
|||
|
|
- **EG-001** (matikan bayar dengan EnakPoint) → **EG-002**
|
|||
|
|
- **EG-101** → **EG-102** → **EG-105** (skema & repository)
|
|||
|
|
- **EG-103** (tipe ledger)
|
|||
|
|
- **EG-104** (audit), **EG-106** (setting limit)
|
|||
|
|
- **EG-301** (Reward Engine, kode murni tanpa database)
|
|||
|
|
- **EG-501**, **EG-503** (fondasi voucher)
|
|||
|
|
|
|||
|
|
Jalur kritis: **EG-105 → EG-204 → EG-303 → EG-402**. Hampir semua fase setelahnya
|
|||
|
|
menunggu complete game selesai.
|