Adds paying with the EnakPoint method (docs/prd-point-coin.md F9, K7,
PC-305).
POST /payments with the EnakPoint method now takes points and the
customer's payment code and goes through PointPaymentProcessor instead of
the generic path, which would record a payment without taking any balance.
After checking the order, its customer (not walk-in, active), the outlet
(accepts EnakPoint, minimum) and the method, it redeems the code, then in
one transaction locks the order row and the wallet, recomputes the F9
limits from fresh data, inserts the payment with points_used and the frozen
point_value, writes the PAYMENT ledger row (key payment:{id}, the outlet,
the cashier) and updates the order. The limits are
min(balance, floor(min(remaining, total × max_payment_percent / 100 − paid
with EnakPoint) / point_value)) in cents, so EnakPoint never pays more than
what is left and gives no change.
Unlike the generic CreatePayment, which always marks the order paid, an
EnakPoint payment leaves it partial with the right remaining amount until
it is settled, so the rest can be paid in cash. Settling it triggers
earning, whose basis leaves out the EnakPoint part. Splitting with the
EnakPoint method is refused. Refusals answer 400. The payment response
carries points_used and point_value for the receipt.
GET /orders/:id/point-payment/preview returns eligibility, balance, point
value and the maximum for the use-maximum button.
The payment and order repositories write outside transactions, so this
path uses its own repository that joins it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the dashboard side of a customer's wallet (docs/prd-point-coin.md F7,
PC-107), under /marketing for admins and managers:
- GET /marketing/customers/:id/wallet returns the customer, the ledger and
spendable balances, every lot that still holds something (flagged when
expired), and a page of history. Unlike the customer's own view, each row
carries the real names behind it: the transfer counterparty, the admin or
cashier, and the outlet, plus the reason and metadata.
- POST /marketing/customers/:id/wallet/adjust takes a signed amount and a
required reason. It writes an ADJUSTMENT pointing at the admin through the
wallet engine, refuses to take more than the customer can spend, and
accepts an optional idempotency key so a retried request adjusts once.
Reasons describing a cash-out are refused (K7).
The customer must belong to the caller's organization; otherwise both
endpoints answer 404. Positive adjustments create non-expiring lots until
the expiry model is decided (F12, note N4).
The mapping from ledger rows to what the apps show is now shared between the
customer and dashboard views.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /customer/wallet now reads the EnakPoint & EnakCoin wallet
(docs/prd-point-coin.md F6, PC-106): spendable point and coin balances, the
rupiah value of one EnakPoint and of the balance, the nearest day each
currency loses balance (grouped by Asia/Jakarta day), and recent ledger
rows. The fields of the pre-wallet response stay, filled from the wallet, so
app versions that read them keep working.
Adds GET /customer/wallet/transactions with pagination and filters for
currency, one or more types, and an inclusive date range. Each row shows
where the value came from (additions) or went to (deductions) as in §8.1,
and additions list their lots and earliest expiry. The counterparty id, the
admin and the metadata are left out; the description already carries the
masked name. A malformed query answers 400, a missing customer 404.
/customer/points and /customer/tokens keep their shape and now read the
wallet too, so customer_points_repository is no longer used for balances.
Balances are what the customer can spend: lots that have expired but that
the expiry job has not processed are not counted. The point value is read
from organization_settings (loyalty.point.value, default 1) through a small
repository that the typed settings reader in PC-109 will build on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>