EnakGame phase 10 of docs/tasks-enakgame.md (EG-1001 to EG-1003). Spin (EG-1001) - PROBABILITY entries take an optional label (a wheel segment). The customer game list shows a PROBABILITY game's prizes (entry, label, amount, never weights), and completing returns the drawn prize, so the client can draw the wheel and stop it on the server's draw. - docs/enakgame-spin.md: the admin steps to set up spin per organization (no seeder) and the customer app flow. An HTTP test plays it end to end. Old game flow removed (EG-1002) - Routes POST /customer/spin, GET /customer/games, GET /customer/ferris-wheel, and admin /marketing/games, /marketing/game-prizes, /marketing/rewards, with their handlers, services, processors, repositories, validators, models, contracts, mappers and tests (GamePlayProcessor, SpinGameService, rewards, ...). This also closes RFC §15 findings 1 and 2 (double charge, spinning another org's game). - Tables games, game_prizes, game_plays and rewards stay for ledger history. entities.StringSlice moves to its own file; the omset tracker (unrouted) keeps game_id but no longer embeds the old game response. games.is_active dropped (EG-1003) - Migration 000115; nothing reads metadata.coin_cost any more. The EnakPoint integration docs now point at /customer/enakgame. The Postgres tests were not run: no test database here. Migration 000115 has not been run anywhere. The customer app must stop calling the removed endpoints before this is deployed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
584 lines
21 KiB
Markdown
584 lines
21 KiB
Markdown
# Prompt: fitur EnakPoint & EnakCoin di Mobile App Customer
|
||
|
||
Kamu mengerjakan aplikasi mobile untuk **customer** (bukan kasir, bukan backoffice).
|
||
Tugasmu: membangun fitur loyalitas EnakPoint & EnakCoin di aplikasi, memakai API backend
|
||
yang sudah jadi dan dijelaskan di dokumen ini. Jangan mengarang endpoint, field, atau
|
||
aturan yang tidak tertulis di sini; kalau ada yang kurang jelas, tanyakan dulu.
|
||
|
||
---
|
||
|
||
## 1. Konteks bisnis
|
||
|
||
| | EnakPoint (`POINT`) | EnakCoin (`COIN`) |
|
||
|---|---|---|
|
||
| Didapat dari | Belanja (order lunas), koreksi admin, tukar EnakCoin | Belanja, koreksi admin |
|
||
| Dipakai untuk | **Ditukar ke voucher** (tidak bisa membayar order) | **Main game**, ditukar ke EnakPoint |
|
||
| Bisa dikirim ke customer lain | Ya | Ya |
|
||
| Bisa kedaluwarsa | Ya, bila owner mengaktifkan | Ya, bila owner mengaktifkan |
|
||
|
||
Tidak ada lagi "token". Semua yang dulu token sekarang EnakCoin, dan endpoint serta
|
||
field bernama token sudah dihapus dari API.
|
||
|
||
> **Perubahan 7 Okt 2026:** bayar dengan EnakPoint (kode bayar di kasir, bayar order
|
||
> dari app) sudah dihapus dari backend. EnakPoint sekarang hanya bisa ditukar ke
|
||
> voucher, tidak bisa dipakai sebagai alat bayar dan tidak bisa dicairkan
|
||
> ([`enakgame-prd.md`](./enakgame-prd.md) §3.2). Jangan membangun layar bayar atau kode
|
||
> bayar; endpoint dan field yang dihapus ada di §9.
|
||
|
||
### Aturan yang wajib dipatuhi di UI
|
||
|
||
1. **Semua jumlah bilangan bulat.** Tidak ada desimal pada EnakPoint atau EnakCoin.
|
||
2. **Saldo bukan uang.** Nilai rupiah EnakPoint selalu ditulis **"setara potongan
|
||
Rp …"**, tidak pernah "saldo Rp …" atau "uang". Tidak ada fitur tarik tunai, dan
|
||
EnakPoint tidak bisa dipakai membayar.
|
||
3. **PIN 6 digit wajib** untuk: tukar EnakCoin dan transfer. **Main game tidak butuh PIN.** Melihat saldo dan riwayat
|
||
tidak butuh PIN.
|
||
4. **PIN terpisah dari password login** dan selalu dikirim sebagai **string** (supaya
|
||
nol di depan tidak hilang). Jangan pernah menyimpan PIN di perangkat, log, atau
|
||
analytics.
|
||
5. **Satu akun customer = satu organisasi.** Saldo berlaku di semua outlet organisasi itu.
|
||
6. **Waktu memakai WIB.** Tanggal kedaluwarsa berarti saldo masih bisa dipakai sampai
|
||
23:59:59 WIB di tanggal itu.
|
||
|
||
---
|
||
|
||
## 2. Koneksi ke API
|
||
|
||
- Base URL: `/api/v1`
|
||
- Semua endpoint customer: header `Authorization: Bearer <token login customer>`
|
||
- Semua jumlah di request dan response berupa integer.
|
||
|
||
### Registrasi customer
|
||
|
||
`POST /api/v1/customer-auth/register/start` menerima `organization_id` (opsional):
|
||
|
||
```json
|
||
{ "phone_number": "0812…", "name": "Budi", "birth_date": "2000-01-31", "organization_id": "648b96a0-1d1d-414e-baee-37e9d6317b4e" }
|
||
```
|
||
|
||
- Customer terdaftar di satu organisasi, dan saldonya berlaku di semua outlet organisasi itu.
|
||
- Bila `organization_id` tidak dikirim dan backend hanya punya satu organisasi, customer
|
||
otomatis masuk ke organisasi itu. Bila ada lebih dari satu, registrasi ditolak
|
||
("organization_id is required"), jadi sebaiknya app selalu mengirimnya dari config per
|
||
environment/brand.
|
||
- `organization_id` yang dikirim harus ada; bila tidak, registrasi ditolak sebelum OTP dikirim.
|
||
- Wallet customer baru belum punya baris sampai saldo pertama kali bergerak;
|
||
`GET /customer/wallet` tetap menjawab saldo 0.
|
||
|
||
### Format response
|
||
|
||
Sukses:
|
||
|
||
```json
|
||
{ "success": true, "data": { … }, "errors": null }
|
||
```
|
||
|
||
Gagal:
|
||
|
||
```json
|
||
{ "success": false, "data": null, "errors": [{ "code": "304", "entity": "wallet_service", "cause": "wallet move refused: not enough EnakCoin" }] }
|
||
```
|
||
|
||
| `errors[0].code` | HTTP | Arti | Yang dilakukan app |
|
||
|---|---|---|---|
|
||
| `303`, `310` | 400 | Request tidak lengkap / salah format | Bug di app; tampilkan pesan umum |
|
||
| `304` | 400 | Ditolak aturan bisnis | Tampilkan pesan yang ramah (lihat tiap fitur); `cause` berbahasa Inggris, jangan tampilkan mentah |
|
||
| `404` | 404 | Tidak ditemukan | Tampilkan "tidak ditemukan" |
|
||
| `429` | 429 | Minta OTP terlalu cepat | Tampilkan hitung mundur sebelum boleh minta lagi |
|
||
| `PIN_NOT_SET` | 403 | Belum punya PIN | Buka alur buat PIN (§6.2) |
|
||
| `PIN_INVALID` | 400 | PIN salah | §6.5 |
|
||
| `PIN_LOCKED` | 423 | PIN terkunci | §6.5 |
|
||
| `TRANSFER_BLOCKED` | 403 | Transfer ditahan setelah reset PIN | §6.5 |
|
||
| `900` | 500 | Error server | "Terjadi kesalahan, coba lagi" |
|
||
|
||
### Idempotency-Key
|
||
|
||
Endpoint **tukar** dan **transfer** wajib header `Idempotency-Key` (string unik, maks.
|
||
50 karakter, mis. UUID v4).
|
||
|
||
- Buat **satu key baru saat customer menekan tombol konfirmasi**.
|
||
- Bila request gagal karena jaringan/timeout, **kirim ulang dengan key yang sama**.
|
||
Server mengembalikan hasil pertama dengan `"replayed": true` dan tidak memotong saldo
|
||
dua kali.
|
||
- Jangan pakai ulang key untuk transaksi yang berbeda; server menolaknya (`304`).
|
||
|
||
---
|
||
|
||
## 3. Layar yang perlu dibuat
|
||
|
||
| Layar | Endpoint utama | Butuh PIN |
|
||
|---|---|---|
|
||
| Beranda wallet | `GET /customer/wallet` | – |
|
||
| Riwayat mutasi | `GET /customer/wallet/transactions` | – |
|
||
| Saldo akan kedaluwarsa | `GET /customer/wallet/expiring` | – |
|
||
| Daftar outlet | `GET /customer/outlets` | – |
|
||
| Riwayat order + detail | `GET /customer/orders`, `GET /customer/orders/:id` | – |
|
||
| Tukar EnakCoin | `GET …/exchange/preview`, `POST /customer/wallet/exchange` | Ya |
|
||
| Transfer | `GET …/transfer/recipient`, `POST /customer/wallet/transfer` | Ya |
|
||
| PIN (buat, ganti, lupa) | `/customer/pin/*` | – |
|
||
| Game | `GET /customer/enakgame/games`, `POST /customer/enakgame/sessions`, `POST …/sessions/:id/complete` | – |
|
||
| (latar belakang) registrasi push | `PUT` / `DELETE /customer/devices` | – |
|
||
|
||
---
|
||
|
||
## 4. Beranda wallet, riwayat, kedaluwarsa
|
||
|
||
### 4.1 Beranda — `GET /customer/wallet`
|
||
|
||
```json
|
||
{
|
||
"point_balance": 12500,
|
||
"coin_balance": 8,
|
||
"point_value": 1,
|
||
"point_discount_value": 12500,
|
||
"nearest_expiring": {
|
||
"point": { "amount": 150, "date": "2026-12-31" },
|
||
"coin": null
|
||
},
|
||
"recent_transactions": [ /* sama dengan item riwayat §4.2, maksimal 5 */ ]
|
||
}
|
||
```
|
||
|
||
Tampilkan:
|
||
- Saldo EnakPoint (`point_balance`) dengan keterangan "setara potongan Rp
|
||
{point_discount_value}" (format ribuan Indonesia: `Rp 12.500`).
|
||
- Saldo EnakCoin (`coin_balance`).
|
||
- Bila `nearest_expiring.point` / `.coin` tidak `null`: banner "{amount} EnakPoint akan
|
||
kedaluwarsa pada {date}" yang membuka layar §4.3.
|
||
- 5 mutasi terakhir dari `recent_transactions`, dengan tautan "Lihat semua" ke §4.2.
|
||
- Tombol aksi: Tukar EnakCoin (§7.1), Transfer (§7.2), Main game (§8).
|
||
|
||
Muat ulang beranda setelah setiap transaksi dan saat menerima push (§5).
|
||
|
||
Field `total_points`, `points_history`, `last_updated` di response ini **deprecated**;
|
||
jangan dipakai.
|
||
|
||
### 4.2 Riwayat — `GET /customer/wallet/transactions`
|
||
|
||
Query (semua opsional):
|
||
|
||
| Query | Contoh | Keterangan |
|
||
|---|---|---|
|
||
| `page` | `1` | Mulai dari 1 |
|
||
| `limit` | `20` | 1–100, default 20 |
|
||
| `currency` | `POINT` | `POINT` atau `COIN`; untuk tab EnakPoint / EnakCoin |
|
||
| `type` | `EARN,TRANSFER_IN` | Satu atau beberapa tipe dipisah koma, untuk filter |
|
||
| `from`, `to` | `2026-09-01` | Tanggal WIB, inklusif |
|
||
|
||
```json
|
||
{
|
||
"data": [
|
||
{
|
||
"id": "…",
|
||
"currency": "POINT",
|
||
"type": "EARN",
|
||
"amount": 875,
|
||
"balance_after": 12500,
|
||
"description": "Belanja #ORD-0123 di Outlet Kemang",
|
||
"source": { "type": "ORDER", "id": "…" },
|
||
"outlet_id": "…",
|
||
"group_id": null,
|
||
"expires_at": "2026-12-31T23:59:59+07:00",
|
||
"lots": [{ "amount": 875, "remaining": 875, "expires_at": "2026-12-31T23:59:59+07:00" }],
|
||
"created_at": "2026-09-30T12:01:00Z"
|
||
}
|
||
],
|
||
"pagination": { "page": 1, "limit": 20, "total_count": 42, "total_pages": 3 }
|
||
}
|
||
```
|
||
|
||
Aturan tampilan:
|
||
- `amount` bertanda: positif tampil hijau dengan `+`, negatif merah dengan `−`.
|
||
- `description` sudah siap tampil (nama lawan transfer sudah disamarkan). Tampilkan apa
|
||
adanya.
|
||
- Mutasi masuk yang punya `expires_at` menampilkan "Berlaku sampai {tanggal}".
|
||
- Infinite scroll memakai `pagination.total_pages`.
|
||
- Riwayat tidak pernah berubah atau hilang; koreksi muncul sebagai baris baru.
|
||
|
||
Label tipe:
|
||
|
||
| `type` | Label | Arah |
|
||
|---|---|---|
|
||
| `EARN` | Dari belanja | + |
|
||
| `EARN_REVERSAL` | Dibatalkan (order di-void/refund) | − |
|
||
| `EXCHANGE_OUT` | Ditukar ke EnakPoint | − |
|
||
| `EXCHANGE_IN` | Hasil tukar EnakCoin | + |
|
||
| `TRANSFER_OUT` | Transfer keluar | − |
|
||
| `TRANSFER_IN` | Transfer masuk | + |
|
||
| `GAME_SPEND` | Main game | − |
|
||
| `EXPIRE` | Kedaluwarsa | − |
|
||
| `ADJUSTMENT` | Koreksi | + / − |
|
||
| `MIGRATION` | Saldo awal | + |
|
||
|
||
### 4.3 Akan kedaluwarsa — `GET /customer/wallet/expiring`
|
||
|
||
```json
|
||
{
|
||
"point": [
|
||
{ "amount": 150, "date": "2026-10-31" },
|
||
{ "amount": 200, "date": "2026-12-31" }
|
||
],
|
||
"coin": []
|
||
}
|
||
```
|
||
|
||
Daftar per tanggal, paling dekat di atas. Daftar kosong: tampilkan "Tidak ada saldo
|
||
yang akan kedaluwarsa". Saldo yang kedaluwarsa hangus tanpa kompensasi.
|
||
|
||
---
|
||
|
||
### 4.4 Daftar outlet — `GET /customer/outlets`
|
||
|
||
Outlet aktif di organisasi customer, tempat saldo EnakPoint & EnakCoin berlaku. Urut
|
||
berdasarkan nama.
|
||
|
||
```json
|
||
[
|
||
{
|
||
"id": "…",
|
||
"name": "Gokuna Kemang",
|
||
"address": "Jl. Kemang Raya 10",
|
||
"earns_points": true,
|
||
"earns_coins": false
|
||
}
|
||
]
|
||
```
|
||
|
||
- `address` bisa `null`.
|
||
- `earns_points` / `earns_coins`: belanja di outlet ini memberi EnakPoint / EnakCoin.
|
||
- Belum ada telepon, koordinat, atau jam buka; data itu belum disimpan di backend.
|
||
|
||
|
||
### 4.5 Riwayat order — `GET /customer/orders` dan `GET /customer/orders/:id`
|
||
|
||
Order milik customer yang login di semua outlet organisasinya, terbaru di atas. Order
|
||
hanya masuk ke sini bila kasir mengaitkannya ke customer.
|
||
|
||
`GET /api/v1/customer/orders?page=1&limit=20` (`limit` 1–100, default 20):
|
||
|
||
```json
|
||
{
|
||
"data": [
|
||
{
|
||
"id": "…",
|
||
"order_number": "ORD-0123",
|
||
"outlet_id": "…",
|
||
"outlet_name": "Gokuna 1",
|
||
"order_type": "dine_in",
|
||
"status": "completed",
|
||
"payment_status": "completed",
|
||
"total_amount": 99000,
|
||
"item_count": 2,
|
||
"is_void": false,
|
||
"is_refund": false,
|
||
"points_earned": 865,
|
||
"coins_earned": 3,
|
||
"created_at": "2026-09-30T12:01:00Z"
|
||
}
|
||
],
|
||
"pagination": { "page": 1, "limit": 20, "total_count": 42, "total_pages": 3 }
|
||
}
|
||
```
|
||
|
||
`GET /api/v1/customer/orders/{id}` mengembalikan field yang sama, ditambah:
|
||
|
||
```json
|
||
{
|
||
"table_number": "A3",
|
||
"subtotal": 90000,
|
||
"discount_amount": 0,
|
||
"tax_amount": 9000,
|
||
"refund_amount": 0,
|
||
"items": [
|
||
{
|
||
"id": "…",
|
||
"product_id": "…",
|
||
"product_name": "Kopi Susu",
|
||
"variant_name": "Large",
|
||
"quantity": 2,
|
||
"unit_price": 25000,
|
||
"total_price": 50000,
|
||
"refund_quantity": 0,
|
||
"modifiers": [],
|
||
"status": "completed"
|
||
},
|
||
{
|
||
"id": "…",
|
||
"product_id": "…",
|
||
"product_name": "Ikan Tude",
|
||
"variant_name": null,
|
||
"quantity": 1,
|
||
"weight": 4.2,
|
||
"unit_name": "ons",
|
||
"unit_price": 4500,
|
||
"total_price": 18900,
|
||
"refund_quantity": 0,
|
||
"modifiers": [],
|
||
"status": "completed"
|
||
}
|
||
],
|
||
"payments": [
|
||
{ "id": "…", "method_name": "Cash", "method_type": "cash", "amount": 99000, "status": "completed", "refund_amount": 0, "created_at": "…" }
|
||
]
|
||
}
|
||
```
|
||
|
||
- Order customer lain atau yang tidak ada → `404`.
|
||
- `points_earned` / `coins_earned`: yang didapat dari order ini; 0 bila tidak ada.
|
||
- Item timbangan membawa `weight` dan `unit_name`; tampilkan "1 × 4,2 ons".
|
||
- Order yang `is_void` atau `is_refund` tetap tampil, beri label "Dibatalkan" /
|
||
"Direfund".
|
||
|
||
|
||
## 5. Notifikasi push (FCM)
|
||
|
||
### 5.1 Registrasi device
|
||
|
||
Setelah login berhasil **dan** setiap kali FCM memberi token baru (`onTokenRefresh`):
|
||
|
||
`PUT /api/v1/customer/devices`
|
||
|
||
```json
|
||
{ "device_id": "<id unik & stabil per instalasi>", "fcm_token": "<token FCM>", "platform": "android", "app_version": "2.4.0" }
|
||
```
|
||
|
||
- `device_id` wajib, stabil untuk satu instalasi (simpan di secure storage).
|
||
- `platform`: `android`, `ios`, atau `web`.
|
||
- Saat **logout**, panggil `DELETE /api/v1/customer/devices/{device_id}` sebelum
|
||
menghapus token login, supaya HP itu tidak lagi menerima notifikasi akun ini.
|
||
|
||
Tanpa registrasi ini, customer tidak menerima push apa pun.
|
||
|
||
### 5.2 Tipe push
|
||
|
||
Semua nilai di `data` berupa string.
|
||
|
||
| `data.type` | Kapan | Isi `data` lain | Aksi saat di-tap |
|
||
|---|---|---|---|
|
||
| `WALLET_TRANSFER_IN` | Menerima transfer | `transaction_id`, `group_id`, `currency`, `amount` | Buka riwayat, sorot transaksi itu |
|
||
| `WALLET_EXPIRING` | Beberapa hari sebelum saldo hangus | `currency`, `amount`, `expiry_date` | Buka layar kedaluwarsa (§4.3) |
|
||
| `WALLET_EXPIRED` | Saldo baru saja hangus | `currency`, `amount` | Buka riwayat |
|
||
| `PIN_LOCKED` | PIN terkunci setelah 5 kali salah | `locked_until` (RFC3339 UTC) | Buka layar lupa PIN (§6.4) |
|
||
|
||
Saat app terbuka dan menerima push wallet, muat ulang beranda.
|
||
|
||
---
|
||
|
||
## 6. PIN
|
||
|
||
### 6.1 Kapan diminta
|
||
|
||
Jangan minta PIN saat registrasi. Minta saat customer **pertama kali** melakukan aksi
|
||
yang butuh PIN. Cek dengan:
|
||
|
||
`GET /api/v1/customer/pin/status` → `{ "has_pin": false, "locked_until": null, "transfer_blocked_until": null }`
|
||
|
||
Bila `has_pin: false`, arahkan ke alur buat PIN, lalu kembali ke aksi semula.
|
||
|
||
### 6.2 Buat PIN
|
||
|
||
1. `POST /api/v1/customer/pin/otp` dengan `{ "purpose": "pin_setup" }`.
|
||
Response: `{ "purpose": "pin_setup", "otp_token": "…", "expires_at": "…" }`.
|
||
OTP dikirim ke WhatsApp customer.
|
||
2. Customer memasukkan kode OTP, lalu PIN dua kali.
|
||
3. `POST /api/v1/customer/pin` dengan
|
||
`{ "otp_token": "…", "otp_code": "123456", "pin": "482913", "confirm_pin": "482913" }`.
|
||
Response: status PIN.
|
||
|
||
Validasi di app sebelum kirim (server juga memeriksa, jawab `304`):
|
||
- Tepat 6 digit angka, dan konfirmasi sama.
|
||
- Bukan satu digit berulang (`111111`).
|
||
- Bukan berurutan naik/turun (`123456`, `654321`).
|
||
- Bukan tanggal lahir customer (`DDMMYY` atau `YYMMDD`).
|
||
|
||
Minta OTP lagi terlalu cepat → `429`: tampilkan hitung mundur.
|
||
|
||
### 6.3 Ganti PIN
|
||
|
||
`PUT /api/v1/customer/pin` dengan `{ "old_pin": "…", "pin": "…", "confirm_pin": "…" }`.
|
||
|
||
### 6.4 Lupa PIN
|
||
|
||
1. `POST /customer/pin/otp` dengan `{ "purpose": "pin_reset" }`.
|
||
2. `POST /customer/pin/reset` dengan `{ "otp_token", "otp_code", "pin", "confirm_pin" }`.
|
||
|
||
Reset juga membuka PIN yang terkunci. Setelah reset, **transfer keluar ditahan 24 jam**;
|
||
tukar tetap bisa. Beri tahu customer hal ini di layar sukses.
|
||
|
||
### 6.5 Menangani error PIN
|
||
|
||
Semua endpoint yang menerima `pin` bisa menjawab error PIN. Pada error ini **`data`
|
||
tidak `null`**:
|
||
|
||
```json
|
||
{ "success": false, "data": { "code": "PIN_INVALID", "remaining_attempts": 3 }, "errors": [ … ] }
|
||
```
|
||
|
||
| `data.code` | Field tambahan | Tampilan |
|
||
|---|---|---|
|
||
| `PIN_NOT_SET` | – | Buka alur buat PIN (§6.2) |
|
||
| `PIN_INVALID` | `remaining_attempts` | "PIN salah, sisa {n} percobaan." Kosongkan input PIN |
|
||
| `PIN_LOCKED` | `locked_until` | "PIN terkunci sampai {jam}." Tombol "Lupa PIN" |
|
||
| `TRANSFER_BLOCKED` | `transfer_blocked_until` | "Transfer bisa dilakukan lagi pada {waktu}." |
|
||
|
||
5 kali salah berturut-turut mengunci PIN 30 menit; selama terkunci PIN yang benar pun
|
||
ditolak. Penghitung ada di server, jadi jangan membuat penghitung sendiri di app.
|
||
|
||
---
|
||
|
||
## 7. Tukar dan transfer
|
||
|
||
### 7.1 Tukar EnakCoin → EnakPoint
|
||
|
||
1. Customer mengetik jumlah EnakCoin. Panggil preview (debounce saat mengetik):
|
||
|
||
`GET /api/v1/customer/wallet/exchange/preview?coins=30`
|
||
|
||
```json
|
||
{ "coin_amount": 10, "point_amount": 3, "coin_balance": 35, "coins": 30, "points": 9, "valid": true }
|
||
```
|
||
|
||
- Kurs: `coin_amount` EnakCoin = `point_amount` EnakPoint. Tampilkan "10 EnakCoin =
|
||
3 EnakPoint".
|
||
- Bila `valid: false`, tampilkan `reason` sebagai alasan dan nonaktifkan tombol. Jumlah
|
||
harus kelipatan `coin_amount`.
|
||
- Tampilkan "Kamu akan mendapat {points} EnakPoint".
|
||
|
||
2. Konfirmasi (tukar tidak bisa dibatalkan) → minta PIN →
|
||
|
||
`POST /api/v1/customer/wallet/exchange` + header `Idempotency-Key`
|
||
|
||
```json
|
||
{ "coins": 30, "pin": "482913" }
|
||
```
|
||
|
||
```json
|
||
{
|
||
"group_id": "…",
|
||
"coins": 30,
|
||
"points": 9,
|
||
"coin_amount": 10,
|
||
"point_amount": 3,
|
||
"lots": [{ "amount": 9, "expires_at": "2026-12-31T23:59:59+07:00" }],
|
||
"coin_balance": 5,
|
||
"point_balance": 9,
|
||
"replayed": false
|
||
}
|
||
```
|
||
|
||
3. Layar sukses: saldo baru, dan bila `lots[].expires_at` ada, "EnakPoint ini berlaku
|
||
sampai {tanggal}".
|
||
|
||
### 7.2 Transfer
|
||
|
||
1. Pilih mata uang (EnakPoint / EnakCoin), isi nomor HP penerima dan jumlah.
|
||
2. Cek penerima:
|
||
|
||
`GET /api/v1/customer/wallet/transfer/recipient?phone=081234561234`
|
||
|
||
```json
|
||
{ "name": "Bu*** Sa***", "phone_number": "08**-****-1234" }
|
||
```
|
||
|
||
| Hasil | Tampilan |
|
||
|---|---|
|
||
| Sukses | "Kirim ke Bu*** Sa*** (08**-****-1234)?" |
|
||
| `404` | "Nomor ini tidak terdaftar" |
|
||
| `304` | "Tidak bisa mengirim ke nomor ini" (diri sendiri, akun nonaktif) |
|
||
|
||
3. Konfirmasi (transfer final, tidak bisa dibatalkan) → minta PIN →
|
||
|
||
`POST /api/v1/customer/wallet/transfer` + header `Idempotency-Key`
|
||
|
||
```json
|
||
{ "currency": "POINT", "amount": 120, "recipient_phone": "081234561234", "pin": "482913" }
|
||
```
|
||
|
||
```json
|
||
{
|
||
"group_id": "…",
|
||
"currency": "POINT",
|
||
"amount": 120,
|
||
"recipient": { "name": "Bu*** Sa***", "phone_number": "08**-****-1234" },
|
||
"lots": [
|
||
{ "amount": 100, "expires_at": "2026-12-31T23:59:59+07:00" },
|
||
{ "amount": 20, "expires_at": null }
|
||
],
|
||
"balance": 30,
|
||
"replayed": false
|
||
}
|
||
```
|
||
|
||
4. Layar sukses: saldo tersisa (`balance`). Bila ada `lots[].expires_at`, tampilkan
|
||
"Saldo yang dikirim berlaku sampai {tanggal}" (tanggal kedaluwarsa ikut terbawa ke
|
||
penerima).
|
||
|
||
Penolakan `304` yang mungkin: transfer dimatikan owner, di bawah minimal, di atas
|
||
maksimal per transaksi, melewati batas harian (reset tengah malam WIB), saldo tidak
|
||
cukup. Tampilkan pesan umum "Transfer tidak bisa diproses" plus alasan yang sesuai
|
||
bila bisa dikenali. Bila kena `TRANSFER_BLOCKED`, ikuti §6.5.
|
||
|
||
Penerima mendapat push `WALLET_TRANSFER_IN`.
|
||
|
||
---
|
||
|
||
## 8. Game (memakai EnakCoin)
|
||
|
||
`POST /api/v1/customer/spin`, `GET /customer/games`, dan `GET /customer/ferris-wheel`
|
||
sudah dihapus. Semua game, termasuk spin, dimainkan lewat EnakGame. Tanpa PIN.
|
||
|
||
1. `GET /api/v1/customer/enakgame/games`: daftar game dengan `entry_cost` (EnakCoin per
|
||
main). Tampilkan biaya sebelum main, dan nonaktifkan tombol bila `coin_balance`
|
||
kurang. Spin punya `prizes` untuk menggambar roda.
|
||
2. `POST /api/v1/customer/enakgame/sessions` dengan `{ "game_id": "…" }` dan header
|
||
`Idempotency-Key` (satu key per tap; retry memakai key yang sama). Response berisi
|
||
`session_id` dan `coin_balance` setelah dipotong.
|
||
3. `POST /api/v1/customer/enakgame/sessions/:id/complete` dengan hasil main (`score`
|
||
atau `outcome`; spin cukup `{}`). Server yang menentukan hadiah: perbarui saldo dari
|
||
`coin_balance`, tampilkan `reward_total`, dan untuk spin hentikan roda di `prize.entry`.
|
||
|
||
Rincian spin ada di [`enakgame-spin.md`](./enakgame-spin.md).
|
||
|
||
---
|
||
|
||
## 9. Yang sudah dihapus / deprecated
|
||
|
||
Sudah **dihapus** dari API (jangan dipanggil, akan error / tidak ada):
|
||
|
||
| Lama | Pengganti |
|
||
|---|---|
|
||
| `GET /customer/tokens` | `GET /customer/wallet` → `coin_balance` |
|
||
| `total_tokens`, `tokens_history` | `coin_balance`, `GET /customer/wallet/transactions?currency=COIN` |
|
||
| `token_used`, `tokens_remaining` di response game | `coins_used`, `coins_remaining` |
|
||
| `POST /customer/wallet/payment-code` | Tidak ada; EnakPoint tidak bisa untuk bayar |
|
||
| `POST /customer/orders/:id/pay-with-points` | Tidak ada; EnakPoint tidak bisa untuk bayar |
|
||
| `GET /orders/:id/point-payment/preview` (POS) | Tidak ada; EnakPoint tidak bisa untuk bayar |
|
||
| `accepts_point_payment` di `GET /customer/outlets` | – |
|
||
| `points_used`, `point_value` di `payments` pada `GET /customer/orders/:id` | – |
|
||
| Tipe mutasi `PAYMENT`, `PAYMENT_REFUND` di riwayat | Tidak ditulis lagi |
|
||
|
||
Masih ada tapi **deprecated** (akan dihapus, jangan dipakai di kode baru):
|
||
|
||
| Lama | Pengganti |
|
||
|---|---|
|
||
| `GET /customer/points` | `GET /customer/wallet` → `point_balance` |
|
||
| `total_points`, `points_history`, `last_updated` di `/customer/wallet` | `point_balance`, `recent_transactions` |
|
||
|
||
---
|
||
|
||
## 10. Checklist selesai
|
||
|
||
- [ ] Beranda menampilkan saldo EnakPoint ("setara potongan Rp …"), EnakCoin, dan banner kedaluwarsa terdekat.
|
||
- [ ] Riwayat dengan tab per mata uang, filter tipe/tanggal, infinite scroll, label tipe sesuai §4.2.
|
||
- [ ] Layar saldo akan kedaluwarsa.
|
||
- [ ] Registrasi device FCM setelah login dan saat token berganti; unregister saat logout.
|
||
- [ ] Penanganan tap untuk keempat tipe push.
|
||
- [ ] PIN diminta hanya saat aksi yang membutuhkan; alur buat, ganti, dan lupa PIN lewat OTP.
|
||
- [ ] Keempat error PIN ditangani di semua layar yang meminta PIN.
|
||
- [ ] Tukar dengan preview, kelipatan kurs, konfirmasi, `Idempotency-Key`, retry dengan key sama.
|
||
- [ ] Transfer dengan cek penerima tersamar, konfirmasi, `Idempotency-Key`, retry dengan key sama.
|
||
- [ ] Game memakai `coins_used` / `coins_remaining` dan menampilkan biaya per game.
|
||
- [ ] Riwayat order dengan pagination dan layar detail (item, pembayaran, EnakPoint/EnakCoin yang didapat).
|
||
- [ ] Tidak ada pemakaian endpoint atau field di §9.
|
||
- [ ] PIN tidak pernah disimpan, di-log, atau dikirim ke analytics.
|