// Package routes provides HTTP route handlers for the Felt API. // Each handler group corresponds to a domain entity and registers its routes // with a chi router group. package routes import ( "database/sql" "encoding/json" "fmt" "net/http" "strconv" "github.com/go-chi/chi/v5" "github.com/felt-app/felt/internal/blind" "github.com/felt-app/felt/internal/server/middleware" "github.com/felt-app/felt/internal/template" ) // TemplateRoutes holds all building block route handlers and their services. type TemplateRoutes struct { chipSets *template.ChipSetService blinds *blind.StructureService payouts *template.PayoutService buyins *template.BuyinService templates *template.TournamentTemplateService wizard *blind.WizardService } // NewTemplateRoutes creates a new TemplateRoutes with all services initialized. func NewTemplateRoutes(db *sql.DB) *TemplateRoutes { return &TemplateRoutes{ chipSets: template.NewChipSetService(db), blinds: blind.NewStructureService(db), payouts: template.NewPayoutService(db), buyins: template.NewBuyinService(db), templates: template.NewTournamentTemplateService(db), wizard: blind.NewWizardService(db), } } // Register mounts all template-related routes on the given chi router. // All routes require auth. Create/update/delete require admin role. func (tr *TemplateRoutes) Register(r chi.Router) { // Chip Sets r.Route("/chip-sets", func(r chi.Router) { r.Get("/", tr.listChipSets) r.Get("/{id}", tr.getChipSet) r.Group(func(r chi.Router) { r.Use(middleware.RequireRole(middleware.RoleAdmin)) r.Post("/", tr.createChipSet) r.Put("/{id}", tr.updateChipSet) r.Delete("/{id}", tr.deleteChipSet) r.Post("/{id}/duplicate", tr.duplicateChipSet) }) }) // Blind Structures r.Route("/blind-structures", func(r chi.Router) { r.Get("/", tr.listBlindStructures) r.Get("/{id}", tr.getBlindStructure) r.Group(func(r chi.Router) { r.Use(middleware.RequireRole(middleware.RoleAdmin)) r.Post("/", tr.createBlindStructure) r.Put("/{id}", tr.updateBlindStructure) r.Delete("/{id}", tr.deleteBlindStructure) r.Post("/{id}/duplicate", tr.duplicateBlindStructure) r.Post("/wizard", tr.wizardGenerate) }) }) // Payout Structures r.Route("/payout-structures", func(r chi.Router) { r.Get("/", tr.listPayoutStructures) r.Get("/{id}", tr.getPayoutStructure) r.Group(func(r chi.Router) { r.Use(middleware.RequireRole(middleware.RoleAdmin)) r.Post("/", tr.createPayoutStructure) r.Put("/{id}", tr.updatePayoutStructure) r.Delete("/{id}", tr.deletePayoutStructure) r.Post("/{id}/duplicate", tr.duplicatePayoutStructure) }) }) // Buy-in Configs r.Route("/buyin-configs", func(r chi.Router) { r.Get("/", tr.listBuyinConfigs) r.Get("/{id}", tr.getBuyinConfig) r.Group(func(r chi.Router) { r.Use(middleware.RequireRole(middleware.RoleAdmin)) r.Post("/", tr.createBuyinConfig) r.Put("/{id}", tr.updateBuyinConfig) r.Delete("/{id}", tr.deleteBuyinConfig) r.Post("/{id}/duplicate", tr.duplicateBuyinConfig) }) }) // Tournament Templates r.Route("/tournament-templates", func(r chi.Router) { r.Get("/", tr.listTournamentTemplates) r.Get("/{id}", tr.getTournamentTemplate) r.Get("/{id}/expanded", tr.getTournamentTemplateExpanded) r.Group(func(r chi.Router) { r.Use(middleware.RequireRole(middleware.RoleAdmin)) r.Post("/", tr.createTournamentTemplate) r.Put("/{id}", tr.updateTournamentTemplate) r.Delete("/{id}", tr.deleteTournamentTemplate) r.Post("/{id}/duplicate", tr.duplicateTournamentTemplate) }) }) } // --- Helpers --- // parseID extracts an integer ID from the URL path parameter. func parseID(r *http.Request) (int64, error) { idStr := chi.URLParam(r, "id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { return 0, fmt.Errorf("invalid id: %s", idStr) } return id, nil } // writeJSON writes a JSON response with the given status code. func writeJSON(w http.ResponseWriter, status int, v interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(v) } // writeError writes an error JSON response. func writeError(w http.ResponseWriter, status int, msg string) { writeJSON(w, status, map[string]string{"error": msg}) } // --- Chip Set Handlers --- func (tr *TemplateRoutes) listChipSets(w http.ResponseWriter, r *http.Request) { sets, err := tr.chipSets.ListChipSets(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, sets) } func (tr *TemplateRoutes) getChipSet(w http.ResponseWriter, r *http.Request) { id, err := parseID(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } cs, err := tr.chipSets.GetChipSet(r.Context(), id) if err != nil { writeError(w, http.StatusNotFound, err.Error()) return } writeJSON(w, http.StatusOK, cs) } type createChipSetRequest struct { Name string `json:"name"` Denominations []template.ChipDenomination `json:"denominations"` } func (tr *TemplateRoutes) createChipSet(w http.ResponseWriter, r *http.Request) { var req createChipSetRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } cs, err := tr.chipSets.CreateChipSet(r.Context(), req.Name, req.Denominations) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusCreated, cs) } type updateChipSetRequest struct { Name string `json:"name"` Denominations []template.ChipDenomination `json:"denominations"` } func (tr *TemplateRoutes) updateChipSet(w http.ResponseWriter, r *http.Request) { id, err := parseID(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } var req updateChipSetRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } if err := tr.chipSets.UpdateChipSet(r.Context(), id, req.Name, req.Denominations); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "updated"}) } func (tr *TemplateRoutes) deleteChipSet(w http.ResponseWriter, r *http.Request) { id, err := parseID(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if err := tr.chipSets.DeleteChipSet(r.Context(), id); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) } type duplicateRequest struct { Name string `json:"name"` } func (tr *TemplateRoutes) duplicateChipSet(w http.ResponseWriter, r *http.Request) { id, err := parseID(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } var req duplicateRequest json.NewDecoder(r.Body).Decode(&req) // name is optional cs, err := tr.chipSets.DuplicateChipSet(r.Context(), id, req.Name) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusCreated, cs) } // --- Blind Structure Handlers --- func (tr *TemplateRoutes) listBlindStructures(w http.ResponseWriter, r *http.Request) { structs, err := tr.blinds.ListStructures(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, structs) } func (tr *TemplateRoutes) getBlindStructure(w http.ResponseWriter, r *http.Request) { id, err := parseID(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } bs, err := tr.blinds.GetStructure(r.Context(), id) if err != nil { writeError(w, http.StatusNotFound, err.Error()) return } writeJSON(w, http.StatusOK, bs) } type createBlindStructureRequest struct { Name string `json:"name"` Levels []blind.BlindLevel `json:"levels"` } func (tr *TemplateRoutes) createBlindStructure(w http.ResponseWriter, r *http.Request) { var req createBlindStructureRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } bs, err := tr.blinds.CreateStructure(r.Context(), req.Name, req.Levels) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusCreated, bs) } type updateBlindStructureRequest struct { Name string `json:"name"` Levels []blind.BlindLevel `json:"levels"` } func (tr *TemplateRoutes) updateBlindStructure(w http.ResponseWriter, r *http.Request) { id, err := parseID(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } var req updateBlindStructureRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } if err := tr.blinds.UpdateStructure(r.Context(), id, req.Name, req.Levels); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "updated"}) } func (tr *TemplateRoutes) deleteBlindStructure(w http.ResponseWriter, r *http.Request) { id, err := parseID(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if err := tr.blinds.DeleteStructure(r.Context(), id); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) } func (tr *TemplateRoutes) duplicateBlindStructure(w http.ResponseWriter, r *http.Request) { id, err := parseID(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } var req duplicateRequest json.NewDecoder(r.Body).Decode(&req) bs, err := tr.blinds.DuplicateStructure(r.Context(), id, req.Name) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusCreated, bs) } // --- Wizard Handler --- type wizardRequest struct { PlayerCount int `json:"player_count"` StartingChips int64 `json:"starting_chips"` TargetDurationMinutes int `json:"target_duration_minutes"` ChipSetID int64 `json:"chip_set_id"` } func (tr *TemplateRoutes) wizardGenerate(w http.ResponseWriter, r *http.Request) { var req wizardRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } levels, err := tr.wizard.Generate(r.Context(), blind.WizardInput{ PlayerCount: req.PlayerCount, StartingChips: req.StartingChips, TargetDurationMinutes: req.TargetDurationMinutes, ChipSetID: req.ChipSetID, }) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]interface{}{ "levels": levels, }) } // --- Payout Structure Handlers --- func (tr *TemplateRoutes) listPayoutStructures(w http.ResponseWriter, r *http.Request) { structs, err := tr.payouts.ListPayoutStructures(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, structs) } func (tr *TemplateRoutes) getPayoutStructure(w http.ResponseWriter, r *http.Request) { id, err := parseID(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } ps, err := tr.payouts.GetPayoutStructure(r.Context(), id) if err != nil { writeError(w, http.StatusNotFound, err.Error()) return } writeJSON(w, http.StatusOK, ps) } type createPayoutStructureRequest struct { Name string `json:"name"` Brackets []template.PayoutBracket `json:"brackets"` } func (tr *TemplateRoutes) createPayoutStructure(w http.ResponseWriter, r *http.Request) { var req createPayoutStructureRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } ps, err := tr.payouts.CreatePayoutStructure(r.Context(), req.Name, req.Brackets) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusCreated, ps) } type updatePayoutStructureRequest struct { Name string `json:"name"` Brackets []template.PayoutBracket `json:"brackets"` } func (tr *TemplateRoutes) updatePayoutStructure(w http.ResponseWriter, r *http.Request) { id, err := parseID(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } var req updatePayoutStructureRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } if err := tr.payouts.UpdatePayoutStructure(r.Context(), id, req.Name, req.Brackets); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "updated"}) } func (tr *TemplateRoutes) deletePayoutStructure(w http.ResponseWriter, r *http.Request) { id, err := parseID(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if err := tr.payouts.DeletePayoutStructure(r.Context(), id); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) } func (tr *TemplateRoutes) duplicatePayoutStructure(w http.ResponseWriter, r *http.Request) { id, err := parseID(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } var req duplicateRequest json.NewDecoder(r.Body).Decode(&req) ps, err := tr.payouts.DuplicatePayoutStructure(r.Context(), id, req.Name) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusCreated, ps) } // --- Buy-in Config Handlers --- func (tr *TemplateRoutes) listBuyinConfigs(w http.ResponseWriter, r *http.Request) { configs, err := tr.buyins.ListBuyinConfigs(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, configs) } func (tr *TemplateRoutes) getBuyinConfig(w http.ResponseWriter, r *http.Request) { id, err := parseID(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } cfg, err := tr.buyins.GetBuyinConfig(r.Context(), id) if err != nil { writeError(w, http.StatusNotFound, err.Error()) return } writeJSON(w, http.StatusOK, cfg) } func (tr *TemplateRoutes) createBuyinConfig(w http.ResponseWriter, r *http.Request) { var cfg template.BuyinConfig if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } result, err := tr.buyins.CreateBuyinConfig(r.Context(), &cfg) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusCreated, result) } func (tr *TemplateRoutes) updateBuyinConfig(w http.ResponseWriter, r *http.Request) { id, err := parseID(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } var cfg template.BuyinConfig if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } if err := tr.buyins.UpdateBuyinConfig(r.Context(), id, &cfg); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "updated"}) } func (tr *TemplateRoutes) deleteBuyinConfig(w http.ResponseWriter, r *http.Request) { id, err := parseID(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if err := tr.buyins.DeleteBuyinConfig(r.Context(), id); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) } func (tr *TemplateRoutes) duplicateBuyinConfig(w http.ResponseWriter, r *http.Request) { id, err := parseID(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } var req duplicateRequest json.NewDecoder(r.Body).Decode(&req) cfg, err := tr.buyins.DuplicateBuyinConfig(r.Context(), id, req.Name) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusCreated, cfg) } // --- Tournament Template Handlers --- func (tr *TemplateRoutes) listTournamentTemplates(w http.ResponseWriter, r *http.Request) { templates, err := tr.templates.ListTemplates(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, templates) } func (tr *TemplateRoutes) getTournamentTemplate(w http.ResponseWriter, r *http.Request) { id, err := parseID(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } tmpl, err := tr.templates.GetTemplate(r.Context(), id) if err != nil { writeError(w, http.StatusNotFound, err.Error()) return } writeJSON(w, http.StatusOK, tmpl) } func (tr *TemplateRoutes) getTournamentTemplateExpanded(w http.ResponseWriter, r *http.Request) { id, err := parseID(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } expanded, err := tr.templates.GetTemplateExpanded(r.Context(), id) if err != nil { writeError(w, http.StatusNotFound, err.Error()) return } writeJSON(w, http.StatusOK, expanded) } func (tr *TemplateRoutes) createTournamentTemplate(w http.ResponseWriter, r *http.Request) { var tmpl template.TournamentTemplate if err := json.NewDecoder(r.Body).Decode(&tmpl); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } result, err := tr.templates.CreateTemplate(r.Context(), &tmpl) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusCreated, result) } func (tr *TemplateRoutes) updateTournamentTemplate(w http.ResponseWriter, r *http.Request) { id, err := parseID(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } var tmpl template.TournamentTemplate if err := json.NewDecoder(r.Body).Decode(&tmpl); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } tmpl.ID = id if err := tr.templates.UpdateTemplate(r.Context(), &tmpl); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "updated"}) } func (tr *TemplateRoutes) deleteTournamentTemplate(w http.ResponseWriter, r *http.Request) { id, err := parseID(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if err := tr.templates.DeleteTemplate(r.Context(), id); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) } func (tr *TemplateRoutes) duplicateTournamentTemplate(w http.ResponseWriter, r *http.Request) { id, err := parseID(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } var req duplicateRequest json.NewDecoder(r.Body).Decode(&req) tmpl, err := tr.templates.DuplicateTemplate(r.Context(), id, req.Name) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusCreated, tmpl) }