diff --git a/docs/api-enakpoint.md b/docs/api-enakpoint.md index 1aeb01b..0f168b1 100644 --- a/docs/api-enakpoint.md +++ b/docs/api-enakpoint.md @@ -41,6 +41,11 @@ Semua endpoint EnakPoint (`POINT`, bisa bayar order) dan EnakCoin (`COIN`, untuk | GET | `/customer/wallet/expiring` | Saldo yang akan kedaluwarsa, per currency dan tanggal | | PUT | `/customer/devices` | Daftarkan token FCM device | | DELETE | `/customer/devices/:device_id` | Hapus device saat logout | +| GET | `/customer/outlets` | Outlet aktif di organisasi customer, dengan `accepts_point_payment`, `earns_points`, `earns_coins` | +| GET | `/customer/orders` | Riwayat order customer (`page`, `limit`), dengan `points_earned` / `coins_earned` | +| GET | `/customer/orders/:id` | Detail order: item, pembayaran, EnakPoint yang dipakai; order customer lain → `404` | + +Registrasi (`POST /customer-auth/register/start`) menerima `organization_id` opsional: bila tidak dikirim dan hanya ada satu organisasi, customer masuk ke organisasi itu. Contoh request dan response lengkap untuk outlet dan order ada di [`mobile-customer-enakpoint.md`](./mobile-customer-enakpoint.md) §4.4–§4.5. ### GET /customer/wallet diff --git a/docs/mobile-customer-enakpoint.md b/docs/mobile-customer-enakpoint.md new file mode 100644 index 0000000..1c7a8d2 --- /dev/null +++ b/docs/mobile-customer-enakpoint.md @@ -0,0 +1,614 @@ +# 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 | **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. + +### 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. +3. **PIN 6 digit wajib** untuk: membuat kode bayar, 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 ` +- 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` | – | +| Kode bayar (angka + QR) | `POST /customer/wallet/payment-code` | Ya | +| 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 | `POST /customer/spin` | – | +| (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: Bayar di kasir (§7.1), Tukar EnakCoin (§8.1), Transfer (§8.2), Main game (§9). + +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,PAYMENT` | 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) | − | +| `PAYMENT` | Bayar pesanan | − | +| `PAYMENT_REFUND` | Pengembalian pembayaran | + | +| `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", + "accepts_point_payment": true, + "earns_points": true, + "earns_coins": false + } +] +``` + +- `address` bisa `null`. +- `accepts_point_payment`: kasir di outlet ini menerima pembayaran EnakPoint. Pakai + untuk label "Bisa bayar pakai EnakPoint". +- `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": "EnakPoint", "method_type": "point", "amount": 12500, "status": "completed", "refund_amount": 0, "points_used": 12500, "point_value": 1, "created_at": "…" }, + { "id": "…", "method_name": "Cash", "method_type": "cash", "amount": 86500, "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". +- Pembayaran EnakPoint membawa `points_used`; tampilkan "EnakPoint 12.500 (Rp 12.500)". +- 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": "", "fcm_token": "", "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**; +bayar dan 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. Membayar dengan EnakPoint + +App customer tidak membuat atau membayar order; order hanya bisa dilihat (§4.5). +EnakPoint hanya dipakai membayar di kasir, lewat kode bayar dari app. Jangan membangun +layar checkout atau memanggil `POST /customer/orders/:id/pay-with-points`. + +### 7.1 Di kasir — kode bayar + +Customer tidak pernah mengetik PIN di mesin kasir. Alurnya: + +1. Customer membuka "Bayar di kasir" dan memasukkan PIN. +2. `POST /api/v1/customer/wallet/payment-code` dengan `{ "pin": "482913" }`: + + ```json + { "code": "482913", "qr_payload": "enakpoint:482913", "expires_at": "2026-09-30T05:02:00Z" } + ``` + +3. Tampilkan `code` besar (angka) **dan** QR dari `qr_payload` (string apa adanya). +4. Tampilkan hitung mundur ke `expires_at` (2 menit). Setelah habis, sembunyikan kode + dan tampilkan tombol "Buat kode baru". +5. Kasir memindai/mengetik kode dan memilih jumlah EnakPoint. App tidak menerima + callback; setelah customer kembali ke beranda, muat ulang saldo. + +Kode sekali pakai. Membuat kode baru membatalkan kode lama. + +### 7.2 Refund + +Bila order yang dibayar EnakPoint dibatalkan atau direfund, EnakPoint kembali sebagai +EnakPoint (tidak pernah tunai) dan muncul di riwayat sebagai `PAYMENT_REFUND`. + +--- + +## 8. Tukar dan transfer + +### 8.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}". + +### 8.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`. + +--- + +## 9. Game (memakai EnakCoin) + +`POST /api/v1/customer/spin` dengan `{ "spin_id": "" }`. Tanpa PIN. + +```json +{ + "game_play": { "id": "…", "game_id": "…", "coins_used": 1, "created_at": "…" }, + "prize_won": { "id": "…", "name": "Voucher 10rb" }, + "coins_remaining": 7 +} +``` + +- Setiap game punya biaya sendiri: `metadata.coin_cost` pada data game dari + `GET /api/v1/customer/games` (atau `GET /customer/ferris-wheel`), default 1 bila kosong. + Tampilkan biaya sebelum main, dan nonaktifkan tombol bila `coin_balance` kurang. +- `304`: EnakCoin kurang, game nonaktif, atau hadiah baru saja habis. Tidak ada + EnakCoin yang terpotong; tampilkan pesan dan biarkan customer mencoba lagi. +- Setelah main, perbarui saldo EnakCoin dari `coins_remaining`. + +--- + +## 10. 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` | + +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, 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. +- [ ] Kode bayar: angka + QR, hitung mundur 2 menit, tombol buat ulang. +- [ ] 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 §10. +- [ ] PIN tidak pernah disimpan, di-log, atau dikirim ke analytics.