Files
apskel-pos-backend/docs/tasks-point-coin.md
2026-10-01 22:26:44 +07:00

21 KiB
Raw Permalink Blame History

Task Breakdown: EnakPoint & EnakCoin

Sumber: PRD EnakPoint & EnakCoin Tanggal: 2026-09-29

Setiap task menyebut bagian PRD yang dikerjakan, lapisan kode yang disentuh, task yang harus selesai lebih dulu, dan kriteria selesai. Ukuran: S ≤ 1 hari, M 2–3 hari, L 4–5 hari.

Konvensi kode mengikuti yang sudah ada: migrations/ (lanjut dari 000089), entities → repository → processor → service → handler / validator → router, dan wiring di internal/app/app.go.


Ringkasan

Fase Task Terblokir oleh catatan PRD
1. Fondasi PC-101 – PC-109 –
2. Earning PC-201 – PC-205 –
3. Pembayaran EnakPoint PC-301 – PC-308 N2 (keuangan), N3 (legal) sebelum rilis
4. Pergerakan saldo PC-401 – PC-404 N3 (legal) sebelum rilis
5. Kedaluwarsa PC-501 – PC-504 N4 (model kedaluwarsa) sebelum dikerjakan
6. Bersih-bersih PC-601 – PC-602 –

Catatan N2 dan N3 hanya memblokir rilis fase 3–4, bukan pengerjaannya. N4 memblokir pengerjaan fase 5, karena rumus kedaluwarsanya belum ditentukan.

PC-101 ─┬─ PC-103 ── PC-104 ─┬─ PC-105 ── PC-106
        │                    ├─ PC-107
        │                    ├─ PC-108
        │                    ├─ PC-203 ── PC-204 ── PC-205
        │                    ├─ PC-305 ── PC-306 / PC-307
        │                    ├─ PC-401 / PC-402 / PC-403 / PC-404
        │                    └─ PC-502 ── PC-503
PC-102 ── PC-109 ─┬─ PC-201 ── PC-202 ── PC-203
                  └─ PC-302
PC-301 ── PC-304 ── PC-305
PC-303 ── PC-305

Fase 1 — Fondasi

Semua fase lain bergantung pada fase ini. PC-104 (wallet engine) adalah inti. Tidak ada kode lain yang boleh mengubah saldo tanpa lewat engine ini.

PC-101 · Migrasi tabel wallet, ledger, dan lot · M

  • PRD: §8 (customer_wallets, wallet_transactions, wallet_lots, wallet_lot_allocations), K5, K9
  • Kerjakan: migrasi 000090_create_wallet_tables (up & down) dengan semua CHECK constraint dan index persis seperti di §8.
  • Selesai jika:
    • Up dan down berjalan bersih di database kosong dan di salinan staging.
    • Uji constraint langsung di SQL, masing-masing harus ditolak: PAYMENT bercurrency COIN; TRANSFER_IN tanpa counterparty_customer_id; EARN_REVERSAL tanpa reverses_transaction_id; ADJUSTMENT tanpa reason; EXPIRE dengan reference_type selain LOT; saldo negatif; lot dengan remaining_amount > original_amount.
  • Bergantung pada: –

PC-102 · Migrasi pengaturan organisasi dan riwayat perubahan setting · S

  • PRD: F2, loyalty_setting_changes di §8
  • Kerjakan: tabel organization_settings (key–value, pola sama dengan outlet_settings, UNIQUE(organization_id, key)) dan loyalty_setting_changes. Saat ini belum ada tempat menyimpan setting per organisasi.
  • Selesai jika: up/down bersih.
  • Bergantung pada: –

