felt/internal/server/routes/financials.go
Mikkel Georgsen 56a7ef1e31 docs(01-13): complete Layout Shell plan
- SUMMARY.md with all accomplishments and deviation documentation
- STATE.md updated: plan 8/14, 50% progress, decisions, session
- ROADMAP.md updated: 7/14 plans complete
- REQUIREMENTS.md: UI-01 through UI-04, UI-07, UI-08 marked complete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 04:15:37 +01:00

222 lines
6.3 KiB
Go

package routes
import (
"encoding/json"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/felt-app/felt/internal/financial"
"github.com/felt-app/felt/internal/server/middleware"
)
// FinancialHandler handles financial API routes.
type FinancialHandler struct {
engine *financial.Engine
}
// NewFinancialHandler creates a new financial route handler.
func NewFinancialHandler(engine *financial.Engine) *FinancialHandler {
return &FinancialHandler{engine: engine}
}
// RegisterRoutes registers financial routes on the given router.
func (h *FinancialHandler) RegisterRoutes(r chi.Router) {
r.Route("/tournaments/{id}", func(r chi.Router) {
// Read-only routes (any authenticated user)
r.Get("/prize-pool", h.handleGetPrizePool)
r.Get("/payouts", h.handleGetPayouts)
r.Get("/payouts/preview", h.handlePreviewPayouts)
r.Get("/transactions", h.handleGetTransactions)
r.Get("/transactions/{txId}/receipt", h.handleGetReceipt)
// Mutation routes (admin or floor)
r.Group(func(r chi.Router) {
r.Use(middleware.RequireRole(middleware.RoleFloor))
r.Post("/bubble-prize", h.handleCreateBubblePrize)
r.Post("/bubble-prize/{proposalId}/confirm", h.handleConfirmBubblePrize)
r.Post("/transactions/{txId}/reprint", h.handleReprintReceipt)
})
})
// Season reserves (venue-level, any authenticated user)
r.Get("/season-reserves", h.handleGetSeasonReserves)
}
// handleGetPrizePool returns the current prize pool summary for a tournament.
func (h *FinancialHandler) handleGetPrizePool(w http.ResponseWriter, r *http.Request) {
tournamentID := chi.URLParam(r, "id")
if tournamentID == "" {
writeError(w, http.StatusBadRequest, "tournament id required")
return
}
summary, err := h.engine.CalculatePrizePool(r.Context(), tournamentID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, summary)
}
// handleGetPayouts returns the calculated payout table.
func (h *FinancialHandler) handleGetPayouts(w http.ResponseWriter, r *http.Request) {
tournamentID := chi.URLParam(r, "id")
if tournamentID == "" {
writeError(w, http.StatusBadRequest, "tournament id required")
return
}
payouts, err := h.engine.CalculatePayouts(r.Context(), tournamentID)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"payouts": payouts,
})
}
// handlePreviewPayouts returns a preview of payouts without applying them.
func (h *FinancialHandler) handlePreviewPayouts(w http.ResponseWriter, r *http.Request) {
tournamentID := chi.URLParam(r, "id")
if tournamentID == "" {
writeError(w, http.StatusBadRequest, "tournament id required")
return
}
summary, err := h.engine.CalculatePrizePool(r.Context(), tournamentID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
payouts, err := h.engine.CalculatePayouts(r.Context(), tournamentID)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"prize_pool": summary,
"payouts": payouts,
"preview": true,
})
}
// handleGetTransactions returns all transactions for a tournament.
func (h *FinancialHandler) handleGetTransactions(w http.ResponseWriter, r *http.Request) {
tournamentID := chi.URLParam(r, "id")
if tournamentID == "" {
writeError(w, http.StatusBadRequest, "tournament id required")
return
}
txs, err := h.engine.GetTransactions(r.Context(), tournamentID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, txs)
}
// handleGetReceipt returns the receipt for a transaction.
func (h *FinancialHandler) handleGetReceipt(w http.ResponseWriter, r *http.Request) {
txID := chi.URLParam(r, "txId")
if txID == "" {
writeError(w, http.StatusBadRequest, "transaction id required")
return
}
receipt, err := h.engine.GetReceipt(r.Context(), txID)
if err != nil {
writeError(w, http.StatusNotFound, err.Error())
return
}
writeJSON(w, http.StatusOK, receipt)
}
// handleReprintReceipt returns the receipt with a new print timestamp.
func (h *FinancialHandler) handleReprintReceipt(w http.ResponseWriter, r *http.Request) {
txID := chi.URLParam(r, "txId")
if txID == "" {
writeError(w, http.StatusBadRequest, "transaction id required")
return
}
receipt, err := h.engine.ReprintReceipt(r.Context(), txID)
if err != nil {
writeError(w, http.StatusNotFound, err.Error())
return
}
writeJSON(w, http.StatusOK, receipt)
}
type bubblePrizeRequest struct {
Amount int64 `json:"amount"` // cents
}
// handleCreateBubblePrize creates a bubble prize proposal.
func (h *FinancialHandler) handleCreateBubblePrize(w http.ResponseWriter, r *http.Request) {
tournamentID := chi.URLParam(r, "id")
if tournamentID == "" {
writeError(w, http.StatusBadRequest, "tournament id required")
return
}
var req bubblePrizeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
return
}
if req.Amount <= 0 {
writeError(w, http.StatusBadRequest, "amount must be positive")
return
}
proposal, err := h.engine.CalculateBubblePrize(r.Context(), tournamentID, req.Amount)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusCreated, proposal)
}
// handleConfirmBubblePrize confirms a bubble prize proposal.
func (h *FinancialHandler) handleConfirmBubblePrize(w http.ResponseWriter, r *http.Request) {
tournamentID := chi.URLParam(r, "id")
proposalIDStr := chi.URLParam(r, "proposalId")
proposalID, err := strconv.ParseInt(proposalIDStr, 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid proposal id")
return
}
if err := h.engine.ConfirmBubblePrize(r.Context(), tournamentID, proposalID); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "confirmed"})
}
// handleGetSeasonReserves returns the season reserve summary.
func (h *FinancialHandler) handleGetSeasonReserves(w http.ResponseWriter, r *http.Request) {
reserves, err := h.engine.GetSeasonReserves(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"reserves": reserves,
})
}