feat(enakgame): game sessions, rewards, vouchers, budgets and events
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2c9753fae7
commit
798a36bd6c
@@ -0,0 +1,146 @@
|
||||
package constants
|
||||
|
||||
// EnakGame values (docs/rfc-enakgame.md §5). Each set matches a CHECK in the
|
||||
// migrations.
|
||||
|
||||
// games.status.
|
||||
const (
|
||||
GameStatusDraft = "DRAFT"
|
||||
GameStatusActive = "ACTIVE"
|
||||
GameStatusInactive = "INACTIVE"
|
||||
// The old games, and games taken out of EnakGame for good. Never changed again.
|
||||
GameStatusArchived = "ARCHIVED"
|
||||
)
|
||||
|
||||
// game_reward_configs.reward_type (§8).
|
||||
const (
|
||||
GameRewardTypeFixed = "FIXED"
|
||||
GameRewardTypeScoreBased = "SCORE_BASED"
|
||||
GameRewardTypeOutcomeBased = "OUTCOME_BASED"
|
||||
GameRewardTypeProbability = "PROBABILITY"
|
||||
)
|
||||
|
||||
// game_reward_configs.status. Only status ever changes on a config (D7).
|
||||
const (
|
||||
GameRewardConfigStatusDraft = "DRAFT"
|
||||
GameRewardConfigStatusActive = "ACTIVE"
|
||||
GameRewardConfigStatusRetired = "RETIRED"
|
||||
)
|
||||
|
||||
// game_sessions.status: STARTED → COMPLETED | REFUNDED | EXPIRED (D4).
|
||||
const (
|
||||
GameSessionStatusStarted = "STARTED"
|
||||
GameSessionStatusCompleted = "COMPLETED"
|
||||
GameSessionStatusRefunded = "REFUNDED"
|
||||
GameSessionStatusExpired = "EXPIRED"
|
||||
)
|
||||
|
||||
// game_sessions.refund_reason (§7.3).
|
||||
const (
|
||||
GameSessionRefundSystemError = "SYSTEM_ERROR"
|
||||
GameSessionRefundGameDeactivated = "GAME_DEACTIVATED"
|
||||
)
|
||||
|
||||
// game_budgets.scope (§5.6).
|
||||
const (
|
||||
GameBudgetScopeGlobal = "GLOBAL"
|
||||
GameBudgetScopeEvent = "EVENT"
|
||||
)
|
||||
|
||||
// audit_logs.actor_type (§5.9).
|
||||
const (
|
||||
AuditActorUser = "USER"
|
||||
AuditActorSystem = "SYSTEM"
|
||||
)
|
||||
|
||||
// audit_logs.source: where a change came from.
|
||||
const (
|
||||
AuditSourceAdminAPI = "admin_api"
|
||||
AuditSourceBudgetController = "budget_controller"
|
||||
AuditSourceSessionJob = "session_job"
|
||||
AuditSourceBudgetPeriodJob = "budget_period_job"
|
||||
AuditSourceCustomerAPI = "customer_api"
|
||||
)
|
||||
|
||||
// audit_logs.entity_type: what an audit row is about (§13).
|
||||
const (
|
||||
AuditEntityGame = "GAME"
|
||||
AuditEntityGameRewardConfig = "GAME_REWARD_CONFIG"
|
||||
AuditEntityGameBudget = "GAME_BUDGET"
|
||||
AuditEntityGameEvent = "GAME_EVENT"
|
||||
AuditEntityGameSession = "GAME_SESSION"
|
||||
AuditEntityVoucher = "VOUCHER"
|
||||
)
|
||||
|
||||
// game_reward_counters.scope_type: what an Economy Guard counter counts for (§5.8).
|
||||
const (
|
||||
GameRewardScopeUser = "USER"
|
||||
GameRewardScopeGame = "GAME"
|
||||
GameRewardScopeEvent = "EVENT"
|
||||
GameRewardScopeGlobal = "GLOBAL"
|
||||
)
|
||||
|
||||
// vouchers.voucher_type (§5.7).
|
||||
const (
|
||||
VoucherTypeFixedValue = "FIXED_VALUE"
|
||||
VoucherTypePercentage = "PERCENTAGE"
|
||||
VoucherTypeFreeItem = "FREE_ITEM"
|
||||
VoucherTypeMerchantBenefit = "MERCHANT_BENEFIT"
|
||||
)
|
||||
|
||||
// vouchers.stock_mode: where a redemption's voucher comes from.
|
||||
const (
|
||||
// A counted stock with nothing to hand out but the redemption itself.
|
||||
VoucherStockStatic = "STATIC"
|
||||
// One imported code per redemption.
|
||||
VoucherStockCodePool = "CODE_POOL"
|
||||
// Issued by a provider (§7.5), not before EG-801.
|
||||
VoucherStockExternal = "EXTERNAL"
|
||||
)
|
||||
|
||||
// vouchers.status.
|
||||
const (
|
||||
VoucherStatusDraft = "DRAFT"
|
||||
VoucherStatusActive = "ACTIVE"
|
||||
VoucherStatusInactive = "INACTIVE"
|
||||
VoucherStatusArchived = "ARCHIVED"
|
||||
)
|
||||
|
||||
// voucher_codes.status.
|
||||
const (
|
||||
VoucherCodeAvailable = "AVAILABLE"
|
||||
VoucherCodeReserved = "RESERVED"
|
||||
VoucherCodeRedeemed = "REDEEMED"
|
||||
VoucherCodeExpired = "EXPIRED"
|
||||
VoucherCodeCancelled = "CANCELLED"
|
||||
)
|
||||
|
||||
// voucher_redemptions.status.
|
||||
const (
|
||||
VoucherRedemptionPending = "PENDING"
|
||||
VoucherRedemptionCompleted = "COMPLETED"
|
||||
VoucherRedemptionFailed = "FAILED"
|
||||
)
|
||||
|
||||
// Budget statuses (PRD §32), from its utilization and forecast against its thresholds.
|
||||
const (
|
||||
GameBudgetHealthy = "HEALTHY"
|
||||
GameBudgetWarning = "WARNING"
|
||||
GameBudgetCritical = "CRITICAL"
|
||||
GameBudgetExhausted = "EXHAUSTED"
|
||||
)
|
||||
|
||||
// Thresholds of a budget that sets none, in percent (PRD §8). Pending RFC §19.2 #4.
|
||||
const (
|
||||
GameBudgetWarningDefault = int64(70)
|
||||
GameBudgetCriticalDefault = int64(90)
|
||||
)
|
||||
|
||||
// game_events.status. An ACTIVE event changes rewards only between its start_at and
|
||||
// end_at.
|
||||
const (
|
||||
GameEventStatusDraft = "DRAFT"
|
||||
GameEventStatusActive = "ACTIVE"
|
||||
GameEventStatusEnded = "ENDED"
|
||||
GameEventStatusCancelled = "CANCELLED"
|
||||
)
|
||||
@@ -69,6 +69,7 @@ const (
|
||||
ExpenseServiceEntity = "expense_service"
|
||||
CashAdvanceServiceEntity = "cash_advance_service"
|
||||
WalletServiceEntity = "wallet_service"
|
||||
EnakGameServiceEntity = "enakgame_service"
|
||||
LoyaltySettingsServiceEntity = "loyalty_settings_service"
|
||||
CustomerPinServiceEntity = "customer_pin_service"
|
||||
)
|
||||
|
||||
@@ -47,6 +47,13 @@ const (
|
||||
LoyaltyExpiryGraceMonthsSuffix = "expiry_grace_months"
|
||||
)
|
||||
|
||||
// Per organization: EnakGame reward limits (docs/rfc-enakgame.md §5.10). 0 means no
|
||||
// limit. A reward over a limit is cut to what is left of it (§9).
|
||||
const (
|
||||
EnakGameLimitUserDailyKey = "enakgame.limit.user_daily"
|
||||
EnakGameLimitGlobalDailyKey = "enakgame.limit.global_daily"
|
||||
)
|
||||
|
||||
// Modes of loyalty.{point,coin}.earn_mode.
|
||||
const (
|
||||
// earn_value for every earn_per_amount rupiah of the basis.
|
||||
|
||||
@@ -25,6 +25,11 @@ const (
|
||||
WalletTxTypeAdjustment = "ADJUSTMENT"
|
||||
WalletTxTypeMigration = "MIGRATION"
|
||||
WalletTxTypeRewardRedeem = "REWARD_REDEEM"
|
||||
|
||||
// EnakGame (docs/rfc-enakgame.md §6.1).
|
||||
WalletTxTypeGameSpendRefund = "GAME_SPEND_REFUND"
|
||||
WalletTxTypeGameReward = "GAME_REWARD"
|
||||
WalletTxTypeRewardRedeemRefund = "REWARD_REDEEM_REFUND"
|
||||
)
|
||||
|
||||
// What a ledger row's reference_id points at: where the value came from for a
|
||||
@@ -38,4 +43,5 @@ const (
|
||||
WalletRefTypeLegacyPoints = "LEGACY_POINTS"
|
||||
WalletRefTypeLegacyTokens = "LEGACY_TOKENS"
|
||||
WalletRefTypeRewardRedemption = "REWARD_REDEMPTION"
|
||||
WalletRefTypeGameSession = "GAME_SESSION"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user