package repository import ( "context" "sync" "sync/atomic" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "apskel-pos-be/internal/constants" ) // The Economy Guard counters against Postgres (docs/rfc-enakgame.md ยง9). Needs // TEST_DATABASE_URL; see wallet_repository_test.go. func TestGameRewardCounterRepository_ConcurrentConsumeNeverPassesTheLimit(t *testing.T) { db := walletTestDB(t) org, game := uuid.New(), uuid.New() t.Cleanup(func() { db.Exec(`DELETE FROM game_reward_counters WHERE organization_id = ?`, org) }) repo := NewGameRewardCounterRepository(db) txm := NewTxManager(db) day := time.Date(2026, 10, 7, 0, 0, 0, 0, time.UTC) for _, round := range []struct { each int64 wantTotal int64 }{{10, 100}, {7, 100}} { var granted atomic.Int64 var wg sync.WaitGroup begin := make(chan struct{}) for i := 0; i < 20; i++ { wg.Add(1) go func() { defer wg.Done() <-begin require.NoError(t, txm.WithTransaction(context.Background(), func(ctx context.Context) error { got, err := repo.Consume(ctx, org, constants.GameRewardScopeGame, game, day, round.each, 100) granted.Add(got) return err })) }() } close(begin) wg.Wait() total, err := repo.Get(context.Background(), org, constants.GameRewardScopeGame, game, day) require.NoError(t, err) assert.EqualValues(t, round.wantTotal, total) if round.each == 10 { assert.EqualValues(t, 100, granted.Load(), "exactly what fits") } else { assert.Zero(t, granted.Load(), "nothing is left") } } } func TestGameRewardCounterRepository(t *testing.T) { db := walletTestDB(t) org, customer := uuid.New(), uuid.New() t.Cleanup(func() { db.Exec(`DELETE FROM game_reward_counters WHERE organization_id = ?`, org) }) repo := NewGameRewardCounterRepository(db) txm := NewTxManager(db) ctx := context.Background() today, tomorrow := time.Date(2026, 10, 7, 23, 0, 0, 0, time.FixedZone("WIB", 7*3600)), time.Date(2026, 10, 8, 0, 0, 0, 0, time.UTC) consume := func(day time.Time, amount, limit int64) int64 { t.Helper() var got int64 require.NoError(t, txm.WithTransaction(ctx, func(ctx context.Context) error { var err error got, err = repo.Consume(ctx, org, constants.GameRewardScopeUser, customer, day, amount, limit) return err })) return got } assert.EqualValues(t, 30, consume(today, 30, 40)) assert.EqualValues(t, 10, consume(today, 30, 40), "cut to what is left") assert.EqualValues(t, 0, consume(today, 5, 40)) assert.EqualValues(t, 5, consume(today, 5, 0), "no limit, still counted") assert.EqualValues(t, 30, consume(tomorrow, 30, 40), "a new day starts over") got, err := repo.Get(ctx, org, constants.GameRewardScopeUser, customer, today) require.NoError(t, err) assert.EqualValues(t, 45, got) require.NoError(t, txm.WithTransaction(ctx, func(ctx context.Context) error { return repo.Release(ctx, org, constants.GameRewardScopeUser, customer, today, 15) })) got, _ = repo.Get(ctx, org, constants.GameRewardScopeUser, customer, today) assert.EqualValues(t, 30, got) assert.Error(t, txm.WithTransaction(ctx, func(ctx context.Context) error { return repo.Release(ctx, org, constants.GameRewardScopeUser, customer, today, 31) }), "cannot release more than counted") // A rolled back reward leaves the counter as it was. _ = txm.WithTransaction(ctx, func(ctx context.Context) error { _, err := repo.Consume(ctx, org, constants.GameRewardScopeUser, customer, today, 10, 0) require.NoError(t, err) return assert.AnError }) got, _ = repo.Get(ctx, org, constants.GameRewardScopeUser, customer, today) assert.EqualValues(t, 30, got) _, err = repo.Consume(ctx, org, constants.GameRewardScopeUser, customer, today, 1, 0) assert.ErrorIs(t, err, ErrGameRewardCounterTxRequired) got, err = repo.Get(ctx, org, constants.GameRewardScopeGlobal, org, today) require.NoError(t, err) assert.Zero(t, got) }