442 lines
20 KiB
Markdown
442 lines
20 KiB
Markdown
# API EnakPoint & EnakCoin
|
||
|
||
30 Sep 2026
|
||
|
||
Semua endpoint EnakPoint (`POINT`, bisa bayar order) dan EnakCoin (`COIN`, untuk game dan ditukar ke EnakPoint) ada di bawah base URL `/api/v1`, memakai satu format response, dan semua jumlah berupa bilangan bulat.
|
||
|
||
## Konvensi umum
|
||
|
||
| Klien | Autentikasi | Prefix |
|
||
| --- | --- | --- |
|
||
| Customer app / self-order | `Authorization: Bearer <token customer>` | `/api/v1/customer` |
|
||
| POS | Token user (kasir/manager) | `/api/v1` |
|
||
| Dashboard | Token user, role Admin atau Manager | `/api/v1/marketing`, `/api/v1/outlets` |
|
||
|
||
**Format response.** Sukses: `{"success": true, "data": {…}, "errors": null}`. Gagal: `{"success": false, "data": null, "errors": [{"code": "304", "entity": "wallet_service", "cause": "…"}]}`. Tampilkan `cause` sebagai alasan penolakan.
|
||
|
||
| `code` | HTTP | Arti |
|
||
| --- | --- | --- |
|
||
| `303`, `310` | 400 | Body atau parameter tidak lengkap / salah format |
|
||
| `304` | 400 | Ditolak aturan bisnis (saldo kurang, di luar batas, dst.) |
|
||
| `404` | 404 | Tidak ditemukan, juga untuk data milik customer atau organisasi lain |
|
||
| `429` | 429 | OTP diminta ulang terlalu cepat |
|
||
| `PIN_NOT_SET` | 403 | Customer belum membuat PIN |
|
||
| `PIN_INVALID` | 400 | PIN salah |
|
||
| `PIN_LOCKED` | 423 | PIN terkunci 30 menit setelah 5 kali salah |
|
||
| `TRANSFER_BLOCKED` | 403 | Transfer ditahan 24 jam setelah reset PIN |
|
||
| `900` | 500 | Kesalahan server |
|
||
|
||
**Error PIN** membawa `data` yang tidak `null`: `{"code": "PIN_INVALID", "remaining_attempts": 3}`, `{"code": "PIN_LOCKED", "locked_until": "…"}`, atau `{"code": "TRANSFER_BLOCKED", "transfer_blocked_until": "…"}`. Endpoint yang menerima `pin` bisa mengembalikan salah satunya. PIN selalu dikirim sebagai string 6 digit.
|
||
|
||
**Idempotency.** Exchange dan transfer wajib header `Idempotency-Key` (maks. 50 karakter, `X-Idempotency-Key` juga diterima): satu key per percobaan, dan key yang sama dipakai ulang saat retry. Retry mengembalikan hasil pertama dengan `replayed: true`. `POST /payments` wajib `X-Idempotency-Key` seperti pembayaran lain.
|
||
|
||
**Waktu.** Tanggal kedaluwarsa dan filter tanggal memakai WIB. Saldo berlaku sampai 23:59:59 WIB pada tanggal kedaluwarsanya.
|
||
|
||
## Customer app: saldo & riwayat
|
||
|
||
| Method | Path | Keterangan |
|
||
| --- | --- | --- |
|
||
| GET | `/customer/wallet` | Saldo, nilai rupiah, kedaluwarsa terdekat, 5 mutasi terakhir |
|
||
| GET | `/customer/wallet/transactions` | Riwayat mutasi, dengan pagination dan filter |
|
||
| 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
|
||
|
||
```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 seperti item riwayat …" ]
|
||
}
|
||
```
|
||
|
||
- `point_balance` / `coin_balance` = saldo yang bisa dipakai sekarang.
|
||
- `point_discount_value` = `point_balance × point_value`; tampilkan sebagai "setara potongan Rp …", bukan saldo uang.
|
||
- `nearest_expiring.point` / `.coin` bernilai `null` bila tidak ada yang akan kedaluwarsa.
|
||
|
||
### GET /customer/wallet/transactions
|
||
|
||
| Query | Tipe | Keterangan |
|
||
| --- | --- | --- |
|
||
| `page` | int | Default 1 |
|
||
| `limit` | int | 1–100, default 20 |
|
||
| `currency` | `POINT` \| `COIN` | Opsional |
|
||
| `type` | string | Satu tipe atau beberapa dipisah koma, mis. `EARN,PAYMENT` |
|
||
| `from`, `to` | `YYYY-MM-DD` | 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 }
|
||
}
|
||
```
|
||
|
||
`amount` bertanda (+ menambah, − mengurangi). Penambahan membawa `source`, pengurangan membawa `destination`, keduanya `{ type, id }`. Dua baris exchange atau transfer berbagi `group_id`. Daftar tipe ada di bagian Referensi.
|
||
|
||
### GET /customer/wallet/expiring
|
||
|
||
```json
|
||
{
|
||
"point": [
|
||
{ "amount": 150, "date": "2026-10-31" },
|
||
{ "amount": 200, "date": "2026-12-31" }
|
||
],
|
||
"coin": []
|
||
}
|
||
```
|
||
|
||
Terurut dari tanggal terdekat. Daftar kosong berarti tidak ada yang akan kedaluwarsa.
|
||
|
||
### PUT /customer/devices
|
||
|
||
```json
|
||
{ "device_id": "a1b2c3", "fcm_token": "…", "platform": "android", "app_version": "2.4.0" }
|
||
```
|
||
|
||
Panggil setelah login dan setiap kali FCM memberi token baru. `device_id` dan `fcm_token` wajib; `platform` = `android` | `ios` | `web`. Satu token hanya milik satu customer: customer lain yang mendaftarkan token yang sama mengambil alih HP itu. Response: `{ "device_id": "a1b2c3" }`.
|
||
|
||
## Customer app: PIN
|
||
|
||
PIN 6 digit wajib untuk bayar, kode bayar, exchange, dan transfer; minta customer membuatnya saat pertama kali melakukan aksi itu.
|
||
|
||
| Method | Path | Body | Response |
|
||
| --- | --- | --- | --- |
|
||
| GET | `/customer/pin/status` | – | `{ "has_pin", "locked_until", "transfer_blocked_until" }` |
|
||
| POST | `/customer/pin/otp` | `{ "purpose": "pin_setup" }` atau `"pin_reset"` | `{ "purpose", "otp_token", "expires_at" }` |
|
||
| POST | `/customer/pin` | `{ "otp_token", "otp_code", "pin", "confirm_pin" }` | Status PIN |
|
||
| PUT | `/customer/pin` | `{ "old_pin", "pin", "confirm_pin" }` | Status PIN |
|
||
| POST | `/customer/pin/reset` | `{ "otp_token", "otp_code", "pin", "confirm_pin" }` | Status PIN |
|
||
|
||
1. **Buat PIN:** minta OTP dengan `purpose: "pin_setup"` (dikirim lewat WhatsApp), lalu `POST /customer/pin` dengan `otp_token` dari response OTP dan kode yang diterima customer.
|
||
2. **Lupa PIN:** minta OTP dengan `purpose: "pin_reset"`, lalu `POST /customer/pin/reset`. Reset membuka kunci PIN, tapi transfer keluar ditahan 24 jam; pembayaran dan exchange tetap bisa.
|
||
3. **Ganti PIN:** `PUT /customer/pin` dengan PIN lama.
|
||
|
||
PIN baru ditolak `304` bila bukan 6 digit, konfirmasinya beda, semua digit sama (`111111`), berurutan (`123456`, `654321`), atau sama dengan tanggal lahir (`DDMMYY` / `YYMMDD`). OTP yang diminta terlalu cepat dijawab `429`. Penanganan `PIN_INVALID`, `PIN_LOCKED`, dan `TRANSFER_BLOCKED` ada di Konvensi umum.
|
||
|
||
## Customer app: bayar, exchange, transfer, game
|
||
|
||
| Method | Path | PIN | Idempotency-Key |
|
||
| --- | --- | --- | --- |
|
||
| POST | `/customer/wallet/payment-code` | Ya | – |
|
||
| POST | `/customer/orders/:id/pay-with-points` | Ya | – |
|
||
| GET | `/customer/wallet/exchange/preview?coins=` | – | – |
|
||
| POST | `/customer/wallet/exchange` | Ya | Wajib |
|
||
| GET | `/customer/wallet/transfer/recipient?phone=` | – | – |
|
||
| POST | `/customer/wallet/transfer` | Ya | Wajib |
|
||
| POST | `/customer/spin` | – | – |
|
||
|
||
### POST /customer/wallet/payment-code
|
||
|
||
Body `{ "pin": "482913" }`. Response:
|
||
|
||
```json
|
||
{ "code": "482913", "qr_payload": "enakpoint:482913", "expires_at": "2026-09-30T05:02:00Z" }
|
||
```
|
||
|
||
Tampilkan `code` sebagai angka dan `qr_payload` sebagai QR untuk kasir. Berlaku 2 menit, sekali pakai, hanya untuk customer ini; kode baru membatalkan kode lama.
|
||
|
||
### POST /customer/orders/:id/pay-with-points
|
||
|
||
Body `{ "points": 12500, "pin": "482913" }`. Hanya untuk order milik customer yang login (order lain `404`). Response sama dengan pembayaran POS (bagian POS). Batas dan aturan penolakan juga sama.
|
||
|
||
### GET /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 (default 1 : 1). Bila `valid: false`, tampilkan `reason`.
|
||
|
||
### POST /customer/wallet/exchange
|
||
|
||
Body `{ "coins": 30, "pin": "482913" }`. Response:
|
||
|
||
```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
|
||
}
|
||
```
|
||
|
||
`coins` harus kelipatan `coin_amount`; jumlah yang salah ditolak `304` sebelum PIN dicek. Exchange tidak bisa dibatalkan. EnakPoint hasil tukar tidak bisa hidup lebih lama dari EnakCoin asalnya (lihat `lots`).
|
||
|
||
### GET /customer/wallet/transfer/recipient?phone=081234561234
|
||
|
||
```json
|
||
{ "name": "Bu*** Sa***", "phone_number": "08**-****-1234" }
|
||
```
|
||
|
||
Nomor di luar organisasi atau tidak terdaftar → `404`. Diri sendiri, customer walk-in, atau nonaktif → `304`.
|
||
|
||
### POST /customer/wallet/transfer
|
||
|
||
Body `{ "currency": "POINT", "amount": 120, "recipient_phone": "081234561234", "pin": "482913" }`. Response:
|
||
|
||
```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`. Batas organisasi (transfer aktif, minimal, maksimal per transaksi, batas harian per currency yang reset tengah malam WIB) ditolak `304` sebelum PIN dicek. Transfer final. Saldo membawa tanggal kedaluwarsa aslinya ke penerima (`lots`), dan penerima mendapat push `WALLET_TRANSFER_IN`.
|
||
|
||
### POST /customer/spin
|
||
|
||
Body `{ "spin_id": "<id game>" }`. Memotong EnakCoin sebesar `metadata.coin_cost` game itu (default 1).
|
||
|
||
```json
|
||
{
|
||
"game_play": { "id": "…", "game_id": "…", "coins_used": 1, "created_at": "…" },
|
||
"prize_won": { "id": "…", "name": "Voucher 10rb" },
|
||
"coins_remaining": 7
|
||
}
|
||
```
|
||
|
||
EnakCoin kurang, game nonaktif, atau hadiah baru saja habis → `304`, tidak ada EnakCoin yang terpotong.
|
||
|
||
## POS: pembayaran EnakPoint
|
||
|
||
Kasir memakai endpoint pembayaran yang sudah ada dengan payment method bertipe `point`, disetujui customer lewat kode bayar dari aplikasinya; PIN tidak pernah diketik di perangkat kasir.
|
||
|
||
| Method | Path | Keterangan |
|
||
| --- | --- | --- |
|
||
| GET | `/orders/:id/point-payment/preview` | Batas pembayaran EnakPoint untuk order ini |
|
||
| POST | `/payments` | Bayar dengan method EnakPoint (`points` + `payment_code`) |
|
||
| POST | `/payments/:id/refund` | Refund pembayaran EnakPoint, kembali sebagai EnakPoint |
|
||
|
||
1. Customer membuat kode di aplikasi (`POST /customer/wallet/payment-code`) dan menunjukkan angka atau QR-nya.
|
||
2. POS memanggil preview untuk tombol "pakai maksimal".
|
||
3. POS memanggil `POST /payments` dengan kode tersebut. Sisa tagihan dibayar dengan method lain seperti biasa.
|
||
|
||
### GET /orders/:id/point-payment/preview
|
||
|
||
```json
|
||
{
|
||
"order_id": "…",
|
||
"customer_id": "…",
|
||
"eligible": true,
|
||
"point_balance": 12500,
|
||
"point_value": 1,
|
||
"remaining_amount": 87500,
|
||
"min_payment_points": 1,
|
||
"max_payment_percent": 100,
|
||
"max_points": 12500,
|
||
"max_amount": 12500
|
||
}
|
||
```
|
||
|
||
Bila `eligible: false`, `reason` menjelaskan kenapa (order walk-in, outlet tidak menerima EnakPoint, saldo di bawah minimal, dst.). Batas yang dipakai:
|
||
|
||
```
|
||
batas_rupiah = min(sisa_tagihan, total × max_payment_percent / 100 − sudah_dibayar_EnakPoint)
|
||
maks_point = min(saldo, floor(batas_rupiah / point_value))
|
||
```
|
||
|
||
### POST /payments
|
||
|
||
Header `X-Idempotency-Key` wajib.
|
||
|
||
```json
|
||
{
|
||
"order_id": "…",
|
||
"payment_method_id": "<id method EnakPoint>",
|
||
"points": 12500,
|
||
"payment_code": "482913"
|
||
}
|
||
```
|
||
|
||
- `amount` tidak perlu dikirim; backend menghitung `points × point_value` dan tidak pernah melebihi sisa tagihan (tidak ada kembalian).
|
||
- `payment_code` boleh angka yang diketik atau hasil scan QR apa adanya (`enakpoint:482913`).
|
||
- Response pembayaran membawa `points_used` dan `point_value` untuk struk; response order membawa `points_earned` dan `coins_earned`.
|
||
- Ditolak `304` bila: order tanpa customer atau walk-in, customer nonaktif, outlet tidak menerima EnakPoint, `points` di luar batas, kode salah/kedaluwarsa/sudah dipakai/milik customer lain, atau method EnakPoint dipakai sebagai split. Kode terpakai begitu diterima; bila pembayaran lalu ditolak, minta kode baru.
|
||
- Method EnakPoint dibuat otomatis per organisasi, tidak bisa dihapus atau diubah tipenya, dan tidak muncul di daftar method `?outlet_id=` bila outlet tidak menerima EnakPoint.
|
||
|
||
### Void dan refund
|
||
|
||
- **Void order:** semua EnakPoint yang dipakai kembali sebagai EnakPoint.
|
||
- **`POST /payments/:id/refund` pada pembayaran EnakPoint:** kembali `floor(rupiah_direfund / point_value_saat_bayar)`; sisa di bawah 1 EnakPoint hangus.
|
||
- **Refund order ke tunai/method lain** hanya sebesar bagian non-EnakPoint; mencoba merefund bagian EnakPoint secara tunai ditolak `304`.
|
||
- EnakPoint yang kembali memakai tanggal kedaluwarsa asal, minimal 7 hari sejak refund. Earning order ikut ditarik; bila saldo sudah terpakai, ditarik sebanyak yang ada dan refund tetap jalan.
|
||
|
||
## Dashboard
|
||
|
||
Semua endpoint dashboard butuh role Admin atau Manager, dan semuanya dibatasi ke organisasi user yang login. Rincian layar ada di [`backoffice-enakpoint.md`](./backoffice-enakpoint.md).
|
||
|
||
| Method | Path | Keterangan |
|
||
| --- | --- | --- |
|
||
| GET, PUT | `/outlets/:outlet_id/loyalty-settings` | Earning dan penerimaan EnakPoint per outlet |
|
||
| GET, PUT | `/marketing/loyalty-settings` | Nilai EnakPoint, kurs, transfer, kedaluwarsa (`?dry_run=true` untuk preview) |
|
||
| GET | `/marketing/loyalty-settings/history` | Riwayat perubahan setting (`page`, `limit`, `outlet_id`) |
|
||
| GET | `/marketing/customers/:id/wallet` | Saldo, lot aktif, riwayat dengan nama asli |
|
||
| POST | `/marketing/customers/:id/wallet/adjust` | Koreksi saldo manual |
|
||
| GET | `/marketing/wallet-transactions/:id/trace` | Telusuri asal saldo per butir |
|
||
| DELETE | `/marketing/customers/:id/pin` | Hapus PIN customer |
|
||
| GET | `/marketing/customers/:id/security-events` | Log keamanan PIN (`page`, `limit`) |
|
||
|
||
Pada kedua `PUT` setting, field yang tidak dikirim tetap memakai nilai sekarang; field yang tidak dikenal ditolak.
|
||
|
||
### /outlets/:outlet_id/loyalty-settings
|
||
|
||
```json
|
||
{
|
||
"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 },
|
||
"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 },
|
||
"point_payment": { "accept_payment": true, "min_payment_points": 1, "max_payment_percent": 100 }
|
||
}
|
||
```
|
||
|
||
Response menambahkan `outlet_id`, `point_value`, `point_cashback_percent` (default di atas = 1%), dan `changes` pada PUT. `earn_mode` adalah `PER_AMOUNT` (setiap `earn_per_amount` rupiah mendapat `earn_value`) atau `PERCENTAGE` (`earn_percent` persen dari basis). Validasi: `earn_per_amount > 0`, `earn_value ≥ 0`, `earn_percent` 0–100 dengan maks. 2 angka desimal, `max_payment_percent` 0–100.
|
||
|
||
### /marketing/loyalty-settings
|
||
|
||
```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
|
||
},
|
||
"coin_expiry": { "…": "sama dengan point_expiry" }
|
||
}
|
||
```
|
||
|
||
| Field kedaluwarsa | Dipakai mode | Nilai |
|
||
| --- | --- | --- |
|
||
| `mode` | – | `FIXED_DATE` (hangus di tanggal tetap tiap tahun) atau `ROLLING` (umur sejak didapat) |
|
||
| `fixed_dates` | `FIXED_DATE` | `MM-DD`, boleh lebih dari satu; `02-29` ditolak |
|
||
| `grace_months` | `FIXED_DATE` | 0–24; saldo yang didapat kurang dari ini sebelum tanggal hangus ikut ke tanggal berikutnya |
|
||
| `period`, `unit` | `ROLLING` | ≥ 1, `DAY` atau `MONTH` |
|
||
| `end_of_month` | `ROLLING` | Dibulatkan ke akhir bulan |
|
||
| `reminder_days` | keduanya | Hari sebelum hangus untuk pengingat; 0 = tanpa pengingat |
|
||
|
||
Response menambahkan:
|
||
|
||
- `impact`: saldo beredar dan nilai rupiahnya sebelum/sesudah perubahan `point_value` atau kurs.
|
||
- `expiry_preview`: `{ "point", "coin" }`, kapan saldo yang didapat sekarang kedaluwarsa (`null` = tidak).
|
||
- `expiry_activations`: bila perubahan ini menyalakan kedaluwarsa pertama kali, `[{ "currency", "lots", "amount", "expires_at" }]` saldo lama yang ikut diberi tanggal.
|
||
- `changes` dan `dry_run`.
|
||
|
||
### POST /marketing/customers/:id/wallet/adjust
|
||
|
||
```json
|
||
{ "currency": "POINT", "amount": -500, "reason": "Komplain #45", "idempotency_key": "adj-45" }
|
||
```
|
||
|
||
`amount` bertanda dan tidak boleh 0; `reason` wajib. Pengurangan yang melebihi saldo ditolak `304`. Response: `{ "transaction", "spendable_point_balance", "spendable_coin_balance", "replayed" }`.
|
||
|
||
### GET /marketing/wallet-transactions/:id/trace
|
||
|
||
```json
|
||
{
|
||
"transaction": { "id": "…", "customer": { "id": "…", "name": "Budi Santoso" }, "type": "PAYMENT", "amount": -30, "…": "…" },
|
||
"lots": [
|
||
{
|
||
"amount": 30,
|
||
"chain": [
|
||
{ "lot": { "id": "…", "expires_at": "…" }, "source": { "type": "TRANSFER_IN", "customer": { "name": "Budi Santoso" } } },
|
||
{ "lot": { "id": "…", "origin_lot_id": null }, "source": { "type": "EARN", "reference_type": "ORDER", "description": "Belanja #ORD-1", "customer": { "name": "Anita" } } }
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
Pengurangan menampilkan lot yang dipakai; penambahan menampilkan lot yang dibuat. Tiap `chain` mundur lewat transfer, exchange, atau refund sampai lot pertama dari `EARN`, `ADJUSTMENT`, atau `MIGRATION`.
|
||
|
||
### PIN customer
|
||
|
||
`DELETE /marketing/customers/:id/pin` dengan `{ "reason": "…" }` memaksa customer membuat PIN baru lewat OTP; admin tidak bisa membuat, mengganti, atau melihat PIN. `security-events` mengembalikan `PIN_SET`, `PIN_CHANGED`, `PIN_RESET`, `PIN_FAILED`, `PIN_LOCKED`, `PIN_REMOVED_BY_ADMIN` beserta waktu, IP, dan perangkat.
|
||
|
||
## Referensi
|
||
|
||
### Tipe mutasi (`type`)
|
||
|
||
| `type` | Arah | Arti | `source` / `destination` |
|
||
| --- | --- | --- | --- |
|
||
| `EARN` | + | Didapat dari order lunas | `ORDER` |
|
||
| `EARN_REVERSAL` | − | Ditarik karena order di-void/refund | `ORDER` |
|
||
| `PAYMENT` | − | Membayar order (EnakPoint saja) | `PAYMENT` |
|
||
| `PAYMENT_REFUND` | + | Kembali karena pembayaran di-void/refund | `PAYMENT` |
|
||
| `EXCHANGE_OUT` | − | EnakCoin ditukar | `WALLET_TX` (baris `EXCHANGE_IN`) |
|
||
| `EXCHANGE_IN` | + | EnakPoint hasil tukar | `WALLET_TX` (baris `EXCHANGE_OUT`) |
|
||
| `TRANSFER_OUT` | − | Dikirim ke customer lain | `WALLET_TX` (baris `TRANSFER_IN`) |
|
||
| `TRANSFER_IN` | + | Diterima dari customer lain | `WALLET_TX` (baris `TRANSFER_OUT`) |
|
||
| `GAME_SPEND` | − | Main game (EnakCoin saja) | `GAME_PLAY` |
|
||
| `EXPIRE` | − | Hangus karena kedaluwarsa | `LOT` |
|
||
| `ADJUSTMENT` | + / − | Koreksi admin | `USER` |
|
||
| `MIGRATION` | + | Saldo dari sistem lama | `LEGACY_POINTS` / `LEGACY_TOKENS` |
|
||
|
||
### Notifikasi push (FCM)
|
||
|
||
Semua nilai `data` berupa string. Push hanya sampai ke device yang terdaftar lewat `PUT /customer/devices`.
|
||
|
||
| `data.type` | Kapan | Isi `data` lainnya |
|
||
| --- | --- | --- |
|
||
| `WALLET_TRANSFER_IN` | Menerima transfer | `transaction_id`, `group_id`, `currency`, `amount` |
|
||
| `WALLET_EXPIRING` | `reminder_days` hari sebelum hangus, 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) |
|
||
|
||
### Endpoint dan field deprecated
|
||
|
||
Masih jalan dan membaca wallet, tapi akan dihapus setelah semua versi aplikasi pindah. Semua yang bernama token (`/customer/tokens`, `total_tokens`, `tokens_history`, `token_used`, `tokens_remaining`, campaign `TOKENS`) sudah dihapus; pakai `coin_balance`, `coins_used`, `coins_remaining`, dan `COINS`.
|
||
|
||
| Lama | Pengganti |
|
||
| --- | --- |
|
||
| `GET /customer/points` | `GET /customer/wallet` → `point_balance` |
|
||
| `total_points`, `points_history`, `last_updated` di `/customer/wallet` | `point_balance`, `recent_transactions` |
|
||
|
||
Panduan alur lengkap per tim ada di [`integration-enakpoint.md`](./integration-enakpoint.md).
|