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
@@ -34,6 +34,10 @@ type App struct {
|
||||
walletRecon *service.WalletReconciliationJob
|
||||
earningRetry *service.EarningBackfillJob
|
||||
walletExpiry *service.WalletExpiryJob
|
||||
gameSessions *service.GameSessionJob
|
||||
gameBudgets *service.GameBudgetPeriodJob
|
||||
voucherCodes *service.VoucherCodeExpiryJob
|
||||
voucherRecover *service.VoucherRedemptionRecoveryJob
|
||||
}
|
||||
|
||||
func NewApp(db *gorm.DB, redisClient *redis.Client) *App {
|
||||
@@ -67,6 +71,14 @@ func (a *App) Initialize(cfg *config.Config) error {
|
||||
// Expires balances whose time is up and reminds customers before (docs/prd-point-coin.md F12)
|
||||
a.walletExpiry = service.NewWalletExpiryJob(processor.NewWalletExpiryProcessor(
|
||||
repository.NewWalletExpiryRepository(a.db), processors.loyaltySettingsProcessor, processor.NewWalletProcessor(repos.walletRepo), repos.txManager, processors.customerDeviceProcessor))
|
||||
// Refunds or expires EnakGame sessions left open (docs/rfc-enakgame.md §7.3)
|
||||
a.gameSessions = service.NewGameSessionJob(processors.gameSessionProcessor)
|
||||
// Opens next month's global EnakGame budget (docs/rfc-enakgame.md §12)
|
||||
a.gameBudgets = service.NewGameBudgetPeriodJob(processors.gameBudgetProcessor)
|
||||
// Expires voucher codes past their date (docs/rfc-enakgame.md §12)
|
||||
a.voucherCodes = service.NewVoucherCodeExpiryJob(processors.voucherAdminProcessor)
|
||||
// Settles EXTERNAL voucher redemptions left PENDING (docs/rfc-enakgame.md §7.5)
|
||||
a.voucherRecover = service.NewVoucherRedemptionRecoveryJob(processors.voucherRedemptionProcessor)
|
||||
|
||||
services := a.initServices(processors, repos, cfg)
|
||||
validators := a.initValidators()
|
||||
@@ -165,6 +177,8 @@ func (a *App) Initialize(cfg *config.Config) error {
|
||||
services.customerDeviceService,
|
||||
services.customerOutletService,
|
||||
services.customerOrderService,
|
||||
services.enakGameAdminService,
|
||||
services.enakGameCustomerService,
|
||||
a.redisClient,
|
||||
)
|
||||
|
||||
@@ -185,6 +199,18 @@ func (a *App) Start(port string) error {
|
||||
if a.walletExpiry != nil {
|
||||
a.walletExpiry.Start(15 * time.Minute)
|
||||
}
|
||||
if a.gameSessions != nil {
|
||||
a.gameSessions.Start(time.Minute)
|
||||
}
|
||||
if a.gameBudgets != nil {
|
||||
a.gameBudgets.Start(24 * time.Hour)
|
||||
}
|
||||
if a.voucherCodes != nil {
|
||||
a.voucherCodes.Start(time.Hour)
|
||||
}
|
||||
if a.voucherRecover != nil {
|
||||
a.voucherRecover.Start(time.Minute)
|
||||
}
|
||||
|
||||
engine := a.router.Init()
|
||||
|
||||
@@ -233,6 +259,18 @@ func (a *App) Shutdown() {
|
||||
if a.walletExpiry != nil {
|
||||
a.walletExpiry.Stop()
|
||||
}
|
||||
if a.gameSessions != nil {
|
||||
a.gameSessions.Stop()
|
||||
}
|
||||
if a.gameBudgets != nil {
|
||||
a.gameBudgets.Stop()
|
||||
}
|
||||
if a.voucherCodes != nil {
|
||||
a.voucherCodes.Stop()
|
||||
}
|
||||
if a.voucherRecover != nil {
|
||||
a.voucherRecover.Stop()
|
||||
}
|
||||
close(a.shutdown)
|
||||
}
|
||||
|
||||
@@ -403,6 +441,11 @@ type processors struct {
|
||||
customerDeviceProcessor *processor.CustomerDeviceProcessor
|
||||
customerOutletProcessor *processor.CustomerOutletProcessor
|
||||
customerOrderProcessor *processor.CustomerOrderProcessor
|
||||
enakGameAdminProcessor *processor.EnakGameAdminProcessor
|
||||
gameBudgetProcessor *processor.GameBudgetProcessor
|
||||
gameSessionProcessor *processor.GameSessionProcessor
|
||||
voucherAdminProcessor *processor.VoucherAdminProcessor
|
||||
voucherRedemptionProcessor *processor.VoucherRedemptionProcessor
|
||||
}
|
||||
|
||||
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
|
||||
@@ -418,6 +461,10 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
||||
|
||||
orderProcessor := processor.NewOrderProcessorImpl(repos.orderRepo, repos.orderItemRepo, repos.paymentRepo, repos.paymentOrderItemRepo, repos.productRepo, repos.paymentMethodRepo, repos.inventoryRepo, repos.inventoryMovementRepo, repos.productVariantRepo, repos.outletRepo, repos.customerRepo, repos.txManager, repos.productRecipeRepo, repos.ingredientRepo, inventoryMovementService, repos.productOutletPriceRepo)
|
||||
loyaltySettingsProcessor := processor.NewLoyaltySettingsProcessor(repos.loyaltySettingsRepo, repos.txManager)
|
||||
auditLogger := processor.NewAuditLogger(repository.NewAuditLogRepository(a.db))
|
||||
enakGameRepo := repository.NewEnakGameRepository(a.db)
|
||||
gameBudgetRepo := repository.NewGameBudgetRepository(a.db)
|
||||
voucherRepo := repository.NewVoucherRepository(a.db)
|
||||
// Earn EnakPoint and EnakCoin when an order becomes fully paid (docs/prd-point-coin.md F3)
|
||||
earningProcessor := processor.NewEarningProcessor(repository.NewEarningRepository(a.db), loyaltySettingsProcessor, processor.NewWalletProcessor(repos.walletRepo), repos.txManager)
|
||||
orderProcessor.SetLoyalty(earningProcessor)
|
||||
@@ -481,6 +528,17 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
||||
customerOutletProcessor: processor.NewCustomerOutletProcessor(repository.NewCustomerOutletRepository(a.db), loyaltySettingsProcessor),
|
||||
customerOrderProcessor: processor.NewCustomerOrderProcessor(repository.NewCustomerOrderRepository(a.db), earningProcessor),
|
||||
walletAdminProcessor: processor.NewWalletAdminProcessor(repository.NewWalletAdminRepository(a.db), repos.walletQueryRepo, processor.NewWalletProcessor(repos.walletRepo), loyaltySettingsProcessor, repos.txManager),
|
||||
// EnakGame (docs/rfc-enakgame.md)
|
||||
enakGameAdminProcessor: processor.NewEnakGameAdminProcessor(enakGameRepo, auditLogger, repos.txManager),
|
||||
gameBudgetProcessor: processor.NewGameBudgetProcessor(gameBudgetRepo, auditLogger, repos.txManager),
|
||||
gameSessionProcessor: processor.NewGameSessionProcessor(repository.NewWalletMoveRepository(a.db), enakGameRepo,
|
||||
repository.NewGameSessionRepository(a.db), gameBudgetRepo, repository.NewGameEventRepository(a.db), repository.NewGameRewardCounterRepository(a.db), loyaltySettingsProcessor, repos.walletQueryRepo, processor.NewWalletProcessor(repos.walletRepo), auditLogger, repos.txManager),
|
||||
voucherAdminProcessor: processor.NewVoucherAdminProcessor(voucherRepo, auditLogger, repos.txManager),
|
||||
voucherRedemptionProcessor: processor.NewVoucherRedemptionProcessor(repository.NewWalletMoveRepository(a.db), voucherRepo,
|
||||
repository.NewVoucherRedemptionRepository(a.db), customerPinProcessor, repos.walletQueryRepo, processor.NewWalletProcessor(repos.walletRepo),
|
||||
// No provider has an adapter yet (EG-801): EXTERNAL vouchers stay out of the
|
||||
// catalog and cannot be redeemed until one is added here.
|
||||
processor.VoucherProviders{}, repos.txManager),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,6 +588,8 @@ type services struct {
|
||||
customerDeviceService *service.CustomerDeviceServiceImpl
|
||||
customerOutletService *service.CustomerOutletServiceImpl
|
||||
customerOrderService *service.CustomerOrderServiceImpl
|
||||
enakGameAdminService *service.EnakGameAdminServiceImpl
|
||||
enakGameCustomerService *service.EnakGameCustomerServiceImpl
|
||||
}
|
||||
|
||||
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
||||
@@ -618,6 +678,12 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
||||
customerDeviceService: service.NewCustomerDeviceService(processors.customerDeviceProcessor),
|
||||
customerOutletService: service.NewCustomerOutletService(processors.customerOutletProcessor),
|
||||
customerOrderService: service.NewCustomerOrderService(processors.customerOrderProcessor),
|
||||
enakGameAdminService: service.NewEnakGameAdminService(processors.enakGameAdminProcessor, processors.gameBudgetProcessor,
|
||||
processor.NewGameBudgetMetricsProcessor(repository.NewGameBudgetRepository(a.db), repository.NewGameBudgetMetricsRepository(a.db)),
|
||||
processor.NewGameEventProcessor(repository.NewGameEventRepository(a.db), repository.NewEnakGameRepository(a.db), repository.NewGameBudgetRepository(a.db),
|
||||
processor.NewAuditLogger(repository.NewAuditLogRepository(a.db)), repos.txManager),
|
||||
processors.voucherAdminProcessor),
|
||||
enakGameCustomerService: service.NewEnakGameCustomerService(processors.gameSessionProcessor, processors.voucherRedemptionProcessor),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user