feat(enakgame): spin as an EnakGame game; remove the old game flow
EnakGame phase 10 of docs/tasks-enakgame.md (EG-1001 to EG-1003). Spin (EG-1001) - PROBABILITY entries take an optional label (a wheel segment). The customer game list shows a PROBABILITY game's prizes (entry, label, amount, never weights), and completing returns the drawn prize, so the client can draw the wheel and stop it on the server's draw. - docs/enakgame-spin.md: the admin steps to set up spin per organization (no seeder) and the customer app flow. An HTTP test plays it end to end. Old game flow removed (EG-1002) - Routes POST /customer/spin, GET /customer/games, GET /customer/ferris-wheel, and admin /marketing/games, /marketing/game-prizes, /marketing/rewards, with their handlers, services, processors, repositories, validators, models, contracts, mappers and tests (GamePlayProcessor, SpinGameService, rewards, ...). This also closes RFC §15 findings 1 and 2 (double charge, spinning another org's game). - Tables games, game_prizes, game_plays and rewards stay for ledger history. entities.StringSlice moves to its own file; the omset tracker (unrouted) keeps game_id but no longer embeds the old game response. games.is_active dropped (EG-1003) - Migration 000115; nothing reads metadata.coin_cost any more. The EnakPoint integration docs now point at /customer/enakgame. The Postgres tests were not run: no test database here. Migration 000115 has not been run anywhere. The customer app must stop calling the removed endpoints before this is deployed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
296708244e
commit
18e87398bb
+7
-11
@@ -151,7 +151,7 @@ PIN baru ditolak `304` bila bukan 6 digit, konfirmasinya beda, semua digit sama
|
||||
| POST | `/customer/wallet/exchange` | Ya | Wajib |
|
||||
| GET | `/customer/wallet/transfer/recipient?phone=` | – | – |
|
||||
| POST | `/customer/wallet/transfer` | Ya | Wajib |
|
||||
| POST | `/customer/spin` | – | – |
|
||||
| POST | `/customer/enakgame/sessions` | – | Wajib |
|
||||
|
||||
### GET /customer/wallet/exchange/preview?coins=30
|
||||
|
||||
@@ -210,19 +210,15 @@ Body `{ "currency": "POINT", "amount": 120, "recipient_phone": "081234561234", "
|
||||
|
||||
`currency` = `POINT` atau `COIN`. Batas organisasi (transfer aktif, minimal, maksimal per transaksi, batas harian per currency yang reset tengah malam WIB) ditolak `304` sebelum PIN dicek. Transfer final. Saldo membawa tanggal kedaluwarsa aslinya ke penerima (`lots`), dan penerima mendapat push `WALLET_TRANSFER_IN`.
|
||||
|
||||
### POST /customer/spin
|
||||
### Game (EnakGame)
|
||||
|
||||
Body `{ "spin_id": "<id game>" }`. Memotong EnakCoin sebesar `metadata.coin_cost` game itu (default 1).
|
||||
`POST /customer/spin`, `GET /customer/games`, dan `GET /customer/ferris-wheel` sudah dihapus. Semua game, termasuk spin, dimainkan lewat `/customer/enakgame`:
|
||||
|
||||
```json
|
||||
{
|
||||
"game_play": { "id": "…", "game_id": "…", "coins_used": 1, "created_at": "…" },
|
||||
"prize_won": { "id": "…", "name": "Voucher 10rb" },
|
||||
"coins_remaining": 7
|
||||
}
|
||||
```
|
||||
- `GET /customer/enakgame/games`: game aktif dengan `entry_cost` (EnakCoin per main) dan, untuk spin, `prizes` (segmen roda).
|
||||
- `POST /customer/enakgame/sessions` dengan `{ "game_id": "…" }` dan `Idempotency-Key`: memotong `entry_cost`.
|
||||
- `POST /customer/enakgame/sessions/:id/complete`: server menghitung hadiah EnakCoin; untuk spin, response berisi `prize` (segmen yang keluar).
|
||||
|
||||
EnakCoin kurang, game nonaktif, atau hadiah baru saja habis → `304`, tidak ada EnakCoin yang terpotong.
|
||||
Alur spin lengkap ada di [`enakgame-spin.md`](./enakgame-spin.md).
|
||||
|
||||
## POS: earning, void, dan refund
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Semua endpoint di bawah base URL `/api/v1`, butuh login user dengan role Admin a
|
||||
| Wallet customer | `GET /marketing/customers/:id/wallet`, `POST …/wallet/adjust` | Customer → detail customer → tab Wallet |
|
||||
| Telusuri mutasi | `GET /marketing/wallet-transactions/:id/trace` | Dibuka dari baris riwayat wallet |
|
||||
| PIN & keamanan customer | `DELETE /marketing/customers/:id/pin`, `GET …/security-events` | Customer → detail customer → tab Keamanan |
|
||||
| Biaya main game | `PUT` game yang sudah ada, `metadata.coin_cost` | Marketing → Game → edit game |
|
||||
| Biaya main game | `entry_cost` di `/marketing/enakgame/games` | Marketing → EnakGame → game |
|
||||
|
||||
Penempatan menu di atas adalah usulan; sesuaikan dengan struktur backoffice yang ada.
|
||||
|
||||
@@ -269,7 +269,7 @@ Admin tidak bisa membuat, mengganti, atau melihat PIN customer; satu-satunya aks
|
||||
|
||||
### Biaya main game
|
||||
|
||||
Semua game (spin, raffle, minigame) memakai EnakCoin yang sama. Biaya per main diisi di `metadata.coin_cost` saat membuat atau mengedit game (`/marketing/games`): bilangan bulat ≥ 1, default 1 bila kosong. Nilai pecahan, 0, atau teks membuat game tidak bisa dimainkan. Karena `metadata` dikirim utuh, pertahankan key metadata lain saat menyimpan. Hadiah game juga bernilai rupiah secara tidak langsung, karena EnakCoin bisa ditukar ke EnakPoint.
|
||||
Semua game (spin, raffle, minigame) memakai EnakCoin yang sama dan dikelola di `/marketing/enakgame/games`. Menu lama `/marketing/games`, `/marketing/game-prizes`, dan `/marketing/rewards` sudah dihapus. Biaya per main adalah `entry_cost` game (bilangan bulat ≥ 1), hadiahnya diatur di reward config game itu. Langkah membuat spin ada di [`enakgame-spin.md`](./enakgame-spin.md). Hadiah game juga bernilai rupiah secara tidak langsung, karena EnakCoin bisa ditukar ke EnakPoint.
|
||||
|
||||
## Pesan error dan checklist
|
||||
|
||||
@@ -293,7 +293,7 @@ Pesan `cause` saat ini berbahasa Inggris, mis. `invalid loyalty settings: loyalt
|
||||
- [ ] Adjustment mewajibkan alasan dan mengirim `idempotency_key`.
|
||||
- [ ] Tombol Telusuri ada di setiap baris riwayat.
|
||||
- [ ] Hapus PIN mewajibkan alasan; tab Keamanan menampilkan log.
|
||||
- [ ] Form game punya input `coin_cost`.
|
||||
- [ ] Form game EnakGame punya input `entry_cost`.
|
||||
- [ ] Semua nilai rupiah EnakPoint ditulis "setara potongan Rp …".
|
||||
|
||||
Transfer belum boleh dirilis sebelum tinjauan legal (N3) selesai. Layar backoffice boleh disiapkan lebih dulu.
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# Spin sebagai Game EnakGame
|
||||
|
||||
**Sumber:** [RFC EnakGame](rfc-enakgame.md) §14, task EG-1001
|
||||
**Pembaca:** admin organisasi dan tim backoffice / aplikasi customer
|
||||
|
||||
Spin lama (`POST /customer/spin`) diganti dengan game EnakGame biasa: tipe `SPIN`,
|
||||
reward `PROBABILITY`, hadiah berupa EnakCoin. Spin dimainkan lewat endpoint session
|
||||
yang sama dengan game lain, sehingga ikut mendapat idempotency, refund otomatis, budget,
|
||||
event, dan Economy Guard.
|
||||
|
||||
Tidak ada seeder: setiap organisasi membuat spin-nya sendiri lewat API admin di bawah.
|
||||
Semua langkah memakai token admin organisasi; langkah 2–4 butuh loyalty manager.
|
||||
|
||||
## Langkah Admin
|
||||
|
||||
### 1. Buat game
|
||||
|
||||
`POST /api/v1/marketing/enakgame/games`
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Spin Harian",
|
||||
"slug": "spin",
|
||||
"type": "SPIN",
|
||||
"entry_cost": 5,
|
||||
"status": "ACTIVE",
|
||||
"thumbnail_url": "https://…/spin.png",
|
||||
"game_url": "https://…/spin/index.html"
|
||||
}
|
||||
```
|
||||
|
||||
`entry_cost` adalah EnakCoin yang dipotong setiap kali spin (minimal 1).
|
||||
|
||||
### 2. Buat reward config `PROBABILITY`
|
||||
|
||||
`POST /api/v1/marketing/enakgame/games/:id/reward-configs`
|
||||
|
||||
```json
|
||||
{
|
||||
"reward_type": "PROBABILITY",
|
||||
"max_reward": 50,
|
||||
"rules": {
|
||||
"table": [
|
||||
{ "weight": 50, "amount": 0, "label": "Zonk" },
|
||||
{ "weight": 30, "amount": 3, "label": "3 Coin" },
|
||||
{ "weight": 15, "amount": 10, "label": "10 Coin" },
|
||||
{ "weight": 5, "amount": 50, "label": "Jackpot" }
|
||||
]
|
||||
},
|
||||
"reason": "Spin pertama"
|
||||
}
|
||||
```
|
||||
|
||||
- Satu baris `table` = satu segmen roda, urut searah gambar roda.
|
||||
- `weight` bilangan bulat ≥ 1. Peluang segmen = `weight` ÷ total weight (di atas: 50%,
|
||||
30%, 15%, 5%). Weight tidak pernah dikirim ke customer.
|
||||
- `amount` EnakCoin yang didapat (boleh 0). `label` opsional, maksimal 100 karakter,
|
||||
ditampilkan di roda.
|
||||
- `max_reward` minimal sebesar `amount` terbesar, kalau tidak hadiah besar terpotong.
|
||||
|
||||
### 3. Aktifkan config
|
||||
|
||||
`POST /api/v1/marketing/enakgame/reward-configs/:id/activate`
|
||||
|
||||
Mengganti hadiah nanti berarti membuat versi config baru lalu mengaktifkannya; session
|
||||
yang sedang berjalan tetap memakai versi saat dimulai.
|
||||
|
||||
### 4. Pastikan ada budget global bulan berjalan
|
||||
|
||||
`POST /api/v1/marketing/enakgame/budgets` dengan `scope: "GLOBAL"`, bila belum ada. Tanpa
|
||||
budget global, customer tidak bisa memulai game apa pun.
|
||||
|
||||
## Alur di Aplikasi Customer
|
||||
|
||||
1. `GET /api/v1/customer/enakgame/games`: game spin punya `prizes`, yaitu segmen roda
|
||||
(`entry`, `label`, `amount`) dalam urutan config. Gambar roda dari sini.
|
||||
2. `POST /api/v1/customer/enakgame/sessions` dengan `{"game_id": "…"}` dan header
|
||||
`Idempotency-Key`. Entry cost dipotong di sini.
|
||||
3. `POST /api/v1/customer/enakgame/sessions/:id/complete` dengan body `{}`. Server yang
|
||||
mengundi. Response berisi `prize` (`entry`, `label`, `amount`): putar roda sampai
|
||||
berhenti di segmen `entry` itu. `reward_total` adalah Coin yang benar-benar masuk,
|
||||
bisa lebih besar dari `prize.amount` karena event, atau lebih kecil karena limit
|
||||
harian (`limited_by`).
|
||||
|
||||
Aplikasi tidak boleh mengundi sendiri atau mengirim hadiah: apa pun yang dikirim selain
|
||||
data hasil diabaikan.
|
||||
@@ -367,23 +367,18 @@ Kurs per organisasi: `coin_amount` EnakCoin = `point_amount` EnakPoint (default
|
||||
|
||||
## 7. Game
|
||||
|
||||
`POST /api/v1/customer/spin` dengan `{ "spin_id": "<id game>" }`. Tanpa PIN.
|
||||
Game lama (`POST /api/v1/customer/spin`, `GET /customer/games`,
|
||||
`GET /customer/ferris-wheel`, dan admin `/marketing/games`, `/marketing/game-prizes`,
|
||||
`/marketing/rewards`) sudah dihapus. Semua game, termasuk spin, sekarang game EnakGame:
|
||||
|
||||
Setiap game memotong EnakCoin sebesar `metadata.coin_cost` game itu (default 1).
|
||||
Response:
|
||||
- Customer: `GET /api/v1/customer/enakgame/games`, lalu
|
||||
`POST /api/v1/customer/enakgame/sessions` (wajib `Idempotency-Key`, memotong
|
||||
`entry_cost` EnakCoin), lalu `POST /api/v1/customer/enakgame/sessions/:id/complete`
|
||||
(server menghitung hadiah EnakCoin). Tanpa PIN.
|
||||
- Dashboard: game dan biaya per main (`entry_cost`, bilangan bulat ≥ 1) diatur di
|
||||
`/marketing/enakgame/games`, hadiahnya di reward config.
|
||||
|
||||
```json
|
||||
{
|
||||
"game_play": { "id": "…", "game_id": "…", "coins_used": 1, "created_at": "…" },
|
||||
"prize_won": { "id": "…", "name": "Voucher 10rb", … },
|
||||
"coins_remaining": 7
|
||||
}
|
||||
```
|
||||
|
||||
EnakCoin kurang, game nonaktif, atau hadiah baru saja habis dijawab `304`; tidak ada
|
||||
EnakCoin yang terpotong.
|
||||
|
||||
Di dashboard, `metadata.coin_cost` diisi per game dengan bilangan bulat ≥ 1.
|
||||
Langkah admin dan alur aplikasi untuk spin ada di [`enakgame-spin.md`](enakgame-spin.md).
|
||||
|
||||
---
|
||||
|
||||
@@ -549,4 +544,5 @@ Riwayat perubahan: `GET /api/v1/marketing/loyalty-settings/history?page=1&limit=
|
||||
**Dashboard**
|
||||
- [ ] Tampilkan `point_cashback_percent`, `impact`, `expiry_preview`, dan
|
||||
`expiry_activations` sebelum owner menyimpan setting.
|
||||
- [ ] Isi `metadata.coin_cost` untuk setiap game.
|
||||
- [ ] Buat ulang game (termasuk spin) di `/marketing/enakgame/games` dengan `entry_cost`
|
||||
dan reward config ([`enakgame-spin.md`](enakgame-spin.md)).
|
||||
|
||||
@@ -116,7 +116,7 @@ Endpoint **tukar** dan **transfer** wajib header `Idempotency-Key` (string unik,
|
||||
| Tukar EnakCoin | `GET …/exchange/preview`, `POST /customer/wallet/exchange` | Ya |
|
||||
| Transfer | `GET …/transfer/recipient`, `POST /customer/wallet/transfer` | Ya |
|
||||
| PIN (buat, ganti, lupa) | `/customer/pin/*` | – |
|
||||
| Game | `POST /customer/spin` | – |
|
||||
| Game | `GET /customer/enakgame/games`, `POST /customer/enakgame/sessions`, `POST …/sessions/:id/complete` | – |
|
||||
| (latar belakang) registrasi push | `PUT` / `DELETE /customer/devices` | – |
|
||||
|
||||
---
|
||||
@@ -524,22 +524,20 @@ Penerima mendapat push `WALLET_TRANSFER_IN`.
|
||||
|
||||
## 8. Game (memakai EnakCoin)
|
||||
|
||||
`POST /api/v1/customer/spin` dengan `{ "spin_id": "<id game>" }`. Tanpa PIN.
|
||||
`POST /api/v1/customer/spin`, `GET /customer/games`, dan `GET /customer/ferris-wheel`
|
||||
sudah dihapus. Semua game, termasuk spin, dimainkan lewat EnakGame. Tanpa PIN.
|
||||
|
||||
```json
|
||||
{
|
||||
"game_play": { "id": "…", "game_id": "…", "coins_used": 1, "created_at": "…" },
|
||||
"prize_won": { "id": "…", "name": "Voucher 10rb" },
|
||||
"coins_remaining": 7
|
||||
}
|
||||
```
|
||||
1. `GET /api/v1/customer/enakgame/games`: daftar game dengan `entry_cost` (EnakCoin per
|
||||
main). Tampilkan biaya sebelum main, dan nonaktifkan tombol bila `coin_balance`
|
||||
kurang. Spin punya `prizes` untuk menggambar roda.
|
||||
2. `POST /api/v1/customer/enakgame/sessions` dengan `{ "game_id": "…" }` dan header
|
||||
`Idempotency-Key` (satu key per tap; retry memakai key yang sama). Response berisi
|
||||
`session_id` dan `coin_balance` setelah dipotong.
|
||||
3. `POST /api/v1/customer/enakgame/sessions/:id/complete` dengan hasil main (`score`
|
||||
atau `outcome`; spin cukup `{}`). Server yang menentukan hadiah: perbarui saldo dari
|
||||
`coin_balance`, tampilkan `reward_total`, dan untuk spin hentikan roda di `prize.entry`.
|
||||
|
||||
- Setiap game punya biaya sendiri: `metadata.coin_cost` pada data game dari
|
||||
`GET /api/v1/customer/games` (atau `GET /customer/ferris-wheel`), default 1 bila kosong.
|
||||
Tampilkan biaya sebelum main, dan nonaktifkan tombol bila `coin_balance` kurang.
|
||||
- `304`: EnakCoin kurang, game nonaktif, atau hadiah baru saja habis. Tidak ada
|
||||
EnakCoin yang terpotong; tampilkan pesan dan biarkan customer mencoba lagi.
|
||||
- Setelah main, perbarui saldo EnakCoin dari `coins_remaining`.
|
||||
Rincian spin ada di [`enakgame-spin.md`](./enakgame-spin.md).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -385,6 +385,12 @@ beredar. Karena itu:
|
||||
|
||||
### F8 — Game Memakai EnakCoin
|
||||
|
||||
> **Diganti EnakGame (2026-10-07).** Alur game di bawah (`POST /customer/spin`,
|
||||
> `metadata.coin_cost`, `game_plays`) sudah dihapus. Game sekarang dimainkan lewat
|
||||
> `/customer/enakgame/sessions` dengan `games.entry_cost`; lihat
|
||||
> [RFC EnakGame](rfc-enakgame.md) §14 dan [enakgame-spin.md](enakgame-spin.md).
|
||||
> EnakCoin tetap mata uang untuk bermain game.
|
||||
|
||||
- **Semua jenis game** (`SPIN`, ferris wheel, `RAFFLE`, `MINIGAME`) memotong EnakCoin
|
||||
yang sama. `POST /customer/spin` memotong EnakCoin, bukan Token `SPIN`.
|
||||
- Biaya per main diatur per game di `games.metadata.coin_cost` (default 1), sehingga
|
||||
|
||||
Reference in New Issue
Block a user