2026-09-30 15:31:44 +07:00
# Integrasi EnakPoint & EnakCoin — Customer App, POS & Dashboard
**Migrasi:** `000090` – `000097` · **Base URL:** `/api/v1` · **Kompatibilitas:** endpoint
lama tetap jalan sebagai alias (lihat §8)
Panduan untuk memakai saldo loyalitas dari sisi klien. Alasan di balik setiap aturan
ada di [`prd-point-coin.md` ](./prd-point-coin.md ).
2026-10-07 13:48:29 +07:00
> **Perubahan 7 Okt 2026:** bayar order dengan EnakPoint sudah dihapus (migrasi
> `000102`). 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). Endpoint dan field yang ikut dihapus
> ada di §8.
2026-09-30 15:31:44 +07:00
---
## 1. Konsep inti
| | EnakPoint (`POINT` ) | EnakCoin (`COIN` ) |
|---|---|---|
| Didapat dari | Order lunas (per outlet), adjustment admin, exchange | Order lunas (per outlet), adjustment admin |
2026-10-07 13:48:29 +07:00
| Dipakai untuk | **Ditukar ke voucher** (tidak bisa membayar order) | **Main game** , ditukar ke EnakPoint |
2026-09-30 15:31:44 +07:00
| Bisa ditransfer | Ya | Ya |
| Bisa kedaluwarsa | Ya, bila diaktifkan owner | Ya, bila diaktifkan owner |
Aturan yang berlaku di seluruh dokumen ini:
1. **Semua jumlah bilangan bulat.** Tidak ada "setengah EnakPoint".
2026-10-07 13:48:29 +07:00
2. **Saldo tidak pernah jadi uang.** Tidak ada pencairan, dan EnakPoint tidak bisa
dipakai membayar order. Tampilkan nilai rupiahnya sebagai ** "setara potongan
Rp …"**, bukan "saldo Rp …".
3. **Semua aksi customer yang memindahkan saldo butuh PIN 6 digit** (§3): exchange
dan transfer. Main game tidak butuh PIN.
2026-09-30 15:31:44 +07:00
4. **Wallet milik customer di satu organisasi.** Saldo berlaku di semua outlet
organisasi itu. Nilai rupiah EnakPoint, kurs exchange, batas transfer, dan
2026-10-07 13:48:29 +07:00
kedaluwarsa diatur per organisasi; earning per outlet.
2026-09-30 15:31:44 +07:00
5. **Setiap mutasi tercatat** di riwayat beserta asal atau tujuannya, dan tidak pernah
dihapus. Koreksi muncul sebagai baris baru.
### Format response
Semua endpoint memakai amplop yang sama:
```json
{ "success" : true , "data" : { … }, "errors" : null }
```
```json
{
"success" : false ,
"data" : null ,
"errors" : [{ "code" : "304" , "entity" : "wallet_service" , "cause" : "wallet move refused: not enough EnakCoin" }]
}
```
| `code` | HTTP | Arti |
|---|---|---|
| `303` , `310` | 400 | Body atau parameter tidak lengkap / salah format |
| `304` | 400 | Permintaan ditolak aturan bisnis; `cause` menjelaskan alasannya |
| `404` | 404 | Tidak ditemukan (juga dipakai untuk data milik customer/organisasi lain) |
| `429` | 429 | Terlalu cepat meminta ulang (OTP) |
| `PIN_NOT_SET` | 403 | Customer belum membuat PIN |
| `PIN_INVALID` | 400 | PIN salah |
| `PIN_LOCKED` | 423 | PIN terkunci |
| `TRANSFER_BLOCKED` | 403 | Transfer ditahan setelah reset PIN |
| `900` | 500 | Kesalahan server |
---
## 2. Customer app — saldo & riwayat
Semua endpoint customer memakai header `Authorization: Bearer <token customer>` .
### 2.1 Saldo
`GET /api/v1/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" : [ … ]
}
```
- `point_balance` dan `coin_balance` adalah saldo yang **bisa dipakai sekarang** .
- `point_discount_value` = `point_balance × point_value` . Tampilkan sebagai
"setara potongan Rp 12.500".
- `nearest_expiring` bernilai `null` per currency bila tidak ada yang akan kedaluwarsa.
- `recent_transactions` berisi 5 mutasi terakhir dengan bentuk yang sama seperti §2.2.
### 2.2 Riwayat
2026-10-07 13:48:29 +07:00
`GET /api/v1/customer/wallet/transactions?page=1&limit=20¤cy=POINT&type=EARN,TRANSFER_IN&from=2026-09-01&to=2026-09-30`
2026-09-30 15:31:44 +07:00
Semua query opsional. `limit` 1– 100 (default 20). `type` boleh beberapa, dipisah koma.
`from` / `to` 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" : "…" ,
"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 }
}
```
- `amount` bertanda: positif menambah saldo, negatif mengurangi.
- Penambahan punya `source` , pengurangan punya `destination` . Keduanya berbentuk
2026-10-07 13:48:29 +07:00
`{ type, id }` dan menunjuk hal yang bisa dibuka di detail (order, game play,
dst.).
2026-09-30 15:31:44 +07:00
- `description` sudah siap tampil dan tidak berubah walau nama outlet atau customer
berubah belakangan. Nama lawan transfer sudah disamarkan.
- Dua baris exchange atau transfer berbagi `group_id` yang sama.
| `type` | Arah | Arti | `source` / `destination` |
|---|---|---|---|
| `EARN` | + | Didapat dari order lunas | `ORDER` |
| `EARN_REVERSAL` | − | Ditarik karena order di-void/refund | `ORDER` |
| `EXCHANGE_OUT` / `EXCHANGE_IN` | − / + | Tukar EnakCoin ke EnakPoint | `WALLET_TX` (baris pasangannya) |
| `TRANSFER_OUT` / `TRANSFER_IN` | − / + | Transfer antar customer | `WALLET_TX` (baris pasangannya) |
| `GAME_SPEND` | − | Main game | `GAME_PLAY` |
| `EXPIRE` | − | Hangus karena kedaluwarsa | `LOT` |
| `ADJUSTMENT` | + / − | Koreksi oleh admin | `USER` |
| `MIGRATION` | + | Saldo dari sistem lama | `LEGACY_POINTS` / `LEGACY_TOKENS` |
### 2.3 Yang akan kedaluwarsa
`GET /api/v1/customer/wallet/expiring`
```json
{
"point" : [
{ "amount" : 150 , "date" : "2026-10-31" },
{ "amount" : 200 , "date" : "2026-12-31" }
],
"coin" : []
}
```
Dikelompokkan per tanggal (WIB), paling dekat lebih dulu. Saldo bisa dipakai sampai
akhir hari tanggal itu. Daftar kosong berarti tidak ada yang akan kedaluwarsa.
### 2.4 Notifikasi push (FCM)
Aplikasi mendaftarkan token FCM-nya **setelah login dan setiap kali FCM memberi token
baru**:
`PUT /api/v1/customer/devices`
```json
{ "device_id" : "a1b2c3" , "fcm_token" : "…" , "platform" : "android" , "app_version" : "2.4.0" }
```
`platform` : `android` , `ios` , atau `web` (opsional). Saat logout, panggil
`DELETE /api/v1/customer/devices/:device_id` supaya HP itu tidak lagi menerima
notifikasi customer tersebut. Satu token hanya milik satu customer: bila customer lain
login di HP yang sama dan mendaftarkan token yang sama, customer sebelumnya otomatis
tidak menerima notifikasi di HP itu lagi.
Push yang dikirim, dibedakan lewat `data.type` :
| `data.type` | Kapan | Isi `data` lainnya |
|---|---|---|
| `WALLET_TRANSFER_IN` | Menerima transfer | `transaction_id` , `group_id` , `currency` , `amount` |
| `WALLET_EXPIRING` | `reminder_days` hari sebelum saldo kedaluwarsa, sekali per tanggal | `currency` , `amount` , `expiry_date` |
| `WALLET_EXPIRED` | Saldo baru saja hangus | `currency` , `amount` |
| `PIN_LOCKED` | PIN terkunci setelah 5 kali salah | `locked_until` (RFC3339, UTC) |
Semua nilai di `data` berupa string, sesuai aturan FCM.
---
## 3. Customer app — PIN
PIN 6 digit, terpisah dari password login, dikirim sebagai **string** supaya angka nol
di depan tidak hilang. PIN tidak pernah dikembalikan di response.
### 3.1 Cek status
`GET /api/v1/customer/pin/status`
```json
{ "has_pin" : true , "locked_until" : null , "transfer_blocked_until" : null }
```
Minta customer membuat PIN saat pertama kali ia melakukan aksi yang butuh PIN
(`has_pin: false` ), bukan saat registrasi.
### 3.2 Membuat PIN pertama kali
1. `POST /api/v1/customer/pin/otp` dengan `{ "purpose": "pin_setup" }` . OTP dikirim ke
nomor customer lewat WhatsApp. Response: `{ "purpose", "otp_token", "expires_at" }` .
2. `POST /api/v1/customer/pin` dengan
`{ "otp_token": "…", "otp_code": "123456", "pin": "482913", "confirm_pin": "482913" }` .
PIN ditolak (`304` ) bila bukan 6 digit, konfirmasinya beda, semua digit sama
(`111111` ), berurutan (`123456` , `654321` ), atau sama dengan tanggal lahir
(`DDMMYY` / `YYMMDD` ). Tampilkan `cause` apa adanya. Meminta OTP terlalu cepat
menghasilkan `429` .
### 3.3 Mengganti dan mereset PIN
- **Ganti:** `PUT /api/v1/customer/pin` dengan `{ "old_pin", "pin", "confirm_pin" }` .
- **Lupa PIN:** minta OTP dengan `purpose: "pin_reset"` , lalu
`POST /api/v1/customer/pin/reset` dengan body yang sama seperti §3.2. Reset juga
membuka PIN yang terkunci. Setelah reset, **transfer keluar ditahan 24 jam** ;
2026-10-07 13:48:29 +07:00
exchange tetap bisa.
2026-09-30 15:31:44 +07:00
### 3.4 Menangani error PIN
Setiap endpoint yang menerima `pin` bisa mengembalikan error PIN. Pada error ini `data`
**tidak** `null` :
```json
{
"success" : false ,
"data" : { "code" : "PIN_INVALID" , "remaining_attempts" : 3 },
"errors" : [{ "code" : "PIN_INVALID" , "entity" : "customer_pin_service" , "cause" : "wrong PIN, 3 attempts left" }]
}
```
| `data.code` | Field tambahan | Yang ditampilkan aplikasi |
|---|---|---|
| `PIN_NOT_SET` | – | Arahkan ke pembuatan PIN (§3.2) |
| `PIN_INVALID` | `remaining_attempts` | "PIN salah, sisa 3 percobaan" |
| `PIN_LOCKED` | `locked_until` | "PIN terkunci sampai 14:30", tawarkan reset PIN |
| `TRANSFER_BLOCKED` | `transfer_blocked_until` | "Transfer bisa dilakukan lagi pada …" |
Lima kali salah berturut-turut mengunci PIN selama 30 menit. Selama terkunci, PIN yang
benar pun ditolak. Penghitung disimpan di server, jadi tidak bisa diakali dengan
reinstall atau ganti HP.
---
2026-10-07 13:48:29 +07:00
## 4. Earning, void, dan refund
2026-09-30 15:31:44 +07:00
2026-10-07 13:48:29 +07:00
EnakPoint bukan payment method: tidak ada payment method bertipe `point` , dan
`POST /api/v1/payments` memakai `amount` seperti pembayaran lain. Kode bayar,
bayar dari aplikasi, dan preview pembayaran EnakPoint sudah dihapus (§8).
2026-09-30 15:31:44 +07:00
Response order membawa `points_earned` dan `coins_earned` (0 bila order tidak
2026-10-07 13:48:29 +07:00
menghasilkan apa-apa). Earning dihitung dari `subtotal − discount` , sebelum pajak, dan
diberikan saat order lunas.
EnakPoint dan EnakCoin yang didapat dari order ikut ditarik saat void/refund. Bila
saldo customer sudah terpakai, yang ditarik sebanyak yang ada; refund tidak pernah
diblokir karena ini.
2026-09-30 15:31:44 +07:00
---
## 5. Exchange EnakCoin → EnakPoint
Kurs per organisasi: `coin_amount` EnakCoin = `point_amount` EnakPoint (default 1 : 1).
1. **Preview** sebelum minta PIN:
`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 }
` ``
Bila ` valid: false`, tampilkan ` reason` (misalnya harus kelipatan ` coin_amount`,
atau EnakCoin tidak cukup).
2. **Tukar:**
` POST /api/v1/customer/wallet/exchange` dengan header **` Idempotency-Key`** (wajib,
maks. 50 karakter, satu key per percobaan tukar)
` ``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
}
` ``
- Jumlah EnakCoin harus kelipatan ` coin_amount`. Kesalahan jumlah ditolak **sebelum**
PIN dicek, jadi tidak memakan jatah percobaan PIN.
- Exchange tidak bisa dibatalkan; tampilkan konfirmasi.
- Kirim ulang dengan ` Idempotency-Key` yang sama bila koneksi putus: hasil pertama
dikembalikan dengan ` replayed: true` tanpa menukar lagi, dengan kurs saat itu.
` Idempotency-Key` yang sama untuk jumlah berbeda ditolak.
- EnakPoint hasil tukar tidak bisa hidup lebih lama dari EnakCoin asalnya (` lots`
menunjukkan tanggalnya).
---
## 6. Transfer ke customer lain
1. **Cek penerima** sebelum konfirmasi:
` GET /api/v1/customer/wallet/transfer/recipient?phone=081234561234`
` ``json
{ "name": "Bu*** Sa***", "phone_number": "08**-****-1234" }
` ``
Nomor yang tidak terdaftar di organisasi yang sama dijawab ` 404`. Diri sendiri,
customer walk-in, atau customer nonaktif dijawab ` 304`.
2. **Kirim:**
` POST /api/v1/customer/wallet/transfer` dengan header **` Idempotency-Key`** (wajib)
` ``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
}
` ``
- ` currency`: ` POINT` atau ` COIN`, satu jenis per transfer.
- Batas dari organisasi: transfer bisa dimatikan, ada minimal, maksimal per
transaksi, dan batas harian per currency (reset tengah malam WIB). Pelanggaran batas
ditolak ` 304` sebelum PIN dicek.
- Transfer final dan tidak bisa dibatalkan customer.
- Saldo yang dikirim membawa tanggal kedaluwarsa aslinya ke penerima (` lots`).
Tampilkan ini ke pengirim.
- Penerima mendapat push ` WALLET_TRANSFER_IN` (§2.4).
- Retry dengan ` Idempotency-Key` yang sama mengembalikan hasil pertama
(` replayed: true`) dan tidak dihitung dua kali terhadap batas harian.
---
## 7. Game
` POST /api/v1/customer/spin` dengan ` { "spin_id": "<id game>" }`. Tanpa PIN.
Setiap game memotong EnakCoin sebesar ` metadata.coin_cost` game itu (default 1).
Response:
` ``json
{
2026-09-30 16:55:27 +07:00
"game_play": { "id": "…", "game_id": "…", "coins_used": 1, "created_at": "…" },
2026-09-30 15:31:44 +07:00
"prize_won": { "id": "…", "name": "Voucher 10rb", … },
2026-09-30 16:55:27 +07:00
"coins_remaining": 7
2026-09-30 15:31:44 +07:00
}
` ``
EnakCoin kurang, game nonaktif, atau hadiah baru saja habis dijawab ` 304`; tidak ada
2026-09-30 16:55:27 +07:00
EnakCoin yang terpotong.
2026-09-30 15:31:44 +07:00
Di dashboard, ` metadata.coin_cost` diisi per game dengan bilangan bulat ≥ 1.
---
## 8. Endpoint lama (deprecated)
Masih jalan dan membaca saldo wallet, tapi akan dihapus setelah semua versi aplikasi
pindah. Aplikasi baru jangan memakainya.
| Lama | Ganti dengan |
|---|---|
| ` GET /customer/points` | ` GET /customer/wallet` (` point_balance`) |
2026-09-30 16:55:27 +07:00
| ` total_points`, ` points_history`, ` last_updated` di ` /customer/wallet` | ` point_balance`, ` recent_transactions` |
2026-09-30 15:31:44 +07:00
Beri tahu tim backend setelah aplikasi yang beredar tidak lagi memakai kolom kiri,
2026-09-30 16:55:27 +07:00
supaya alias ini bisa dihapus.
Semua yang bernama token sudah dihapus: ` GET /customer/tokens`, ` total_tokens`,
` tokens_history`, ` token_used`, ` tokens_remaining`, dan nilai ` TOKENS` di campaign. Pakai
` coin_balance`, ` coins_used`, ` coins_remaining`, dan ` COINS`.
2026-09-30 15:31:44 +07:00
2026-10-07 13:48:29 +07:00
Bayar dengan EnakPoint juga sudah dihapus (7 Okt 2026, migrasi ` 000102`) karena
EnakPoint sekarang hanya untuk voucher ([` enakgame-prd.md`](./enakgame-prd.md) §3.2).
Tidak ada penggantinya:
- Endpoint ` POST /customer/wallet/payment-code`, ` POST /customer/orders/:id/pay-with-points`,
dan ` GET /orders/:id/point-payment/preview`.
- Payment method tipe ` point`, serta field ` points` dan ` payment_code` di
` POST /payments`; ` amount` kembali wajib seperti pembayaran lain.
- ` points_used` dan ` point_value` di response pembayaran dan di ` payments` pada
` GET /customer/orders/:id`; ` accepts_point_payment` di ` GET /customer/outlets`.
- Objek ` point_payment` (` accept_payment`, ` min_payment_points`,
` max_payment_percent`) di setting outlet (§9.1).
- Di analytics payment method: ` point_amount`, ` points_used`, ` total_with_points` di
` summary`, serta ` points_used` dan ` counts_as_cash_in` per baris.
` summary.total_amount` kembali total semua method, dan persentase dihitung dari total
itu.
- Tipe mutasi ` PAYMENT` dan ` PAYMENT_REFUND` tidak ditulis lagi.
2026-09-30 15:31:44 +07:00
---
## 9. Dashboard
Semua endpoint di bagian ini butuh login user dengan role Admin atau Manager.
### 9.1 Pengaturan per outlet
` GET` / ` PUT /api/v1/outlets/:outlet_id/loyalty-settings`
` ``json
{
2026-10-02 14:19:12 +07:00
"point": { "enabled": true, "earn_mode": "PER_AMOUNT", "earn_per_amount": 100, "earn_value": 1, "earn_percent": 1, "min_order_amount": 0, "max_per_order": null },
2026-10-07 13:48:29 +07:00
"coin": { "enabled": true, "earn_mode": "PER_AMOUNT", "earn_per_amount": 25000, "earn_value": 1, "earn_percent": 1, "min_order_amount": 0, "max_per_order": null }
2026-09-30 15:31:44 +07:00
}
` ``
2026-10-07 13:48:29 +07:00
Field yang tidak dikirim di ` PUT` tetap memakai nilai sekarang. ` PUT` yang masih
mengirim ` point_payment` ditolak ` 310` (field tidak dikenal). Response menambahkan
2026-09-30 15:31:44 +07:00
` point_value` organisasi dan ` point_cashback_percent`
2026-10-02 14:19:12 +07:00
(` earn_value × point_value / earn_per_amount × 100`, atau ` earn_percent × point_value`
pada ` earn_mode` ` PERCENTAGE`). **Tampilkan persentase ini di
2026-09-30 15:31:44 +07:00
samping setting** supaya owner tidak salah membaca skala: default di atas setara
cashback 1%.
### 9.2 Pengaturan organisasi
` GET` / ` PUT /api/v1/marketing/loyalty-settings` (tambah ` ?dry_run=true` untuk preview
tanpa menyimpan)
` ``json
{
"point_value": 1,
"exchange": { "coin_amount": 1, "point_amount": 1 },
"transfer": { "enabled": true, "min_amount": 1, "max_per_transaction": null, "daily_limit": null },
"point_expiry": {
"enabled": false,
"mode": "FIXED_DATE",
"fixed_dates": ["12-31"],
"grace_months": 3,
"period": 12,
"unit": "MONTH",
"end_of_month": false,
"reminder_days": 7
},
2026-10-07 20:53:14 +07:00
"coin_expiry": { … sama … },
"enakgame": { "user_daily_limit": 0, "global_daily_limit": 0 }
2026-09-30 15:31:44 +07:00
}
` ``
Field yang tidak dikirim di ` PUT` tetap memakai nilai sekarang. Response menambahkan:
- ` impact`: total saldo beredar dan nilai rupiahnya **sebelum dan sesudah** perubahan
` point_value` atau kurs. Tampilkan sebagai peringatan sebelum owner menyimpan.
- ` expiry_preview`: ` { "point": …, "coin": … }`, kapan saldo yang didapat hari ini
akan kedaluwarsa (` null` bila tidak kedaluwarsa). Tampilkan sebagai "EnakPoint yang
didapat hari ini kedaluwarsa pada 31 Des 2026".
- ` expiry_activations`: bila perubahan ini **menyalakan** kedaluwarsa untuk pertama
kali, berapa saldo lama yang ikut diberi tanggal (` lots`, ` amount`) dan tanggalnya
(` expires_at`). Selalu minta konfirmasi dengan ` dry_run=true` dulu.
- ` changes`: key yang berubah.
**Kedaluwarsa** diatur per currency dengan salah satu model:
| ` mode` | Cara kerja | Field yang dipakai |
|---|---|---|
| ` FIXED_DATE` (default) | Semua saldo hangus di tanggal tetap setiap tahun. Saldo yang didapat kurang dari ` grace_months` sebelum tanggal itu ikut ke tanggal berikutnya | ` fixed_dates` (format ` MM-DD`, boleh lebih dari satu, ` 02-29` ditolak), ` grace_months` (0– 24) |
| ` ROLLING` | Tiap saldo berlaku sekian lama sejak didapat | ` period`, ` unit` (` DAY` / ` MONTH`), ` end_of_month` |
- ` reminder_days` berlaku untuk keduanya: customer diingatkan sekian hari sebelum
hangus (0 = tanpa pengingat).
- Mengubah pengaturan hanya berlaku untuk saldo yang masuk setelahnya.
- Menyalakan kedaluwarsa pertama kali memberi saldo lama masa berlaku penuh: tanggal
hangus kedua berikutnya (` FIXED_DATE`) atau satu periode penuh (` ROLLING`).
- Mematikan kedaluwarsa tidak membatalkan tanggal yang sudah terjadwal.
Riwayat perubahan: ` GET /api/v1/marketing/loyalty-settings/history?page=1&limit=20`
(tambah ` outlet_id=` untuk setting outlet).
### 9.3 Wallet customer
- ` GET /api/v1/marketing/customers/:id/wallet` — saldo buku dan saldo yang bisa
dipakai, semua lot yang masih berisi, dan riwayat dengan nama asli (lawan transfer,
admin, kasir, outlet). Query riwayat sama seperti §2.2.
- ` POST /api/v1/marketing/customers/:id/wallet/adjust`
` ``json
{ "currency": "POINT", "amount": -500, "reason": "Komplain #45", "idempotency_key": "adj-45" }
` ``
` amount` bertanda. ` reason` wajib. Pengurangan yang melebihi saldo ditolak.
Adjustment tidak disertai pembayaran uang, jadi jangan pakai alasan "pencairan".
- ` GET /api/v1/marketing/wallet-transactions/:id/trace` — telusuri satu mutasi per
2026-10-07 13:48:29 +07:00
butir: lot mana yang dipakai atau dibuat, lalu rantai asalnya lewat transfer atau
exchange sampai ke earning/adjustment/migrasi pertama. Contoh: dari transfer keluar
B bisa terlihat bahwa EnakPoint-nya berasal dari order #ORD-1 milik A yang
mentransfer ke B.
2026-09-30 15:31:44 +07:00
### 9.4 PIN customer
- ` DELETE /api/v1/marketing/customers/:id/pin` dengan ` { "reason": "…" }` — hapus PIN
bila customer kehilangan akses. Customer lalu membuat PIN baru lewat OTP. Admin
**tidak bisa** membuat, mengganti, atau melihat PIN.
- ` GET /api/v1/marketing/customers/:id/security-events?page=1&limit=20` — log keamanan:
` PIN_SET`, ` PIN_CHANGED`, ` PIN_RESET`, ` PIN_FAILED`, ` PIN_LOCKED`,
` PIN_REMOVED_BY_ADMIN`, beserta waktu, IP, dan perangkat.
---
## 10. Checklist integrasi
**Customer app**
- [ ] Daftarkan token FCM setelah login dan saat token berganti; hapus saat logout.
- [ ] Tangani empat kode error PIN (§3.4) di semua layar yang meminta PIN.
- [ ] Kirim ` Idempotency-Key` baru untuk setiap exchange dan transfer, dan pakai ulang
key yang sama saat retry.
- [ ] Tampilkan nilai rupiah sebagai "setara potongan", bukan saldo uang.
- [ ] Baca ` coins_used` / ` coins_remaining` dan ` /customer/wallet`, bukan field lama.
**POS**
2026-10-07 13:48:29 +07:00
- [ ] Cetak ` points_earned` dan ` coins_earned` di struk.
- [ ] Jangan menampilkan EnakPoint sebagai payment method (§4).
2026-09-30 15:31:44 +07:00
**Dashboard**
- [ ] Tampilkan ` point_cashback_percent`, ` impact`, ` expiry_preview`, dan
` expiry_activations` sebelum owner menyimpan setting.
- [ ] Isi ` metadata.coin_cost` untuk setiap game.