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>
126 lines
2.8 KiB
Go
126 lines
2.8 KiB
Go
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=..."
|
|
var (
|
|
version = "dev"
|
|
commit = "unknown"
|
|
buildDate = "unknown"
|
|
)
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
usage()
|
|
os.Exit(2)
|
|
}
|
|
|
|
cmd, args := os.Args[1], os.Args[2:]
|
|
|
|
switch cmd {
|
|
case "server":
|
|
serverCmd(args)
|
|
case "version":
|
|
versionCmd()
|
|
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()
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "fuj: unknown command %q\n\n", cmd)
|
|
usage()
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
func serverCmd(args []string) {
|
|
fs := flag.NewFlagSet("server", flag.ExitOnError)
|
|
addr := fs.String("addr", "", "listen address (default from SERVER_ADDR env or :8080)")
|
|
fs.Usage = func() {
|
|
fmt.Fprintln(os.Stderr, "usage: fuj server [--addr :8080]")
|
|
fs.PrintDefaults()
|
|
}
|
|
if err := fs.Parse(args); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(2)
|
|
}
|
|
|
|
cfg := config.Load()
|
|
if *addr != "" {
|
|
cfg.ServerAddr = *addr
|
|
}
|
|
|
|
logger := logging.New(cfg.LogLevel)
|
|
build := web.BuildInfo{Version: version, Commit: commit, BuildDate: buildDate}
|
|
|
|
if err := web.Run(logger, cfg.ServerAddr, build); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
func usage() {
|
|
fmt.Fprintln(os.Stderr, `usage: fuj <command> [flags]
|
|
|
|
Commands:
|
|
server Start HTTP server (default :8080)
|
|
version Print version information
|
|
fees Calculate monthly fees
|
|
reconcile Show balance report
|
|
sync Sync Fio transactions [M4]
|
|
infer Infer payment details [M4]`)
|
|
}
|