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>
13 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, cookie, log, atau analytics. - 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 ─► 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 Cek status — GET /customer/enakgame/sessions/:id
Untuk memulihkan keadaan, mis. game dimuat ulang saat session masih berjalan:
{
"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
- 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.
- 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.