GET /customer/enakgame/sessions takes optional game_id and status, so a game
reloaded mid-play finds the session it was running (status=STARTED) instead
of starting a new one and charging EnakCoin again. An invalid game_id or
status is refused.
integration-enakgame.md §4.4 now describes recovery after a reload: keep the
session_id in sessionStorage, continue a STARTED session before expires_at,
and call complete again for a COMPLETED one to get the full answer, prize
included. The mobile guide and RFC §11 mention the filters.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One guide per team, covering EnakPoint, EnakCoin, EnakGame and vouchers:
- integration-mobile-customer.md: wallet, history (with the game and voucher
ledger types), push, PIN, exchange, transfer, game list and webview, play
history, voucher catalog, redeem and my vouchers.
- integration-pos.md: linking customers to orders, earning, receipts,
void/refund, and vouchers as a known gap (no POS endpoint to mark one used).
- integration-enakgame.md: the Phaser client's side of a play: start with
Idempotency-Key, complete, rewards, spin, expiry and refunds, retries.
- integration-backoffice.md: loyalty settings and customer wallets, plus
games, reward configs, spin setup, budgets, metrics and recommendations,
events, vouchers and code import, analytics.
The JS bridge between the app and the game is a proposal both teams still
have to agree on. Replaces api-enakpoint.md, integration-enakpoint.md,
mobile-customer-enakpoint.md, backoffice-enakpoint.md and enakgame-spin.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vouchers are what EnakPoint is redeemed for, wherever it came from, so they are
not part of EnakGame. Their only link to it is the budget attribution, which
does not change.
- Admin: /marketing/enakgame/vouchers... -> /marketing/vouchers...
- Customer: /customer/enakgame/vouchers -> /customer/vouchers,
/customer/enakgame/vouchers/:id/redeem -> /customer/vouchers/:id/redeem,
/customer/enakgame/redemptions -> /customer/vouchers/redemptions
Roles, handlers and logic stay the same. No client calls these endpoints yet.
RFC §7.4 and §11 updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 phase 9 of docs/tasks-enakgame.md (EG-901 to EG-903).
Budget Controller (EG-901, EG-902)
- GET /marketing/enakgame/budgets/:id/recommendation, GLOBAL budgets only: the
multiplier (budget − realized) / (forecast − realized), within one step of 1,
rounded down to two decimals, either way. Shows each game's new rules.
- POST .../recommendation/accept with the multiplier the admin saw: recomputed in the
transaction, then one new ACTIVE version per game, the old one RETIRED, audited
with source budget_controller and RECOMMENDATION_ACCEPTED on the budget.
- Migration 000112: base_config_id, multiplier and budget_id on
game_reward_configs. Rules are always scaled from the admin's last version, so
rounding does not compound and min/max are against what the admin set.
- Guardrails in game_budgets.thresholds: max_step_percent 10, min/max multiplier
50-150%, cooldown_days 7 per organization. Provisional pending RFC §19.2 #4.
- RewardCalculator.Scale for the four reward types: amounts only, rounded down.
Analytics (EG-903)
- GET /marketing/enakgame/analytics/games and /analytics/economy over a range of
Asia/Jakarta days (at most 366), from game_sessions and the wallet ledger.
- Migrations 000113 (game_sessions by organization and start) and 000114
(wallet_transactions by organization and time, CONCURRENTLY).
The Postgres tests for accepting and analytics were not run: no test database here.
Migrations 000112-000114 have not been run anywhere.
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>
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>
EnakGame: pay EnakCoin to play, earn EnakCoin from the result, exchange
into EnakPoint, and redeem EnakPoint for vouchers only.
- enakgame-prd.md: economy and business rules, including entry cost and
automatic refund, monthly global budget with a separate budget per
event (event = campaign), and EnakPoint being voucher-only.
- rfc-enakgame.md: built on the existing wallet, ledger and lots. Game
sessions with a state machine, versioned reward configs, Economy Guard
counters, vouchers with internal codes and external providers, and
realized cost attributed to budgets by tracing the lots spent.
- tasks-enakgame.md: EG-001 to EG-1003 in eleven phases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds docs/mobile-customer-enakpoint.md, written as a brief for building the
customer app: the UI rules, the API conventions, each screen with its
requests and responses, push handling, PIN flows, paying at the cashier,
exchange, transfer, games, what was removed, and a checklist. It covers the
new GET /customer/outlets, GET /customer/orders and /customer/orders/:id,
and the optional organization_id at registration, which the API reference
now lists too.
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 docs/api-enakpoint.md, the endpoint reference for the customer app,
POS and dashboard, and docs/backoffice-enakpoint.md, the screens the
backoffice needs: outlet and organization settings with the save flow and
impact dialog, both expiry models, the customer wallet with adjustment and
trace, PIN removal and security log, settings history, game coin_cost and
the EnakPoint payment method.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds docs/integration-enakpoint.md for the customer app, POS and dashboard
teams (PC-602), in the style of the weight-based products guide.
It covers the response envelope and error codes, balances and history with
every ledger type, the expiring list and FCM push types with their data,
the PIN flows and the four PIN error codes, paying with EnakPoint at the
cashier (payment code, preview, POST /payments) and in the app, void and
refund rules, exchange and transfer with Idempotency-Key, games on
EnakCoin, the deprecated endpoints and fields with their replacements, the
dashboard's outlet and organization settings including both expiry models,
the customer wallet, adjustments, trace and PIN removal, and a checklist
per team.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A product sold by weight with no unit produces order lines with nothing to
print: the receipt would read "4,2" with no idea of what. Until now nothing
stopped that — the mistake only surfaced at the cashier.
Enforce it in two places, because neither alone sees the whole picture. On
create, the validator has everything it needs. On update, the request may
omit unit_id for a product that already has one, so the check runs in the
processor against the merged product: what is rejected is the end state, a
product sold by weight with no unit.
Also fixes two things this uncovered:
The struct tags on the product contracts are decorative — this validator is
hand-written and never calls validator.Struct — so `oneof=unit weight` was
never enforced, and an unknown sell_by was silently rewritten to "unit" by
the mapper. It is now rejected with a message that names the valid values.
The update validator's "at least one field" guard did not list unit_id,
sell_by or print_to_checker, so an update carrying only one of those was
turned away as an empty request.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Client-facing companion to the RFC, aimed at the POS Mobile and Backoffice
teams: endpoints and payloads for setting up a weight product, placing an
order, rendering the line, and voiding, refunding or splitting it.
Documents two gaps the teams have to work around rather than discover:
unit_id is not yet enforced when sell_by is "weight", so Backoffice must
require it in the form; and money rounds to 2 decimals rather than whole
rupiah, which is still an open decision.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Products like fish are sold per weighing (4.2 ons, 5.6 ons), which the
order line could not represent: quantity is INTEGER and prices are always
computed as quantity * unit_price.
Model one weighing as one order line. quantity stays INTEGER and keeps
meaning "how many items"; the measured amount goes into a new nullable
order_items.weight, and the line is priced weight * unit_price. Two
weighings of the same product are two lines, never merged into one.
Keeping quantity integral avoids float comparisons in void, refund and
split bill, where accumulated rounding error would silently misbehave —
"1.4 + 1.4 + 1.4" is not 4.2 in float64, which would leave a fully paid
split-bill item marked unpaid.
BillableQuantity() is now the single place that decides between weight
and count; every price and cost calculation goes through it. Missing one
would bill a 4.2 ons fish as a single ons — wrong money, no error.
Two database constraints back the design: a weighed line always carries a
positive weight, and its quantity is pinned to 1. The latter also makes
void all-or-nothing for weighed lines, so the row-splitting branch can
never produce a zero-weight remainder row.
Also wires product.unit_id through the API, which was previously not
settable at all, and corrects the misleading comment on the request's
unit_price field — that value has never been used; price always comes
from the database.
Design notes and the audit of every price multiplication site are in
docs/rfc-weight-based-products.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>