One guide per team, covering EnakPoint, EnakCoin, EnakGame and vouchers: - integration-mobile-customer.md: wallet, history (with the game and voucher ledger types), push, PIN, exchange, transfer, game list and webview, play history, voucher catalog, redeem and my vouchers. - integration-pos.md: linking customers to orders, earning, receipts, void/refund, and vouchers as a known gap (no POS endpoint to mark one used). - integration-enakgame.md: the Phaser client's side of a play: start with Idempotency-Key, complete, rewards, spin, expiry and refunds, retries. - integration-backoffice.md: loyalty settings and customer wallets, plus games, reward configs, spin setup, budgets, metrics and recommendations, events, vouchers and code import, analytics. The JS bridge between the app and the game is a proposal both teams still have to agree on. Replaces api-enakpoint.md, integration-enakpoint.md, mobile-customer-enakpoint.md, backoffice-enakpoint.md and enakgame-spin.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
299 lines
13 KiB
Markdown
299 lines
13 KiB
Markdown
# Integrasi EnakGame: Game Client (Phaser)
|
||
|
||
**Untuk:** tim game EnakGame (client Phaser) · **Base URL:** `/api/v1` · **Per:** 8 Okt 2026
|
||
|
||
Kamu mengerjakan **game EnakGame**: game web (Phaser) yang dibuka aplikasi customer di
|
||
dalam webview dari `game_url` sebuah game. Game inilah yang menjalankan satu kali main
|
||
dari awal sampai akhir: memulai session (EnakCoin dipotong), menjalankan permainan,
|
||
mengirim hasil, dan menampilkan hadiah. Jangan mengarang endpoint, field, atau aturan
|
||
yang tidak tertulis di sini; kalau ada yang kurang jelas, tanyakan ke tim backend.
|
||
|
||
Pembagian tugas dengan aplikasi customer:
|
||
|
||
| Aplikasi customer ([`integration-mobile-customer.md`](./integration-mobile-customer.md)) | Game EnakGame (dokumen ini) |
|
||
|---|---|
|
||
| Login customer, menyimpan token | Menerima token dari aplikasi lewat bridge (§2) |
|
||
| Daftar game, membuka `game_url` di webview | Start session, main, complete, tampilkan hadiah |
|
||
| Saldo, riwayat, voucher, PIN | Memberi tahu aplikasi saat saldo berubah atau game ditutup |
|
||
|
||
Alasan di balik aturannya ada di [`rfc-enakgame.md`](./rfc-enakgame.md) dan
|
||
[`enakgame-prd.md`](./enakgame-prd.md).
|
||
|
||
---
|
||
|
||
## 1. Aturan yang tidak boleh dilanggar
|
||
|
||
1. **Server yang menentukan hadiah.** Game hanya mengirim **hasil main**: `score`,
|
||
`outcome`, dan `data`. Jangan pernah mengirim jumlah hadiah. Kalaupun terkirim,
|
||
backend mengabaikannya. Untuk spin, server yang mengundi segmennya.
|
||
2. **Tampilkan hadiah dari response, bukan dari hitungan sendiri.** Angka di layar akhir
|
||
selalu `reward_total` dari backend.
|
||
3. **Satu tap "Main" = satu `Idempotency-Key`.** Retry memakai key yang sama.
|
||
4. **Token customer adalah rahasia.** Hanya diterima lewat bridge, disimpan di memori,
|
||
tidak pernah ditaruh di URL, `localStorage`, cookie, log, atau analytics.
|
||
5. **Semua jumlah bilangan bulat.** Tidak ada pecahan EnakCoin.
|
||
6. **Main game tidak butuh PIN.**
|
||
|
||
---
|
||
|
||
## 2. Bridge dengan aplikasi customer
|
||
|
||
> **Usulan.** Bentuk bridge di bawah belum diimplementasikan di sisi mana pun. Sepakati
|
||
> dengan tim aplikasi customer sebelum mulai; aplikasi memakai kontrak yang sama
|
||
> ([`integration-mobile-customer.md`](./integration-mobile-customer.md) §8.3).
|
||
|
||
Semua pesan berupa JSON string dengan field `type`.
|
||
|
||
- **Game → aplikasi:** `window.EnakGameHost.postMessage(JSON.stringify(pesan))`
|
||
(JavaScript channel webview bernama `EnakGameHost`).
|
||
- **Aplikasi → game:** aplikasi memanggil `window.enakGame.receive(jsonString)`. Game
|
||
wajib mendefinisikan fungsi ini sebelum mengirim `ready`.
|
||
|
||
| Arah | `type` | Isi | Kapan |
|
||
|---|---|---|---|
|
||
| game → app | `ready` | – | Halaman game selesai dimuat |
|
||
| app → game | `init` | `api_base_url`, `token`, `game_id` | Jawaban atas `ready` |
|
||
| game → app | `token_expired` | – | Backend menolak token (§3) |
|
||
| app → game | `token` | `token` | Token baru setelah `token_expired` |
|
||
| game → app | `balance_changed` | `coin_balance` | Setelah start dan complete berhasil |
|
||
| game → app | `close` | – | Customer keluar dari game |
|
||
|
||
Contoh `init`:
|
||
|
||
```json
|
||
{ "type": "init", "api_base_url": "https://api.example.com/api/v1", "token": "eyJ…", "game_id": "8a1f…" }
|
||
```
|
||
|
||
Jangan memanggil API apa pun sebelum `init` diterima. Untuk development di browser
|
||
tanpa aplikasi, sediakan mode dev yang mengisi `init` dari config lokal; mode itu tidak
|
||
boleh ikut di build produksi.
|
||
|
||
---
|
||
|
||
## 3. Koneksi ke API
|
||
|
||
- Header: `Authorization: Bearer <token>` dari `init`.
|
||
- Sukses: `{ "success": true, "data": { … }, "errors": null }`.
|
||
- Gagal: `{ "success": false, "data": null, "errors": [{ "code", "entity", "cause" }] }`.
|
||
`cause` berbahasa Inggris; jangan tampilkan mentah ke customer.
|
||
|
||
| `errors[0].code` | HTTP | Arti | Yang dilakukan game |
|
||
|---|---|---|---|
|
||
| `303`, `310` | 400 | Request salah format | Bug di game; pesan umum |
|
||
| `304` | 400 | Ditolak aturan bisnis | Lihat tabel per endpoint |
|
||
| `404` | 404 | Game/session tidak ada atau bukan milik customer | Pesan "tidak ditemukan", kembali ke aplikasi |
|
||
| `900` | 500 | Error server | Retry (§6) |
|
||
|
||
**Token tidak berlaku** (kedaluwarsa, salah) dijawab HTTP 400 dengan code `304`, sama
|
||
seperti penolakan bisnis. Bedakan lewat `entity`: `auth_handler` untuk token,
|
||
`enakgame_service` untuk aturan EnakGame. Pada `auth_handler`, kirim `token_expired`,
|
||
tunggu `token`, lalu ulangi request yang sama.
|
||
|
||
---
|
||
|
||
## 4. Alur satu kali main
|
||
|
||
```
|
||
init ─► GET /customer/enakgame/games ─► tampilkan biaya (dan roda, untuk spin)
|
||
─► tap Main ─► POST /customer/enakgame/sessions (EnakCoin dipotong)
|
||
─► permainan berjalan (batas waktu: expires_at)
|
||
─► POST /customer/enakgame/sessions/:id/complete (server menghitung hadiah)
|
||
─► tampilkan hadiah ─► main lagi atau close
|
||
```
|
||
|
||
### 4.1 Data game — `GET /customer/enakgame/games`
|
||
|
||
Mengembalikan semua game aktif organisasi customer. Ambil yang `id`-nya sama dengan
|
||
`game_id` dari `init`.
|
||
|
||
```json
|
||
[
|
||
{
|
||
"id": "8a1f…",
|
||
"slug": "spin",
|
||
"name": "Spin Harian",
|
||
"description": null,
|
||
"thumbnail_url": "https://…/spin.png",
|
||
"game_url": "https://…/spin/index.html",
|
||
"version": "1.2.0",
|
||
"entry_cost": 5,
|
||
"session_ttl_seconds": 600,
|
||
"events": [
|
||
{ "id": "…", "name": "Ramadan 2x", "banner_url": "https://…", "multiplier": 2, "bonus": null, "end_at": "2026-10-31T16:59:59Z" }
|
||
],
|
||
"prizes": [
|
||
{ "entry": 1, "label": "Zonk", "amount": 0 },
|
||
{ "entry": 2, "label": "3 Coin", "amount": 3 },
|
||
{ "entry": 3, "label": "10 Coin", "amount": 10 },
|
||
{ "entry": 4, "label": "Jackpot", "amount": 50 }
|
||
]
|
||
}
|
||
]
|
||
```
|
||
|
||
- `entry_cost`: EnakCoin per main. Tampilkan di tombol Main ("Main · 5 EnakCoin").
|
||
- `events`: event yang sedang berlaku, prioritas tertinggi dulu. Tampilkan sebagai
|
||
label, mis. "2x hadiah sampai 31 Okt". `multiplier` 2 berarti hadiah dasar ditambah
|
||
sekali lagi; `bonus` menambah sejumlah EnakCoin.
|
||
- `prizes`: hanya ada untuk game ber-reward `PROBABILITY` (spin). Urutan = urutan segmen
|
||
roda. `label` bisa `null`. Bobot peluang tidak pernah dikirim.
|
||
- Game tidak ada di daftar → game sudah dinonaktifkan; tampilkan pesan dan `close`.
|
||
|
||
### 4.2 Mulai — `POST /customer/enakgame/sessions`
|
||
|
||
Header `Idempotency-Key` wajib (maks. 50 karakter, mis. UUID v4). Buat key baru saat
|
||
customer menekan Main; pakai key yang sama bila request diulang karena jaringan.
|
||
|
||
```json
|
||
{ "game_id": "8a1f…" }
|
||
```
|
||
|
||
```json
|
||
{
|
||
"session_id": "c0d3…",
|
||
"game_id": "8a1f…",
|
||
"entry_cost": 5,
|
||
"expires_at": "2026-10-08T05:10:00Z",
|
||
"coin_balance": 15,
|
||
"replayed": false
|
||
}
|
||
```
|
||
|
||
- EnakCoin sudah terpotong. Kirim `balance_changed` dengan `coin_balance`.
|
||
- `replayed: true`: request ini mengulang start yang sudah berhasil; pakai session yang
|
||
sama, EnakCoin tidak terpotong dua kali.
|
||
- `expires_at`: batas waktu mengirim hasil (default 10 menit sejak start, diatur per
|
||
game). Tampilkan timer bila permainan bisa lama.
|
||
|
||
| Penolakan `304` (`cause`) | Tampilan |
|
||
|---|---|
|
||
| `not enough EnakCoin` | "EnakCoin kamu kurang." Tombol kembali ke aplikasi |
|
||
| `the game is not available` | "Game sedang tidak tersedia." |
|
||
| `the game has no active reward configuration` | "Game sedang tidak tersedia." |
|
||
| `no EnakGame budget is set for this period` | "Game sedang tidak tersedia." |
|
||
| `the customer is not active` | "Akun tidak aktif." |
|
||
| `this Idempotency-Key was already used to start another game` | Bug di game: key dipakai ulang untuk game lain |
|
||
| `the Idempotency-Key header is required` / `… at most 50 characters` | Bug di game |
|
||
|
||
### 4.3 Kirim hasil — `POST /customer/enakgame/sessions/:id/complete`
|
||
|
||
Kirim sekali saat permainan selesai, sebelum `expires_at`. Body berisi hasil saja:
|
||
|
||
| Field | Tipe | Untuk |
|
||
|---|---|---|
|
||
| `score` | integer ≥ 0, opsional | Game berbasis skor |
|
||
| `outcome` | string, opsional | Game berbasis hasil, mis. `"WIN"`, `"PERFECT"` |
|
||
| `data` | objek JSON, opsional, maks. 16 KB | Data tambahan untuk audit (durasi per level, dsb.) |
|
||
|
||
Spin cukup mengirim `{}`. Game skor: `{ "score": 800 }`. Game hasil:
|
||
`{ "outcome": "WIN" }`. Nilai `outcome` yang diterima ditentukan admin per game;
|
||
sepakati daftarnya dengan tim backoffice.
|
||
|
||
```json
|
||
{
|
||
"session_id": "c0d3…",
|
||
"status": "COMPLETED",
|
||
"reward_total": 10,
|
||
"reward": { "base": 5, "event": 5 },
|
||
"coin_balance": 25,
|
||
"limited_by": ["USER_DAILY"],
|
||
"prize": { "entry": 2, "label": "3 Coin", "amount": 3 }
|
||
}
|
||
```
|
||
|
||
| Field | Arti | Tampilan |
|
||
|---|---|---|
|
||
| `reward_total` | EnakCoin yang **benar-benar masuk** | Angka utama di layar hadiah |
|
||
| `reward.base` / `reward.event` | Hadiah dasar dan tambahan event | "5 + 5 bonus event" |
|
||
| `coin_balance` | Saldo EnakCoin setelah hadiah | Kirim `balance_changed` |
|
||
| `limited_by` | Batas harian yang memotong hadiah: `USER_DAILY`, `GAME_DAILY`, `GLOBAL_DAILY` | "Hadiah hari ini sudah mencapai batas" |
|
||
| `prize` | Untuk spin: segmen hasil undian. `amount` = hadiah dasar segmen, sebelum event dan batas | Hentikan roda di `prize.entry` |
|
||
| `status` | `COMPLETED`, atau `REFUNDED` bila game dinonaktifkan selama dimainkan | Lihat di bawah |
|
||
|
||
- **`status: "REFUNDED"`** (`refund_reason: "GAME_DEACTIVATED"`): entry cost
|
||
dikembalikan dan tidak ada hadiah. Tampilkan "Game sedang dihentikan, EnakCoin kamu
|
||
dikembalikan."
|
||
- **`reward_total` 0** bisa terjadi: hadiahnya memang 0 (mis. segmen Zonk), batas harian
|
||
sudah habis, atau hasilnya tidak lolos validasi server (skor di atas batas, terlalu
|
||
cepat selesai, `outcome` tidak dikenal). Server tidak memberi tahu alasan validasi;
|
||
tampilkan hasil apa adanya.
|
||
- **Mengirim ulang aman.** Complete untuk session yang sudah selesai mengembalikan
|
||
jawaban yang sama, tanpa hadiah dua kali. Tidak perlu `Idempotency-Key`.
|
||
|
||
| Penolakan | Arti | Tampilan |
|
||
|---|---|---|
|
||
| `304` `the session has expired` | Lewat `expires_at` | "Waktu bermain habis." (lihat §5) |
|
||
| `304` `data must be …` | `data` bukan JSON atau lebih dari 16 KB | Bug di game |
|
||
| `310` | `score` bukan bilangan bulat atau `outcome` bukan string | Bug di game |
|
||
| `404` | Session tidak ada / milik customer lain | Pesan umum |
|
||
|
||
### 4.4 Cek status — `GET /customer/enakgame/sessions/:id`
|
||
|
||
Untuk memulihkan keadaan, mis. game dimuat ulang saat session masih berjalan:
|
||
|
||
```json
|
||
{
|
||
"id": "c0d3…", "game_id": "8a1f…", "status": "STARTED", "entry_cost": 5, "reward_total": 0,
|
||
"started_at": "…", "expires_at": "…", "ended_at": null, "refund_reason": null
|
||
}
|
||
```
|
||
|
||
`status`: `STARTED`, `COMPLETED`, `REFUNDED`, atau `EXPIRED`. Riwayat main customer ada
|
||
di `GET /customer/enakgame/sessions?page=1&limit=20` (dipakai aplikasi, bukan game).
|
||
|
||
---
|
||
|
||
## 5. Batas waktu dan refund
|
||
|
||
| Keadaan | Yang terjadi pada EnakCoin |
|
||
|---|---|
|
||
| Hasil dikirim sebelum `expires_at` | Entry cost terpakai, hadiah masuk |
|
||
| Customer menutup game / game crash, hasil tidak pernah dikirim | Session menjadi `EXPIRED` setelah `expires_at`. **Entry cost tidak dikembalikan** |
|
||
| Complete gagal karena error server (`5xx`) dan tidak berhasil sampai `expires_at` | Session direfund otomatis (`refund_reason: "SYSTEM_ERROR"`) dalam ±1 menit setelah `expires_at` |
|
||
| Game dinonaktifkan admin saat dimainkan | Session direfund (`GAME_DEACTIVATED`) |
|
||
|
||
Karena itu kirim hasil **segera** setelah permainan selesai, sebelum animasi panjang.
|
||
Saat customer menekan keluar di tengah permainan, tampilkan konfirmasi "EnakCoin yang
|
||
sudah dipakai tidak kembali".
|
||
|
||
---
|
||
|
||
## 6. Retry dan jaringan
|
||
|
||
| Request | Gagal karena jaringan / `5xx` | Aturan |
|
||
|---|---|---|
|
||
| Start | Ulangi dengan **`Idempotency-Key` yang sama** | Key baru = potong EnakCoin lagi |
|
||
| Complete | Ulangi dengan body yang sama sampai berhasil atau `expires_at` lewat | Aman diulang |
|
||
| Token ditolak (`entity` `auth_handler`) | `token_expired` → tunggu `token` → ulangi | Jangan minta customer login dari dalam game |
|
||
|
||
Gunakan backoff (mis. 1 s, 2 s, 4 s) dan tampilkan indikator "Menyimpan hasil…" selama
|
||
complete diulang.
|
||
|
||
---
|
||
|
||
## 7. Spin
|
||
|
||
1. Gambar roda dari `prizes` (§4.1): satu segmen per entri, urut, dengan `label`
|
||
(atau `amount` bila `label` `null`).
|
||
2. Tap Putar → start session (§4.2).
|
||
3. Mulai animasi berputar, lalu langsung kirim complete dengan `{}`.
|
||
4. Dari response, hentikan roda di segmen `prize.entry`, lalu tampilkan `reward_total`.
|
||
|
||
Jangan menentukan segmen sendiri lalu "mencocokkan" dengan server. Bila `prize` tidak
|
||
ada di response, hasil tidak bisa ditampilkan sebagai roda; tampilkan `reward_total`
|
||
saja.
|
||
|
||
---
|
||
|
||
## 8. Checklist
|
||
|
||
- [ ] Bridge sesuai kontrak §2 yang sudah disepakati dengan tim aplikasi.
|
||
- [ ] Token hanya di memori; tidak ada di URL, storage, log, atau analytics.
|
||
- [ ] Biaya main dan label event tampil sebelum main.
|
||
- [ ] Satu `Idempotency-Key` per tap Main, dipakai ulang saat retry.
|
||
- [ ] Complete hanya mengirim `score` / `outcome` / `data`, tidak pernah hadiah.
|
||
- [ ] Hadiah di layar dari `reward_total`; `limited_by` dan `REFUNDED` ditangani.
|
||
- [ ] Spin berhenti di `prize.entry`.
|
||
- [ ] Complete diulang dengan aman saat gagal; timeout `expires_at` ditangani.
|
||
- [ ] `balance_changed` dikirim setelah start dan complete; `close` saat keluar.
|