All checks were successful
Deploy to K8s / deploy (push) Successful in 10s
- Add web/api/handler.go: Handler struct wiring Sources+Config into ServeAdults, ServeJuniors, ServePayments, ServeVersion - Add web/api/build_common.go: getMonthLabels, groupRawPaymentsByPerson, settledBalance, domain-to-wire converters, ensureSlice generic helper - Add web/api/build_adults.go: buildAdultsResponse + buildAdultMemberRow mirroring scripts/views.py:build_adults_view_model - Add web/api/build_juniors.go: buildJuniorsResponse + buildJuniorMemberRow mirroring scripts/views.py:build_juniors_view_model, including "?" sentinel and :NJ,MA breakdown - Add web/api/build_payments.go: buildPaymentsResponse with Unmatched/Unknown bucket - Extend reconcile.FeeData/MonthData with IsUnknown, JuniorAttendance, AdultAttendance - Extend reconcile.Transaction with ManualFix, VS, BankID, SyncID for raw_payments wire field - Export membership.AdultMergedMonths and JuniorMergedMonths - Update sources.go to propagate new FeeData fields and parse extra transaction columns - Wire sources+cfg into web.Run; register /api/* routes via Go 1.22 method+path patterns - Fix pre-existing gofumpt formatting in fio_test.go and fio_table.go Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
126 lines
3.5 KiB
Go
126 lines
3.5 KiB
Go
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) {
|
|
ctx := r.Context()
|
|
members, sortedMonths, txns, exceptions, err := h.loadAll(ctx, true)
|
|
if err != nil {
|
|
h.writeError(w, r, err)
|
|
return
|
|
}
|
|
result := domreconcile.Reconcile(members, sortedMonths, txns, exceptions, time.Now().Year())
|
|
writeJSON(w, buildAdultsResponse(members, sortedMonths, result, txns, h.Config, time.Now().Format("2006-01")))
|
|
}
|
|
|
|
// ServeJuniors handles GET /api/juniors.
|
|
func (h *Handler) ServeJuniors(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
members, sortedMonths, txns, exceptions, err := h.loadAll(ctx, false)
|
|
if err != nil {
|
|
h.writeError(w, r, err)
|
|
return
|
|
}
|
|
result := domreconcile.Reconcile(members, sortedMonths, txns, exceptions, time.Now().Year())
|
|
writeJSON(w, buildJuniorsResponse(members, sortedMonths, result, txns, h.Config, time.Now().Format("2006-01")))
|
|
}
|
|
|
|
// 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)
|
|
}
|