Files
apskel-pos-backend/docs/integration-enakgame.md
T
efrilmandClaude Opus 5.5 52e8fe11c6 feat(enakgame): filter play history by game and status
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>
2026-10-08 12:51:39 +07:00

15 KiB
Raw Blame History

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

  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, sessionStorage, cookie, log, atau analytics. (session_id boleh disimpan di sessionStorage, §4.4.)
  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 §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:

{ "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 ─► 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". 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.

{ "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_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.

{
  "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 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:

  1. Simpan session_id di sessionStorage setiap kali start berhasil, dan hapus setelah hasilnya ditampilkan. session_id bukan rahasia; token tetap hanya di memori (§1).
  2. Bila ada session_id tersimpan, panggil GET /sessions/:id. Bila tidak ada (mis. webview dibuka ulang dari awal), panggil GET /sessions?game_id=<game_id>&status=STARTED&limit=1.
  3. 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

  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.
  • Pemulihan setelah reload (§4.4): session STARTED dilanjutkan, bukan start baru; COMPLETED ditampilkan lewat complete ulang.
  • 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.