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>
179 lines
5.4 KiB
Go
179 lines
5.4 KiB
Go
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"
|
|
)
|
|
|
|
// audit_logs.action on a GAME_BUDGET whose recommendation was accepted.
|
|
const AuditActionRecommendationAccepted = "RECOMMENDATION_ACCEPTED"
|
|
|
|
// 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"
|
|
)
|
|
|
|
// Budget Controller guardrails of a budget that sets none (PRD §31). Pending RFC
|
|
// §19.2 #4.
|
|
const (
|
|
// Percent a recommendation may move rewards by, either way.
|
|
GameBudgetMaxStepDefault = int64(10)
|
|
// Bounds of a game's multiplier, in percent of the configuration its admin wrote.
|
|
GameBudgetMinMultiplierDefault = int64(50)
|
|
GameBudgetMaxMultiplierDefault = int64(150)
|
|
// Days after an accepted recommendation before the next one, in the organization.
|
|
// The same as the forecast window, so the next one sees the effect of the last.
|
|
GameBudgetCooldownDaysDefault = int64(7)
|
|
)
|
|
|
|
// What a Budget Controller recommendation says (docs/rfc-enakgame.md §10). Only
|
|
// RECOMMENDED can be accepted.
|
|
const (
|
|
GameBudgetRecommended = "RECOMMENDED"
|
|
// The forecast meets the budget closely enough that rewards stay.
|
|
GameBudgetRecommendationNoChange = "NO_CHANGE"
|
|
// The last accepted recommendation of the organization is too recent.
|
|
GameBudgetRecommendationCooldown = "COOLDOWN"
|
|
// Every game is already at its min or max multiplier.
|
|
GameBudgetRecommendationAtLimit = "AT_LIMIT"
|
|
// No cost in the forecast window to extrapolate from.
|
|
GameBudgetRecommendationNoData = "INSUFFICIENT_DATA"
|
|
// The budget's period has not started or has no day left after today.
|
|
GameBudgetRecommendationOutOfPeriod = "OUT_OF_PERIOD"
|
|
)
|