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>
This commit is contained in:
2026-05-06 17:50:31 +02:00
parent ea8622a541
commit 56aa2303a8
15 changed files with 1096 additions and 6 deletions

View File

@@ -1,12 +1,15 @@
package main
import (
"context"
"flag"
"fmt"
"fuj-management/go/internal/config"
"fuj-management/go/internal/logging"
"fuj-management/go/internal/services/membership"
"fuj-management/go/internal/web"
"os"
"time"
)
// Injected at build time via -ldflags "-X main.version=... -X main.commit=... -X main.buildDate=..."
@@ -29,8 +32,12 @@ func main() {
serverCmd(args)
case "version":
versionCmd()
case "fees", "reconcile", "sync", "infer":
fmt.Fprintf(os.Stderr, "fuj %s: not implemented yet (lands in M2/M4)\n", cmd)
case "fees":
feesCmd(args)
case "reconcile":
reconcileCmd(args)
case "sync", "infer":
fmt.Fprintf(os.Stderr, "fuj %s: not implemented yet (lands in M4)\n", cmd)
os.Exit(2)
case "-h", "--help", "help":
usage()
@@ -67,6 +74,40 @@ func serverCmd(args []string) {
}
}
func feesCmd(args []string) {
fs := flag.NewFlagSet("fees", flag.ExitOnError)
fs.Usage = func() {
fmt.Fprintln(os.Stderr, "usage: fuj fees")
}
if err := fs.Parse(args); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
sources := membership.NewStubSources()
if err := membership.FeesReport(context.Background(), sources, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "fuj fees: %v\n", err)
os.Exit(1)
}
}
func reconcileCmd(args []string) {
fs := flag.NewFlagSet("reconcile", flag.ExitOnError)
fs.Usage = func() {
fmt.Fprintln(os.Stderr, "usage: fuj reconcile")
}
if err := fs.Parse(args); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
sources := membership.NewStubSources()
if err := membership.ReconcileReport(context.Background(), sources, time.Now().Year(), os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "fuj reconcile: %v\n", err)
os.Exit(1)
}
}
func versionCmd() {
fmt.Printf("fuj %s (%s) built %s\n", version, commit, buildDate)
}
@@ -77,8 +118,8 @@ func usage() {
Commands:
server Start HTTP server (default :8080)
version Print version information
fees Calculate monthly fees [M2]
reconcile Show balance report [M2]
fees Calculate monthly fees
reconcile Show balance report
sync Sync Fio transactions [M4]
infer Infer payment details [M4]`)
}