Commit Graph

20 Commits

Author SHA1 Message Date
723152cdad fix(go): pass raw value to FormatDate so numeric serial-day dates format
All checks were successful
Deploy to K8s / deploy (push) Successful in 11s
The transaction-row parser in services/membership/sources.go used a
helper (`getVal`) that did `fmt.Sprint(row[i])` before passing to
`matching.FormatDate`.  The Sheets API returns date-formatted cells
as `float64` (Sheets serial-day numbers); pre-stringifying defeated
`FormatDate`'s `case float64:` dispatch, so values like 46147 leaked
through unchanged as the string "46147" instead of being converted
to "2026-05-05".

Surfaced by `make parity` (M5.4) — every `transactions[].date` on
/api/adults and /api/juniors differed between Python and Go.  Python
side passes the raw value through directly (`isinstance(val, (int,
float))` in scripts/match_payments.py format_date), so it was always
correct.

Added a `getRaw` helper that returns row[i] without stringifying;
only the date column needs it.  Extended TestLoadTransactions with
a numeric-serial-day row to lock in the regression.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 23:17:45 +02:00
8b3064ffab fix(go): default CacheDir to tmp/go to avoid Python collision
All checks were successful
Deploy to K8s / deploy (push) Successful in 9s
Previously both backends defaulted to `CacheDir=tmp` and used the
same cache keys (`attendance_regular`, `attendance_juniors`,
`payments_transactions`, `exceptions_dict`) but stored different
shapes: Python caches post-processed view-model tuples
(e.g. `(members, sorted_months)`), Go caches raw sheet rows.
Whichever backend wrote last poisoned the cache for the other,
producing `ValueError: too many values to unpack (expected 2,
got 68)` on Python's /adults after the Go side populated the
file with 68 raw CSV rows.

This breaks the M5.4 `make parity` workflow that requires both
backends running side-by-side.

Fix: change Go's default to `tmp/go` so the two cache trees
never overlap.  `CACHE_DIR` env var override still works.
`os.MkdirAll` already handles creating the new subdirectory on
first write.

