- ChipSetService with full CRUD, duplication, builtin protection - BlindStructure service with level validation and CRUD - PayoutStructure service with bracket/tier nesting and 100% sum validation - BuyinConfig service with rake split validation and all rebuy/addon fields - TournamentTemplate service with FK validation and expanded view - WizardService generates blind structures from high-level inputs - API routes: /chip-sets, /blind-structures, /payout-structures, /buyin-configs, /tournament-templates - All mutations require admin role, reads require floor+ - Wired template routes into server protected group Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
258 lines
7.7 KiB
Go
258 lines
7.7 KiB
Go
// 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)
|
|
}
|