package entities import ( "database/sql/driver" "encoding/json" "errors" "time" "github.com/google/uuid" ) // JSONDocument is a JSONB column kept as raw JSON, for documents whose shape depends // on another column or is only read back as a whole. Empty is SQL NULL. type JSONDocument json.RawMessage func (d JSONDocument) Value() (driver.Value, error) { if len(d) == 0 { return nil, nil } return string(d), nil } func (d *JSONDocument) Scan(value interface{}) error { switch v := value.(type) { case nil: *d = nil case []byte: *d = append(JSONDocument(nil), v...) case string: *d = JSONDocument(v) default: return errors.New("JSONDocument: unsupported type") } return nil } func (d JSONDocument) MarshalJSON() ([]byte, error) { if len(d) == 0 { return []byte("null"), nil } return d, nil } func (d *JSONDocument) UnmarshalJSON(data []byte) error { *d = append(JSONDocument(nil), data...) return nil } // GameResultRules are a game's limits on a result (docs/rfc-enakgame.md §5.1, §7.2) // and its daily reward limit (§5.10, §9). A limit left out does not apply. type GameResultRules struct { MaxScore *int64 `json:"max_score,omitempty"` MinDurationSeconds *int64 `json:"min_duration_seconds,omitempty"` MaxScorePerSecond *float64 `json:"max_score_per_second,omitempty"` // The outcomes a result may report. Empty accepts any. Outcomes []string `json:"outcomes,omitempty"` // EnakCoin the game may give out per day, in Asia/Jakarta. DailyRewardLimit *int64 `json:"daily_reward_limit,omitempty"` } func (r GameResultRules) Value() (driver.Value, error) { b, err := json.Marshal(r) if err != nil { return nil, err } return string(b), nil } func (r *GameResultRules) Scan(value interface{}) error { *r = GameResultRules{} switch v := value.(type) { case nil: return nil case []byte: return json.Unmarshal(v, r) case string: return json.Unmarshal([]byte(v), r) default: return errors.New("GameResultRules: unsupported type") } } // GameRewardConfig is one version of how a game computes its base reward (§5.2, §8). // Never updated apart from Status (D7): a change is a new version. type GameRewardConfig struct { ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id"` GameID uuid.UUID `gorm:"type:uuid;not null" json:"game_id"` Version int `gorm:"not null" json:"version"` // FIXED, SCORE_BASED, OUTCOME_BASED or PROBABILITY. RewardType string `gorm:"size:30;not null" json:"reward_type"` // Shape per RewardType in §8. Rules JSONDocument `gorm:"type:jsonb;not null" json:"rules"` MaxReward int64 `gorm:"not null" json:"max_reward"` // DRAFT, ACTIVE or RETIRED. Status string `gorm:"size:20;not null" json:"status"` EffectiveAt *time.Time `json:"effective_at"` CreatedBy uuid.UUID `gorm:"type:uuid;not null" json:"created_by"` Reason *string `gorm:"size:255" json:"reason"` // Set together on a version made by accepting a Budget Controller recommendation // (§10): the admin's version it scales, by how much, and for which budget. BaseConfigID *uuid.UUID `gorm:"type:uuid" json:"base_config_id"` Multiplier *float64 `gorm:"type:numeric(6,4)" json:"multiplier"` BudgetID *uuid.UUID `gorm:"type:uuid" json:"budget_id"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` } func (GameRewardConfig) TableName() string { return "game_reward_configs" } // GameSession is one play of a game by a customer (§5.3): STARTED, then COMPLETED, // REFUNDED or EXPIRED, each move a conditional update (D4). type GameSession struct { ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id"` CustomerID uuid.UUID `gorm:"type:uuid;not null" json:"customer_id"` GameID uuid.UUID `gorm:"type:uuid;not null" json:"game_id"` // Snapshots taken at start (D7, P3). RewardConfigID uuid.UUID `gorm:"type:uuid;not null" json:"reward_config_id"` EntryCost int64 `gorm:"not null" json:"entry_cost"` Status string `gorm:"size:20;not null" json:"status"` StartedAt time.Time `gorm:"not null" json:"started_at"` ExpiresAt time.Time `gorm:"not null" json:"expires_at"` EndedAt *time.Time `json:"ended_at"` // What the client sent: data only, never a reward amount (P1). Result JSONDocument `gorm:"type:jsonb" json:"result"` RewardBreakdown JSONDocument `gorm:"type:jsonb" json:"reward_breakdown"` RewardTotal int64 `gorm:"not null;default:0" json:"reward_total"` Flagged bool `gorm:"not null;default:false" json:"flagged"` SpendTransactionID uuid.UUID `gorm:"type:uuid;not null" json:"spend_transaction_id"` RefundTransactionID *uuid.UUID `gorm:"type:uuid" json:"refund_transaction_id"` // SYSTEM_ERROR or GAME_DEACTIVATED. RefundReason *string `gorm:"size:30" json:"refund_reason"` CompletionFailedAt *time.Time `json:"completion_failed_at"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` } func (GameSession) TableName() string { return "game_sessions" } // GameSessionReward is the part of a session's reward one budget paid for, with its // own ledger row (D6). type GameSessionReward struct { SessionID uuid.UUID `gorm:"type:uuid;primary_key" json:"session_id"` BudgetID uuid.UUID `gorm:"type:uuid;primary_key" json:"budget_id"` Amount int64 `gorm:"not null" json:"amount"` WalletTransactionID uuid.UUID `gorm:"type:uuid;not null" json:"wallet_transaction_id"` } func (GameSessionReward) TableName() string { return "game_session_rewards" } // GameBudget is what an organization may spend on EnakGame rewards over a period // (§5.6): GLOBAL for base rewards, EVENT for what an event adds. type GameBudget struct { ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id"` // GLOBAL or EVENT. Scope string `gorm:"size:20;not null" json:"scope"` Name string `gorm:"size:255;not null" json:"name"` // Dates, inclusive at both ends. PeriodStart time.Time `gorm:"type:date;not null" json:"period_start"` PeriodEnd time.Time `gorm:"type:date;not null" json:"period_end"` // Rupiah. Amount int64 `gorm:"not null" json:"amount"` // {"warning": 70, "critical": 90}. Thresholds JSONDocument `gorm:"type:jsonb;not null;default:'{}'" json:"thresholds"` ExhaustionPolicy *string `gorm:"size:30" json:"exhaustion_policy"` CreatedBy uuid.UUID `gorm:"type:uuid;not null" json:"created_by"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` } func (GameBudget) TableName() string { return "game_budgets" } // GameEvent is a period in which some games pay more, the extra paid by the event's // own budget (docs/rfc-enakgame.md §5.5, D6). Event and campaign are the same thing. type GameEvent struct { ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id"` Name string `gorm:"size:255;not null" json:"name"` Slug string `gorm:"size:100;not null" json:"slug"` Description *string `gorm:"type:text" json:"description"` BannerURL *string `gorm:"column:banner_url;size:500" json:"banner_url"` StartAt time.Time `gorm:"not null" json:"start_at"` EndAt time.Time `gorm:"not null" json:"end_at"` Timezone string `gorm:"size:50;not null" json:"timezone"` // DRAFT, ACTIVE, ENDED or CANCELLED. Status string `gorm:"size:20;not null" json:"status"` Priority int `gorm:"not null" json:"priority"` // NUMERIC(5,2): at most two decimals. Multiplier *float64 `gorm:"type:numeric(5,2)" json:"multiplier"` Bonus *int64 `json:"bonus"` BudgetID uuid.UUID `gorm:"type:uuid;not null" json:"budget_id"` RewardLimit *int64 `json:"reward_limit"` UserDailyLimit *int64 `json:"user_daily_limit"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` } func (GameEvent) TableName() string { return "game_events" }