791 lines
30 KiB
Markdown
791 lines
30 KiB
Markdown
# Integrasi Mobile App Customer: EnakPoint, EnakCoin, EnakGame & Voucher
|
||||
|
|
|
|||
|
|
**Untuk:** tim aplikasi mobile customer · **Base URL:** `/api/v1` · **Per:** 8 Okt 2026
|
|||
|
|
|
|||
|
|
Kamu mengerjakan aplikasi mobile untuk **customer** (bukan kasir, bukan backoffice).
|
|||
|
|
Tugasmu: membangun fitur loyalitas di aplikasi, yaitu saldo EnakPoint & EnakCoin,
|
|||
|
|
PIN, tukar, transfer, voucher, dan pintu masuk ke game EnakGame. Semuanya 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.
|
|||
|
|
|
|||
|
|
Dokumen ini menggantikan `mobile-customer-enakpoint.md`, `integration-enakpoint.md`,
|
|||
|
|
`api-enakpoint.md`, dan `enakgame-spin.md` untuk sisi aplikasi customer. Game-nya
|
|||
|
|
sendiri (Phaser) dikerjakan tim EnakGame dengan
|
|||
|
|
[`integration-enakgame.md`](./integration-enakgame.md).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. Konteks bisnis
|
|||
|
|
|
|||
|
|
| | EnakPoint (`POINT`) | EnakCoin (`COIN`) |
|
|||
|
|
|---|---|---|
|
|||
|
|
| Didapat dari | Belanja (order lunas), tukar EnakCoin, koreksi admin | Belanja, **hadiah game**, 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.
|
|||
|
|
|
|||
|
|
### 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 tarik tunai, dan EnakPoint
|
|||
|
|
tidak bisa dipakai membayar. Jangan membangun layar bayar atau kode bayar.
|
|||
|
|
3. **PIN 6 digit wajib** untuk: tukar EnakCoin, transfer, dan **tukar EnakPoint ke
|
|||
|
|
voucher**. Main game, 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.
|
|||
|
|
7. **Hadiah game ditentukan server.** Aplikasi tidak menghitung atau mengirim hadiah.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 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.
|
|||
|
|
- Belum ada endpoint refresh token: bila token ditolak (§2.2, `entity` `auth_handler`),
|
|||
|
|
customer login ulang.
|
|||
|
|
|
|||
|
|
### 2.1 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.
|
|||
|
|
|
|||
|
|
### 2.2 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, **atau token tidak berlaku** bila `entity` = `auth_handler` | Pesan yang ramah per fitur; `cause` berbahasa Inggris, jangan tampilkan mentah. Token: login ulang |
|
|||
|
|
| `404` | 404 | Tidak ditemukan, juga untuk data milik customer lain | Tampilkan "tidak ditemukan" |
|
|||
|
|
| `429` | 429 | Minta OTP terlalu cepat | 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" |
|
|||
|
|
|
|||
|
|
### 2.3 Idempotency-Key
|
|||
|
|
|
|||
|
|
Endpoint **tukar**, **transfer**, dan **tukar voucher** wajib header `Idempotency-Key`
|
|||
|
|
(string unik, maks. 50 karakter, mis. UUID v4; `X-Idempotency-Key` juga diterima).
|
|||
|
|
|
|||
|
|
- 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/*` | – |
|
|||
|
|
| Daftar game + webview game | `GET /customer/enakgame/games` | – |
|
|||
|
|
| Riwayat main | `GET /customer/enakgame/sessions` | – |
|
|||
|
|
| Katalog voucher | `GET /customer/vouchers` | – |
|
|||
|
|
| Tukar voucher | `POST /customer/vouchers/:id/redeem` | Ya |
|
|||
|
|
| Voucher saya | `GET /customer/vouchers/redemptions` | – |
|
|||
|
|
| (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), Voucher (§9).
|
|||
|
|
|
|||
|
|
Muat ulang beranda setelah setiap transaksi, saat webview game ditutup, 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 `−`.
|
|||
|
|
- Penambahan membawa `source`, pengurangan membawa `destination`, keduanya `{ type, id }`.
|
|||
|
|
- `description` sudah siap tampil (nama lawan transfer sudah disamarkan, nama game dan
|
|||
|
|
voucher sudah tertulis). 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` | Mata uang | Label | Arah |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| `EARN` | keduanya | Dari belanja | + |
|
|||
|
|
| `EARN_REVERSAL` | keduanya | Dibatalkan (order di-void/refund) | − |
|
|||
|
|
| `EXCHANGE_OUT` | EnakCoin | Ditukar ke EnakPoint | − |
|
|||
|
|
| `EXCHANGE_IN` | EnakPoint | Hasil tukar EnakCoin | + |
|
|||
|
|
| `TRANSFER_OUT` | keduanya | Transfer keluar | − |
|
|||
|
|
| `TRANSFER_IN` | keduanya | Transfer masuk | + |
|
|||
|
|
| `GAME_SPEND` | EnakCoin | Main game | − |
|
|||
|
|
| `GAME_SPEND_REFUND` | EnakCoin | Biaya main dikembalikan | + |
|
|||
|
|
| `GAME_REWARD` | EnakCoin | Hadiah game | + |
|
|||
|
|
| `REWARD_REDEEM` | EnakPoint | Ditukar ke voucher | − |
|
|||
|
|
| `REWARD_REDEEM_REFUND` | EnakPoint | Penukaran voucher dibatalkan | + |
|
|||
|
|
| `EXPIRE` | keduanya | Kedaluwarsa | − |
|
|||
|
|
| `ADJUSTMENT` | keduanya | Koreksi | + / − |
|
|||
|
|
| `MIGRATION` | keduanya | Saldo awal | + |
|
|||
|
|
|
|||
|
|
Tipe yang tidak dikenal (bila backend menambah tipe baru): tampilkan `description` dan
|
|||
|
|
arah dari tanda `amount`, tanpa label.
|
|||
|
|
|
|||
|
|
### 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`.
|
|||
|
|
- Satu token FCM hanya milik satu customer: bila customer lain login di HP yang sama,
|
|||
|
|
customer sebelumnya tidak lagi menerima notifikasi di HP itu.
|
|||
|
|
- Saat **logout**, panggil `DELETE /api/v1/customer/devices/{device_id}` sebelum
|
|||
|
|
menghapus token login.
|
|||
|
|
|
|||
|
|
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` | `reminder_days` 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 (tukar, transfer, tukar voucher). 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 EnakCoin dan tukar voucher 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}". EnakPoint hasil tukar tidak bisa hidup lebih lama dari EnakCoin
|
|||
|
|
asalnya.
|
|||
|
|
|
|||
|
|
Jumlah yang salah ditolak sebelum PIN dicek, jadi tidak memakan jatah percobaan PIN.
|
|||
|
|
|
|||
|
|
### 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 (EnakGame)
|
|||
|
|
|
|||
|
|
Game dimainkan di **webview** yang memuat `game_url` tiap game. Pembagian tugasnya:
|
|||
|
|
aplikasi menampilkan daftar game, membuka webview, dan memberi token lewat bridge; game
|
|||
|
|
EnakGame sendiri yang memulai session, memotong EnakCoin, mengirim hasil, dan
|
|||
|
|
menampilkan hadiah ([`integration-enakgame.md`](./integration-enakgame.md)). Aplikasi
|
|||
|
|
**tidak** memanggil `POST /customer/enakgame/sessions` atau `…/complete`.
|
|||
|
|
|
|||
|
|
### 8.1 Daftar game — `GET /customer/enakgame/games`
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
[
|
|||
|
|
{
|
|||
|
|
"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 } ]
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Tampilkan:
|
|||
|
|
- Kartu per game: `thumbnail_url`, `name`, biaya "{entry_cost} EnakCoin".
|
|||
|
|
- Badge event bila `events` tidak kosong: `name` atau `banner_url`, dan "berakhir
|
|||
|
|
{end_at}" (tampilkan dalam WIB).
|
|||
|
|
- Tombol Main nonaktif dengan teks "EnakCoin kurang" bila `coin_balance` (§4.1) lebih
|
|||
|
|
kecil dari `entry_cost`.
|
|||
|
|
- `prizes` hanya dipakai game spin di dalam webview; aplikasi boleh mengabaikannya.
|
|||
|
|
|
|||
|
|
Game yang dinonaktifkan admin hilang dari daftar ini. Muat ulang daftar setiap kali
|
|||
|
|
layar dibuka.
|
|||
|
|
|
|||
|
|
### 8.2 Membuka game
|
|||
|
|
|
|||
|
|
1. Customer menekan Main → buka webview layar penuh dengan `game_url`.
|
|||
|
|
2. Pasang bridge (§8.3) **sebelum** halaman dimuat.
|
|||
|
|
3. Saat game mengirim `ready`, jawab dengan `init`.
|
|||
|
|
4. Saat game mengirim `close`, tutup webview, lalu muat ulang beranda wallet (§4.1).
|
|||
|
|
|
|||
|
|
Jangan menaruh token di URL `game_url` (query string atau fragment): URL bisa tercatat di
|
|||
|
|
log server game dan riwayat webview.
|
|||
|
|
|
|||
|
|
### 8.3 Bridge (sisi aplikasi)
|
|||
|
|
|
|||
|
|
> **Usulan.** Kontrak ini sama dengan [`integration-enakgame.md`](./integration-enakgame.md)
|
|||
|
|
> §2 dan belum diimplementasikan. Sepakati dengan tim EnakGame sebelum mulai.
|
|||
|
|
|
|||
|
|
- Game → aplikasi: JavaScript channel webview bernama **`EnakGameHost`**; setiap pesan
|
|||
|
|
berupa JSON string.
|
|||
|
|
- Aplikasi → game: jalankan `window.enakGame.receive('<json>')` di webview.
|
|||
|
|
|
|||
|
|
| Pesan masuk dari game | Yang dilakukan aplikasi |
|
|||
|
|
|---|---|
|
|||
|
|
| `{ "type": "ready" }` | Kirim `{ "type": "init", "api_base_url": "<base URL>/api/v1", "token": "<token customer>", "game_id": "<id game yang dibuka>" }` |
|
|||
|
|
| `{ "type": "token_expired" }` | Login ulang customer (tidak ada refresh token), lalu kirim `{ "type": "token", "token": "<token baru>" }` |
|
|||
|
|
| `{ "type": "balance_changed", "coin_balance": 15 }` | Perbarui saldo EnakCoin yang ditampilkan aplikasi |
|
|||
|
|
| `{ "type": "close" }` | Tutup webview, muat ulang beranda |
|
|||
|
|
|
|||
|
|
Abaikan pesan dengan `type` lain. Tombol back Android jangan langsung menutup webview:
|
|||
|
|
tampilkan konfirmasi "Keluar dari game? EnakCoin yang sudah dipakai untuk main tidak
|
|||
|
|
kembali", lalu tutup. Tidak perlu mengirim pesan ke game.
|
|||
|
|
|
|||
|
|
### 8.4 Riwayat main — `GET /customer/enakgame/sessions?page=1&limit=20`
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"data": [
|
|||
|
|
{
|
|||
|
|
"id": "…", "game_id": "8a1f…", "status": "COMPLETED", "entry_cost": 5, "reward_total": 10,
|
|||
|
|
"started_at": "…", "expires_at": "…", "ended_at": "…", "refund_reason": null
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"pagination": { "page": 1, "limit": 20, "total_count": 3, "total_pages": 1 }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| `status` | Label | Keterangan |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `STARTED` | Sedang dimainkan | |
|
|||
|
|
| `COMPLETED` | Selesai | "Dapat {reward_total} EnakCoin" |
|
|||
|
|
| `REFUNDED` | Dikembalikan | Entry cost kembali; `refund_reason` `SYSTEM_ERROR` atau `GAME_DEACTIVATED` |
|
|||
|
|
| `EXPIRED` | Tidak selesai | Hasil tidak dikirim sebelum batas waktu; entry cost tidak kembali |
|
|||
|
|
|
|||
|
|
Nama game diambil dari daftar game (§8.1) lewat `game_id`. Detail satu session:
|
|||
|
|
`GET /customer/enakgame/sessions/:id`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 9. Voucher (tukar EnakPoint)
|
|||
|
|
|
|||
|
|
### 9.1 Katalog — `GET /customer/vouchers`
|
|||
|
|
|
|||
|
|
Voucher yang bisa ditukar sekarang: aktif, dalam masa berlaku, dan masih ada stoknya.
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
[
|
|||
|
|
{
|
|||
|
|
"id": "…",
|
|||
|
|
"name": "Kopi Susu Gratis",
|
|||
|
|
"description": "Berlaku untuk ukuran regular",
|
|||
|
|
"image_url": "https://…/kopi.png",
|
|||
|
|
"voucher_type": "FREE_ITEM",
|
|||
|
|
"face_value": 20000,
|
|||
|
|
"point_cost": 15000,
|
|||
|
|
"max_per_customer": 2,
|
|||
|
|
"valid_until": "2026-12-31T16:59:59Z",
|
|||
|
|
"terms": { "…": "syarat & ketentuan, objek JSON bebas" },
|
|||
|
|
"available": 120
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- `point_cost`: EnakPoint yang dipotong. Tombol Tukar nonaktif bila `point_balance`
|
|||
|
|
kurang.
|
|||
|
|
- `face_value`: nilai voucher dalam rupiah, tampilkan sebagai "senilai Rp 20.000".
|
|||
|
|
- `available`: sisa stok; `null` berarti stok tidak dihitung. Bila 0, tampilkan "Habis".
|
|||
|
|
- `max_per_customer`: batas tukar per customer; `null` = tanpa batas.
|
|||
|
|
- `terms`: objek JSON yang isinya diatur admin. Sepakati bentuknya dengan tim
|
|||
|
|
backoffice; sebelum itu tampilkan `description` saja.
|
|||
|
|
|
|||
|
|
| `voucher_type` | Label usulan |
|
|||
|
|
|---|---|
|
|||
|
|
| `FIXED_VALUE` | Potongan Rp {face_value} |
|
|||
|
|
| `PERCENTAGE` | Potongan persen |
|
|||
|
|
| `FREE_ITEM` | Gratis item |
|
|||
|
|
| `MERCHANT_BENEFIT` | Benefit merchant |
|
|||
|
|
|
|||
|
|
### 9.2 Tukar — `POST /customer/vouchers/:id/redeem`
|
|||
|
|
|
|||
|
|
Konfirmasi ("Tukar {point_cost} EnakPoint dengan {name}? Tidak bisa dibatalkan.") →
|
|||
|
|
minta PIN → kirim dengan header `Idempotency-Key`:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{ "pin": "482913" }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"id": "…",
|
|||
|
|
"voucher_id": "…",
|
|||
|
|
"voucher_name": "Kopi Susu Gratis",
|
|||
|
|
"voucher_image_url": "https://…/kopi.png",
|
|||
|
|
"voucher_type": "FREE_ITEM",
|
|||
|
|
"status": "COMPLETED",
|
|||
|
|
"face_value": 20000,
|
|||
|
|
"point_cost": 15000,
|
|||
|
|
"code": "KOPI-7F3C-2291",
|
|||
|
|
"code_expires_at": "2026-12-31T16:59:59Z",
|
|||
|
|
"completed_at": "…",
|
|||
|
|
"created_at": "…",
|
|||
|
|
"point_balance": 2500,
|
|||
|
|
"replayed": false
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- Layar sukses: voucher, `code` bila ada (bisa disalin), masa berlaku, dan saldo
|
|||
|
|
EnakPoint baru (`point_balance`).
|
|||
|
|
- `code` bisa `null`: voucher ini tidak memakai kode; tunjukkan layar voucher ke kasir.
|
|||
|
|
- `status: "PENDING"`: voucher sedang diproses penyedia luar (belum ada voucher seperti
|
|||
|
|
ini di katalog, tapi tangani dari sekarang). EnakPoint sudah terpotong;
|
|||
|
|
tampilkan "Voucher sedang diproses" dan cek lagi di Voucher saya (§9.3). Bila akhirnya
|
|||
|
|
`FAILED`, EnakPoint dikembalikan otomatis (mutasi `REWARD_REDEEM_REFUND`).
|
|||
|
|
- Error PIN ditangani sesuai §6.5.
|
|||
|
|
|
|||
|
|
| Penolakan `304` (`cause`) | Tampilan |
|
|||
|
|
|---|---|
|
|||
|
|
| `not enough EnakPoint` | "EnakPoint kamu kurang." |
|
|||
|
|
| `the voucher is out of stock` | "Voucher sudah habis." Muat ulang katalog |
|
|||
|
|
| `this voucher can be redeemed at most … times per customer` | "Kamu sudah mencapai batas penukaran voucher ini." |
|
|||
|
|
| `the voucher is not available`, `… cannot be redeemed yet`, `… has ended`, `… not available yet` | "Voucher tidak tersedia." Muat ulang katalog |
|
|||
|
|
| `the customer is not active` | "Akun tidak aktif." |
|
|||
|
|
| `this Idempotency-Key was already used to redeem another voucher` | Bug di app: key dipakai ulang |
|
|||
|
|
|
|||
|
|
### 9.3 Voucher saya — `GET /customer/vouchers/redemptions?page=1&limit=20`
|
|||
|
|
|
|||
|
|
Daftar penukaran customer, terbaru di atas, dengan bentuk item sama seperti response
|
|||
|
|
§9.2 (tanpa `point_balance` dan `replayed`), dibungkus `data` + `pagination`.
|
|||
|
|
|
|||
|
|
| `status` | Tampilan |
|
|||
|
|
|---|---|
|
|||
|
|
| `COMPLETED` | Voucher siap dipakai: nama, `code` (bila ada), berlaku sampai `code_expires_at` |
|
|||
|
|
| `PENDING` | "Sedang diproses" |
|
|||
|
|
| `FAILED` | "Gagal, EnakPoint sudah dikembalikan" |
|
|||
|
|
|
|||
|
|
**Memakai voucher di outlet:** customer menunjukkan layar voucher ke kasir. POS belum
|
|||
|
|
bisa menandai voucher terpakai, jadi aplikasi belum bisa menampilkan status "sudah
|
|||
|
|
dipakai" ([`integration-pos.md`](./integration-pos.md) §5).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 10. Yang sudah dihapus / deprecated
|
|||
|
|
|
|||
|
|
Sudah **dihapus** dari API (jangan dipanggil, akan error / tidak ada):
|
|||
|
|
|
|||
|
|
| Lama | Pengganti |
|
|||
|
|
|---|---|
|
|||
|
|
| `POST /customer/spin` | Game EnakGame di webview (§8) |
|
|||
|
|
| `GET /customer/games`, `GET /customer/ferris-wheel` | `GET /customer/enakgame/games` |
|
|||
|
|
| `coins_used`, `coins_remaining`, `prize_won`, `game_play` di response spin | Tidak ada; hasil game ditampilkan di dalam game |
|
|||
|
|
| `metadata.coin_cost` pada data game | `entry_cost` |
|
|||
|
|
| `GET /customer/tokens` | `GET /customer/wallet` → `coin_balance` |
|
|||
|
|
| `total_tokens`, `tokens_history`, `token_used`, `tokens_remaining` | `coin_balance`, `GET /customer/wallet/transactions?currency=COIN` |
|
|||
|
|
| `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 |
|
|||
|
|
| `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` |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 11. 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, dan label semua tipe di §4.2, termasuk tipe game dan voucher.
|
|||
|
|
- [ ] 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, transfer, voucher).
|
|||
|
|
- [ ] Tukar dengan preview, kelipatan kurs, konfirmasi, `Idempotency-Key`, retry dengan key sama.
|
|||
|
|
- [ ] Transfer dengan cek penerima tersamar, konfirmasi, `Idempotency-Key`, retry dengan key sama.
|
|||
|
|
- [ ] Daftar game dengan biaya, badge event, dan tombol nonaktif bila EnakCoin kurang.
|
|||
|
|
- [ ] Webview game dengan bridge §8.3; token tidak pernah di URL; beranda dimuat ulang saat game ditutup.
|
|||
|
|
- [ ] Riwayat main dengan label status.
|
|||
|
|
- [ ] Katalog voucher, tukar dengan PIN dan `Idempotency-Key`, status `PENDING` ditangani.
|
|||
|
|
- [ ] Voucher saya dengan kode yang bisa disalin.
|
|||
|
|
- [ ] Riwayat order dengan pagination dan layar detail (item, pembayaran, EnakPoint/EnakCoin yang didapat).
|
|||
|
|
- [ ] Tidak ada pemakaian endpoint atau field di §10.
|
|||
|
|
- [ ] PIN dan token tidak pernah disimpan sembarangan, di-log, atau dikirim ke analytics.
|