Commit Graph
5 Commits
Author SHA1 Message Date
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
efrilm 582dc75543 Reapply "feat(loyalty): EnakPoint & EnakCoin" (#32)
This reverts commit 4e24f9bbb0.
2026-09-30 15:31:44 +07:00
efrilmandClaude Opus 5.5 4e24f9bbb0 Revert "feat(loyalty): EnakPoint & EnakCoin" (#32)
This reverts merge commit 645da30, returning main to f0ff59f.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:16:15 +07:00
efrilmandClaude Opus 5.5 0c4dd72583 feat(loyalty): one-time EnakPoint payment code
Adds POST /customer/wallet/payment-code (docs/prd-point-coin.md F9, K8,
PC-304). The customer approves with their PIN on their own phone and gets a
6-digit code, as digits and as a QR payload (enakpoint:<code>) for the app
to render, valid for two minutes. The PIN is never typed at the cashier.

Codes are drawn from crypto/rand and stored in Redis with SET NX and a TTL,
bound to the customer; a new code retires the previous one. Redeeming is a
single Lua step that uses the code up only if it belongs to the order's
customer, so it stays one-time under a race, and a cashier scanning it
against the wrong order does not burn it for its owner, which a plain
GETDEL would. Expired, used, unknown and other customers' codes are all
refused alike.

Tests run against miniredis, added as a test dependency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:37:20 +07:00
efrilmandClaude Opus 5.5 8370851ed2 feat(loyalty): customer PIN
Adds the 6-digit customer PIN that approves every action moving EnakPoint
or EnakCoin on the customer's request (docs/prd-point-coin.md K8, F11, Q16,
Q17, PC-301).

Migration 000093 adds the PIN columns to customers and the
customer_security_events table. PIN data is read and written only through
CustomerPinRepository, never the Customer entity, so the hash cannot reach
a customer response. Only a bcrypt hash is stored.

- /customer/pin: status, OTP (pin_setup, pin_reset), create, change,
  reset. The OTP must be for that purpose and sent to the customer's own
  number; the existing OTP validation checks neither. A new PIN is checked
  (6 digits, confirmed, not one digit, not a run up or down, not the birth
  date as DDMMYY or YYMMDD) before the OTP is spent.
- Five wrong attempts in a row lock the PIN for 30 minutes; the counter is
  incremented in one statement so attempts at the same time all count,
  and a lock that ran out starts a new series. A locked PIN is refused even
  when right. The customer is told by WhatsApp, as there is no push channel
  to customers yet; only the attempt that reached the limit alerts.
- A reset through OTP lifts the lock and holds outgoing transfers for 24
  hours; paying and exchanging still work, and a held transfer costs no
  attempt.
- VerifyPin(ctx, customer, pin, action) for the flows that follow, with
  PIN_NOT_SET, PIN_INVALID (attempts left), PIN_LOCKED and
  TRANSFER_BLOCKED (until when), which PinErrorResponse turns into
  distinct codes and statuses.
- DELETE /marketing/customers/:id/pin (loyalty managers, reason required)
  and GET /marketing/customers/:id/security-events, scoped to the
  organization.

Every PIN event is in the security log with IP and user agent. No message
or binding error contains a PIN.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:20:29 +07:00