2026-10-07 20:53:14 +07:00
package repository
import (
"context"
"sync"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/entities"
)
// EnakGame tables against Postgres (docs/rfc-enakgame.md §5): what the constraints
// refuse, and what the repositories do. Needs TEST_DATABASE_URL; see
// wallet_repository_test.go.
type enakGameFixture struct {
db * gorm . DB
games EnakGameRepository
sessions GameSessionRepository
budgets GameBudgetRepository
org uuid . UUID
other uuid . UUID
customer uuid . UUID
admin uuid . UUID
}
func newEnakGameFixture ( t * testing . T ) * enakGameFixture {
t . Helper ()
db := walletTestDB ( t )
f := & enakGameFixture {
db : db , games : NewEnakGameRepository ( db ), sessions : NewGameSessionRepository ( db ), budgets : NewGameBudgetRepository ( db ),
org : uuid . New (), other : uuid . New (), customer : uuid . New (), admin : uuid . New (),
}
orgs := [] uuid . UUID { f . org , f . other }
for _ , org := range orgs {
require . NoError ( t , db . Exec ( `INSERT INTO organizations (id, name, plan_type) VALUES (?, 'enakgame test', 'basic')` , org ). Error )
}
require . NoError ( t , db . Exec ( `INSERT INTO customers (id, organization_id, name) VALUES (?, ?, 'enakgame test')` , f . customer , f . org ). Error )
t . Cleanup ( func () {
for _ , q := range [] string {
`DELETE FROM game_session_rewards WHERE session_id IN (SELECT id FROM game_sessions WHERE organization_id IN ?)` ,
`DELETE FROM game_sessions WHERE organization_id IN ?` ,
`DELETE FROM game_reward_configs WHERE organization_id IN ?` ,
`DELETE FROM game_budgets WHERE organization_id IN ?` ,
`DELETE FROM games WHERE organization_id IN ?` ,
`DELETE FROM wallet_transactions WHERE organization_id IN ?` ,
`DELETE FROM customers WHERE organization_id IN ?` ,
`DELETE FROM organizations WHERE id IN ?` ,
} {
db . Exec ( q , orgs )
}
})
return f
}
func ( f * enakGameFixture ) game ( t * testing . T , org uuid . UUID , slug string ) * entities . Game {
t . Helper ()
game := & entities . Game {
Name : "Tap " + slug , Type : entities . GameTypeMinigame , OrganizationID : & org , Slug : & slug ,
Status : constants . GameStatusActive , EntryCost : 5 , SessionTTLSeconds : 600 ,
}
require . NoError ( t , f . games . CreateGame ( context . Background (), game ))
return game
}
func ( f * enakGameFixture ) config ( t * testing . T , game * entities . Game ) * entities . GameRewardConfig {
t . Helper ()
config := & entities . GameRewardConfig {
OrganizationID : * game . OrganizationID , GameID : game . ID , RewardType : constants . GameRewardTypeFixed ,
Rules : entities . JSONDocument ( `{"amount": 10}` ), MaxReward : 10 , CreatedBy : f . admin ,
}
require . NoError ( t , f . games . CreateRewardConfig ( context . Background (), config ))
return config
}
// walletTx writes a bare ledger row for the session foreign keys to point at.
// reverses is the row a refund reverses, nil otherwise.
func ( f * enakGameFixture ) walletTx ( t * testing . T , txType string , amount int64 , reverses * uuid . UUID ) uuid . UUID {
t . Helper ()
id := uuid . New ()
require . NoError ( t , f . db . Exec ( `
INSERT INTO wallet_transactions (id, organization_id, customer_id, currency, type, amount, balance_after,
reference_type, reference_id, reverses_transaction_id, description)
VALUES (?, ?, ?, 'COIN', ?, ?, 0, 'GAME_SESSION', ?, ?, 'test')` ,
id , f . org , f . customer , txType , amount , uuid . New (), reverses ). Error )
return id
}
func ( f * enakGameFixture ) session ( t * testing . T , game * entities . Game , config * entities . GameRewardConfig , expiresAt time . Time ) * entities . GameSession {
t . Helper ()
session := & entities . GameSession {
OrganizationID : f . org , CustomerID : f . customer , GameID : game . ID , RewardConfigID : config . ID ,
EntryCost : game . EntryCost , ExpiresAt : expiresAt , SpendTransactionID : f . walletTx ( t , constants . WalletTxTypeGameSpend , - game . EntryCost , nil ),
}
require . NoError ( t , f . sessions . CreateSession ( context . Background (), session ))
return session
}
func ( f * enakGameFixture ) budget ( t * testing . T , org uuid . UUID , scope string , start , end time . Time ) * entities . GameBudget {
t . Helper ()
budget := & entities . GameBudget {
OrganizationID : org , Scope : scope , Name : "Oktober" , PeriodStart : start , PeriodEnd : end , Amount : 100_000_000 , CreatedBy : f . admin ,
}
require . NoError ( t , f . budgets . CreateBudget ( context . Background (), budget ))
return budget
}
func date ( y int , m time . Month , d int ) time . Time { return time . Date ( y , m , d , 0 , 0 , 0 , 0 , time . UTC ) }
func TestEnakGameSchema_RejectsWhatTheRFCForbids ( t * testing . T ) {
f := newEnakGameFixture ( t )
game := f . game ( t , f . org , "tap" )
config := f . config ( t , game )
ctx := context . Background ()
rejected := func ( t * testing . T , err error , constraint string ) {
t . Helper ()
require . Error ( t , err )
assert . Contains ( t , err . Error (), constraint )
}
t . Run ( "ACTIVE game without organization" , func ( t * testing . T ) {
rejected ( t , f . db . Exec ( `INSERT INTO games (name, type, slug, entry_cost) VALUES ('x', 'MINIGAME', 'x', 1)` ). Error , "chk_games_enakgame_identity" )
})
t . Run ( "ACTIVE game without slug" , func ( t * testing . T ) {
rejected ( t , f . db . Exec ( `INSERT INTO games (name, type, organization_id, entry_cost) VALUES ('x', 'MINIGAME', ?, 1)` , f . org ). Error , "chk_games_enakgame_identity" )
})
t . Run ( "entry cost 0" , func ( t * testing . T ) {
rejected ( t , f . db . Exec ( `INSERT INTO games (name, type, organization_id, slug, entry_cost) VALUES ('x', 'MINIGAME', ?, 'free', 0)` , f . org ). Error , "chk_games_entry_cost" )
})
t . Run ( "no entry cost" , func ( t * testing . T ) {
rejected ( t , f . db . Exec ( `INSERT INTO games (name, type, organization_id, slug) VALUES ('x', 'MINIGAME', ?, 'none')` , f . org ). Error , "entry_cost" )
})
t . Run ( "same slug twice in an organization" , func ( t * testing . T ) {
rejected ( t , f . db . Exec ( `INSERT INTO games (name, type, organization_id, slug, entry_cost) VALUES ('x', 'MINIGAME', ?, 'tap', 1)` , f . org ). Error , "uq_games_org_slug" )
f . game ( t , f . other , "tap" ) // another organization may use it
})
t . Run ( "two ACTIVE configs for a game" , func ( t * testing . T ) {
second := f . config ( t , game )
ok , err := f . games . SetRewardConfigStatus ( ctx , f . org , config . ID , constants . GameRewardConfigStatusDraft , constants . GameRewardConfigStatusActive )
require . NoError ( t , err )
require . True ( t , ok )
_ , err = f . games . SetRewardConfigStatus ( ctx , f . org , second . ID , constants . GameRewardConfigStatusDraft , constants . GameRewardConfigStatusActive )
rejected ( t , err , "uq_game_reward_configs_active" )
})
t . Run ( "REFUNDED session without refund transaction" , func ( t * testing . T ) {
session := f . session ( t , game , config , time . Now (). Add ( time . Minute ))
rejected ( t , f . db . Exec ( `UPDATE game_sessions SET status = 'REFUNDED', refund_reason = 'SYSTEM_ERROR' WHERE id = ?` , session . ID ). Error , "chk_game_sessions_refund" )
rejected ( t , f . db . Exec ( `UPDATE game_sessions SET refund_transaction_id = spend_transaction_id, refund_reason = 'SYSTEM_ERROR' WHERE id = ?` , session . ID ). Error , "chk_game_sessions_refund" )
})
t . Run ( "two GLOBAL budgets with the same start" , func ( t * testing . T ) {
f . budget ( t , f . org , constants . GameBudgetScopeGlobal , date ( 2026 , 10 , 1 ), date ( 2026 , 10 , 31 ))
err := f . budgets . CreateBudget ( ctx , & entities . GameBudget {
OrganizationID : f . org , Scope : constants . GameBudgetScopeGlobal , Name : "x" ,
PeriodStart : date ( 2026 , 10 , 1 ), PeriodEnd : date ( 2026 , 10 , 15 ), Amount : 1 , CreatedBy : f . admin ,
})
assert . ErrorIs ( t , err , ErrGameBudgetPeriodTaken , "uq_game_budgets_global_period" )
f . budget ( t , f . org , constants . GameBudgetScopeEvent , date ( 2026 , 10 , 1 ), date ( 2026 , 10 , 7 ))
f . budget ( t , f . other , constants . GameBudgetScopeGlobal , date ( 2026 , 10 , 1 ), date ( 2026 , 10 , 31 ))
})
t . Run ( "same reward ledger row twice" , func ( t * testing . T ) {
session := f . session ( t , game , config , time . Now (). Add ( time . Minute ))
global := f . budget ( t , f . org , constants . GameBudgetScopeGlobal , date ( 2026 , 11 , 1 ), date ( 2026 , 11 , 30 ))
event := f . budget ( t , f . org , constants . GameBudgetScopeEvent , date ( 2026 , 11 , 1 ), date ( 2026 , 11 , 7 ))
reward := f . walletTx ( t , constants . WalletTxTypeGameReward , 10 , nil )
err := f . sessions . CreateSessionRewards ( ctx , [] entities . GameSessionReward {
{ SessionID : session . ID , BudgetID : global . ID , Amount : 10 , WalletTransactionID : reward },
{ SessionID : session . ID , BudgetID : event . ID , Amount : 10 , WalletTransactionID : reward },
})
rejected ( t , err , "uq_game_session_rewards_wallet_transaction" )
})
}
// D4: of two moves out of STARTED at once, exactly one gets the row.
func TestGameSessionRepository_ConcurrentTransitionsOnlyOneWins ( t * testing . T ) {
f := newEnakGameFixture ( t )
game := f . game ( t , f . org , "race" )
config := f . config ( t , game )
txm := NewTxManager ( f . db )
for round := 0 ; round < 5 ; round ++ {
session := f . session ( t , game , config , time . Now (). Add ( time . Minute ))
refund := f . walletTx ( t , constants . WalletTxTypeGameSpendRefund , game . EntryCost , & session . SpendTransactionID )
moves := [] func ( ctx context . Context ) ( bool , error ){
func ( ctx context . Context ) ( bool , error ) {
return f . sessions . CompleteSession ( ctx , session . ID , GameSessionCompletion {
Result : entities . JSONDocument ( `{"score": 10}` ), RewardTotal : 10 , EndedAt : time . Now (),
})
},
func ( ctx context . Context ) ( bool , error ) {
return f . sessions . RefundSession ( ctx , session . ID , refund , constants . GameSessionRefundSystemError , time . Now ())
},
func ( ctx context . Context ) ( bool , error ) { return f . sessions . ExpireSession ( ctx , session . ID , time . Now ()) },
}
var wg sync . WaitGroup
var mu sync . Mutex
won := 0
start := make ( chan struct {})
for _ , move := range moves {
move := move
wg . Add ( 1 )
go func () {
defer wg . Done ()
<- start
err := txm . WithTransaction ( context . Background (), func ( ctx context . Context ) error {
ok , err := move ( ctx )
if ok {
mu . Lock ()
won ++
mu . Unlock ()
}
return err
})
assert . NoError ( t , err )
}()
}
close ( start )
wg . Wait ()
assert . Equal ( t , 1 , won , "round %d" , round )
got , err := f . sessions . GetSession ( context . Background (), f . org , session . ID )
require . NoError ( t , err )
assert . NotEqual ( t , constants . GameSessionStatusStarted , got . Status )
assert . NotNil ( t , got . EndedAt )
ok , err := f . sessions . MarkCompletionFailed ( context . Background (), session . ID , time . Now ())
require . NoError ( t , err )
assert . False ( t , ok , "a session that left STARTED is not marked" )
}
}
func TestGameSessionRepository_ReadsAndMoves ( t * testing . T ) {
f := newEnakGameFixture ( t )
game := f . game ( t , f . org , "tap" )
config := f . config ( t , game )
ctx := context . Background ()
now := time . Now ()
expired := f . session ( t , game , config , now . Add ( - time . Minute ))
open := f . session ( t , game , config , now . Add ( time . Hour ))
got , err := f . sessions . GetCustomerSession ( ctx , f . customer , open . ID )
require . NoError ( t , err )
assert . Equal ( t , constants . GameSessionStatusStarted , got . Status )
assert . Equal ( t , game . EntryCost , got . EntryCost )
assert . Equal ( t , config . ID , got . RewardConfigID )
_ , err = f . sessions . GetCustomerSession ( ctx , uuid . New (), open . ID )
assert . ErrorIs ( t , err , ErrGameSessionNotFound , "another customer's session" )
_ , err = f . sessions . GetSession ( ctx , f . other , open . ID )
assert . ErrorIs ( t , err , ErrGameSessionNotFound , "another organization's session" )
bySpend , err := f . sessions . GetSessionBySpendTransaction ( ctx , open . SpendTransactionID )
require . NoError ( t , err )
assert . Equal ( t , open . ID , bySpend . ID )
due , err := f . sessions . ListExpiredSessions ( ctx , now , 10 )
require . NoError ( t , err )
require . Len ( t , due , 1 )
assert . Equal ( t , expired . ID , due [ 0 ]. ID )
byGame , err := f . sessions . ListOpenSessionsByGame ( ctx , f . org , game . ID , 10 )
require . NoError ( t , err )
assert . Len ( t , byGame , 2 )
marked , err := f . sessions . MarkCompletionFailed ( ctx , open . ID , now )
require . NoError ( t , err )
assert . True ( t , marked )
ok , err := f . sessions . CompleteSession ( ctx , open . ID , GameSessionCompletion {
Result : entities . JSONDocument ( `{"score": 120}` ), RewardBreakdown : entities . JSONDocument ( `{"base": 10}` ),
RewardTotal : 10 , Flagged : true , EndedAt : now ,
})
require . NoError ( t , err )
require . True ( t , ok )
got , err = f . sessions . GetSession ( ctx , f . org , open . ID )
require . NoError ( t , err )
assert . Equal ( t , constants . GameSessionStatusCompleted , got . Status )
assert . JSONEq ( t , `{"score": 120}` , string ( got . Result ))
assert . JSONEq ( t , `{"base": 10}` , string ( got . RewardBreakdown ))
assert . EqualValues ( t , 10 , got . RewardTotal )
assert . True ( t , got . Flagged )
assert . NotNil ( t , got . CompletionFailedAt )
2026-10-08 12:51:39 +07:00
sessions , total , err := f . sessions . ListCustomerSessions ( ctx , CustomerSessionFilter { CustomerID : f . customer , Limit : 1 })
2026-10-07 20:53:14 +07:00
require . NoError ( t , err )
assert . EqualValues ( t , 2 , total )
assert . Len ( t , sessions , 1 )
budget := f . budget ( t , f . org , constants . GameBudgetScopeGlobal , date ( 2026 , 10 , 1 ), date ( 2026 , 10 , 31 ))
reward := f . walletTx ( t , constants . WalletTxTypeGameReward , 10 , nil )
require . NoError ( t , f . sessions . CreateSessionRewards ( ctx , [] entities . GameSessionReward {
{ SessionID : open . ID , BudgetID : budget . ID , Amount : 10 , WalletTransactionID : reward },
}))
rewards , err := f . sessions . ListSessionRewards ( ctx , open . ID )
require . NoError ( t , err )
assert . Equal ( t , [] entities . GameSessionReward {{ SessionID : open . ID , BudgetID : budget . ID , Amount : 10 , WalletTransactionID : reward }}, rewards )
}
func TestEnakGameRepository_GamesAndRewardConfigs ( t * testing . T ) {
f := newEnakGameFixture ( t )
ctx := context . Background ()
game := f . game ( t , f . org , "tap" )
theirs := f . game ( t , f . other , "theirs" )
max := int64 ( 1000 )
game . ResultRules = entities . GameResultRules { MaxScore : & max , Outcomes : [] string { "WIN" , "LOSE" }}
game . EntryCost = 7
require . NoError ( t , f . games . UpdateGame ( ctx , game ))
got , err := f . games . GetGame ( ctx , f . org , game . ID )
require . NoError ( t , err )
assert . EqualValues ( t , 7 , got . EntryCost )
assert . Equal ( t , game . ResultRules , got . ResultRules )
_ , err = f . games . GetGame ( ctx , f . org , theirs . ID )
assert . ErrorIs ( t , err , ErrEnakGameNotFound , "another organization's game" )
theirs . OrganizationID = & f . org
assert . ErrorIs ( t , f . games . UpdateGame ( ctx , theirs ), ErrEnakGameNotFound )
assert . ErrorIs ( t , f . games . SetGameStatus ( ctx , f . org , theirs . ID , constants . GameStatusInactive ), ErrEnakGameNotFound )
games , total , err := f . games . ListGames ( ctx , EnakGameFilter { OrganizationID : f . org , Limit : 10 })
require . NoError ( t , err )
assert . EqualValues ( t , 1 , total , "only the organization's games; the archived old ones have none" )
assert . Equal ( t , game . ID , games [ 0 ]. ID )
require . NoError ( t , f . games . SetGameStatus ( ctx , f . org , game . ID , constants . GameStatusArchived ))
assert . ErrorIs ( t , f . games . SetGameStatus ( ctx , f . org , game . ID , constants . GameStatusActive ), ErrEnakGameNotFound , "an archived game never changes" )
assert . ErrorIs ( t , f . games . UpdateGame ( ctx , game ), ErrEnakGameNotFound )
games , _ , err = f . games . ListGames ( ctx , EnakGameFilter { OrganizationID : f . org , Statuses : [] string { constants . GameStatusActive }, Limit : 10 })
require . NoError ( t , err )
assert . Empty ( t , games )
// Reward configs: versions count up per game, only status ever changes.
other := f . game ( t , f . org , "other" )
v1 , v2 := f . config ( t , other ), f . config ( t , other )
assert . Equal ( t , 1 , v1 . Version )
assert . Equal ( t , 2 , v2 . Version )
assert . Equal ( t , 1 , f . config ( t , game ). Version , "per game" )
err = f . games . CreateRewardConfig ( ctx , & entities . GameRewardConfig {
OrganizationID : f . other , GameID : other . ID , RewardType : constants . GameRewardTypeFixed ,
Rules : entities . JSONDocument ( `{}` ), CreatedBy : f . admin ,
})
assert . ErrorIs ( t , err , ErrEnakGameNotFound , "a config for another organization's game" )
_ , err = f . games . GetActiveRewardConfig ( ctx , f . org , other . ID )
assert . ErrorIs ( t , err , ErrGameRewardConfigNotFound )
ok , err := f . games . SetRewardConfigStatus ( ctx , f . org , v2 . ID , constants . GameRewardConfigStatusDraft , constants . GameRewardConfigStatusActive )
require . NoError ( t , err )
require . True ( t , ok )
ok , err = f . games . SetRewardConfigStatus ( ctx , f . org , v2 . ID , constants . GameRewardConfigStatusDraft , constants . GameRewardConfigStatusActive )
require . NoError ( t , err )
assert . False ( t , ok , "no longer DRAFT" )
ok , err = f . games . SetRewardConfigStatus ( ctx , f . other , v1 . ID , constants . GameRewardConfigStatusDraft , constants . GameRewardConfigStatusRetired )
require . NoError ( t , err )
assert . False ( t , ok , "another organization" )
active , err := f . games . GetActiveRewardConfig ( ctx , f . org , other . ID )
require . NoError ( t , err )
assert . Equal ( t , v2 . ID , active . ID )
assert . JSONEq ( t , `{"amount": 10}` , string ( active . Rules ))
configs , err := f . games . ListRewardConfigs ( ctx , f . org , other . ID )
require . NoError ( t , err )
require . Len ( t , configs , 2 )
assert . Equal ( t , [] int { 2 , 1 }, [] int { configs [ 0 ]. Version , configs [ 1 ]. Version })
_ , err = f . games . GetRewardConfig ( ctx , f . other , v1 . ID )
assert . ErrorIs ( t , err , ErrGameRewardConfigNotFound )
// LockGame takes the row lock: a second locker waits for the first transaction.
txm := NewTxManager ( f . db )
locked := make ( chan struct {})
release := make ( chan struct {})
go func () {
_ = txm . WithTransaction ( ctx , func ( ctx context . Context ) error {
_ , err := f . games . LockGame ( ctx , f . org , other . ID )
assert . NoError ( t , err )
close ( locked )
<- release
return nil
})
}()
<- locked
acquired := make ( chan struct {})
go func () {
_ = txm . WithTransaction ( ctx , func ( ctx context . Context ) error {
_ , err := f . games . LockGame ( ctx , f . org , other . ID )
assert . NoError ( t , err )
return nil
})
close ( acquired )
}()
select {
case <- acquired :
t . Fatal ( "the second lock did not wait" )
case <- time . After ( 200 * time . Millisecond ):
}
close ( release )
<- acquired
}
func TestGameBudgetRepository ( t * testing . T ) {
f := newEnakGameFixture ( t )
ctx := context . Background ()
// A late evening in Jakarta is still the previous day in UTC: the date is taken as
// given, not converted.
jakarta := time . FixedZone ( "WIB" , 7 * 3600 )
october := f . budget ( t , f . org , constants . GameBudgetScopeGlobal ,
time . Date ( 2026 , 10 , 1 , 0 , 30 , 0 , 0 , jakarta ), time . Date ( 2026 , 10 , 31 , 23 , 30 , 0 , 0 , jakarta ))
f . budget ( t , f . org , constants . GameBudgetScopeGlobal , date ( 2026 , 11 , 1 ), date ( 2026 , 11 , 30 ))
event := f . budget ( t , f . org , constants . GameBudgetScopeEvent , date ( 2026 , 10 , 10 ), date ( 2026 , 10 , 20 ))
f . budget ( t , f . other , constants . GameBudgetScopeGlobal , date ( 2026 , 10 , 1 ), date ( 2026 , 10 , 31 ))
got , err := f . budgets . GetBudget ( ctx , f . org , october . ID )
require . NoError ( t , err )
assert . Equal ( t , "2026-10-01" , got . PeriodStart . Format ( "2006-01-02" ))
assert . Equal ( t , "2026-10-31" , got . PeriodEnd . Format ( "2006-01-02" ))
assert . JSONEq ( t , `{}` , string ( got . Thresholds ))
_ , err = f . budgets . GetBudget ( ctx , f . other , october . ID )
assert . ErrorIs ( t , err , ErrGameBudgetNotFound )
for day , want := range map [ time . Time ] uuid . UUID {
date ( 2026 , 10 , 1 ): october . ID ,
date ( 2026 , 10 , 15 ): october . ID ,
date ( 2026 , 10 , 31 ): october . ID ,
} {
got , err := f . budgets . GetGlobalBudgetOn ( ctx , f . org , day )
require . NoError ( t , err )
assert . Equal ( t , want , got . ID , day )
}
_ , err = f . budgets . GetGlobalBudgetOn ( ctx , f . org , date ( 2026 , 9 , 30 ))
assert . ErrorIs ( t , err , ErrGameBudgetNotFound )
event . Amount = 5_000_000
event . Thresholds = entities . JSONDocument ( `{"warning": 70, "critical": 90}` )
require . NoError ( t , f . budgets . UpdateBudget ( ctx , event ))
got , err = f . budgets . GetBudget ( ctx , f . org , event . ID )
require . NoError ( t , err )
assert . EqualValues ( t , 5_000_000 , got . Amount )
assert . JSONEq ( t , `{"warning": 70, "critical": 90}` , string ( got . Thresholds ))
event . OrganizationID = f . other
assert . ErrorIs ( t , f . budgets . UpdateBudget ( ctx , event ), ErrGameBudgetNotFound )
budgets , total , err := f . budgets . ListBudgets ( ctx , GameBudgetFilter { OrganizationID : f . org , Scope : constants . GameBudgetScopeGlobal , Limit : 10 })
require . NoError ( t , err )
assert . EqualValues ( t , 2 , total )
assert . Equal ( t , "2026-11-01" , budgets [ 0 ]. PeriodStart . Format ( "2006-01-02" ), "latest period first" )
}
// EG-701: what game_events refuses.
func TestGameEventSchema_RejectsWhatTheRFCForbids ( t * testing . T ) {
f := newEnakGameFixture ( t )
event := f . budget ( t , f . org , constants . GameBudgetScopeEvent , date ( 2026 , 10 , 1 ), date ( 2026 , 10 , 7 ))
t . Cleanup ( func () { f . db . Exec ( `DELETE FROM game_events WHERE organization_id = ?` , f . org ) })
insert := func ( budget any , start , end string ) error {
return f . db . Exec ( `INSERT INTO game_events (organization_id, name, slug, start_at, end_at, budget_id)
VALUES (?, 'e', ?, ?, ?, ?)` , f . org , uuid . NewString (), start , end , budget ). Error
}
err := insert ( nil , "2026-10-01T00:00:00Z" , "2026-10-02T00:00:00Z" )
require . Error ( t , err )
assert . Contains ( t , err . Error (), "budget_id" )
err = insert ( event . ID , "2026-10-02T00:00:00Z" , "2026-10-02T00:00:00Z" )
require . Error ( t , err )
assert . Contains ( t , err . Error (), "chk_game_events_period" )
err = insert ( event . ID , "2026-10-02T00:00:00Z" , "2026-10-01T00:00:00Z" )
require . Error ( t , err )
assert . Contains ( t , err . Error (), "chk_game_events_period" )
require . NoError ( t , insert ( event . ID , "2026-10-01T00:00:00Z" , "2026-10-02T00:00:00Z" ))
}