package financial import ( "context" "database/sql" "testing" _ "github.com/tursodatabase/go-libsql" "github.com/felt-app/felt/internal/audit" "github.com/felt-app/felt/internal/server/middleware" "github.com/felt-app/felt/internal/template" ) // testDB creates an in-memory SQLite database with the required schema. func testDB(t *testing.T) *sql.DB { t.Helper() db, err := sql.Open("libsql", "file::memory:?cache=shared") if err != nil { t.Fatalf("open test db: %v", err) } t.Cleanup(func() { db.Close() }) // Create minimal schema stmts := []string{ `CREATE TABLE IF NOT EXISTS venue_settings ( id INTEGER PRIMARY KEY CHECK (id = 1), venue_name TEXT NOT NULL DEFAULT '', currency_code TEXT NOT NULL DEFAULT 'DKK', currency_symbol TEXT NOT NULL DEFAULT 'kr', rounding_denomination INTEGER NOT NULL DEFAULT 5000, receipt_mode TEXT NOT NULL DEFAULT 'digital', timezone TEXT NOT NULL DEFAULT 'Europe/Copenhagen', created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0 )`, `CREATE TABLE IF NOT EXISTS buyin_configs ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, buyin_amount INTEGER NOT NULL DEFAULT 0, starting_chips INTEGER NOT NULL DEFAULT 0, rake_total INTEGER NOT NULL DEFAULT 0, bounty_amount INTEGER NOT NULL DEFAULT 0, bounty_chip INTEGER NOT NULL DEFAULT 0, rebuy_allowed INTEGER NOT NULL DEFAULT 0, rebuy_cost INTEGER NOT NULL DEFAULT 0, rebuy_chips INTEGER NOT NULL DEFAULT 0, rebuy_rake INTEGER NOT NULL DEFAULT 0, rebuy_limit INTEGER NOT NULL DEFAULT 0, rebuy_level_cutoff INTEGER, rebuy_time_cutoff_seconds INTEGER, rebuy_chip_threshold INTEGER, addon_allowed INTEGER NOT NULL DEFAULT 0, addon_cost INTEGER NOT NULL DEFAULT 0, addon_chips INTEGER NOT NULL DEFAULT 0, addon_rake INTEGER NOT NULL DEFAULT 0, addon_level_start INTEGER, addon_level_end INTEGER, reentry_allowed INTEGER NOT NULL DEFAULT 0, reentry_limit INTEGER NOT NULL DEFAULT 0, late_reg_level_cutoff INTEGER, late_reg_time_cutoff_seconds INTEGER, created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0 )`, `CREATE TABLE IF NOT EXISTS rake_splits ( id INTEGER PRIMARY KEY AUTOINCREMENT, buyin_config_id INTEGER NOT NULL, category TEXT NOT NULL, amount INTEGER NOT NULL DEFAULT 0, UNIQUE(buyin_config_id, category) )`, `CREATE TABLE IF NOT EXISTS chip_sets ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, is_builtin INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0 )`, `CREATE TABLE IF NOT EXISTS blind_structures ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, is_builtin INTEGER NOT NULL DEFAULT 0, game_type_default TEXT NOT NULL DEFAULT 'nlhe', notes TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0 )`, `CREATE TABLE IF NOT EXISTS payout_structures ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, is_builtin INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0 )`, `CREATE TABLE IF NOT EXISTS tournaments ( id TEXT PRIMARY KEY, name TEXT NOT NULL, template_id INTEGER, chip_set_id INTEGER NOT NULL DEFAULT 1, blind_structure_id INTEGER NOT NULL DEFAULT 1, payout_structure_id INTEGER NOT NULL DEFAULT 1, buyin_config_id INTEGER NOT NULL, points_formula_id INTEGER, status TEXT NOT NULL DEFAULT 'created', min_players INTEGER NOT NULL DEFAULT 2, max_players INTEGER, early_signup_bonus_chips INTEGER NOT NULL DEFAULT 0, early_signup_cutoff TEXT, punctuality_bonus_chips INTEGER NOT NULL DEFAULT 0, is_pko INTEGER NOT NULL DEFAULT 0, current_level INTEGER NOT NULL DEFAULT 0, clock_state TEXT NOT NULL DEFAULT 'stopped', clock_remaining_ns INTEGER NOT NULL DEFAULT 0, total_elapsed_ns INTEGER NOT NULL DEFAULT 0, hand_for_hand INTEGER NOT NULL DEFAULT 0, started_at INTEGER, ended_at INTEGER, created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0 )`, `CREATE TABLE IF NOT EXISTS players ( id TEXT PRIMARY KEY, name TEXT NOT NULL, nickname TEXT, email TEXT, phone TEXT, photo_url TEXT, notes TEXT, custom_fields TEXT, created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0 )`, `CREATE TABLE IF NOT EXISTS tournament_players ( id INTEGER PRIMARY KEY AUTOINCREMENT, tournament_id TEXT NOT NULL, player_id TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'registered', seat_table_id INTEGER, seat_position INTEGER, buy_in_at INTEGER, bust_out_at INTEGER, bust_out_order INTEGER, finishing_position INTEGER, current_chips INTEGER NOT NULL DEFAULT 0, rebuys INTEGER NOT NULL DEFAULT 0, addons INTEGER NOT NULL DEFAULT 0, reentries INTEGER NOT NULL DEFAULT 0, bounty_value INTEGER NOT NULL DEFAULT 0, bounties_collected INTEGER NOT NULL DEFAULT 0, prize_amount INTEGER NOT NULL DEFAULT 0, points_awarded INTEGER NOT NULL DEFAULT 0, early_signup_bonus_applied INTEGER NOT NULL DEFAULT 0, punctuality_bonus_applied INTEGER NOT NULL DEFAULT 0, hitman_player_id TEXT, created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0, UNIQUE(tournament_id, player_id) )`, `CREATE TABLE IF NOT EXISTS transactions ( id TEXT PRIMARY KEY, tournament_id TEXT NOT NULL, player_id TEXT NOT NULL, type TEXT NOT NULL, amount INTEGER NOT NULL DEFAULT 0, chips INTEGER NOT NULL DEFAULT 0, operator_id TEXT NOT NULL, receipt_data TEXT, undone INTEGER NOT NULL DEFAULT 0, undone_by TEXT, metadata TEXT, created_at INTEGER NOT NULL DEFAULT 0 )`, `CREATE TABLE IF NOT EXISTS audit_entries ( id TEXT PRIMARY KEY, tournament_id TEXT, timestamp INTEGER NOT NULL DEFAULT 0, operator_id TEXT NOT NULL, action TEXT NOT NULL, target_type TEXT NOT NULL DEFAULT '', target_id TEXT NOT NULL DEFAULT '', previous_state TEXT, new_state TEXT, metadata TEXT, undone_by TEXT )`, // Seed required references `INSERT INTO chip_sets (id, name) VALUES (1, 'Test Chips')`, `INSERT INTO blind_structures (id, name) VALUES (1, 'Test Blinds')`, `INSERT INTO payout_structures (id, name) VALUES (1, 'Test Payouts')`, } for _, stmt := range stmts { if _, err := db.Exec(stmt); err != nil { t.Fatalf("exec schema: %s: %v", stmt[:50], err) } } return db } // setupTestEngine creates an Engine with test dependencies. func setupTestEngine(t *testing.T) (*Engine, *sql.DB) { t.Helper() db := testDB(t) trail := audit.NewTrail(db, nil) undo := audit.NewUndoEngine(trail) buyinSvc := template.NewBuyinService(db) eng := NewEngine(db, trail, undo, nil, buyinSvc) return eng, db } // seedBuyinConfig creates a test buy-in config and returns its ID. func seedBuyinConfig(t *testing.T, db *sql.DB, amount, chips, rake int64) int64 { t.Helper() res, err := db.Exec( `INSERT INTO buyin_configs (name, buyin_amount, starting_chips, rake_total, rebuy_allowed, rebuy_cost, rebuy_chips, rebuy_rake, rebuy_limit, addon_allowed, addon_cost, addon_chips, addon_rake, reentry_allowed, reentry_limit) VALUES ('Test Buyin', ?, ?, ?, 1, ?, ?, ?, 3, 1, ?, ?, ?, 1, 2)`, amount, chips, rake, amount, chips, rake/2, // rebuy costs same as buyin, half rake amount/2, chips/2, rake/4, // addon costs half, quarter rake ) if err != nil { t.Fatalf("seed buyin config: %v", err) } id, _ := res.LastInsertId() // Add rake splits if rake > 0 { houseRake := rake * 60 / 100 staffRake := rake - houseRake db.Exec("INSERT INTO rake_splits (buyin_config_id, category, amount) VALUES (?, 'house', ?)", id, houseRake) db.Exec("INSERT INTO rake_splits (buyin_config_id, category, amount) VALUES (?, 'staff', ?)", id, staffRake) } return id } // seedTournament creates a test tournament. func seedTournament(t *testing.T, db *sql.DB, id string, buyinConfigID int64, isPKO bool) { t.Helper() pko := 0 if isPKO { pko = 1 } _, err := db.Exec( `INSERT INTO tournaments (id, name, buyin_config_id, is_pko, status) VALUES (?, 'Test Tournament', ?, ?, 'running')`, id, buyinConfigID, pko, ) if err != nil { t.Fatalf("seed tournament: %v", err) } } // seedPlayer creates a test player and registers them in a tournament. func seedPlayer(t *testing.T, db *sql.DB, tournamentID, playerID, status string) { t.Helper() db.Exec("INSERT OR IGNORE INTO players (id, name) VALUES (?, ?)", playerID, "Player "+playerID) _, err := db.Exec( "INSERT INTO tournament_players (tournament_id, player_id, status) VALUES (?, ?, ?)", tournamentID, playerID, status, ) if err != nil { t.Fatalf("seed player: %v", err) } } func ctxWithOperator() context.Context { ctx := context.Background() ctx = context.WithValue(ctx, middleware.OperatorIDKey, "op-test") ctx = context.WithValue(ctx, middleware.OperatorRoleKey, "admin") return ctx } func TestProcessBuyIn(t *testing.T) { eng, db := setupTestEngine(t) ctx := ctxWithOperator() cfgID := seedBuyinConfig(t, db, 10000, 15000, 2000) // 100 EUR buyin, 150 chips, 20 rake seedTournament(t, db, "t1", cfgID, false) seedPlayer(t, db, "t1", "p1", "registered") tx, err := eng.ProcessBuyIn(ctx, "t1", "p1", false) if err != nil { t.Fatalf("process buyin: %v", err) } if tx.Amount != 10000 { t.Errorf("expected amount 10000, got %d", tx.Amount) } if tx.Chips != 15000 { t.Errorf("expected chips 15000, got %d", tx.Chips) } if tx.Type != TxTypeBuyIn { t.Errorf("expected type buyin, got %s", tx.Type) } // Verify player state was updated var status string var chips int64 db.QueryRow("SELECT status, current_chips FROM tournament_players WHERE tournament_id = 't1' AND player_id = 'p1'").Scan(&status, &chips) if status != "active" { t.Errorf("expected player status 'active', got %q", status) } if chips != 15000 { t.Errorf("expected player chips 15000, got %d", chips) } // Verify rake transactions were created var rakeCount int db.QueryRow("SELECT COUNT(*) FROM transactions WHERE tournament_id = 't1' AND type = 'rake'").Scan(&rakeCount) if rakeCount != 2 { // house + staff t.Errorf("expected 2 rake transactions, got %d", rakeCount) } } func TestProcessBuyIn_LateRegClosed(t *testing.T) { eng, db := setupTestEngine(t) ctx := ctxWithOperator() cfgID := seedBuyinConfig(t, db, 10000, 15000, 2000) // Set late reg cutoff to level 6 db.Exec("UPDATE buyin_configs SET late_reg_level_cutoff = 6 WHERE id = ?", cfgID) seedTournament(t, db, "t1", cfgID, false) // Simulate clock past cutoff db.Exec("UPDATE tournaments SET clock_state = 'running', current_level = 7 WHERE id = 't1'") seedPlayer(t, db, "t1", "p1", "registered") _, err := eng.ProcessBuyIn(ctx, "t1", "p1", false) if err != ErrLateRegistrationClosed { t.Fatalf("expected ErrLateRegistrationClosed, got: %v", err) } } func TestProcessBuyIn_AdminOverride(t *testing.T) { eng, db := setupTestEngine(t) ctx := ctxWithOperator() cfgID := seedBuyinConfig(t, db, 10000, 15000, 2000) db.Exec("UPDATE buyin_configs SET late_reg_level_cutoff = 6 WHERE id = ?", cfgID) seedTournament(t, db, "t1", cfgID, false) db.Exec("UPDATE tournaments SET clock_state = 'running', current_level = 7 WHERE id = 't1'") seedPlayer(t, db, "t1", "p1", "registered") // With override=true, should succeed tx, err := eng.ProcessBuyIn(ctx, "t1", "p1", true) if err != nil { t.Fatalf("expected admin override to succeed: %v", err) } if tx.Amount != 10000 { t.Errorf("expected amount 10000, got %d", tx.Amount) } } func TestProcessRebuy(t *testing.T) { eng, db := setupTestEngine(t) ctx := ctxWithOperator() cfgID := seedBuyinConfig(t, db, 10000, 15000, 2000) seedTournament(t, db, "t1", cfgID, false) seedPlayer(t, db, "t1", "p1", "active") tx, err := eng.ProcessRebuy(ctx, "t1", "p1") if err != nil { t.Fatalf("process rebuy: %v", err) } if tx.Type != TxTypeRebuy { t.Errorf("expected type rebuy, got %s", tx.Type) } // Verify rebuy count incremented var rebuys int db.QueryRow("SELECT rebuys FROM tournament_players WHERE tournament_id = 't1' AND player_id = 'p1'").Scan(&rebuys) if rebuys != 1 { t.Errorf("expected rebuys 1, got %d", rebuys) } } func TestProcessRebuy_LimitReached(t *testing.T) { eng, db := setupTestEngine(t) ctx := ctxWithOperator() cfgID := seedBuyinConfig(t, db, 10000, 15000, 2000) seedTournament(t, db, "t1", cfgID, false) seedPlayer(t, db, "t1", "p1", "active") // Set rebuy count to limit db.Exec("UPDATE tournament_players SET rebuys = 3 WHERE tournament_id = 't1' AND player_id = 'p1'") _, err := eng.ProcessRebuy(ctx, "t1", "p1") if err != ErrRebuyLimitReached { t.Fatalf("expected ErrRebuyLimitReached, got: %v", err) } } func TestProcessAddOn(t *testing.T) { eng, db := setupTestEngine(t) ctx := ctxWithOperator() cfgID := seedBuyinConfig(t, db, 10000, 15000, 2000) seedTournament(t, db, "t1", cfgID, false) seedPlayer(t, db, "t1", "p1", "active") tx, err := eng.ProcessAddOn(ctx, "t1", "p1") if err != nil { t.Fatalf("process addon: %v", err) } if tx.Type != TxTypeAddon { t.Errorf("expected type addon, got %s", tx.Type) } // Verify addon count var addons int db.QueryRow("SELECT addons FROM tournament_players WHERE tournament_id = 't1' AND player_id = 'p1'").Scan(&addons) if addons != 1 { t.Errorf("expected addons 1, got %d", addons) } } func TestProcessAddOn_AlreadyUsed(t *testing.T) { eng, db := setupTestEngine(t) ctx := ctxWithOperator() cfgID := seedBuyinConfig(t, db, 10000, 15000, 2000) seedTournament(t, db, "t1", cfgID, false) seedPlayer(t, db, "t1", "p1", "active") db.Exec("UPDATE tournament_players SET addons = 1 WHERE tournament_id = 't1' AND player_id = 'p1'") _, err := eng.ProcessAddOn(ctx, "t1", "p1") if err != ErrAddonAlreadyUsed { t.Fatalf("expected ErrAddonAlreadyUsed, got: %v", err) } } func TestProcessReEntry(t *testing.T) { eng, db := setupTestEngine(t) ctx := ctxWithOperator() cfgID := seedBuyinConfig(t, db, 10000, 15000, 2000) seedTournament(t, db, "t1", cfgID, false) seedPlayer(t, db, "t1", "p1", "busted") // Must be busted tx, err := eng.ProcessReEntry(ctx, "t1", "p1") if err != nil { t.Fatalf("process reentry: %v", err) } if tx.Type != TxTypeReentry { t.Errorf("expected type reentry, got %s", tx.Type) } // Player should be reactivated var status string db.QueryRow("SELECT status FROM tournament_players WHERE tournament_id = 't1' AND player_id = 'p1'").Scan(&status) if status != "active" { t.Errorf("expected player reactivated to 'active', got %q", status) } } func TestProcessReEntry_NotBusted(t *testing.T) { eng, db := setupTestEngine(t) ctx := ctxWithOperator() cfgID := seedBuyinConfig(t, db, 10000, 15000, 2000) seedTournament(t, db, "t1", cfgID, false) seedPlayer(t, db, "t1", "p1", "active") // Active, not busted _, err := eng.ProcessReEntry(ctx, "t1", "p1") if err != ErrPlayerNotBusted { t.Fatalf("expected ErrPlayerNotBusted, got: %v", err) } } func TestProcessBountyTransfer_PKO(t *testing.T) { eng, db := setupTestEngine(t) ctx := ctxWithOperator() cfgID := seedBuyinConfig(t, db, 10000, 15000, 2000) db.Exec("UPDATE buyin_configs SET bounty_amount = 5000 WHERE id = ?", cfgID) seedTournament(t, db, "t1", cfgID, true) // PKO seedPlayer(t, db, "t1", "p1", "busted") // Eliminated seedPlayer(t, db, "t1", "p2", "active") // Hitman // Set bounty values db.Exec("UPDATE tournament_players SET bounty_value = 5000 WHERE tournament_id = 't1' AND player_id = 'p1'") db.Exec("UPDATE tournament_players SET bounty_value = 2500 WHERE tournament_id = 't1' AND player_id = 'p2'") err := eng.ProcessBountyTransfer(ctx, "t1", "p1", "p2") if err != nil { t.Fatalf("process bounty transfer: %v", err) } // Verify: p1's bounty cleared var p1Bounty int64 db.QueryRow("SELECT bounty_value FROM tournament_players WHERE tournament_id = 't1' AND player_id = 'p1'").Scan(&p1Bounty) if p1Bounty != 0 { t.Errorf("expected eliminated player bounty 0, got %d", p1Bounty) } // p2's bounty increased by half of p1's bounty (2500) var p2Bounty int64 db.QueryRow("SELECT bounty_value FROM tournament_players WHERE tournament_id = 't1' AND player_id = 'p2'").Scan(&p2Bounty) expected := int64(2500 + 2500) // original 2500 + half of 5000 if p2Bounty != expected { t.Errorf("expected hitman bounty %d, got %d", expected, p2Bounty) } // Verify transactions created var collectCount, paidCount int db.QueryRow("SELECT COUNT(*) FROM transactions WHERE tournament_id = 't1' AND type = 'bounty_collected'").Scan(&collectCount) db.QueryRow("SELECT COUNT(*) FROM transactions WHERE tournament_id = 't1' AND type = 'bounty_paid'").Scan(&paidCount) if collectCount != 1 { t.Errorf("expected 1 bounty_collected tx, got %d", collectCount) } if paidCount != 1 { t.Errorf("expected 1 bounty_paid tx, got %d", paidCount) } // Verify cash portion is correct (half of 5000 = 2500) var cashAmount int64 db.QueryRow("SELECT amount FROM transactions WHERE tournament_id = 't1' AND type = 'bounty_collected' AND player_id = 'p2'").Scan(&cashAmount) if cashAmount != 2500 { t.Errorf("expected cash portion 2500, got %d", cashAmount) } } func TestUndoTransaction(t *testing.T) { eng, db := setupTestEngine(t) ctx := ctxWithOperator() cfgID := seedBuyinConfig(t, db, 10000, 15000, 2000) seedTournament(t, db, "t1", cfgID, false) seedPlayer(t, db, "t1", "p1", "registered") // Buy in tx, err := eng.ProcessBuyIn(ctx, "t1", "p1", false) if err != nil { t.Fatalf("process buyin: %v", err) } // Undo err = eng.UndoTransaction(ctx, tx.ID) if err != nil { t.Fatalf("undo transaction: %v", err) } // Verify transaction is marked undone var undone bool db.QueryRow("SELECT undone FROM transactions WHERE id = ?", tx.ID).Scan(&undone) if !undone { t.Error("expected transaction to be marked undone") } } func TestIsLateRegistrationOpen(t *testing.T) { eng, db := setupTestEngine(t) ctx := ctxWithOperator() cfgID := seedBuyinConfig(t, db, 10000, 15000, 2000) // Set level cutoff to 6 AND time cutoff to 5400 seconds (90 min) db.Exec("UPDATE buyin_configs SET late_reg_level_cutoff = 6, late_reg_time_cutoff_seconds = 5400 WHERE id = ?", cfgID) seedTournament(t, db, "t1", cfgID, false) // Clock stopped: always open open, err := eng.IsLateRegistrationOpen(ctx, "t1") if err != nil { t.Fatalf("check late reg: %v", err) } if !open { t.Error("expected late reg open when clock stopped") } // Clock running, level 3: open db.Exec("UPDATE tournaments SET clock_state = 'running', current_level = 3 WHERE id = 't1'") open, _ = eng.IsLateRegistrationOpen(ctx, "t1") if !open { t.Error("expected late reg open at level 3") } // Clock running, level 7: closed (past level cutoff) db.Exec("UPDATE tournaments SET current_level = 7 WHERE id = 't1'") open, _ = eng.IsLateRegistrationOpen(ctx, "t1") if open { t.Error("expected late reg closed at level 7") } // Clock running, level 3 but time past cutoff db.Exec("UPDATE tournaments SET current_level = 3, total_elapsed_ns = 6000000000000 WHERE id = 't1'") // 6000 seconds > 5400 open, _ = eng.IsLateRegistrationOpen(ctx, "t1") if open { t.Error("expected late reg closed when time past cutoff") } } func TestGetTransactions(t *testing.T) { eng, db := setupTestEngine(t) ctx := ctxWithOperator() cfgID := seedBuyinConfig(t, db, 10000, 15000, 2000) seedTournament(t, db, "t1", cfgID, false) seedPlayer(t, db, "t1", "p1", "registered") seedPlayer(t, db, "t1", "p2", "registered") eng.ProcessBuyIn(ctx, "t1", "p1", false) eng.ProcessBuyIn(ctx, "t1", "p2", false) txs, err := eng.GetTransactions(ctx, "t1") if err != nil { t.Fatalf("get transactions: %v", err) } // Should have 2 buyins + 4 rake transactions (2 per player: house + staff) if len(txs) != 6 { t.Errorf("expected 6 transactions, got %d", len(txs)) } // Player-specific ptxs, err := eng.GetPlayerTransactions(ctx, "t1", "p1") if err != nil { t.Fatalf("get player transactions: %v", err) } if len(ptxs) != 3 { // 1 buyin + 2 rake t.Errorf("expected 3 player transactions, got %d", len(ptxs)) } } func TestScaleRakeSplits(t *testing.T) { eng, _ := setupTestEngine(t) original := []template.RakeSplit{ {Category: "house", Amount: 1200}, {Category: "staff", Amount: 800}, } // Scale from 2000 to 1000 scaled := eng.scaleRakeSplits(original, 2000, 1000) if len(scaled) != 2 { t.Fatalf("expected 2 scaled splits, got %d", len(scaled)) } // Verify sum equals new total var sum int64 for _, s := range scaled { sum += s.Amount } if sum != 1000 { t.Errorf("expected scaled sum 1000, got %d", sum) } }