Files
apskel-pos-backend/docs/tasks-point-coin.md
T
efrilmandClaude Opus 5.5 2c9753fae7 feat(loyalty): remove paying with EnakPoint
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>
2026-10-07 13:48:29 +07:00

446 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Task Breakdown: EnakPoint & EnakCoin
**Sumber:** [PRD EnakPoint & EnakCoin](prd-point-coin.md)
**Tanggal:** 2026-09-29
> **Catatan 2026-10-07:** fase 3 (Pembayaran EnakPoint) dihapus sesuai
> [`enakgame-prd.md`](./enakgame-prd.md) §3.2: EnakPoint hanya bisa ditukar ke voucher,
> bukan alat bayar. Payment method EnakPoint, kode bayar, bayar di kasir & app, refund
> EnakPoint, dan EnakPoint di laporan (PC-303 – PC-308) sudah dihapus dari backend
> (migrasi `000102`). PIN customer (PC-301) dan pengaturan loyalitas organisasi (PC-302)
> tetap dipakai. Sisa dokumen ini dibiarkan sebagai riwayat.
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.