PC-103 · Entities dan repository wallet · M

  • PRD: §7.1–§7.4
  • Kerjakan:
    • Entities untuk empat tabel PC-101.
    • Repository yang selalu memakai DBFromContext (berbeda dari repository gamification yang sekarang memakai r.db langsung).
    • LockWallet(ctx, customerID) dengan SELECT … FOR UPDATE. Membuat baris wallet jika belum ada (INSERT … ON CONFLICT DO NOTHING, lalu lock).
    • LockWallets(ctx, a, b) yang selalu mengunci berurutan berdasarkan customer_id.
    • Update saldo bersyarat yang mengembalikan error jika 0 baris ter-update.
    • Query lot aktif sesuai urutan K9: expires_at NULLS LAST, created_at.
  • Selesai jika: test repository menunjukkan update bersyarat gagal saat saldo kurang, dan dua goroutine yang mengunci wallet yang sama berjalan bergantian.
  • Bergantung pada: PC-101

PC-104 · Wallet engine (credit / debit / lot / idempotensi) · L

  • PRD: K5, K6, K9, §7, §8.1
  • Kerjakan: processor WalletProcessor sebagai satu-satunya pintu perubahan saldo:
    • Credit(ctx, CreditInput): menulis ledger, membuat lot (dengan expires_at dan origin_lot_id dari input), dan menambah saldo.
    • Debit(ctx, DebitInput): mengambil dari lot sesuai urutan K9 (atau dari lot tertentu lebih dulu, untuk reversal di F10), menulis alokasi dan ledger, lalu mengurangi saldo. Mengembalikan daftar alokasi, supaya transfer/exchange bisa membuat lot penerima dengan expires_at yang sama.
    • DebitUpTo: mengambil sebanyak yang tersedia, untuk reversal dengan shortfall (F10, Q3).
    • Idempotensi: jika idempotency_key sudah ada, kembalikan hasil pertama tanpa mutasi baru.
    • Validasi di level kode untuk aturan §8.1 (tipe ↔ currency ↔ referensi wajib), sebagai lapisan kedua di atas constraint database.
    • Semua method mewajibkan caller sudah berada di dalam transaksi dan wallet sudah dikunci.
  • Selesai jika: unit test mencakup debit yang melewati beberapa lot, urutan lot dengan dan tanpa expires_at, debit melebihi saldo (ditolak), DebitUpTo dengan shortfall, idempotency key ganda, dan saldo = SUM(ledger) = SUM(lot.remaining) setelah setiap skenario.
  • Bergantung pada: PC-103

PC-105 · Migrasi data Point & Token lama · M

  • PRD: §10, Q6
  • Kerjakan: migrasi data (atau command satu kali) yang:
    • Membuat wallet dari customer_points dan penjumlahan semua jenis customer_tokens.
    • Menulis ledger MIGRATION dan lot tanpa expires_at per customer per currency, dengan rincian per jenis token di metadata.
    • Mengganti TOKENS menjadi COINS di campaigns.type dan campaign_rules.reward_type.
  • Selesai jika: jumlah total Point dan Token sebelum = jumlah total saldo wallet sesudah (diuji pada salinan data staging), dan migrasi aman dijalankan dua kali (tidak menggandakan).
  • Bergantung pada: PC-104

PC-106 · Endpoint saldo & riwayat customer · M

  • PRD: F6, §9 (customer app)
  • Kerjakan: GET /customer/wallet dan GET /customer/wallet/transactions (pagination, filter currency/tipe/tanggal). Ganti isi /customer/points dan /customer/tokens menjadi alias yang membaca customer_wallets. Hapus pemakaian customer_points_repository untuk saldo.
  • Selesai jika: response menampilkan asal/tujuan setiap mutasi sesuai §8.1, dan aplikasi lama yang memanggil /points / /tokens tetap mendapat angka yang benar.
  • Bergantung pada: PC-105

PC-107 · Wallet customer dan adjustment di dashboard · M

  • PRD: F7
  • Kerjakan: GET /marketing/customers/:id/wallet (saldo, lot aktif, mutasi) dan POST /marketing/customers/:id/wallet/adjust (alasan wajib, tidak boleh membuat saldo negatif, tercatat created_by_user).
  • Selesai jika: adjustment muncul di riwayat customer dengan nama admin dan alasan. Adjustment yang melebihi saldo ditolak.
  • Bergantung pada: PC-104

