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>
245 lines
6.4 KiB
Go
245 lines
6.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"fuj-management/go/internal/config"
|
|
"fuj-management/go/internal/io/fio"
|
|
"fuj-management/go/internal/io/sheets"
|
|
"fuj-management/go/internal/logging"
|
|
"fuj-management/go/internal/services/banksync"
|
|
"fuj-management/go/internal/services/membership"
|
|
"fuj-management/go/internal/web"
|
|
"log/slog"
|
|
"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)
|
|
}
|
|
|
|
// Honour LOG_LEVEL for slog calls in any package (e.g. internal/io/fio debug logs).
|
|
slog.SetDefault(logging.New(os.Getenv("LOG_LEVEL")))
|
|
|
|
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":
|
|
syncCmd(args)
|
|
case "infer":
|
|
inferCmd(args)
|
|
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
|
|
}
|
|
|
|
ctx := context.Background()
|
|
logger := logging.New(cfg.LogLevel)
|
|
|
|
sources, err := membership.NewSources(ctx, cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "fuj server: init sources: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
build := web.BuildInfo{Version: version, Commit: commit, BuildDate: buildDate}
|
|
|
|
if err := web.Run(logger, cfg.ServerAddr, build, sources, cfg); 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)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
cfg := config.Load()
|
|
sources, err := membership.NewSources(ctx, cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "fuj fees: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err := membership.FeesReport(ctx, 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)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
cfg := config.Load()
|
|
sources, err := membership.NewSources(ctx, cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "fuj reconcile: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err := membership.ReconcileReport(ctx, 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 syncCmd(args []string) {
|
|
fs := flag.NewFlagSet("sync", flag.ExitOnError)
|
|
days := fs.Int("days", 30, "look-back window in days (ignored when --from/--to are set)")
|
|
fromStr := fs.String("from", "", "start date YYYY-MM-DD")
|
|
toStr := fs.String("to", "", "end date YYYY-MM-DD")
|
|
sort := fs.Bool("sort", true, "sort sheet by date after appending")
|
|
dryRun := fs.Bool("dry-run", false, "print planned writes without modifying the sheet")
|
|
printFioTable := fs.Bool("print-fio-table", false, "with --dry-run: print aligned table of every Fio transaction with NEW/DUP status")
|
|
fs.Usage = func() {
|
|
fmt.Fprintln(os.Stderr, "usage: fuj sync [--days N] [--from YYYY-MM-DD --to YYYY-MM-DD] [--sort] [--dry-run] [--print-fio-table]")
|
|
fs.PrintDefaults()
|
|
}
|
|
if err := fs.Parse(args); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(2)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
cfg := config.Load()
|
|
|
|
sheetsCli, err := sheets.New(ctx, cfg.CredentialsPath, cfg.DriveTimeout)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "fuj sync: sheets client: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fioCli := fio.New(cfg.FioAPIToken, config.IBANAccountNum(cfg.BankAccount), nil)
|
|
|
|
opts := banksync.SyncOpts{Days: *days, Sort: *sort, DryRun: *dryRun, PrintFioTable: *printFioTable}
|
|
if *fromStr != "" && *toStr != "" {
|
|
opts.From, err = time.Parse("2006-01-02", *fromStr)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "fuj sync: invalid --from: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
opts.To, err = time.Parse("2006-01-02", *toStr)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "fuj sync: invalid --to: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
n, err := banksync.SyncToSheets(ctx, config.PaymentsSheetID, fioCli, sheetsCli, opts)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "fuj sync: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if *dryRun {
|
|
fmt.Printf("Dry run: would sync %d new transaction(s).\n", n)
|
|
} else {
|
|
fmt.Printf("Synced %d new transaction(s).\n", n)
|
|
}
|
|
}
|
|
|
|
func inferCmd(args []string) {
|
|
fs := flag.NewFlagSet("infer", flag.ExitOnError)
|
|
dryRun := fs.Bool("dry-run", false, "print planned updates without writing to the sheet")
|
|
fs.Usage = func() {
|
|
fmt.Fprintln(os.Stderr, "usage: fuj infer [--dry-run]")
|
|
fs.PrintDefaults()
|
|
}
|
|
if err := fs.Parse(args); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(2)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
cfg := config.Load()
|
|
|
|
sheetsCli, err := sheets.New(ctx, cfg.CredentialsPath, cfg.DriveTimeout)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "fuj infer: sheets client: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
sources, err := membership.NewSources(ctx, cfg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "fuj infer: sources: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
n, err := banksync.InferPayments(ctx, config.PaymentsSheetID, sheetsCli, sources, banksync.InferOpts{DryRun: *dryRun})
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "fuj infer: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if *dryRun {
|
|
fmt.Printf("Dry run: would update %d row(s).\n", n)
|
|
} else {
|
|
fmt.Printf("Updated %d row(s).\n", n)
|
|
}
|
|
}
|
|
|
|
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 to payments sheet
|
|
infer Infer payment details in payments sheet`)
|
|
}
|