GET /customer/enakgame/sessions takes optional game_id and status, so a game reloaded mid-play finds the session it was running (status=STARTED) instead of starting a new one and charging EnakCoin again. An invalid game_id or status is refused. integration-enakgame.md §4.4 now describes recovery after a reload: keep the session_id in sessionStorage, continue a STARTED session before expires_at, and call complete again for a COMPLETED one to get the full answer, prize included. The mobile guide and RFC §11 mention the filters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
15 KiB
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) |
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 dan
enakgame-prd.md.
1. Aturan yang tidak boleh dilanggar
- Server yang menentukan hadiah. Game hanya mengirim hasil main:
score,outcome, dandata. Jangan pernah mengirim jumlah hadiah. Kalaupun terkirim, backend mengabaikannya. Untuk spin, server yang mengundi segmennya. - Tampilkan hadiah dari response, bukan dari hitungan sendiri. Angka di layar akhir
selalu
reward_totaldari backend. - Satu tap "Main" = satu
Idempotency-Key. Retry memakai key yang sama. - Token customer adalah rahasia. Hanya diterima lewat bridge, disimpan di memori,
tidak pernah ditaruh di URL,
localStorage,sessionStorage, cookie, log, atau analytics. (session_idboleh disimpan disessionStorage, §4.4.) - Semua jumlah bilangan bulat. Tidak ada pecahan EnakCoin.
- 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§8.3).
Semua pesan berupa JSON string dengan field type.
- Game → aplikasi:
window.EnakGameHost.postMessage(JSON.stringify(pesan))(JavaScript channel webview bernamaEnakGameHost). - Aplikasi → game: aplikasi memanggil
window.enakGame.receive(jsonString). Game wajib mendefinisikan fungsi ini sebelum mengirimready.
| 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:
{ "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>dariinit. - Sukses:
{ "success": true, "data": { … }, "errors": null }. - Gagal:
{ "success": false, "data": null, "errors": [{ "code", "entity", "cause" }] }.causeberbahasa 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 ─► cek session yang masih berjalan (§4.4)
─► 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.
[
{
"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".multiplier2 berarti hadiah dasar ditambah sekali lagi;bonusmenambah sejumlah EnakCoin.prizes: hanya ada untuk game ber-rewardPROBABILITY(spin). Urutan = urutan segmen roda.labelbisanull. 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.
{ "game_id": "8a1f…" }
{
"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_changeddengancoin_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.
{
"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_total0 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,outcometidak 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 Pemulihan setelah reload
Webview bisa memuat ulang halaman game (aplikasi ke background, memori habis, crash) saat customer sedang main. EnakCoin sudah terpotong, jadi game wajib menemukan lagi session-nya. Dua endpoint dipakai:
Satu session — GET /customer/enakgame/sessions/:id
{
"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. Response ini tidak memuat
prize atau rincian hadiah; untuk itu kirim ulang complete (lihat di bawah).
Mencari session — GET /customer/enakgame/sessions?game_id=8a1f…&status=STARTED&limit=1
Bentuk item sama dengan di atas, dibungkus data + pagination, terbaru di atas.
Semua query opsional: game_id, status (STARTED, COMPLETED, REFUNDED,
EXPIRED), page, limit. status atau game_id yang tidak valid ditolak 304.
Alurnya, setiap kali menerima init:
- Simpan
session_iddisessionStoragesetiap kali start berhasil, dan hapus setelah hasilnya ditampilkan.session_idbukan rahasia; token tetap hanya di memori (§1). - Bila ada
session_idtersimpan, panggilGET /sessions/:id. Bila tidak ada (mis. webview dibuka ulang dari awal), panggilGET /sessions?game_id=<game_id>&status=STARTED&limit=1. - Tindak lanjuti sesuai status:
| Keadaan | Yang dilakukan game |
|---|---|
STARTED, sekarang sebelum expires_at |
Lanjutkan session itu: jangan start baru (EnakCoin akan terpotong lagi). Spin: langsung kirim complete {} dan tampilkan hasilnya. Game lain: progres main hilang, jadi mulai ulang permainan di session yang sama dengan timer sampai expires_at, lalu kirim complete |
STARTED, expires_at sudah lewat |
Anggap selesai. Server mengubahnya menjadi EXPIRED (atau merefund bila complete sebelumnya gagal karena error server) dalam ±1 menit. Tampilkan "Waktu bermain habis", lalu customer boleh start baru |
COMPLETED |
Hasil sudah dihitung tapi mungkin belum ditampilkan. Kirim ulang POST /sessions/:id/complete dengan body apa saja ({}): server mengembalikan jawaban yang sama persis, termasuk prize, tanpa hadiah dobel. Tampilkan hasilnya |
REFUNDED |
"EnakCoin kamu dikembalikan." |
EXPIRED |
"Waktu bermain habis." |
| Tidak ada session | Tampilkan layar awal seperti biasa |
Riwayat main lengkap (tanpa filter) dipakai aplikasi customer, 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
- Gambar roda dari
prizes(§4.1): satu segmen per entri, urut, denganlabel(atauamountbilalabelnull). - Tap Putar → start session (§4.2).
- Mulai animasi berputar, lalu langsung kirim complete dengan
{}. - Dari response, hentikan roda di segmen
prize.entry, lalu tampilkanreward_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.
- Pemulihan setelah reload (§4.4): session
STARTEDdilanjutkan, bukan start baru;COMPLETEDditampilkan lewat complete ulang. - Biaya main dan label event tampil sebelum main.
- Satu
Idempotency-Keyper tap Main, dipakai ulang saat retry. - Complete hanya mengirim
score/outcome/data, tidak pernah hadiah. - Hadiah di layar dari
reward_total;limited_bydanREFUNDEDditangani. - Spin berhenti di
prize.entry. - Complete diulang dengan aman saat gagal; timeout
expires_atditangani. balance_changeddikirim setelah start dan complete;closesaat keluar.