package api import ( "context" "encoding/json" "fmt" "fuj-management/go/internal/config" "fuj-management/go/internal/services/membership" "log/slog" "net/http" "time" domreconcile "fuj-management/go/internal/domain/reconcile" ) // Handler holds the shared dependencies for all /api/* routes. type Handler struct { BuildVersion string BuildCommit string BuildDate string Sources membership.Sources Config config.Config Logger *slog.Logger } // ServeVersion handles GET /api/version. func (h *Handler) ServeVersion(w http.ResponseWriter, r *http.Request) { writeJSON(w, VersionResponse{ Tag: h.BuildVersion, Commit: h.BuildCommit, BuildDate: h.BuildDate, }) } // ServeAdults handles GET /api/adults. func (h *Handler) ServeAdults(w http.ResponseWriter, r *http.Request) { resp, err := h.AssembleAdults(r.Context()) if err != nil { h.writeError(w, r, err) return } writeJSON(w, resp) } // AssembleAdults loads all data and builds the adults view model. // Shared between the JSON API route and the HTML handler. func (h *Handler) AssembleAdults(ctx context.Context) (AdultsResponse, error) { members, sortedMonths, txns, exceptions, err := h.loadAll(ctx, true) if err != nil { return AdultsResponse{}, err } result := domreconcile.Reconcile(members, sortedMonths, txns, exceptions, time.Now().Year()) return buildAdultsResponse(members, sortedMonths, result, txns, h.Config, time.Now().Format("2006-01")), nil } // ServeJuniors handles GET /api/juniors. func (h *Handler) ServeJuniors(w http.ResponseWriter, r *http.Request) { resp, err := h.AssembleJuniors(r.Context()) if err != nil { h.writeError(w, r, err) return } writeJSON(w, resp) } // AssembleJuniors loads all data and builds the juniors view model. // Shared between the JSON API route and the HTML handler. func (h *Handler) AssembleJuniors(ctx context.Context) (JuniorsResponse, error) { members, sortedMonths, txns, exceptions, err := h.loadAll(ctx, false) if err != nil { return JuniorsResponse{}, err } result := domreconcile.Reconcile(members, sortedMonths, txns, exceptions, time.Now().Year()) return buildJuniorsResponse(members, sortedMonths, result, txns, h.Config, time.Now().Format("2006-01")), nil } // ServePayments handles GET /api/payments. func (h *Handler) ServePayments(w http.ResponseWriter, r *http.Request) { ctx := r.Context() txns, err := h.Sources.LoadTransactions(ctx) if err != nil { h.writeError(w, r, fmt.Errorf("load transactions: %w", err)) return } writeJSON(w, buildPaymentsResponse(txns, h.allMemberNames(ctx))) } func (h *Handler) loadAll(ctx context.Context, adults bool) ( members []domreconcile.Member, sortedMonths []string, txns []domreconcile.Transaction, exceptions map[domreconcile.ExceptionKey]domreconcile.Exception, err error, ) { if adults { members, sortedMonths, err = h.Sources.LoadAdults(ctx) } else { members, sortedMonths, err = h.Sources.LoadJuniors(ctx) } if err != nil { err = fmt.Errorf("load members: %w", err) return } txns, err = h.Sources.LoadTransactions(ctx) if err != nil { err = fmt.Errorf("load transactions: %w", err) return } exceptions, err = h.Sources.LoadExceptions(ctx) if err != nil { err = fmt.Errorf("load exceptions: %w", err) } return } func (h *Handler) allMemberNames(ctx context.Context) []string { var names []string if adults, _, err := h.Sources.LoadAdults(ctx); err == nil { for _, m := range adults { names = append(names, m.Name) } } if juniors, _, err := h.Sources.LoadJuniors(ctx); err == nil { for _, m := range juniors { names = append(names, m.Name) } } return names } func (h *Handler) writeError(w http.ResponseWriter, r *http.Request, err error) { if h.Logger != nil { h.Logger.Error("api error", "path", r.URL.Path, "err", err) } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusInternalServerError) _ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) } func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") _ = json.NewEncoder(w).Encode(v) }