EnakGame phase 10 of docs/tasks-enakgame.md (EG-1001 to EG-1003).
Spin (EG-1001)
- PROBABILITY entries take an optional label (a wheel segment). The customer game
list shows a PROBABILITY game's prizes (entry, label, amount, never weights), and
completing returns the drawn prize, so the client can draw the wheel and stop it
on the server's draw.
- docs/enakgame-spin.md: the admin steps to set up spin per organization (no
seeder) and the customer app flow. An HTTP test plays it end to end.
Old game flow removed (EG-1002)
- Routes POST /customer/spin, GET /customer/games, GET /customer/ferris-wheel, and
admin /marketing/games, /marketing/game-prizes, /marketing/rewards, with their
handlers, services, processors, repositories, validators, models, contracts,
mappers and tests (GamePlayProcessor, SpinGameService, rewards, ...). This also
closes RFC §15 findings 1 and 2 (double charge, spinning another org's game).
- Tables games, game_prizes, game_plays and rewards stay for ledger history.
entities.StringSlice moves to its own file; the omset tracker (unrouted) keeps
game_id but no longer embeds the old game response.
games.is_active dropped (EG-1003)
- Migration 000115; nothing reads metadata.coin_cost any more.
The EnakPoint integration docs now point at /customer/enakgame. The Postgres tests
were not run: no test database here. Migration 000115 has not been run anywhere.
The customer app must stop calling the removed endpoints before this is deployed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EnakGame phases 1-8 of docs/tasks-enakgame.md (EG-101 to EG-803), built on the
existing EnakPoint/EnakCoin wallet (docs/rfc-enakgame.md).
Foundation (phase 1)
- Migrations 000103-000106: games extended with organization, slug, status,
entry cost and result rules, old games archived (not deleted); budgets,
versioned reward configs, sessions and session rewards; the ledger types
GAME_SPEND_REFUND, GAME_REWARD and REWARD_REDEEM_REFUND; audit_logs.
- AuditLogger writes in the caller's transaction only.
- enakgame.limit.user_daily and global_daily organization settings.
Games and sessions (phases 2-4)
- Admin /marketing/enakgame: games, reward config versions (immutable but for
status, one ACTIVE per game), budgets with non-overlapping global periods and
a daily job opening the next month.
- Customer /customer/enakgame: start (Idempotency-Key, entry cost and config
frozen on the session), complete (result validation, reward engine, max_reward
cap, daily limits via game_reward_counters, one GAME_REWARD per budget),
automatic refunds for system errors and deactivated games, and a session job.
- Reward engine: FIXED, SCORE_BASED, OUTCOME_BASED, PROBABILITY (crypto/rand),
rounded down.
Vouchers and budgets (phases 5-6)
- Migration 000108 and 000107: vouchers, codes, redemptions, cost attribution;
Economy Guard counters.
- STATIC and CODE_POOL redemption in one transaction with the REDEEM PIN action;
realized cost traced through the lots to the budget that paid the reward.
- Budget metrics: realized cost, forecast, exposure and status. Migrations
000109-000110 add the wallet_lots indexes they need, built CONCURRENTLY.
Events (phase 7)
- Migration 000111: game events, each with its own EVENT budget. Event extras
stack per PRD §16 defaults, with event and per-customer limits.
External vouchers (phase 8)
- VoucherProvider contract, two-step PENDING redemption and a recovery job,
tested with a fake provider. No provider adapter is registered yet, so
EXTERNAL vouchers stay out of the catalog.
Not yet decided before release: reward rounding, event stacking, budget
exhaustion policy and thresholds (RFC §19.2). Migrations 000103-000111 have
not been run on any shared database.
Also fixes a leftover PAYMENT filter in a wallet test and a data race in a
test PIN fake.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tokens are EnakCoin and no app uses the token names any more, so their
compatibility layer goes:
- GET /customer/tokens and its handler, service, processor and response
types.
- total_tokens and tokens_history on GET /customer/wallet; last_updated
now comes from the most recent row of either currency.
- token_used and tokens_remaining on game and spin responses, and
sort_by=token_used on the game play list.
- TOKENS as a campaign type and reward type, with the mapping to COINS:
migration 000092 already renamed the stored values.
The customer_tokens table and its entity stay, as cmd/wallet-migrate still
reads them, and LEGACY_TOKENS stays as the reference of the MIGRATION rows
it wrote. The docs list the removed names and their replacements.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Adds GET and PUT /outlets/:id/loyalty-settings (docs/prd-point-coin.md F1,
PC-201) on top of the typed settings processor.
The response shows every setting with its default when unset, the
organization's point value, and the effective EnakPoint cashback
(earn_value × point_value / earn_per_amount), so an owner cannot misread
the scale. PUT applies the body on top of the current settings: fields left
out keep their value, null clears an optional limit, and unknown fields are
refused so a typo cannot be ignored silently. The read-only fields of the
GET response are accepted and ignored, so a client can send back what it
received. It returns the keys that changed. Values outside the F1 bounds
answer 400, and an outlet of another organization 404.
RequireAdminOrManager also lets the purchasing role through, so loyalty
settings and the manual wallet adjustment from PC-107 now use a stricter
RequireLoyaltyManager (superadmin, admin, manager, owner).
Adds a test that registers every route, since gin panics at startup when
two routes name the same path parameter differently.
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>