package models import ( "encoding/json" "time" "github.com/google/uuid" "apskel-pos-be/internal/entities" ) // EnakGameInput is what an admin sends to create or change a game // (docs/rfc-enakgame.md §11). On a change, fields left out keep their value. type EnakGameInput struct { Name string `json:"name"` // SPIN, RAFFLE or MINIGAME; MINIGAME when left out on create. Type string `json:"type"` Slug string `json:"slug"` Description *string `json:"description"` ThumbnailURL *string `json:"thumbnail_url"` GameURL *string `json:"game_url"` Version *string `json:"version"` // Create only: DRAFT (default), ACTIVE or INACTIVE. Changed later through the // status endpoint. Status string `json:"status"` EntryCost int64 `json:"entry_cost"` // 600 when left out on create. SessionTTLSeconds int `json:"session_ttl_seconds"` ResultRules entities.GameResultRules `json:"result_rules"` } type EnakGame struct { ID uuid.UUID `json:"id"` Name string `json:"name"` Type string `json:"type"` Slug string `json:"slug"` Description *string `json:"description"` ThumbnailURL *string `json:"thumbnail_url"` GameURL *string `json:"game_url"` Version *string `json:"version"` Status string `json:"status"` EntryCost int64 `json:"entry_cost"` SessionTTLSeconds int `json:"session_ttl_seconds"` ResultRules entities.GameResultRules `json:"result_rules"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // EnakGameListQuery filters the admin game list. ARCHIVED games are left out unless // asked for by status. type EnakGameListQuery struct { Status string `form:"status"` Search string `form:"search"` Page int `form:"page"` Limit int `form:"limit"` } type EnakGameStatusInput struct { Status string `json:"status"` Reason *string `json:"reason"` } // GameRewardConfigInput is a new version of a game's reward configuration (§5.2). type GameRewardConfigInput struct { RewardType string `json:"reward_type"` Rules json.RawMessage `json:"rules"` MaxReward int64 `json:"max_reward"` EffectiveAt *time.Time `json:"effective_at"` Reason *string `json:"reason"` } type GameRewardConfigActivateInput struct { Reason *string `json:"reason"` } type GameRewardConfig struct { ID uuid.UUID `json:"id"` GameID uuid.UUID `json:"game_id"` Version int `json:"version"` RewardType string `json:"reward_type"` Rules json.RawMessage `json:"rules"` MaxReward int64 `json:"max_reward"` Status string `json:"status"` EffectiveAt *time.Time `json:"effective_at"` CreatedBy uuid.UUID `json:"created_by"` Reason *string `json:"reason"` // Set on a version made by accepting a Budget Controller recommendation: the // admin's version it scales, by how much, and for which budget. BaseConfigID *uuid.UUID `json:"base_config_id"` Multiplier *float64 `json:"multiplier"` BudgetID *uuid.UUID `json:"budget_id"` CreatedAt time.Time `json:"created_at"` } // GameBudgetThresholds are percents of utilization or forecast (PRD §8, §32), and the // guardrails of the Budget Controller (PRD §31). type GameBudgetThresholds struct { Warning *int64 `json:"warning,omitempty"` Critical *int64 `json:"critical,omitempty"` // Percent one recommendation may move rewards by, either way. MaxStepPercent *int64 `json:"max_step_percent,omitempty"` // Bounds of a game's multiplier, in percent of the configuration its admin wrote. MinMultiplierPercent *int64 `json:"min_multiplier_percent,omitempty"` MaxMultiplierPercent *int64 `json:"max_multiplier_percent,omitempty"` // Days after an accepted recommendation before the organization gets another. CooldownDays *int64 `json:"cooldown_days,omitempty"` } // GameBudgetInput creates or changes a budget (§5.6). Dates are YYYY-MM-DD and // inclusive. On a change, fields left out keep their value; the scope never changes. type GameBudgetInput struct { Scope string `json:"scope"` Name string `json:"name"` PeriodStart string `json:"period_start"` PeriodEnd string `json:"period_end"` Amount int64 `json:"amount"` Thresholds GameBudgetThresholds `json:"thresholds"` } type GameBudget struct { ID uuid.UUID `json:"id"` Scope string `json:"scope"` Name string `json:"name"` PeriodStart string `json:"period_start"` PeriodEnd string `json:"period_end"` Amount int64 `json:"amount"` Thresholds GameBudgetThresholds `json:"thresholds"` CreatedBy uuid.UUID `json:"created_by"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type GameBudgetListQuery struct { Scope string `form:"scope"` Page int `form:"page"` Limit int `form:"limit"` } // CustomerEnakGame is a game as the customer app sees it: what it costs and where to // load it, without the validation limits. type CustomerEnakGame struct { ID uuid.UUID `json:"id"` Slug string `json:"slug"` Name string `json:"name"` Description *string `json:"description"` ThumbnailURL *string `json:"thumbnail_url"` GameURL *string `json:"game_url"` Version *string `json:"version"` EntryCost int64 `json:"entry_cost"` SessionTTLSeconds int `json:"session_ttl_seconds"` // The events making the game pay more right now, highest priority first. Events []CustomerGameEvent `json:"events"` // What a PROBABILITY game, such as a spin wheel, can pay, in the order of its // configuration. Left out for the other reward types. Prizes []GamePrize `json:"prizes,omitempty"` } // GamePrize is one entry of a PROBABILITY game, such as a segment of a spin wheel, as // the customer sees it: never its odds. type GamePrize struct { // 1 for the first entry of the configuration. Entry int `json:"entry"` Label *string `json:"label"` Amount int64 `json:"amount"` } // GameSessionStart is the response of starting a game (§7.1). type GameSessionStart struct { SessionID uuid.UUID `json:"session_id"` GameID uuid.UUID `json:"game_id"` EntryCost int64 `json:"entry_cost"` ExpiresAt time.Time `json:"expires_at"` CoinBalance int64 `json:"coin_balance"` // True when this repeats an earlier start with the same Idempotency-Key. Replayed bool `json:"replayed"` } // CustomerGameSession is a session as its customer sees it. The reward breakdown, // the RNG draw and whether it was flagged stay internal. type CustomerGameSession struct { ID uuid.UUID `json:"id"` GameID uuid.UUID `json:"game_id"` Status string `json:"status"` EntryCost int64 `json:"entry_cost"` RewardTotal int64 `json:"reward_total"` StartedAt time.Time `json:"started_at"` ExpiresAt time.Time `json:"expires_at"` EndedAt *time.Time `json:"ended_at"` RefundReason *string `json:"refund_reason"` } // GameSessionListQuery filters a customer's play history. The game client uses // game_id with status=STARTED to find the play it was running before a reload. type GameSessionListQuery struct { GameID string `form:"game_id"` // STARTED, COMPLETED, REFUNDED or EXPIRED; empty for all. Status string `form:"status"` Page int `form:"page"` Limit int `form:"limit"` } // GameSessionCompleteInput is what the client reports at the end of a play (§7.2): data // only. Anything else it sends, a reward amount above all, is ignored (P1). type GameSessionCompleteInput struct { Score *int64 `json:"score"` Outcome *string `json:"outcome"` Data json.RawMessage `json:"data"` } // GameSessionCompletion is the response of completing a session. Sending the same // completion again returns the same response. type GameSessionCompletion struct { SessionID uuid.UUID `json:"session_id"` // COMPLETED, or REFUNDED when the game was turned off during the play. Status string `json:"status"` RefundReason *string `json:"refund_reason,omitempty"` RewardTotal int64 `json:"reward_total"` // The parts of the reward that are safe to show. Reward GameSessionRewardParts `json:"reward"` CoinBalance int64 `json:"coin_balance"` // The daily limits that made the reward smaller than earned: USER_DAILY, // GAME_DAILY or GLOBAL_DAILY. LimitedBy []string `json:"limited_by,omitempty"` // The entry a PROBABILITY game drew, for a spin wheel to stop on. Its amount is // the base reward, before events and limits. Prize *GamePrize `json:"prize,omitempty"` } type GameSessionRewardParts struct { Base int64 `json:"base"` // Added by events; 0 until events exist. Event int64 `json:"event"` } // VoucherInput creates or changes a voucher (§5.7). On a change, fields left out keep // their value; the stock mode never changes. type VoucherInput struct { Name string `json:"name"` Description *string `json:"description"` ImageURL *string `json:"image_url"` VoucherType string `json:"voucher_type"` FaceValue int64 `json:"face_value"` PointCost int64 `json:"point_cost"` BusinessCost *int64 `json:"business_cost"` StockMode string `json:"stock_mode"` // STATIC only. Stock *int64 `json:"stock"` // EXTERNAL only. Provider *string `json:"provider"` ProviderRef *string `json:"provider_ref"` MaxPerCustomer *int `json:"max_per_customer"` ValidFrom *time.Time `json:"valid_from"` ValidUntil *time.Time `json:"valid_until"` Terms json.RawMessage `json:"terms"` // Create only: DRAFT (default), ACTIVE or INACTIVE. Status string `json:"status"` } type Voucher struct { ID uuid.UUID `json:"id"` Name string `json:"name"` Description *string `json:"description"` ImageURL *string `json:"image_url"` VoucherType string `json:"voucher_type"` FaceValue int64 `json:"face_value"` PointCost int64 `json:"point_cost"` BusinessCost *int64 `json:"business_cost"` StockMode string `json:"stock_mode"` Stock *int64 `json:"stock"` Provider *string `json:"provider"` ProviderRef *string `json:"provider_ref"` MaxPerCustomer *int `json:"max_per_customer"` ValidFrom *time.Time `json:"valid_from"` ValidUntil *time.Time `json:"valid_until"` Terms json.RawMessage `json:"terms"` Status string `json:"status"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type VoucherListQuery struct { Status string `form:"status"` Search string `form:"search"` Page int `form:"page"` Limit int `form:"limit"` } type VoucherStatusInput struct { Status string `json:"status"` Reason *string `json:"reason"` } // VoucherCodeImportResult says what an import of codes did. type VoucherCodeImportResult struct { Imported int `json:"imported"` // Codes already in the pool or repeated in the file, skipped. DuplicateCount int `json:"duplicate_count"` Duplicates []string `json:"duplicates"` // Lines that could not be read, skipped. Invalid []VoucherCodeImportProblem `json:"invalid"` } type VoucherCodeImportProblem struct { Line int `json:"line"` Reason string `json:"reason"` } type VoucherCodeListQuery struct { Status string `form:"status"` Page int `form:"page"` Limit int `form:"limit"` } type VoucherCode struct { ID uuid.UUID `json:"id"` Code string `json:"code"` Status string `json:"status"` RedemptionID *uuid.UUID `json:"redemption_id"` ExpiresAt *time.Time `json:"expires_at"` CreatedAt time.Time `json:"created_at"` } // VoucherCodes is a pool's codes: how many in each status, and one page of them. type VoucherCodes struct { Counts map[string]int64 `json:"counts"` Codes PaginatedResponse[VoucherCode] `json:"codes"` } // CustomerVoucher is a voucher in the customer app's catalog. Available is how many // are left, without saying how many were redeemed or expired. type CustomerVoucher struct { ID uuid.UUID `json:"id"` Name string `json:"name"` Description *string `json:"description"` ImageURL *string `json:"image_url"` VoucherType string `json:"voucher_type"` FaceValue int64 `json:"face_value"` PointCost int64 `json:"point_cost"` MaxPerCustomer *int `json:"max_per_customer"` ValidUntil *time.Time `json:"valid_until"` Terms json.RawMessage `json:"terms"` Available *int64 `json:"available"` } // CustomerVoucherRedemption is a redemption as its customer sees it, with the code to // use. The response of a redeem adds the EnakPoint balance. type CustomerVoucherRedemption struct { ID uuid.UUID `json:"id"` VoucherID uuid.UUID `json:"voucher_id"` VoucherName string `json:"voucher_name"` VoucherImageURL *string `json:"voucher_image_url"` VoucherType string `json:"voucher_type"` Status string `json:"status"` FaceValue int64 `json:"face_value"` PointCost int64 `json:"point_cost"` Code *string `json:"code"` CodeExpiresAt *time.Time `json:"code_expires_at"` CompletedAt *time.Time `json:"completed_at"` CreatedAt time.Time `json:"created_at"` } type VoucherRedeemResult struct { CustomerVoucherRedemption PointBalance int64 `json:"point_balance"` // True when this repeats an earlier redeem with the same Idempotency-Key. Replayed bool `json:"replayed"` } // GameBudgetMetrics is how a budget stands (docs/rfc-enakgame.md §10, PRD §8). Money is // in rupiah, percents have two decimals. type GameBudgetMetrics struct { BudgetID uuid.UUID `json:"budget_id"` Scope string `json:"scope"` PeriodStart string `json:"period_start"` PeriodEnd string `json:"period_end"` // The day, in Asia/Jakarta, the metrics are for. AsOf string `json:"as_of"` Amount int64 `json:"amount"` RealizedCost int64 `json:"realized_cost"` Remaining int64 `json:"remaining"` UtilizationPercent float64 `json:"utilization_percent"` // Average realized cost per day over the last WindowDays days, today included. DailyBurn int64 `json:"daily_burn"` WindowDays int64 `json:"window_days"` // Days of the period after today. RemainingDays int64 `json:"remaining_days"` ForecastCost int64 `json:"forecast_cost"` ForecastRemaining int64 `json:"forecast_remaining"` ForecastUtilizationPercent float64 `json:"forecast_utilization_percent"` CoinIssued int64 `json:"coin_issued"` // What the budget's rewards still hold, spendable: the most that can still turn // into cost. Exposure GameBudgetExposure `json:"exposure"` // The thresholds used: the budget's own, or the defaults for those it does not set. Thresholds GameBudgetThresholds `json:"thresholds"` // HEALTHY, WARNING, CRITICAL or EXHAUSTED. Status string `json:"status"` } type GameBudgetExposure struct { Coins int64 `json:"coins"` Points int64 `json:"points"` } // GameBudgetRecommendation is what the Budget Controller suggests for a global budget // (docs/rfc-enakgame.md §10, PRD §29–§31): a multiplier on every game's reward that // brings the forecast to the budget, within the guardrails. type GameBudgetRecommendation struct { BudgetID uuid.UUID `json:"budget_id"` // RECOMMENDED, NO_CHANGE, COOLDOWN, AT_LIMIT, INSUFFICIENT_DATA or OUT_OF_PERIOD. // Only RECOMMENDED can be accepted. State string `json:"state"` Message string `json:"message"` Metrics GameBudgetMetrics `json:"metrics"` // The guardrails used: the budget's own, or the defaults for those it does not set. Guardrails GameBudgetThresholds `json:"guardrails"` // The multiplier that would make the forecast meet the budget, before the // guardrails; nil when there is no cost to extrapolate. TargetMultiplier *float64 `json:"target_multiplier"` // The target within one step, rounded down to two decimals. Accepting sends it back. Multiplier float64 `json:"multiplier"` // When the organization may accept again, during a cooldown. CooldownUntil *time.Time `json:"cooldown_until,omitempty"` // The games whose reward would change, each with its new version. Games []GameRewardAdjustment `json:"games"` } // GameRewardAdjustment is how accepting a recommendation changes a game's reward: a // new version of its active configuration, with the base's amounts scaled. type GameRewardAdjustment struct { GameID uuid.UUID `json:"game_id"` GameName string `json:"game_name"` // The active version, and the admin's version both are measured against. RewardConfigID uuid.UUID `json:"reward_config_id"` Version int `json:"version"` BaseConfigID uuid.UUID `json:"base_config_id"` RewardType string `json:"reward_type"` CurrentMultiplier float64 `json:"current_multiplier"` NewMultiplier float64 `json:"new_multiplier"` CurrentRules json.RawMessage `json:"current_rules"` NewRules json.RawMessage `json:"new_rules"` CurrentMaxReward int64 `json:"current_max_reward"` NewMaxReward int64 `json:"new_max_reward"` } // GameBudgetRecommendationAcceptInput accepts the recommendation the admin saw. When // the recommendation changed since, nothing is applied. type GameBudgetRecommendationAcceptInput struct { Multiplier *float64 `json:"multiplier"` Reason *string `json:"reason"` } // GameBudgetRecommendationAccepted is what accepting made: one active version per // game. type GameBudgetRecommendationAccepted struct { BudgetID uuid.UUID `json:"budget_id"` Multiplier float64 `json:"multiplier"` RewardConfigs []GameRewardConfig `json:"reward_configs"` } // EnakGameAnalyticsQuery is a range of days in Asia/Jakarta, both ends included. type EnakGameAnalyticsQuery struct { From string `form:"from"` To string `form:"to"` // Games analytics only: one game instead of all. GameID string `form:"game_id"` } // EnakGameAnalytics is how an organization's games were played over a range of days // (PRD §36 Game), by the day each session started. type EnakGameAnalytics struct { From string `json:"from"` To string `json:"to"` Totals EnakGameStats `json:"totals"` Games []EnakGameGameStats `json:"games"` } type EnakGameGameStats struct { GameID uuid.UUID `json:"game_id"` GameName string `json:"game_name"` EnakGameStats } type EnakGameStats struct { // Sessions started, whatever became of them. Plays int64 `json:"plays"` Completed int64 `json:"completed"` Refunded int64 `json:"refunded"` Expired int64 `json:"expired"` Flagged int64 `json:"flagged"` // Customers who started at least one session. Players int64 `json:"players"` // Over completed sessions that reported a score; nil when none did. AverageScore *float64 `json:"average_score"` // EnakCoin per completed session, and per play. AverageReward float64 `json:"average_reward"` RewardPerPlay float64 `json:"reward_per_play"` CoinIssued int64 `json:"coin_issued"` // EnakCoin paid to start, and the part refunded. EntryCostPaid int64 `json:"entry_cost_paid"` CoinRefunded int64 `json:"coin_refunded"` } // EnakGameEconomyAnalytics is how EnakCoin and EnakPoint moved in an organization over // a range of days (PRD §36 Economy), from the ledger. type EnakGameEconomyAnalytics struct { From string `json:"from"` To string `json:"to"` Coin EnakGameCoinFlows `json:"coin"` Point EnakGamePointFlows `json:"point"` // Every ledger type that moved, for what the headline numbers leave out. ByType []WalletFlowTotals `json:"by_type"` } type EnakGameCoinFlows struct { // New EnakCoin: game rewards, earning less its reversals, migration and upward // adjustments. Generated int64 `json:"generated"` GameRewards int64 `json:"game_rewards"` // Entry costs, less the refunded ones. SpentOnGames int64 `json:"spent_on_games"` // Exchanged into EnakPoint. Exchanged int64 `json:"exchanged"` // SpentOnGames + Exchanged. Spent int64 `json:"spent"` Expired int64 `json:"expired"` // Held by customers at the end of the range. Outstanding int64 `json:"outstanding"` } type EnakGamePointFlows struct { // From shopping, less its reversals. Earned int64 `json:"earned"` // Received from exchanging EnakCoin. Exchanged int64 `json:"exchanged"` // Spent on vouchers, less the refunded redemptions. Redeemed int64 `json:"redeemed"` Expired int64 `json:"expired"` // Held by customers at the end of the range. Balance int64 `json:"balance"` } // WalletFlowTotals is what one ledger type moved in one currency. type WalletFlowTotals struct { Currency string `json:"currency"` Type string `json:"type"` Credit int64 `json:"credit"` Debit int64 `json:"debit"` Transactions int64 `json:"transactions"` } // GameEventInput creates or changes an event (§5.5). On a change, fields left out // keep their value. type GameEventInput struct { Name string `json:"name"` Slug string `json:"slug"` Description *string `json:"description"` BannerURL *string `json:"banner_url"` StartAt time.Time `json:"start_at"` EndAt time.Time `json:"end_at"` // Asia/Jakarta when left out. Timezone string `json:"timezone"` Priority int `json:"priority"` // At least 1, two decimals at most. 2 adds the base reward once more. Multiplier *float64 `json:"multiplier"` Bonus *int64 `json:"bonus"` // An EVENT budget of the organization; it pays what the event adds. BudgetID uuid.UUID `json:"budget_id"` RewardLimit *int64 `json:"reward_limit"` UserDailyLimit *int64 `json:"user_daily_limit"` GameIDs []uuid.UUID `json:"game_ids"` // Create only: DRAFT (default) or ACTIVE. Status string `json:"status"` } type GameEvent struct { ID uuid.UUID `json:"id"` Name string `json:"name"` Slug string `json:"slug"` Description *string `json:"description"` BannerURL *string `json:"banner_url"` StartAt time.Time `json:"start_at"` EndAt time.Time `json:"end_at"` Timezone string `json:"timezone"` Status string `json:"status"` Priority int `json:"priority"` Multiplier *float64 `json:"multiplier"` Bonus *int64 `json:"bonus"` BudgetID uuid.UUID `json:"budget_id"` RewardLimit *int64 `json:"reward_limit"` UserDailyLimit *int64 `json:"user_daily_limit"` GameIDs []uuid.UUID `json:"game_ids"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type GameEventListQuery struct { Status string `form:"status"` Page int `form:"page"` Limit int `form:"limit"` } type GameEventStatusInput struct { Status string `json:"status"` Reason *string `json:"reason"` } // CustomerGameEvent is an event running on a game, as the customer app shows it. type CustomerGameEvent struct { ID uuid.UUID `json:"id"` Name string `json:"name"` BannerURL *string `json:"banner_url"` Multiplier *float64 `json:"multiplier"` Bonus *int64 `json:"bonus"` EndAt time.Time `json:"end_at"` }