PC-108 · Job rekonsiliasi · S

  • PRD: §7.5
  • Kerjakan: query dan job terjadwal yang memeriksa semua invarian §7.5 dan melaporkan selisih ke log dan notifikasi admin.
  • Selesai jika: job menemukan selisih yang sengaja dibuat di data test, dan diam saat data konsisten.
  • Bergantung pada: PC-104

PC-109 · Pembaca setting loyalitas dan riwayat perubahan · M

  • PRD: F1, F2 (bagian mekanisme, bukan UI), §8 loyalty_setting_changes
  • Kerjakan: service yang membaca setting outlet dan organisasi dengan nilai default PRD bila key belum diisi, mengembalikan struct bertipe (bukan string mentah), dan menulis loyalty_setting_changes setiap kali setting loyalitas diubah.
  • Selesai jika: outlet tanpa setting mendapat semua default PRD, dan setiap perubahan tercatat nilai lama, nilai baru, dan pelakunya.
  • Bergantung pada: PC-102

Fase 2 — Earning

PC-201 · API pengaturan loyalitas outlet · M

  • PRD: F1
  • Kerjakan: GET/PUT /outlets/:id/loyalty-settings dengan validasi F1. Response menyertakan persentase cashback efektif (earn_value × nilai EnakPoint / earn_per_amount).
  • Selesai jika: validasi menolak nilai di luar batas, dan hanya Admin/Manager yang bisa mengubah.
  • Bergantung pada: PC-109

PC-202 · Kalkulator earning · S

  • PRD: F1 (rumus), Q1, Q10
  • Kerjakan: fungsi murni CalculateEarning(order, pointPaidAmount, settings) yang mengembalikan Point, Coin, basis, dan snapshot setting.
  • Selesai jika: unit test mencakup contoh di PRD (Rp 87.500 → 875 Point, 3 Coin; dengan Rp 20.000 dibayar EnakPoint → 675 Point, 2 Coin), basis di bawah minimum, max_per_order, setting nonaktif, dan pajak tidak ikut dihitung.
  • Bergantung pada: PC-201

PC-203 · Earning saat order lunas · L

  • PRD: F3
  • Kerjakan:
    • Satukan titik "order menjadi lunas". Saat ini payment_status = completed di-set di empat tempat: OrderProcessorImpl.UpdateOrder, OrderProcessorImpl.updateOrderStatus, dan dua tempat di split_bill_processor.go. Buat satu hook onOrderPaid(orderID) yang dipanggil dari keempatnya.
    • Hook menjalankan earning setelah transaksi pembayaran commit, sehingga kegagalan earning tidak menggagalkan pembayaran.
    • Idempotency key earn:{order_id}:{currency}.
    • Lewati order tanpa customer, customer default, atau customer nonaktif.
    • Jaring pengaman: job yang mencari order lunas beberapa hari terakhir yang belum punya EARN (dan seharusnya punya), lalu menjalankan ulang earning.
  • Selesai jika: test untuk pembayaran penuh, split bill (lunas di pembayaran terakhir), self-order, dan pemanggilan ganda (hasilnya tetap satu earning). Earning yang sengaja digagalkan tidak menggagalkan pembayaran dan terambil oleh job.
  • Bergantung pada: PC-104, PC-202

PC-204 · Reversal earning saat void / refund · M

  • PRD: F10, Q3
  • Kerjakan: panggil reversal dari VoidOrder, RefundOrder, dan RefundPayment. Ambil pertama dari lot EARN order tersebut, lalu lot lain. Gunakan DebitUpTo dan catat shortfall. Refund tidak pernah diblokir.
  • Selesai jika: test untuk void penuh, refund sebagian (proporsional, akumulasi tidak melebihi earning), dan saldo yang sudah terpakai (shortfall tercatat, saldo 0).
  • Bergantung pada: PC-203

PC-205 · Tampilkan earning di order dan struk · S

  • PRD: F3
  • Kerjakan: tambah points_earned dan coins_earned ke response order dan data struk.
  • Selesai jika: nilai sama dengan baris EARN di ledger, dan bernilai 0 untuk order tanpa earning.
  • Bergantung pada: PC-203

Fase 3 — Pembayaran EnakPoint

