// Package template provides CRUD operations for tournament building blocks: // chip sets, payout structures, buy-in configs, and tournament templates. package template import ( "context" "database/sql" "fmt" "time" ) // ChipSet represents a collection of chip denominations used in tournaments. type ChipSet struct { ID int64 `json:"id"` Name string `json:"name"` IsBuiltin bool `json:"is_builtin"` Denominations []ChipDenomination `json:"denominations,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // ChipDenomination represents a single chip denomination within a chip set. type ChipDenomination struct { ID int64 `json:"id,omitempty"` ChipSetID int64 `json:"chip_set_id,omitempty"` Value int64 `json:"value"` ColorHex string `json:"color_hex"` Label string `json:"label"` SortOrder int `json:"sort_order"` } // ChipSetService provides CRUD operations for chip sets. type ChipSetService struct { db *sql.DB } // NewChipSetService creates a new ChipSetService. func NewChipSetService(db *sql.DB) *ChipSetService { return &ChipSetService{db: db} } // CreateChipSet creates a new chip set with its denominations. func (s *ChipSetService) CreateChipSet(ctx context.Context, name string, denominations []ChipDenomination) (*ChipSet, error) { if name == "" { return nil, fmt.Errorf("chip set name is required") } if len(denominations) == 0 { return nil, fmt.Errorf("at least one denomination is required") } tx, err := s.db.BeginTx(ctx, nil) if err != nil { return nil, fmt.Errorf("begin transaction: %w", err) } defer tx.Rollback() now := time.Now().Unix() res, err := tx.ExecContext(ctx, "INSERT INTO chip_sets (name, is_builtin, created_at, updated_at) VALUES (?, 0, ?, ?)", name, now, now, ) if err != nil { return nil, fmt.Errorf("insert chip set: %w", err) } id, err := res.LastInsertId() if err != nil { return nil, fmt.Errorf("get chip set id: %w", err) } for i, d := range denominations { if d.Value <= 0 { return nil, fmt.Errorf("denomination %d: value must be positive", i) } _, err := tx.ExecContext(ctx, "INSERT INTO chip_denominations (chip_set_id, value, color_hex, label, sort_order) VALUES (?, ?, ?, ?, ?)", id, d.Value, d.ColorHex, d.Label, d.SortOrder, ) if err != nil { return nil, fmt.Errorf("insert denomination %d: %w", i, err) } } if err := tx.Commit(); err != nil { return nil, fmt.Errorf("commit: %w", err) } return s.GetChipSet(ctx, id) } // GetChipSet retrieves a chip set by ID, including its denominations. func (s *ChipSetService) GetChipSet(ctx context.Context, id int64) (*ChipSet, error) { cs := &ChipSet{} var createdAt, updatedAt int64 var isBuiltin int err := s.db.QueryRowContext(ctx, "SELECT id, name, is_builtin, created_at, updated_at FROM chip_sets WHERE id = ?", id, ).Scan(&cs.ID, &cs.Name, &isBuiltin, &createdAt, &updatedAt) if err == sql.ErrNoRows { return nil, fmt.Errorf("chip set not found: %d", id) } if err != nil { return nil, fmt.Errorf("get chip set: %w", err) } cs.IsBuiltin = isBuiltin != 0 cs.CreatedAt = time.Unix(createdAt, 0) cs.UpdatedAt = time.Unix(updatedAt, 0) rows, err := s.db.QueryContext(ctx, "SELECT id, chip_set_id, value, color_hex, label, sort_order FROM chip_denominations WHERE chip_set_id = ? ORDER BY sort_order", id, ) if err != nil { return nil, fmt.Errorf("get denominations: %w", err) } defer rows.Close() for rows.Next() { var d ChipDenomination if err := rows.Scan(&d.ID, &d.ChipSetID, &d.Value, &d.ColorHex, &d.Label, &d.SortOrder); err != nil { return nil, fmt.Errorf("scan denomination: %w", err) } cs.Denominations = append(cs.Denominations, d) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("iterate denominations: %w", err) } return cs, nil } // ListChipSets returns all chip sets (without denominations for list performance). func (s *ChipSetService) ListChipSets(ctx context.Context) ([]ChipSet, error) { rows, err := s.db.QueryContext(ctx, "SELECT id, name, is_builtin, created_at, updated_at FROM chip_sets ORDER BY is_builtin DESC, name", ) if err != nil { return nil, fmt.Errorf("list chip sets: %w", err) } defer rows.Close() var sets []ChipSet for rows.Next() { var cs ChipSet var isBuiltin int var createdAt, updatedAt int64 if err := rows.Scan(&cs.ID, &cs.Name, &isBuiltin, &createdAt, &updatedAt); err != nil { return nil, fmt.Errorf("scan chip set: %w", err) } cs.IsBuiltin = isBuiltin != 0 cs.CreatedAt = time.Unix(createdAt, 0) cs.UpdatedAt = time.Unix(updatedAt, 0) sets = append(sets, cs) } return sets, rows.Err() } // UpdateChipSet updates a chip set name and replaces its denominations. func (s *ChipSetService) UpdateChipSet(ctx context.Context, id int64, name string, denominations []ChipDenomination) error { if name == "" { return fmt.Errorf("chip set name is required") } if len(denominations) == 0 { return fmt.Errorf("at least one denomination is required") } tx, err := s.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("begin transaction: %w", err) } defer tx.Rollback() now := time.Now().Unix() res, err := tx.ExecContext(ctx, "UPDATE chip_sets SET name = ?, updated_at = ? WHERE id = ?", name, now, id, ) if err != nil { return fmt.Errorf("update chip set: %w", err) } n, _ := res.RowsAffected() if n == 0 { return fmt.Errorf("chip set not found: %d", id) } // Replace denominations: delete old, insert new if _, err := tx.ExecContext(ctx, "DELETE FROM chip_denominations WHERE chip_set_id = ?", id); err != nil { return fmt.Errorf("delete old denominations: %w", err) } for i, d := range denominations { if d.Value <= 0 { return fmt.Errorf("denomination %d: value must be positive", i) } _, err := tx.ExecContext(ctx, "INSERT INTO chip_denominations (chip_set_id, value, color_hex, label, sort_order) VALUES (?, ?, ?, ?, ?)", id, d.Value, d.ColorHex, d.Label, d.SortOrder, ) if err != nil { return fmt.Errorf("insert denomination %d: %w", i, err) } } return tx.Commit() } // DeleteChipSet deletes a chip set. Built-in chip sets cannot be deleted. // Returns an error if the chip set is referenced by active tournaments. func (s *ChipSetService) DeleteChipSet(ctx context.Context, id int64) error { // Check if builtin var isBuiltin int err := s.db.QueryRowContext(ctx, "SELECT is_builtin FROM chip_sets WHERE id = ?", id).Scan(&isBuiltin) if err == sql.ErrNoRows { return fmt.Errorf("chip set not found: %d", id) } if err != nil { return fmt.Errorf("check chip set: %w", err) } if isBuiltin != 0 { return fmt.Errorf("cannot delete built-in chip set") } // Check if referenced by active tournaments var refCount int err = s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM tournaments WHERE chip_set_id = ? AND status NOT IN ('completed', 'cancelled')", id, ).Scan(&refCount) if err != nil { return fmt.Errorf("check references: %w", err) } if refCount > 0 { return fmt.Errorf("chip set is referenced by %d active tournament(s)", refCount) } res, err := s.db.ExecContext(ctx, "DELETE FROM chip_sets WHERE id = ?", id) if err != nil { return fmt.Errorf("delete chip set: %w", err) } n, _ := res.RowsAffected() if n == 0 { return fmt.Errorf("chip set not found: %d", id) } return nil } // DuplicateChipSet creates an independent copy of a chip set with a new name. func (s *ChipSetService) DuplicateChipSet(ctx context.Context, id int64, newName string) (*ChipSet, error) { original, err := s.GetChipSet(ctx, id) if err != nil { return nil, fmt.Errorf("get original: %w", err) } if newName == "" { newName = original.Name + " (Copy)" } return s.CreateChipSet(ctx, newName, original.Denominations) }