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>
20 KiB
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.
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§3.2). Endpoint dan field yang ikut dihapus ada di §8.
1. Konsep inti
EnakPoint (POINT) |
EnakCoin (COIN) |
|
|---|---|---|
| Didapat dari | Order lunas (per outlet), adjustment admin, exchange | Order lunas (per outlet), adjustment admin |
| Dipakai untuk | Ditukar ke voucher (tidak bisa membayar order) | Main game, ditukar ke EnakPoint |
| Bisa ditransfer | Ya | Ya |
| Bisa kedaluwarsa | Ya, bila diaktifkan owner | Ya, bila diaktifkan owner |
Aturan yang berlaku di seluruh dokumen ini:
- Semua jumlah bilangan bulat. Tidak ada "setengah EnakPoint".
- 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 …".
- Semua aksi customer yang memindahkan saldo butuh PIN 6 digit (§3): exchange dan transfer. Main game tidak butuh PIN.
- Wallet milik customer di satu organisasi. Saldo berlaku di semua outlet organisasi itu. Nilai rupiah EnakPoint, kurs exchange, batas transfer, dan kedaluwarsa diatur per organisasi; earning per outlet.
- 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:
{ "success": true, "data": { … }, "errors": null }
{
"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
{
"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_balancedancoin_balanceadalah saldo yang bisa dipakai sekarang.point_discount_value=point_balance × point_value. Tampilkan sebagai "setara potongan Rp 12.500".nearest_expiringbernilainullper currency bila tidak ada yang akan kedaluwarsa.recent_transactionsberisi 5 mutasi terakhir dengan bentuk yang sama seperti §2.2.
2.2 Riwayat
GET /api/v1/customer/wallet/transactions?page=1&limit=20¤cy=POINT&type=EARN,TRANSFER_IN&from=2026-09-01&to=2026-09-30
Semua query opsional. limit 1–100 (default 20). type boleh beberapa, dipisah koma.
from / to 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": "…",
"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 }
}
amountbertanda: positif menambah saldo, negatif mengurangi.- Penambahan punya
source, pengurangan punyadestination. Keduanya berbentuk{ type, id }dan menunjuk hal yang bisa dibuka di detail (order, game play, dst.). descriptionsudah siap tampil dan tidak berubah walau nama outlet atau customer berubah belakangan. Nama lawan transfer sudah disamarkan.- Dua baris exchange atau transfer berbagi
group_idyang 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
{
"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
{ "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
{ "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
POST /api/v1/customer/pin/otpdengan{ "purpose": "pin_setup" }. OTP dikirim ke nomor customer lewat WhatsApp. Response:{ "purpose", "otp_token", "expires_at" }.POST /api/v1/customer/pindengan{ "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/pindengan{ "old_pin", "pin", "confirm_pin" }. - Lupa PIN: minta OTP dengan
purpose: "pin_reset", laluPOST /api/v1/customer/pin/resetdengan body yang sama seperti §3.2. Reset juga membuka PIN yang terkunci. Setelah reset, transfer keluar ditahan 24 jam; exchange tetap bisa.
3.4 Menangani error PIN
Setiap endpoint yang menerima pin bisa mengembalikan error PIN. Pada error ini data
tidak null:
{
"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.
4. Earning, void, dan refund
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).
Response order membawa points_earned dan coins_earned (0 bila order tidak
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.
5. Exchange EnakCoin → EnakPoint
Kurs per organisasi: coin_amount EnakCoin = point_amount EnakPoint (default 1 : 1).
-
Preview sebelum minta PIN:
GET /api/v1/customer/wallet/exchange/preview?coins=30{ "coin_amount": 10, "point_amount": 3, "coin_balance": 35, "coins": 30, "points": 9, "valid": true }Bila
valid: false, tampilkanreason(misalnya harus kelipatancoin_amount, atau EnakCoin tidak cukup). -
Tukar:
POST /api/v1/customer/wallet/exchangedengan headerIdempotency-Key(wajib, maks. 50 karakter, satu key per percobaan tukar){ "coins": 30, "pin": "482913" }{ "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-Keyyang sama bila koneksi putus: hasil pertama dikembalikan denganreplayed: truetanpa menukar lagi, dengan kurs saat itu.Idempotency-Keyyang sama untuk jumlah berbeda ditolak. - EnakPoint hasil tukar tidak bisa hidup lebih lama dari EnakCoin asalnya (
lotsmenunjukkan tanggalnya).
6. Transfer ke customer lain
-
Cek penerima sebelum konfirmasi:
GET /api/v1/customer/wallet/transfer/recipient?phone=081234561234{ "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 dijawab304. -
Kirim:
POST /api/v1/customer/wallet/transferdengan headerIdempotency-Key(wajib){ "currency": "POINT", "amount": 120, "recipient_phone": "081234561234", "pin": "482913" }{ "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:POINTatauCOIN, 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
304sebelum 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-Keyyang 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:
{
"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 dijawab 304; tidak ada
EnakCoin yang terpotong.
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) |
total_points, points_history, last_updated di /customer/wallet |
point_balance, recent_transactions |
Beri tahu tim backend setelah aplikasi yang beredar tidak lagi memakai kolom kiri, 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.
Bayar dengan EnakPoint juga sudah dihapus (7 Okt 2026, migrasi 000102) karena
EnakPoint sekarang hanya untuk voucher (enakgame-prd.md §3.2).
Tidak ada penggantinya:
- Endpoint
POST /customer/wallet/payment-code,POST /customer/orders/:id/pay-with-points, danGET /orders/:id/point-payment/preview. - Payment method tipe
point, serta fieldpointsdanpayment_codediPOST /payments;amountkembali wajib seperti pembayaran lain. points_useddanpoint_valuedi response pembayaran dan dipaymentspadaGET /customer/orders/:id;accepts_point_paymentdiGET /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_pointsdisummary, sertapoints_useddancounts_as_cash_inper baris.summary.total_amountkembali total semua method, dan persentase dihitung dari total itu. - Tipe mutasi
PAYMENTdanPAYMENT_REFUNDtidak ditulis lagi.
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
{
"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 }
}
Field yang tidak dikirim di PUT tetap memakai nilai sekarang. PUT yang masih
mengirim point_payment ditolak 310 (field tidak dikenal). Response menambahkan
point_value organisasi dan point_cashback_percent
(earn_value × point_value / earn_per_amount × 100, atau earn_percent × point_value
pada earn_mode PERCENTAGE). Tampilkan persentase ini di
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)
{
"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 … }
}
Field yang tidak dikirim di PUT tetap memakai nilai sekarang. Response menambahkan:
impact: total saldo beredar dan nilai rupiahnya sebelum dan sesudah perubahanpoint_valueatau kurs. Tampilkan sebagai peringatan sebelum owner menyimpan.expiry_preview:{ "point": …, "coin": … }, kapan saldo yang didapat hari ini akan kedaluwarsa (nullbila 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 dengandry_run=truedulu.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_daysberlaku 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{ "currency": "POINT", "amount": -500, "reason": "Komplain #45", "idempotency_key": "adj-45" }amountbertanda.reasonwajib. 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 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.
9.4 PIN customer
DELETE /api/v1/marketing/customers/:id/pindengan{ "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-Keybaru 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_remainingdan/customer/wallet, bukan field lama.
POS
- Cetak
points_earneddancoins_earneddi struk. - Jangan menampilkan EnakPoint sebagai payment method (§4).
Dashboard
- Tampilkan
point_cashback_percent,impact,expiry_preview, danexpiry_activationssebelum owner menyimpan setting. - Isi
metadata.coin_costuntuk setiap game.