Files
apskel-pos-backend/docs/integration-enakpoint.md
T
2026-09-30 15:31:44 +07:00

24 KiB
Raw Blame History

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.


1. Konsep inti

EnakPoint (POINT) EnakCoin (COIN)
Didapat dari Order lunas (per outlet), adjustment admin, exchange Order lunas (per outlet), adjustment admin
Dipakai untuk 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:

  1. Semua jumlah bilangan bulat. Tidak ada "setengah EnakPoint".
  2. Saldo tidak pernah jadi uang. Tidak ada pencairan, tidak ada kembalian, dan bagian order yang dibayar EnakPoint hanya bisa kembali sebagai EnakPoint. Tampilkan nilai rupiahnya sebagai "setara potongan Rp …", bukan "saldo Rp …".
  3. Semua aksi customer yang memindahkan saldo butuh PIN 6 digit (§3): bayar, buat kode bayar, exchange, transfer. Main game tidak butuh PIN.
  4. 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 dan penerimaan pembayaran per outlet.
  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:

{ "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_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

GET /api/v1/customer/wallet/transactions?page=1&limit=20&currency=POINT&type=EARN,PAYMENT&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 }
}
  • amount bertanda: positif menambah saldo, negatif mengurangi.
  • Penambahan punya source, pengurangan punya destination. Keduanya berbentuk { type, id } dan menunjuk hal yang bisa dibuka di detail (order, pembayaran, game play, dst.).
  • 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
PAYMENT − Membayar order PAYMENT
PAYMENT_REFUND + Kembali karena pembayaran di-void/refund PAYMENT
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

  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; pembayaran dan 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. Membayar dengan EnakPoint

Ada dua jalur. Keduanya memakai logika perhitungan yang sama.

4.1 Batas pembayaran

EnakPoint maksimal yang bisa dipakai untuk satu order:

batas_rupiah = min(sisa_tagihan, total_order × max_payment_percent / 100 − yang_sudah_dibayar_EnakPoint)
maks_point   = min(saldo_customer, floor(batas_rupiah / point_value))

Ditambah minimal min_payment_points per pembayaran. Nominal rupiah pembayaran selalu points × point_value dan tidak pernah melebihi sisa tagihan, jadi tidak ada kembalian. Sisa tagihan dibayar dengan method lain seperti biasa (split).

4.2 POS — kode bayar dari aplikasi customer