Recovery for users with poisoned `tmp/`: hit /flush-cache on
the Python side once after pulling, then restart the Go server.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 23:06:34 +02:00
7d48e8f607 feat(go): M5.2 — HTTP handlers for /api/adults, /api/juniors, /api/payments, /api/version
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>
2026-05-07 20:13:38 +02:00
f253e3fcb1 feat(go): M5.1 — hand-author /api/* wire types + JSON Schemas
All checks were successful
Deploy to K8s / deploy (push) Successful in 7s
Add internal/web/api package with Go structs for every /api/X route:
AdultsResponse, JuniorsResponse, PaymentsResponse, VersionResponse.
All fields carry explicit json: tags matching the Python view-model keys.

Key design choices:
- member_data / month_labels / raw_payments are nested objects (not
  the pre-serialised JSON strings used in Jinja templates)
- Expected{Value int; Unknown bool} with custom MarshalJSON emits int
  or the string "?" for junior single-attendance months
- RawTransaction covers the full 11-column payments sheet row

schemagen_test.go reflects all four response types via
github.com/invopop/jsonschema and golden-compares against committed
schemas in tests/fixtures/api-schema/. The JSONSchema() method on
Expected lives in the test file so the prod binary has no jsonschema
dependency.

Closes M5.1 in docs/plans/2026-05-03-2349-go-backend-rewrite-progress.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 17:36:46 +02:00
b41b8ef29c fix(go/fio): accept 2-digit year format in transparent date parser
All checks were successful
Deploy to K8s / deploy (push) Successful in 9s
Fio's transparent account page now serves dates as DD.MM.YY (e.g.
07.05.26) rather than the previously expected 4-digit-year format.
Extends parseCzechDate to try all eight layout variants: padded and
non-padded, dot and slash separators, 4-digit and 2-digit years.

Go maps 2-digit year 00-68 → 2000-2068, so 26 → 2026.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 14:12:34 +02:00
f87adeff9f feat(go/fio): debug logging via slog at LOG_LEVEL=DEBUG
All checks were successful
Deploy to K8s / deploy (push) Successful in 8s
Wires slog.SetDefault to honour LOG_LEVEL in all CLI commands and adds
debug logs on the Fio fetch path so a silent "fetched 0 transaction(s)"
can be diagnosed without code changes:

- fio.New: which client variant (api/transparent) was selected
- apiClient: GET URL (token redacted as ****), HTTP status, body bytes,
  parsed transaction count
- transparentClient: GET URL, HTTP status, body bytes, plus parser
  stats (raw rows from second table, kept, dropped_bad_date,
  dropped_nonpositive_amount)

Also suppresses the --print-fio-table block when zero transactions were
fetched, so the bare header no longer prints under that condition.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 13:59:22 +02:00
a7cf45fc95 feat(go): add --print-fio-table flag to fuj sync --dry-run
All checks were successful
Deploy to K8s / deploy (push) Successful in 7s
Prints an aligned tabwriter table of every Fio transaction in the
look-back window, with a STATUS column showing NEW (would be appended)
or DUP (already in sheet). Only fires when --dry-run is also set, so
it can't affect real syncs. Refactors Sync ID computation into a single
pre-pass shared by both the table printer and the row builder.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 13:49:42 +02:00
fcb83691f5 fix(go/fio): nested-table early exit + non-padded date parsing
All checks were successful
Deploy to K8s / deploy (push) Successful in 9s
extractSecondTableRows tracked a boolean inTarget flag and exited on
the first </table> token while inside the target. Any nested <table>
(e.g. pagination markup in the real Fio page) would cause an early
return before reading any data rows, explaining the 0-transaction report.
Fixed by tracking targetDepth instead: depth increments on every <table>
inside the target and we only return when it reaches 0 again.

parseCzechDate also only tried zero-padded layouts ("02.01.2006").
The real Fio transparent page emits non-padded dates ("7.5.2026");
added "2.1.2006" and "2/1/2006" as the preferred layouts.

Also adds a dry-run diagnostic line ("fetched N transaction(s) from Fio")
so the fetch vs dedup split is visible without reading logs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 10:47:54 +02:00
8275db1a63 fix(go/fio): nil http client panic in fio.New
All checks were successful
Deploy to K8s / deploy (push) Successful in 10s
When token is empty, New falls back to transparentClient with the
caller-supplied hc. main.go passes nil, so the first Do() call panicked.
Default to http.DefaultClient when hc is nil.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 10:36:20 +02:00
36a28a40d2 feat(go): add --dry-run to fuj sync
All checks were successful
Deploy to K8s / deploy (push) Successful in 18s
Mirror fuj infer's read-only mode: SyncOpts.DryRun skips WriteHeader,
AppendValues, and SortByDateColumn, printing one "Dry run: would …"
line per planned operation instead. ID-dedup still runs so the output
reflects exactly what the next real sync would write.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 10:33:55 +02:00
6465e2a221 feat(go): IO layer behind interfaces (M4)
All checks were successful
Deploy to K8s / deploy (push) Successful in 11s
- io/attendance: CSV-over-public-URL client + Fake for adult/junior tabs
- io/drive: Drive v3 modifiedTime client + Fake
- io/sheets: Sheets v4 client (GetValues/AppendValues/BatchUpdateValues/
  WriteHeader/SortByDateColumn) + Fake with call-capture
- io/cache: Drive-modifiedTime-gated FileCache; two TTL knobs; atomic
  writes; generic Get[T]; Python-compatible JSON format; Flush()
- io/fio: Client interface backed by Fio REST API (apiClient) and HTML
  scraper (transparentClient); Fake; testdata fixtures
- membership/sources: NewSources wires attendance CSV + Sheets + cache
  into LoadAdults/LoadJuniors/LoadTransactions/LoadExceptions; Czech
  month parsing + merged-month maps
- banksync: SyncToSheets (SHA-256 dedup, optional sort) and
  InferPayments ([?] review prefix, dry-run) — tested with fakes
- cmd/fuj: sync and infer subcommands wired; fees and reconcile use
  real NewSources; go.mod gains google.golang.org/api + x/net
- gofumpt extra-rules applied across all packages; lint clean

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 01:05:59 +02:00
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
34ce0be5a0 feat(go/M2.10): port domain/reconcile.Reconcile
Three-phase payment allocation (greedy / proportional / even-split)
ported verbatim from scripts/match_payments.py reconcile().
Includes 12 unit tests covering all Python test cases plus Go-only
extras: [?] stripping, other: purpose, out-of-window credit, inference
fallback, and no-match/empty-transaction guards.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 16:52:22 +02:00
e596f0000e feat(go/M2.7-2.9): port domain/matching package
New go/internal/domain/matching package porting three helpers from
scripts/match_payments.py:

- BuildNameVariants: normalized ASCII variants from a member name (nickname
  in parens, last/first split, len<3 filtered); variants[0] is always the
  full base name — MatchMembers relies on this invariant.
- MatchMembers: auto/review confidence matching with an exact-name
  short-circuit pass that prevents nickname substrings (tov) from firing
  inside longer surnames (ottova); common-surname filter for review tier.
- FormatDate: nil/empty/""/serial int/float64 (since 1899-12-30, fractional
  days supported)/YYYY-MM-DD passthrough/garbage → never errors.
- InferTransactionDetails: composes BuildNameVariants+MatchMembers+
  ParseMonthReferences; falls back to sender-only member match and
  date-derived month when text carries no signal.

21 table-driven tests; all expected values verified against live Python
on 2026-05-06. go-build, go-test, go-lint all clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 13:19:42 +02:00
54a783ea00 feat(go/M2.6): port domain/synch.GenerateSyncID
All checks were successful
Deploy to K8s / deploy (push) Successful in 6s
SHA-256 dedup hash from sync_fio_to_sheets.py generate_sync_id.
Key subtlety: Python str(float) emits "500.0" for whole-valued floats
and switches to scientific notation at |f|>=1e16 or |f|<1e-4 —
replicated via formatAmount using 'f'/'e' format selection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 12:43:41 +02:00
d24d20553a feat(go/M2.5): port domain/money.ParseCZK
Port scripts/infer_payments.py parse_czk_amount to Go as
internal/domain/money.ParseCZK. Preserves the Czech-locale heuristic
(comma = decimal sep; 2+ dots = thousand seps; single dot = decimal)
and returns (float64, error) so callers can opt into Python's
silent-zero contract via v, _ := money.ParseCZK(s).
All expected values verified against live Python on 2026-05-06.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 09:38:28 +02:00
57ec817044 feat(go/M2.3+M2.4): port domain/fees.CalculateFee and CalculateJuniorFee
All checks were successful
Deploy to K8s / deploy (push) Successful in 6s
Ports calculate_fee and calculate_junior_fee from scripts/attendance.py
into a new go/internal/domain/fees package. Introduces the Expected type
(Value int, Unknown bool) for the junior "?" sentinel, keeping the Go
API strictly typed instead of mirroring Python's str|int return.

All 20 table-driven tests pass with -race; golangci-lint clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 00:38:09 +02:00
6d971b61d4 feat(go/M2.2): port czech.ParseMonthReferences
All checks were successful
Deploy to K8s / deploy (push) Successful in 8s
Three-pass regex parser matching python/czech_utils.py parse_month_references:
1. Numeric slash notation — "11+12/2025", "01/26"; 2-digit year → +2000
2. Dot notation — "12.2025" (4-digit year only)
3. Czech month names — range walk (listopad-leden wrap logic) then
   standalone with m≥10 → defaultYear-1 heuristic; longest-match
   alternation (sorted desc by name length) handles cervenec vs cerven

35 table-driven tests, all expected outputs verified against live Python
on 2026-05-05 before locking. Plan at
docs/plans/2026-05-05-2337-go-rewrite-m2-2-parse-month-references.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 00:05:40 +02:00
d9a61b338c feat(go/M2.1): port czech.Normalize — NFKD + Mn strip + lowercase
All checks were successful
Deploy to K8s / deploy (push) Successful in 8s
Adds internal/domain/czech.Normalize, the first pure-domain function in
the Go rewrite (M2 milestone). Matches Python czech_utils.normalize byte-
for-byte: NFKD decompose via golang.org/x/text/unicode/norm, drop Mn-
category combining marks (unicode.Mn, not IsMark, to match Python's
unicodedata.combining() semantics), then strings.ToLower.

Includes 13-case table-driven test; all inputs spot-checked against the
Python implementation before locking. Adds golang.org/x/text v0.36.0 as
first external dependency.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 22:23:40 +02:00
cf0f176d3f feat: Go rewrite M1 — skeleton, tooling, and hello server
Stand up the Go project alongside the Python backend so both run
independently during migration. `make web-go` builds and serves on :8080;
`make web-py` (alias: `make web`) keeps the Python side on :5001.

- go/: new module `fuj-management/go` (Go 1.26)
  - cmd/fuj: stdlib-flag dispatcher; `server` + `version` work,
    fees/reconcile/sync/infer stubbed for M2/M4
  - internal/config: env loader mirroring scripts/config.py
  - internal/logging: slog setup, level taken from config
  - internal/web: net/http ServeMux + request-timer middleware
  - build/Dockerfile: golang:1.26 → alpine:3 multi-stage image
  - .golangci.yml: govet, staticcheck, errcheck, gofumpt, unused
- Makefile: web→web-py alias; go-build/go-test/go-run/go-lint/web-go
- CI: parallel build-go job in .gitea/workflows/build.yaml (<tag>-go image)
- docs/plans/: M1 kickoff plan + progress tracker (M1 complete)
- .claude/settings.json: gofumpt + golangci-lint permissions

Gate: make go-build ✓  make go-lint ✓  make go-test ✓  curl :8080 ✓

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 12:05:46 +02:00