EnakPoint can only be redeemed for vouchers now: it can no longer pay for orders and is never cashed out (docs/enakgame-prd.md §3.2, EG-001, EG-002). No order was ever paid with EnakPoint, so there is no data to move. Removed: - POST /customer/wallet/payment-code, POST /customer/orders/:id/pay-with-points and GET /orders/:id/point-payment/preview, with their processors, repositories, services, handlers and tests. - The point payment method type: paying, splitting and refunding with it, the outlet filter on the method list, and the system-method guard. - points and payment_code on CreatePayment; points_used and point_value on payments; accepts_point_payment on the customer outlets. - The outlet point_payment settings. A PUT that still sends them is rejected as an unknown field. - The EnakPoint split in the payment method analytics. - PAYMENT and PAYMENT_REFUND from the wallet type rules. Tests that used them as a generic EnakPoint debit use REWARD_REDEEM. - The EnakPoint-paid part from the earning basis, which is subtotal − discount again. Migration 000102 drops the trigger, the point methods and their index, the payments columns, and the outlet settings, and restores the method type CHECK without point. payments.payment_method_id is ON DELETE RESTRICT, so it fails rather than lose a payment made with EnakPoint. The integration docs list the removed endpoints and fields, and the EnakPoint & EnakCoin PRD and tasks note what is superseded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
18 KiB
API EnakPoint & EnakCoin
30 Sep 2026
Semua endpoint EnakPoint (POINT, hanya untuk ditukar ke voucher) 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.
Perubahan 7 Okt 2026: bayar order dengan EnakPoint sudah dihapus, karena EnakPoint sekarang hanya bisa ditukar ke voucher: tidak bisa dipakai sebagai alat bayar dan tidak bisa dicairkan (
enakgame-prd.md§3.2). Endpoint dan field yang ikut dihapus ada di Referensi → Endpoint dan field yang dihapus.
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.
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 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/EnakCoin yang didapat; 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 §4.4–§4.5.
GET /customer/wallet
{
"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/.coinbernilainullbila 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,TRANSFER_IN |
from, to |
YYYY-MM-DD |
Tanggal WIB, inklusif |
{
"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
{
"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
{ "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 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 |
- Buat PIN: minta OTP dengan
purpose: "pin_setup"(dikirim lewat WhatsApp), laluPOST /customer/pindenganotp_tokendari response OTP dan kode yang diterima customer. - Lupa PIN: minta OTP dengan
purpose: "pin_reset", laluPOST /customer/pin/reset. Reset membuka kunci PIN, tapi transfer keluar ditahan 24 jam; exchange tetap bisa. - Ganti PIN:
PUT /customer/pindengan 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: exchange, transfer, game
| Method | Path | PIN | Idempotency-Key |
|---|---|---|---|
| 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 |
– | – |
GET /customer/wallet/exchange/preview?coins=30
{ "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:
{
"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
{ "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:
{
"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).
{
"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: earning, void, dan refund
EnakPoint bukan payment method: tidak ada lagi tipe point, dan POST /payments memakai amount seperti pembayaran lain. Response order membawa points_earned dan coins_earned untuk struk. Saat order di-void atau direfund, EnakPoint dan EnakCoin yang didapat dari order itu ikut ditarik (EARN_REVERSAL); 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.
| Method | Path | Keterangan |
|---|---|---|
| GET, PUT | /outlets/:outlet_id/loyalty-settings |
Earning EnakPoint dan EnakCoin 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
{
"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 }
}
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. Objek point_payment sudah dihapus; PUT yang masih mengirimnya ditolak 310 (field tidak dikenal).
/marketing/loyalty-settings
{
"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 perubahanpoint_valueatau 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.changesdandry_run.
POST /marketing/customers/:id/wallet/adjust
{ "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
{
"transaction": { "id": "…", "customer": { "id": "…", "name": "Budi Santoso" }, "type": "TRANSFER_OUT", "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 atau exchange 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 |
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 |
Endpoint dan field yang dihapus
Bayar dengan EnakPoint dihapus pada 7 Okt 2026 karena EnakPoint sekarang hanya untuk voucher (enakgame-prd.md §3.2). Tidak ada penggantinya; jangan dipanggil lagi.
| Dihapus | Catatan |
|---|---|
POST /customer/wallet/payment-code |
Kode bayar untuk kasir |
POST /customer/orders/:id/pay-with-points |
Bayar order dari app / self-order |
GET /orders/:id/point-payment/preview |
Batas pembayaran EnakPoint di POS |
Payment method tipe point; field points dan payment_code di POST /payments |
amount kembali wajib seperti pembayaran lain |
points_used, point_value di response pembayaran dan di payments pada GET /customer/orders/:id |
– |
accepts_point_payment di GET /customer/outlets |
– |
point_payment (accept_payment, min_payment_points, max_payment_percent) di /outlets/:outlet_id/loyalty-settings |
PUT yang masih mengirimnya ditolak 310 |
summary.point_amount, summary.points_used, summary.total_with_points, serta points_used dan counts_as_cash_in per baris di analytics payment method |
summary.total_amount kembali total semua method; persentase dihitung dari total itu |
Tipe mutasi PAYMENT dan PAYMENT_REFUND |
Tidak ditulis lagi |
Panduan alur lengkap per tim ada di integration-enakpoint.md.