PIN tidak pernah diketik di perangkat kasir. Customer menyetujui di HP-nya sendiri:

  1. Customer app: POST /api/v1/customer/wallet/payment-code dengan { "pin": "482913" }.

    { "code": "482913", "qr_payload": "enakpoint:482913", "expires_at": "2026-09-30T05:02:00Z" }
    

    Tampilkan code sebagai angka dan qr_payload sebagai QR. Kode berlaku 2 menit, sekali pakai, dan hanya untuk customer itu. Membuat kode baru membatalkan kode lama.

  2. POS: tampilkan batas untuk tombol "pakai maksimal":

    GET /api/v1/orders/:id/point-payment/preview

    {
      "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.).

  3. POS: bayar lewat endpoint pembayaran yang sudah ada, dengan payment method bertipe point:

    POST /api/v1/payments (header X-Idempotency-Key wajib seperti pembayaran lain)

    {
      "order_id": "…",
      "payment_method_id": "<id method EnakPoint>",
      "points": 12500,
      "payment_code": "482913"
    }
    

    amount tidak perlu dikirim; backend menghitungnya. payment_code boleh berupa angka yang diketik kasir atau hasil scan QR apa adanya (enakpoint:482913).

Response pembayaran membawa points_used dan point_value untuk struk, misalnya "EnakPoint: 12.500 (Rp 12.500)". Jika pembayaran ini melunasi order, order menjadi completed; jika belum, sisanya dibayar dengan method lain.

Pembayaran ditolak (304, cause menjelaskan) bila: order tanpa customer atau customer walk-in, customer nonaktif, outlet tidak menerima EnakPoint, points di luar batas §4.1, kode salah/kedaluwarsa/sudah dipakai/milik customer lain, atau method EnakPoint dipakai sebagai split (bayar bagian EnakPoint sebagai pembayaran tersendiri, lalu split sisanya seperti biasa). Kode bayar dipakai habis begitu diterima, sebelum batas dicek ulang; bila pembayaran lalu ditolak (misalnya saldo berubah), minta customer membuat kode baru.

Method EnakPoint dibuat otomatis untuk setiap organisasi dan tidak bisa dihapus atau diubah tipenya (namanya boleh diganti). Daftar payment method yang dikirim ?outlet_id= tidak menampilkannya bila outlet itu tidak menerima EnakPoint.

4.3 Customer app / self-order — bayar order sendiri

POST /api/v1/customer/orders/:id/pay-with-points

{ "points": 12500, "pin": "482913" }

Hanya untuk order milik customer yang login; order lain dijawab 404. Response sama dengan response pembayaran di §4.2.

4.4 Void dan refund

  • Void order: semua EnakPoint yang dipakai kembali ke customer sebagai EnakPoint.
  • Refund pembayaran EnakPoint (POST /api/v1/payments/:id/refund pada pembayaran EnakPoint): yang kembali floor(rupiah_direfund / point_value_saat_bayar). Perubahan nilai EnakPoint setelah pembayaran tidak mengubah jumlah yang kembali; sisa di bawah 1 EnakPoint hangus.
  • Refund order ke tunai / method lain hanya boleh sebesar bagian yang dibayar dengan method lain. Bagian EnakPoint harus direfund lewat pembayaran EnakPoint-nya sendiri; mencoba lewat tunai dijawab 304.
  • EnakPoint yang kembali mengikuti tanggal kedaluwarsa asalnya, tapi minimal 7 hari sejak refund.
  • 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.

4.5 Earning di layar order dan struk

Response order membawa points_earned dan coins_earned (0 bila order tidak menghasilkan apa-apa). Earning dihitung dari subtotal − discount − bagian yang dibayar EnakPoint, sebelum pajak, dan diberikan saat order lunas.


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

    { "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)

    { "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-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

    { "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)

    { "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: 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:

{
  "game_play": { "id": "…", "game_id": "…", "coins_used": 1, "token_used": 1, "created_at": "…" },
  "prize_won": { "id": "…", "name": "Voucher 10rb", … },
  "coins_remaining": 7,
  "tokens_remaining": 7
}

EnakCoin kurang, game nonaktif, atau hadiah baru saja habis dijawab 304; tidak ada EnakCoin yang terpotong. Baca coins_used dan coins_remaining; token_used dan tokens_remaining hanya salinan untuk versi aplikasi lama.

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)
GET /customer/tokens GET /customer/wallet (coin_balance)
total_points, total_tokens, points_history, tokens_history, last_updated di /customer/wallet point_balance, coin_balance, recent_transactions
token_used, tokens_remaining di respons game coins_used, coins_remaining
sort_by=token_used di daftar game play sort_by=coins_used

Beri tahu tim backend setelah aplikasi yang beredar tidak lagi memakai kolom kiri, supaya alias dan tabel lama (customer_points, customer_tokens) bisa dihapus.


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_per_amount": 100, "earn_value": 1, "min_order_amount": 0, "max_per_order": null },
  "coin": { "enabled": true, "earn_per_amount": 25000, "earn_value": 1, "min_order_amount": 0, "max_per_order": null },
  "point_payment": { "accept_payment": true, "min_payment_points": 1, "max_payment_percent": 100 }
}

Field yang tidak dikirim di PUT tetap memakai nilai sekarang. Response menambahkan point_value organisasi dan point_cashback_percent (earn_value × point_value / earn_per_amount × 100). 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 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

    { "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 butir: lot mana yang dipakai atau dibuat, lalu rantai asalnya lewat transfer, exchange, atau refund sampai ke earning/adjustment/migrasi pertama. Contoh: dari pembayaran 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/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

  • Scan QR atau ketik kode bayar, jangan pernah meminta PIN customer di layar kasir.
  • Pakai point-payment/preview untuk tombol "pakai maksimal".
  • Cetak points_used, points_earned, dan coins_earned di struk.
  • Refund bagian EnakPoint lewat pembayaran EnakPoint-nya, bukan tunai.

Dashboard

  • Tampilkan point_cashback_percent, impact, expiry_preview, dan expiry_activations sebelum owner menyimpan setting.
  • Isi metadata.coin_cost untuk setiap game.