Files
apskel-pos-backend/docs/integration-enakgame.md
T
efrilmandClaude Opus 5.5 b5d2cd491a docs: integration guides for mobile customer, POS, EnakGame and backoffice
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>
2026-10-08 11:10:05 +07:00

13 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, 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 §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 ─► 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 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

  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.