Boleh dikerjakan sekarang. Tidak boleh dirilis sebelum catatan N2 (keuangan) dan N3 (legal) ditutup.

PC-301 · PIN customer · L

  • PRD: K8, F11, Q16, Q17
  • Kerjakan:
    • Migrasi kolom PIN di customers dan tabel customer_security_events.
    • Purpose OTP baru pin_setup dan pin_reset di OtpProcessor.
    • Endpoint /customer/pin/* (status, OTP, buat, ganti, reset).
    • Hash bcrypt, tolak PIN lemah (digit sama, berurutan, tanggal lahir).
    • Kunci 30 menit setelah 5 kali salah, dengan penghitung di database.
    • Tahan transfer keluar 24 jam setelah reset.
    • VerifyPin(ctx, customerID, pin) untuk dipakai task lain, dengan error PIN_NOT_SET, PIN_INVALID, PIN_LOCKED, TRANSFER_BLOCKED.
    • Hapus PIN oleh admin: DELETE /marketing/customers/:id/pin, dan GET /marketing/customers/:id/security-events.
    • PIN tidak pernah muncul di log (termasuk log request body).
  • Selesai jika: test untuk semua aturan di atas, termasuk 5 kali salah lalu PIN benar tetap ditolak selama terkunci, dan reset OTP membuka kunci.
  • Bergantung pada: –

PC-302 · API pengaturan loyalitas organisasi · S

  • PRD: F2
  • Kerjakan: GET/PUT /marketing/loyalty-settings untuk nilai EnakPoint, kurs exchange, dan batas transfer, serta GET /marketing/loyalty-settings/history. Sebelum menyimpan perubahan nilai EnakPoint atau kurs, response preview menampilkan total saldo beredar dan nilai rupiahnya sebelum/sesudah.
  • Selesai jika: perubahan tercatat di riwayat, dan transaksi lama tetap memakai nilai yang dibekukan.
  • Bergantung pada: PC-109

PC-303 · Payment method EnakPoint · M

  • PRD: F9 (payment method), §8 (perubahan payments), §10.5
  • Kerjakan:
    • Tambah tipe point ke PaymentMethodType dan validator (oneof=cash card digital_wallet point).
    • Migrasi kolom points_used dan point_value di payments.
    • Buat payment method sistem "EnakPoint" untuk setiap organisasi yang sudah ada, dan otomatis untuk organisasi baru.
    • Tolak hapus / ubah tipe method sistem.
    • Sembunyikan method ini di kasir jika outlet tidak mengaktifkan accept_payment.
  • Selesai jika: setiap organisasi punya tepat satu method EnakPoint yang tidak bisa dihapus.
  • Bergantung pada: –

PC-304 · Kode bayar sekali pakai · M

  • PRD: F9 (persetujuan customer), K8
  • Kerjakan: POST /customer/wallet/payment-code (butuh PIN) menghasilkan kode 6 digit dan QR yang berlaku 2 menit, disimpan di Redis dengan TTL dan diambil sekali-pakai (GETDEL). Kode terikat ke customer_id.
  • Selesai jika: kode kedaluwarsa, kode yang sudah dipakai, dan kode milik customer lain semuanya ditolak.
  • Bergantung pada: PC-301

PC-305 · Bayar EnakPoint di kasir · L

  • PRD: F9 (perhitungan, pencatatan), K7
  • Kerjakan:
    • GET /orders/:id/point-payment/preview.
    • Cabang method point di CreatePayment: validasi kode bayar, cocokkan customer order, hitung maks_point, lalu lakukan langkah 1–6 F9 dalam satu transaksi bersama pembuatan baris payments.
    • Idempotency payment:{payment_id}.
    • Tolak jika nominal_rupiah > remaining_amount (tidak ada kembalian).
  • Selesai jika: test untuk pembayaran penuh, sebagian + tunai (split), batas max_payment_percent, order walk-in (ditolak), kode salah (ditolak), dan dua pembayaran bersamaan untuk customer yang sama (saldo tidak terpakai dua kali). Earning (PC-203) menghitung basis tanpa bagian EnakPoint.
  • Bergantung pada: PC-104, PC-303, PC-304

PC-306 · Bayar EnakPoint dari app / self-order · M

  • PRD: F9
  • Kerjakan: POST /customer/orders/:id/pay-with-points (butuh PIN, hanya untuk order milik customer itu sendiri), memakai logika yang sama dengan PC-305.
  • Selesai jika: customer tidak bisa membayar order milik customer lain.
  • Bergantung pada: PC-305

PC-307 · Refund pembayaran EnakPoint · M

  • PRD: F9 (void/refund), K7, Q13
  • Kerjakan: PAYMENT_REFUND di jalur void dan refund memakai point_value yang dibekukan, dengan pembulatan ke bawah. Kembalikan ke lot dengan expires_at asal (aturan perpanjangan 7 hari mengikuti N4, jadi untuk sekarang cukup pakai tanggal asal). Tolak permintaan refund bagian EnakPoint lewat method lain.
  • Selesai jika: test untuk void penuh, refund sebagian, perubahan nilai EnakPoint di antara bayar dan refund (jumlah EnakPoint yang kembali tetap sama), dan percobaan refund tunai atas pembayaran EnakPoint (ditolak).
  • Bergantung pada: PC-305

PC-308 · EnakPoint di laporan · S

  • PRD: F9 (laporan)
  • Kerjakan: laporan per payment method menampilkan EnakPoint terpisah dan tidak menghitungnya sebagai kas masuk. Cek laporan analytics yang menjumlahkan pembayaran.
  • Selesai jika: total kas masuk di laporan tidak berubah saat sebagian order dibayar EnakPoint. Perlakuan akuntansi lanjutan menunggu N2.
  • Bergantung pada: PC-305

Fase 4 — Pergerakan Saldo

Boleh dikerjakan sekarang. Transfer (PC-402) tidak boleh dirilis sebelum N3 (legal) ditutup.

PC-401 · Exchange EnakCoin → EnakPoint · M

  • PRD: F4, K3
  • Kerjakan: GET /customer/wallet/exchange/preview dan POST /customer/wallet/exchange (butuh PIN, Idempotency-Key). Jumlah harus kelipatan coin_amount. Kurs dibekukan di metadata. Lot EnakPoint kedaluwarsa pada min(lot EnakCoin asal, sekarang + masa berlaku EnakPoint).
  • Selesai jika: test untuk kurs default 1:1, kurs 10:3, jumlah bukan kelipatan (ditolak), dan expires_at lot hasil exchange tidak pernah lebih lama dari lot asal.
  • Bergantung pada: PC-104, PC-301, PC-302

PC-402 · Transfer antar customer · L

  • PRD: F5, Q4, Q16
  • Kerjakan: GET /customer/wallet/transfer/recipient (nama disamarkan) dan POST /customer/wallet/transfer (butuh PIN, Idempotency-Key). Kunci dua wallet berurutan, cek batas organisasi (per transaksi dan harian), cek transfer_blocked_until. Lot penerima mewarisi expires_at dan origin_lot_id dari lot pengirim. Kirim notifikasi push ke penerima.
  • Selesai jika: test untuk transfer ke diri sendiri / customer lain organisasi / customer default (semua ditolak), batas harian, transfer dua arah bersamaan (tidak deadlock), dan expires_at di penerima sama persis dengan pengirim.
  • Bergantung pada: PC-104, PC-301, PC-302

PC-403 · Semua game memakai EnakCoin · M

  • PRD: F8, K1
  • Kerjakan:
    • GamePlayProcessor.PlayGame memotong EnakCoin lewat wallet engine sesuai games.metadata.coin_cost.
    • Seluruh permainan dalam satu transaksi: ubah repository game, game play, dan game prize ke DBFromContext. Gagal mengurangi stok hadiah membatalkan permainan (saat ini hanya di-Printf), dan rollback manual AddTokens dihapus.
    • Rename game_plays.token_used menjadi coins_used.
  • Selesai jika: test untuk saldo kurang (ditolak, tidak ada game_plays yang tercatat), stok hadiah gagal (semua dibatalkan), dan GAME_SPEND menunjuk game_plays.id.
  • Bergantung pada: PC-104

PC-404 · Telusuri per butir di dashboard · S

  • PRD: F7, §8.1 (per butir)
  • Kerjakan: GET /marketing/wallet-transactions/:id/trace yang mengembalikan alokasi lot dan rantai origin_lot_id sampai ke lot pertama.
  • Selesai jika: contoh di §8 (A transfer 120 ke B, B bayar 30) menelusuri ke order #ORD-1 milik A.
  • Bergantung pada: PC-402

Fase 5 — Kedaluwarsa

Jangan dikerjakan sebelum catatan N4 (model kedaluwarsa) diputuskan. Struktur lot sudah ada sejak PC-101, jadi yang tersisa hanya rumus, job, dan tampilan.

PC-501 · Setting kedaluwarsa · M

  • PRD: F12 (pengaturan), N4
  • Kerjakan: key setting sesuai model yang dipilih di N4, dengan validasi dan preview "saldo yang didapat hari ini kedaluwarsa pada …".
  • Bergantung pada: PC-302, N4

PC-502 · Hitung expires_at saat lot dibuat · M

  • PRD: F12 (tabel kedaluwarsa per lot), N4
  • Kerjakan: satu fungsi ComputeExpiry(currency, receivedAt, settings) yang dipakai oleh EARN dan ADJUSTMENT, serta aturan aktivasi pertama untuk lot lama (termasuk lot MIGRATION).
  • Bergantung pada: PC-104, PC-501

PC-503 · Job kedaluwarsa · M

  • PRD: F12 (proses kedaluwarsa)
  • Kerjakan: job per jam yang memproses lot lewat tanggal dengan EXPIRE (idempotency expire:{lot_id}) di bawah lock wallet.
    • Jangan menyalin pola OmsetMilestoneScheduler. Scheduler itu menyimpan state di memori, dan menurut komentarnya sendiri bisa mengirim ulang notifikasi setelah restart. Job ini harus aman dijalankan di banyak instance sekaligus: pilih lot dengan FOR UPDATE SKIP LOCKED, dan andalkan idempotency key.
  • Selesai jika: dua instance yang berjalan bersamaan tidak menghanguskan lot yang sama dua kali, dan tidak ada lot yang lewat tanggal lebih dari 1 jam tanpa diproses.
  • Bergantung pada: PC-502

PC-504 · Pengingat dan tampilan saldo yang akan kedaluwarsa · M

  • PRD: F6 (/wallet/expiring), F12 (pengingat)
  • Kerjakan: endpoint GET /customer/wallet/expiring, field saldo kedaluwarsa terdekat di /wallet, dan notifikasi pengingat yang dikelompokkan per tanggal. Jadwal pengingat mengikuti N4.
  • Bergantung pada: PC-503

Fase 6 — Bersih-bersih

PC-601 · Hapus tabel dan kode lama · S

  • PRD: §10.7
  • Kerjakan: satu rilis setelah PC-105 berjalan di production, drop customer_points dan customer_tokens. Hapus CustomerPointsProcessor dan CustomerTokensProcessor beserta stub not implemented, rute yang di-comment di router.go, dan alias /customer/points / /customer/tokens setelah aplikasi diperbarui.
  • Bergantung pada: PC-106, PC-403, dan konfirmasi bahwa aplikasi sudah tidak memanggil endpoint lama.

PC-602 · Dokumentasi integrasi · S

  • Kerjakan: panduan integrasi untuk tim aplikasi dan POS (seperti integration-weight-based-products.md): endpoint, kode error PIN, alur kode bayar, dan contoh request/response.
  • Bergantung pada: PC-305, PC-402

Yang Bisa Dimulai Sekarang

Bisa dikerjakan paralel tanpa menunggu apa pun:

  • PC-101 (migrasi wallet) → langsung lanjut PC-103, lalu PC-104
  • PC-102 (setting organisasi) → PC-109
  • PC-301 (PIN customer)
  • PC-303 (payment method EnakPoint)

PC-104 adalah jalur kritis: hampir semua task lain menunggunya.