2026-10-07 20:53:14 +07:00
|
|
|
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"`
|
2026-10-07 21:18:31 +07:00
|
|
|
// 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"`
|
2026-10-07 20:53:14 +07:00
|
|
|
}
|
|
|
|
|
|
2026-10-07 21:18:31 +07:00
|
|
|
// GameBudgetThresholds are percents of utilization or forecast (PRD §8, §32), and the
|
|
|
|
|
// guardrails of the Budget Controller (PRD §31).
|
2026-10-07 20:53:14 +07:00
|
|
|
type GameBudgetThresholds struct {
|
|
|
|
|
Warning *int64 `json:"warning,omitempty"`
|
|
|
|
|
Critical *int64 `json:"critical,omitempty"`
|
2026-10-07 21:18:31 +07:00
|
|
|
// 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"`
|
2026-10-07 20:53:14 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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"`
|
2026-10-07 21:31:56 +07:00
|
|
|
// 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"`
|
2026-10-07 20:53:14 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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"`
|
2026-10-07 21:31:56 +07:00
|
|
|
// 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"`
|
2026-10-07 20:53:14 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-07 21:18:31 +07:00
|
|
|
// 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"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-07 20:53:14 +07:00
|
|
|
// 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"`
|
|
|
|
|
}
|