436 lines
16 KiB
Go
436 lines
16 KiB
Go
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"`
|
||
|
|
CreatedAt time.Time `json:"created_at"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// GameBudgetThresholds are percents of utilization or forecast (PRD §8, §32).
|
||
|
|
type GameBudgetThresholds struct {
|
||
|
|
Warning *int64 `json:"warning,omitempty"`
|
||
|
|
Critical *int64 `json:"critical,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"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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"`
|
||
|
|
}
|
||
|
|
|
||
|
|
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"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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"`
|
||
|
|
}
|