Files
fuj-management/go/internal/services/membership/format_fees.go
Jan Novak 56aa2303a8 feat(go/M2.11-12): wire fuj fees + fuj reconcile subcommands
Add internal/services/membership package: AttendanceLoader,
TransactionLoader, ExceptionLoader interfaces + NewStubSources stub
(returns ErrIOPending until M4 lands real Sheets loaders).

FeesReport and ReconcileReport orchestrate domain/fees + domain/reconcile
and write fixed-width text reports matching Python calculate_fees.py and
match_payments.py print_report output. 13 unit tests cover all formatter
branches and orchestration wiring via fake loaders.

cmd/fuj/main.go: fees and reconcile subcommands now dispatch; sync/infer
retain the [M4] placeholder.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 17:50:31 +02:00

90 lines
2.0 KiB
Go

package membership
import (
"fmt"
"fuj-management/go/internal/domain/reconcile"
"io"
"strings"
"time"
)
// printFeesTable writes a fixed-width adult-fees table to w.
// Mirrors scripts/calculate_fees.py main().
//
// Verify with:
//
// PYTHONPATH=scripts:. python scripts/calculate_fees.py
func printFeesTable(w io.Writer, members []reconcile.Member, sortedMonths []string) {
type row struct {
name string
fees map[string]reconcile.FeeData
}
var adults []row
for _, m := range members {
if m.Tier == "A" {
adults = append(adults, row{name: m.Name, fees: m.Fees})
}
}
if len(adults) == 0 {
fmt.Fprintln(w, "No data.")
return
}
monthLabel := func(m string) string {
t, err := time.Parse("2006-01", m)
if err != nil {
return m
}
return t.Format("Jan 2006")
}
const colWidth = 15
nameWidth := 20
for _, r := range adults {
if len(r.name) > nameWidth {
nameWidth = len(r.name)
}
}
// separator length: nameWidth + N*(colWidth+3) where +3 is " | "
sepLen := nameWidth + len(sortedMonths)*(colWidth+3)
// Header row
fmt.Fprintf(w, "%-*s", nameWidth, "Member")
for _, m := range sortedMonths {
fmt.Fprintf(w, " | %*s", colWidth, monthLabel(m))
}
fmt.Fprintln(w)
fmt.Fprintln(w, strings.Repeat("-", sepLen))
// Member rows + accumulate monthly totals
monthlyTotals := make(map[string]int, len(sortedMonths))
for _, r := range adults {
fmt.Fprintf(w, "%-*s", nameWidth, r.name)
for _, m := range sortedMonths {
fd := r.fees[m]
monthlyTotals[m] += fd.Expected
var cell string
if fd.Attendance > 0 {
cell = fmt.Sprintf("%d CZK (%d)", fd.Expected, fd.Attendance)
} else {
cell = "-"
}
fmt.Fprintf(w, " | %*s", colWidth, cell)
}
fmt.Fprintln(w)
}
// Totals row
fmt.Fprintln(w, strings.Repeat("-", sepLen))
fmt.Fprintf(w, "%-*s", nameWidth, "TOTAL")
for _, m := range sortedMonths {
cell := fmt.Sprintf("%d CZK", monthlyTotals[m])
fmt.Fprintf(w, " | %*s", colWidth, cell)
}
fmt.Fprintln(w)
}