Files
fuj-management/CHANGELOG.md
Jan Novak 29938d7a0c
All checks were successful
Deploy to K8s / deploy (push) Successful in 8s
chore(changelog): log gitops-update workflow addition
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 19:32:20 +02:00

33 KiB
Raw Permalink Blame History

Changelog

2026-06-12 19:32 CEST — feat(ci): gitops image-update PR workflow

  • Added .gitea/workflows/gitops-update.yaml: after each successful Go image build, uh-cli gitops deployment update opens a PR in kacerr/home-kubernetes bumping the fuj-management Deployment (namespace fuj) to the new image tag.
  • Supports workflow_run auto-trigger and workflow_dispatch with dry_run / uh_cli_version inputs.
  • Requires GITOPS_TOKEN repo secret (Gitea PAT with write+PR access to home-kubernetes).

2026-05-24 21:58 CEST — feat(fees): update adult monthly rates for 2026-05 through 2026-08

  • 2026-05: 700 → 450 CZK; 2026-06/07/08: 600 CZK (new months added).
  • Mirrored in both scripts/attendance.py and go/internal/domain/fees/fees.go.

2026-05-24 21:42 CEST — feat: multi-account Fio sync + switch QR default to 2502035405/2010

  • Added second bank account 2502035405/2010 (IBAN CZ0820100000002502035405) to sync.
  • Both accounts are fetched on every sync; dedup by existing sync_id keeps the payments sheet clean.
  • QR codes now default to the new account (CZ0820100000002502035405).
  • Go: config.go gains hardcoded Accounts/LoadedAccount slice; Config.BankAccount renamed to Config.QRAccount; FioAPIToken removed (tokens are per-account via FIO_API_TOKEN_NEW / FIO_API_TOKEN_OLD).
  • Go: SyncToSheets now accepts []fio.Client; new TestSyncToSheets_MultiAccount test.
  • Python: config.py gains ACCOUNTS / LOADED_ACCOUNTS; fio_utils.py adds fetch_transactions_for and fetch_transactions_all; sync_fio_to_sheets.py uses fetch_transactions_all.
  • Key files: go/internal/config/config.go, go/internal/services/banksync/sync.go, go/cmd/fuj/main.go, scripts/config.py, scripts/fio_utils.py, scripts/sync_fio_to_sheets.py.

2026-05-11 23:58 CEST — fix(reconcile): fill earliest month deficit first in multi-month allocations

  • Multi-month payment allocation now fills the earliest in-window deficit first and spills any remainder to later months, accounting for prior transactions' contributions to each month. Previously a single transaction was split proportionally to each month's total expected fee, ignoring what earlier transactions had already paid — surfaced by Matyáš Thér's 200+550 case showing 566/183 instead of 500/250.
  • Files: scripts/match_payments.py, go/internal/domain/reconcile/reconcile.go, tests, parity fixtures.

2026-05-11 22:56 CEST — fix(python): parse Fio 2-digit-year dates + add make sync-debug dry-run tool

  • Fix: scripts/fio_utils.py parse_czech_date now accepts DD.MM.YY / D.M.YY in addition to the 4-digit-year variants. Fio's transparent page now mixes both forms in the same response; the 2-digit rows were being silently dropped, which caused make sync-2026 to miss every recent transfer. Mirrors the Go-side fix from 2026-05-07 (CHANGELOG entry below).
  • Added --dry-run and --print-fio-table flags to scripts/sync_fio_to_sheets.py, plus a make sync-debug [DAYS=N] Makefile target. Mirrors make go-sync-debug: fetches from Fio and dedupes against the sheet, prints STATUS=NEW/DUP per transaction, and prints per-row Dry run: would append … lines + would sort by date instead of touching the sheet.
  • Added always-on stderr diagnostics in scripts/fio_utils.py: which fetcher was selected (authenticated API vs. transparent-page scraper with FIO_API_TOKEN-unset warning), and raw-vs-after-filter transaction counts on both paths — so this class of "scraper drops everything" bug surfaces immediately.

2026-05-08 15:24 CEST — feat(go): M6.7 — single-binary embed verification

  • Confirmed embed.FS wiring is complete: templates parsed via template.ParseFS(templateFS, ...), static assets served via http.FileServerFS(fs.Sub(staticFS, "static")).
  • Added go/internal/web/assets_test.go with two tests: TestEmbedCompleteness (walks disk vs embed.FS to catch forgotten files) and TestStaticAssetsServed (hits /static/css/app.css and all JS files through the mux, asserts 200 + Content-Type + non-empty body + 404 for unknown paths).
  • Closes M6; single binary confirmed self-contained with no adjacent templates/ or static/ required at runtime.
  • Key files: go/internal/web/assets_test.go (new).

2026-05-08 14:55 CEST — feat(go): M6.6.1 — Pay-button QR popup modal

  • Restored the Python showPayQR in-page modal UX that was lost in M6.6 (Pay buttons were navigating the tab to the raw /qr PNG).
  • Replaced <a href="{{qrHref ...}}">Pay</a> with <button data-name|amount|month|raw-month> on /adults and /juniors; click is handled by a new static/js/payment-qr.js IIFE module that opens #qrModal with title, account, amount, message, and the QR image.
  • Added #qrModal markup to both templates; CSS display:none / .active{display:flex} rules added (content rules were already present from M6.1). Esc, [close], and outside-click all dismiss; coexists with the M6.5 member-detail modal.
  • Removed the now-dead qrHref / qrHrefAll template helpers from render.go.
  • Markup tests in html_handler_test.go assert modal IDs, script tag, data-bank-account, and that no bare href="/qr" links remain.
  • Key files: go/internal/web/static/js/payment-qr.js, go/internal/web/templates/adults.tmpl, go/internal/web/templates/juniors.tmpl, go/internal/web/render.go, go/internal/web/static/css/app.css.

2026-05-08 13:57 CEST — feat(go): M6.6 — /qr, /sync-bank, /flush-cache, /version

  • Added GET /qr: generates Czech QR Platba PNG from SPD payload (account, amount, message query params); ports Python's qr_code() handler exactly including account validation, amount clamping, and * stripping.
  • Implemented GET /sync-bank: runs Fio sync → infer payments → cache flush, captures output into sync.tmpl page with success/error banner.
  • Implemented GET /flush-cache + POST /flush-cache: form + action that deletes cache files and shows deleted count.
  • Added GET /version as a JSON alias of GET /api/version (Python parity).
  • Added FlushCache() (int, error) to membership.Sources interface; implemented on realSources via cache.FileCache.Flush().
  • Introduced web.ActionHandlers{BankSync} — closure-based dep injection for sync, constructed in serverCmd with fio + sheets clients.
  • New dependency: github.com/skip2/go-qrcode.
  • Key files: go/internal/web/qr.go, go/internal/web/html_handler.go, go/internal/web/server.go, go/internal/services/membership/loader.go, go/internal/web/templates/sync.tmpl, go/internal/web/templates/flush_cache.tmpl.

2026-05-08 13:19 CEST — feat(go): M6.5 — member-detail modal JS module

  • Added static/js/member-detail.js: fetches /api/adults or /api/juniors once on page load, caches the response, renders a per-member detail modal on [i] row click.
  • Modal sections: status-per-month table, fee exceptions, other transactions, matched payment history, toggleable raw-payments debug view.
  • Keyboard nav: Esc closes, / walk visible (name-filtered) rows; click outside modal content closes it.
  • Added [i] info icon and #memberModal markup to adults.tmpl and juniors.tmpl; all CSS was already in place from M6.1.
  • Added TestModalMarkup assertions to html_handler_test.go.

2026-05-08 12:48 CEST — feat(go): M6.4 — Go-native /payments page

  • Extracted AssemblePayments(ctx) from the inlined JSON handler body, following the M6.2/M6.3 AssembleAdults/AssembleJuniors pattern.
  • Added PaymentsPageData to render.go; wired HTMLHandler.ServePayments to call AssemblePayments and render the new template.
  • Replaced the "Coming in M6.4" placeholder in payments.tmpl with the full grouped-by-person ledger: alphabetical <h2> member blocks, each with a txn-table (Date / Amount / Purpose / Bank Message), newest-first rows, "Unmatched / Unknown" bucket.
  • Appended ledger CSS classes to app.css (.ledger-container, .member-block, .txn-table, .txn-{date,amount,purpose,message}, tr:hover).
  • Added TestPaymentsPage markup test. No filters, no JS — matches the Python page.

2026-05-08 11:25 CEST — feat(go): M6.3 — Go-native /juniors page

  • Extracted AssembleJuniors(ctx) from the JSON handler so HTML and JSON share one view-model path (mirrors AssembleAdults).
  • Added JuniorsPageData to render.go; wired HTMLHandler.ServeJuniors to call AssembleJuniors and render the new template.
  • Replaced the 4-line "Coming in M6.3" placeholder in juniors.tmpl with the full template: member table, name/month-range filters, totals row, Credits + Debts sections, Pay / Pay All links to /qr. No Unmatched section (parity with Python).
  • J/A attendance breakdown and "?" sentinel rendered via MonthCell.Text produced by buildJuniorMemberRow — no extra template logic.
  • make parity reports 3/3 routes OK; all Go tests pass.

2026-05-08 11:11 CEST — fix: period selector showed only Dec 2025+ on adults

  • Restored the 2025-09 → 2025-10 adult merge in scripts/attendance.py and go/internal/services/membership/sources.go (commented-out by 1257f0d); the 2025-12 → 2026-01 mapping stays disabled per product decision (Dec and Jan are billed separately for adults).
  • Dropped the defaultFrom = maxMonthIdx 4 JS auto-default in templates/adults.html and templates/juniors.html (introduced by 7774301); the From-selector now starts at the oldest available month so all non-future periods render on first load. Future-month removal is preserved.
  • go/internal/services/membership/sources_test.go: TestLoadAdults / TestLoadAdults_Fee now assert that Sep dates land in the merged 2025-10 bucket.
  • Independent of these code changes: the live adults attendance Google Sheet header had been pruned to start at 02.12.2025 (Sep/Oct/Nov 2025 columns deleted); restoring those columns from Sheets version history is required to actually see those periods on the dashboard.

2026-05-08 10:15 CEST — fix(go): adults template — use lifted CSS classes for visual parity

  • Use existing .balance-pos / .balance-neg (drop invented balance-cell / balance-negative); Pay-All button now lives inside the balance cell with position: relative (matches Python; no separate trailing column).
  • Credits / Debts / Unmatched sections rewritten from <ul> / <table> to <div class="list-container"><div class="list-item">… / <div class="unmatched-row">… so the lifted CSS actually applies.
  • Section headings get the descriptive Python text: "Credits (Advance Payments / Surplus)", "Debts (Missing Payments)".
  • Source links moved from page bottom to a <div class="description"> block under the h1, matching Python's "Source: Attendance Sheet | Payments Ledger".
  • Total row uses cell-{{Status}} plus the small "received / expected" caption span and Python's inline-styled bold/dark background.
  • Drop the redundant class="cell" wrapper; debts amount turns red via inline style; balance value drops the trailing "CZK".

2026-05-08 01:09 CEST — feat(go): M6.2 — adults page (table, filters, Pay buttons)

  • go/internal/web/api/handler.go: extracted ServeAdults body into AssembleAdults(ctx) — shared by the JSON API route and the new HTML handler.
  • go/internal/web/render.go: added AdultsPageData view model (PageData + api.AdultsResponse + Error); tmplFuncs with qrHref / qrHrefAll (URL-encode QR Platba params, convert YYYY-MM → MM/YYYY).
  • go/internal/web/html_handler.go: HTMLHandler gains *api.Handler; ServeAdults loads real reconcile data and renders the full adults page.
  • go/internal/web/templates/adults.tmpl: full table (per-member rows, per-cell status classes, data-month-idx, Pay button hrefs to /qr), totals row, credits/debts/unmatched sections, filter controls, sheet links.
  • go/internal/web/static/js/filters.js: name filter (NFD-normalize) + month-range hide/show by data-month-idx; future months hidden by default.

2026-05-08 00:44 CEST — feat(go): M6.1 — template skeleton + embed.FS

  • go/internal/web/templates/: base.tmpl (full HTML layout), partials/nav.tmpl (three-tier nav with active-link highlighting), partials/footer.tmpl (build meta), and stub pages for each route (adults/juniors/payments/sync/flush_cache).
  • go/internal/web/static/css/app.css: terminal-green-on-black theme extracted once from Python templates/adults.html — shared by all Go HTML pages via <link>.
  • go/internal/web/assets.go: //go:embed templates static for single-binary deployment.
  • go/internal/web/render.go: Renderer parses a fresh *template.Template per page at startup; Render(w, name, data) executes the "base" template block.
  • go/internal/web/html_handler.go: HTMLHandler with one method per route (ServeAdults, ServeJuniors, ServePayments, ServeSync, ServeFlushCache).
  • go/internal/web/server.go: drops helloHandler; GET /{$} now redirects to /adults; HTML + /static/ routes registered alongside the existing /api/* routes.
  • go/internal/web/html_handler_test.go: smoke test — each route returns 200 text/html with exactly one class="active" on the matching nav link.

2026-05-08 00:26 CEST — fix(py): parity coercions — amount/message types + junior '?' sticky

  • scripts/match_payments.py: added get_float helper — non-numeric amount values (e.g. "---" placeholder rows) now coerce to 0.0 matching Go's parseFloat behaviour; message field now goes through get_str so numeric cell values (bank references) are emitted as strings, matching Go's fmt.Sprint.
  • scripts/views.py: junior month cell "?" text is now sticky across exception overrides. Previously reconcile replaced expected with the exception amount before the view builder ran, silently turning "?" into "-" when the override was 0. Fixed by deriving is_unknown from original_expected == "?" instead of expected == "?". Also aligned tooltip guard: only show Received/Expected for non-unknown months (or when paid > 0), matching Go's !md.IsUnknown condition.

2026-05-07 23:51 CEST — feat(py): M5.4 fix #2 — add vs and sync_id to payments tx projection

  • scripts/match_payments.py: fetch_sheet_data now reads VS and Sync ID columns and includes vs/sync_id keys in every tx dict. Previously only 9 columns were projected, causing make parity to report extra vs/sync_id fields on every raw payment row emitted by the Go backend. Values flow through group_payments_by_person_unwrap_view_model_for_api to raw_payments (adults/juniors) and grouped_payments (payments) automatically.
  • tests/test_app.py: updated /api/* mock fixtures to include vs/sync_id keys for realism.
  • Cache note: after deploying, hit POST /flush-cache once so the in-process cache is cleared and the next request picks up the new column lookups.

2026-05-07 23:37 CEST — fix(go): accept single-digit day/month in attendance date headers

  • go/internal/services/membership/sources.go: parseDates now uses Go time formats 2.1.2006 and 1/2/2006 (single-digit reference forms, which accept both padded and unpadded inputs) instead of 02.01.2006 and 01/02/2006. The Czech attendance sheet headers contain dates like 1.6.2026, 23.3.2026, 6.4.2026 — Go silently dropped those columns under the strict zero-padded format, while Python's strptime("%d.%m.%Y") accepted them. Effect was a missing 2026-06 month entirely on /api/juniors plus undercounted attendance for any month with single-digit columns; both surfaced as diffs in make parity.
  • sources_test.go::TestParseDates_SingleDigitDayMonth added as a regression guard covering both Czech and US format flavours with and without leading zeros.

2026-05-07 23:17 CEST — fix(go): pass raw value to FormatDate so numeric serial-day dates format

  • go/internal/services/membership/sources.go: transaction-row parser now passes row[idxDate] directly to matching.FormatDate (via a new getRaw helper) instead of stringifying first via getVal. The Sheets API returns numeric serial-day values as float64 for date-formatted cells; pre-stringifying them defeated FormatDate's case float64: dispatch, causing all numeric dates to leak through as "46147" style strings instead of "2026-05-05".
  • Surfaced by make parity (M5.4): every transactions[].date field on /api/adults and /api/juniors differed between Python and Go.
  • sources_test.go::TestLoadTransactions extended with a numeric-serial-day row covering the regression.

2026-05-07 23:05 CEST — fix(go): default CacheDir to tmp/go to avoid Python collision

  • go/internal/config/config.go: CacheDir default changed from tmp to tmp/go. Override via CACHE_DIR env var still works.
  • Why: both backends used tmp/<key>_cache.json with the same keys (attendance_regular, attendance_juniors, payments_transactions, exceptions_dict) but different shapes — Python caches post-processed view-model tuples, Go caches raw rows. Whichever wrote last poisoned the cache; running both in parallel produced ValueError: too many values to unpack (expected 2, got 68) on Python's /adults after the Go server populated attendance_regular_cache.json with raw CSV rows.
  • After upgrading: stop the Go server, hit /flush-cache on the Python side once (rewrites tmp/*.json with correct shapes), then restart make web-go — it will use tmp/go/ going forward. Required for the M5.4 make parity workflow which assumes both backends run side-by-side.

2026-05-07 22:55 CEST — feat(go): M5.4 — parity diff binary + make parity

  • go/cmd/parity/main.go: new standalone binary that GETs /api/adults, /api/juniors, /api/payments from both Python (:5001) and Go (:8080) backends, scrubs an allowlist (render_time.total), and prints cmp.Diff for any remaining differences. Exits 0 on full match, 1 on diffs, 2 on fetch/parse errors — CI-friendly for M7.2. /api/version is excluded by design (returns binary identity — tag/commit/build_date — which differs between independently built backends); still accessible via make parity ARGS="-route /api/version".
  • go/cmd/parity/scrub_test.go: 4 unit tests covering top-level delete, nested delete, missing path, and non-map parent.
  • go/go.mod: github.com/google/go-cmp promoted to direct dependency.
  • Makefile: parity target added (.PHONY, help, cd go && go run ./cmd/parity).
  • docs/plans/2026-05-07-2254-m5-4-parity-binary.md: plan archived.

2026-05-07 22:37 CEST — feat(py): M5.3 — Python /api/* shadow endpoints

  • app.py: four new JSON routes (/api/version, /api/adults, /api/juniors, /api/payments) mirroring the Go /api/* handlers; _unwrap_view_model_for_api() helper expands pre-serialised JSON strings and renames month_labels_jsonmonth_labels, raw_payments_jsonraw_payments to match Go wire contract.
  • tests/test_app.py: four new smoke tests asserting top-level key sets and that unwrapped fields are objects (not strings).

2026-05-07 20:13 CEST — feat(go): M5.2 — HTTP handlers for /api/adults, /api/juniors, /api/payments, /api/version

  • web/api/handler.go: Handler struct + ServeAdults, ServeJuniors, ServePayments, ServeVersion using membership.Sources.
  • web/api/build_{adults,juniors,payments,common}.go: ports of scripts/views.py view-model builders; buildJuniorMemberRow handles "?" sentinel, :NJ,MA breakdown, unknown-month skip.
  • Extended reconcile.FeeData/MonthData with IsUnknown, JuniorAttendance, AdultAttendance; Transaction with ManualFix, VS, BankID, SyncID.
  • sources.go exports AdultMergedMonths/JuniorMergedMonths; parses new FeeData and transaction columns.
  • web/server.go + cmd/fuj/main.go wired to register /api/* routes.
  • PR #17.

2026-05-07 17:37 CEST — feat(go): M5.1 — /api/* wire types + JSON Schemas

  • New go/internal/web/api/ package: AdultsResponse, JuniorsResponse, PaymentsResponse, VersionResponse with explicit json: tags matching Python view-model keys.
  • Expected{Value int; Unknown bool} custom MarshalJSON emits integer or "?" for junior single-attendance months.
  • schemagen_test.go golden-tests four JSON Schemas committed to go/tests/fixtures/api-schema/. JSONSchema() on Expected lives in the test file — production binary has no jsonschema dep.
  • PR #16.

2026-05-07 15:26 CEST — refactor(app): extract view-model builders into scripts/views.py

  • Pulled ~350 lines of inline per-row computation out of adults_view, juniors_view, and payments into three pure functions in scripts/views.py: build_adults_view_model, build_juniors_view_model, build_payments_view_model.
  • Moved get_month_labels, group_payments_by_person, adapt_junior_members from app.py to scripts/views.py. Route handlers now ~25 lines each.
  • Hotfixed missing import re that caused 500 on /qr after the refactor.
  • No behaviour change; all 27 tests pass. Prep for /api/* shadow endpoints (M5).

2026-05-07 14:13 CEST — feat(go): --print-fio-table + Fio debug logging + date parser fix

  • Added --print-fio-table flag to fuj sync --dry-run: prints an aligned table of every Fio transaction in the window with STATUS=NEW/DUP, using text/tabwriter. Key files: go/internal/services/banksync/fio_table.go, sync.go, cmd/fuj/main.go.
  • Added LOG_LEVEL=DEBUG debug logging on the Fio fetch path: client variant selected, full GET URL (token redacted on API path), HTTP status, body bytes, and per-parse drop-reason counters (raw_rows, kept, dropped_bad_date, dropped_nonpositive_amount). Key files: go/internal/io/fio/{client,api,transparent}.go.
  • Fixed parseCzechDate to accept DD.MM.YY (2-digit year) in addition to the 4-digit variant — Fio's transparent page now serves this format. Key file: go/internal/io/fio/transparent.go.
  • Added make go-sync-debug [DAYS=N] Makefile target (default 30 days).

2026-05-07 10:32 CEST — feat(go): --dry-run for fuj sync

  • SyncOpts.DryRun bool added; when true, SyncToSheets prints planned writes (would write header row, would append date=… amount=… sender=…, would sort by date) and returns without calling WriteHeader, AppendValues, or SortByDateColumn.
  • fuj sync --dry-run flag wired in cmd/fuj/main.go; mirrors existing fuj infer --dry-run behaviour.
  • TestSyncToSheets_DryRun added to banksync test suite.

2026-05-07 01:03 CEST — feat(go/M4): IO layer behind interfaces

  • go/internal/io/attendance: CSV-over-public-URL client + Fake for both adult and junior tabs.
  • go/internal/io/drive: thin Drive v3 wrapper for modifiedTime reads + Fake.
  • go/internal/io/sheets: Sheets v4 client (GetValues, AppendValues, BatchUpdateValues, WriteHeader, SortByDateColumn) + Fake with call-capture for assertions.
  • go/internal/io/cache: Drive-modifiedTime-gated FileCache with two TTL knobs, atomic writes, and generic Get[T]; Python-compatible JSON format; Flush() support.
  • go/internal/io/fio: Client interface backed by Fio REST API (apiClient) and HTML-scraper (transparentClient); Fake for tests. Fixtures in testdata/.
  • go/internal/services/membership/sources.go: NewSources wires attendance CSV + Sheets + cache into LoadAdults, LoadJuniors, LoadTransactions, LoadExceptions. Includes Czech month/merged-month parsing logic.
  • go/internal/services/banksync: SyncToSheets (dedup via SHA-256 Sync ID, optional sort) and InferPayments (name-match + [?] review prefix, dry-run) — fully tested with fakes.
  • go/cmd/fuj/main.go: sync and infer subcommands wired to real clients; fees and reconcile now use real NewSources.
  • All packages lint-clean (golangci-lint v1.64.8, gofumpt extra-rules).

2026-05-06 23:25 CEST — feat(go/M3): fixture capture + parity test framework

  • scripts/capture_fixtures.py: dispatcher CLI that calls each ported function with seeded inputs and emits captured output as JSON fixtures.
  • scripts/scrub_fixtures.py: deterministic PII scrubber (SHA-256 pseudonyms, digit-preserving account/VS hashes, name-sweep in free text).
  • scripts/_fixture_seeds.py: handcrafted seed registry for all 10 pure functions + 10 reconcile branch-coverage cases.
  • 98 fixture files committed under go/tests/fixtures/pure/<func>/ and go/tests/fixtures/reconcile/; all PII-free.
  • go/tests/parity/parityio.go: shared loader with generic LoadDir/RunAll helpers and typed In/Out structs for all 10 functions.
  • 11 parity test packages under //go:build parity: 10 pure-function tests + bespoke reconcile test with per-cell float tolerance.
  • Makefile: go-parity, go-test-all, capture-fixtures targets.
  • go/tests/fixtures/README.md: refresh workflow, PII audit guide, adding-a-fixture steps.

2026-05-06 17:49 CEST — feat(go/M2.11-12): wire fuj fees + fuj reconcile subcommands

  • New go/internal/services/membership package: AttendanceLoader, TransactionLoader, ExceptionLoader interfaces, a stub (NewStubSources) that returns ErrIOPending, and FeesReport / ReconcileReport orchestration functions backed by real domain/fees + domain/reconcile logic.
  • Text formatters printFeesTable / printReconcileReport port the output of calculate_fees.py and match_payments.py print_report verbatim.
  • cmd/fuj/main.go: fuj fees and fuj reconcile subcommands now dispatch properly; fuj sync / fuj infer retain the [M4] placeholder.
  • Both subcommands exit 1 with a clean "io layer not yet wired up; lands in milestone M4" message until real Sheets loaders are injected in M4.
  • 13 unit tests covering stubs, all formatter branches (OK/partial/UNPAID/dash cells, credits, debts, unmatched, review annotation), and orchestration wiring via fake loaders.

2026-05-06 16:38 CEST — fix: include juniors in payment-inference roster

  • scripts/infer_payments.py: union adults + junior rosters so junior-only members are visible to the matcher.
  • Root cause: get_members_with_fees() reads only the adults sheet; junior-only kids like Jáchym Kubík were absent from member_names, causing the exact-match short-circuit to never fire and a different adult sharing the first name to win via fuzzy review.
  • Two regression tests added to tests/test_match_members.py.

2026-05-06 16:05 CEST — feat(go/M2.10): port domain/reconcile.Reconcile

  • New go/internal/domain/reconcile package porting the three-phase payment allocation from scripts/match_payments.py reconcile().
  • 12 unit tests covering all Python test cases plus Go-only extras (diacritics tolerance, [?] stripping, other: purpose, out-of-window credit, inference fallback, unmatched, no-transaction guard).

2026-05-06 13:18 CEST — 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 — extracts normalized ASCII search variants from a member name, including nickname (from parens) and separate first/last; filters variants shorter than 3 chars; variants[0] is always the full normalized base name.
  • MatchMembers — finds members in free text with "auto" or "review" confidence; exact-name short-circuit prevents nickname substrings (e.g. tov) from matching inside surnames (e.g. ottova).
  • FormatDate — normalizes Google Sheets date values: handles nil, empty, int/float64 serial-days since 1899-12-30 (supports fractional serials), pre-formatted YYYY-MM-DD strings, and garbage input — never errors.
  • InferTransactionDetails — composes name + month matching over sender/message/user_id; falls back to sender-only member match and date-derived month when text gives no signal.
  • 21 table-driven tests; all expected values verified against live Python on 2026-05-06.

2026-05-06 12:43 CEST — feat(go/M2.6): port domain/synch.GenerateSyncID

  • New go/internal/domain/synch package with GenerateSyncID(Transaction) string ported from scripts/sync_fio_to_sheets.py generate_sync_id.
  • Byte-stable SHA-256 hash over date|amount|currency|sender|vs|message|bank_id (lowercased, UTF-8); Currency: "" defaults to "CZK" matching the Python missing-key fallback.
  • Key subtlety: Python's str(float) emits "500.0" for whole-valued floats and switches to scientific notation at |f| >= 1e16 or |f| < 1e-4 — replicated in formatAmount using 'f'/'e' format selection.
  • 6 table-driven hash tests + 9 formatAmount tests; all expected values verified against live Python on 2026-05-06.

2026-05-06 09:38 CEST — feat(go/M2.5): port domain/money.ParseCZK

  • New go/internal/domain/money package with ParseCZK(string) (float64, error) ported from scripts/infer_payments.py parse_czk_amount.
  • Preserves the Czech-locale heuristic: comma → decimal sep; 2+ dots → thousand seps; single dot → decimal (so "1.500"1.5).
  • Returns (0, ErrInvalidAmount) on parse failure; callers wanting Python's silent-zero contract use v, _ := ParseCZK(s).
  • 15 table-driven tests plus a silent-zero contract test; all expected values verified against live Python on 2026-05-06.

2026-05-06 09:24 CEST — feat(go/M2.3+M2.4): port domain/fees.CalculateFee and CalculateJuniorFee

  • New go/internal/domain/fees package with adult and junior fee calculators ported from scripts/attendance.py.
  • CalculateFee(count, monthKey) int0→0, 1→200, 2+→AdultFeeMonthlyRate[month] (fallback 700 CZK).
  • CalculateJuniorFee(count, monthKey) Expected0→{0}, 1→{Unknown:true} (the "?" sentinel, now strictly typed), 2+→JuniorFeeMonthlyRate[month] (fallback 500 CZK).
  • 20 table-driven tests, all verified against live Python; -race clean; golangci-lint clean.

2026-05-06 00:07 CEST — feat(go/M2.2): port czech.ParseMonthReferences

  • internal/domain/czech.ParseMonthReferences: three-pass regex (numeric slash, dot, Czech month names) with range wrap-around and m≥10 → previousYear heuristic, byte-equivalent to Python.
  • 35 table-driven tests; all expected outputs verified against live Python before locking (addresses risk #4 from the rewrite plan).

2026-05-05 23:33 CEST — feat(go/M2.1): port czech.Normalize

  • First M2 pure-domain task: internal/domain/czech.Normalize (NFKD + Mn-strip + lowercase), byte-equivalent to Python czech_utils.normalize.
  • Adds golang.org/x/text v0.36.0 as first external Go dependency.
  • 13-case table-driven test, all spot-checked against Python before locking.

2026-05-04 23:08 CEST — fix: payment inference exact-match short-circuit

  • match_members() now short-circuits on whole-word full-name hits; nickname/partial checks only run when no full name is present.
  • Replaced bare in substring checks with _word_in() word-boundary regex throughout, closing the class of bugs where a short nickname (e.g. tov) matches inside another member's surname (ottova).
  • Added tests/test_match_members.py (6 cases). Affects scripts/match_payments.py.

2026-05-04 23:08 CEST — feat: lower adult monthly fee to 700 CZK from April 2026

  • ADULT_FEE_DEFAULT reduced from 750 → 700 CZK.
  • ADULT_FEE_MONTHLY_RATE now pins Sep 2025 Feb 2026 at 750 to preserve historical billing; Mar 2026 stays 350; AprMay 2026 at 700. Affects scripts/attendance.py.

2026-05-04 12:02 CEST — Go rewrite M1: skeleton + tooling

  • Created go/ tree with module fuj-management/go (Go 1.26).
  • cmd/fuj: stdlib-flag subcommand dispatcher; server and version implemented, stubs for M2/M4 commands.
  • internal/config: env loader mirroring scripts/config.py (same env var names and defaults).
  • internal/logging: slog setup accepting log level from config.
  • internal/web: net/http ServeMux on :8080; middleware/timer.go logs method/path/status/ms.
  • go/build/Dockerfile: multi-stage (golang:1.26alpine:3) producing a static binary image.
  • Makefile: webweb-py alias; added web-go, go-build, go-test, go-run, go-lint.
  • .gitea/workflows/build.yaml: parallel build-go job pushing <tag>-go image.
  • Gate: make go-build, make go-lint, make go-test, curl :8080 all pass.

2026-05-03 20:37 CEST — Fix Balance column to correctly reflect past-month debt

  • Balance (and Pay-All) are now computed as sum(paid expected) over past months only, iterating directly over the ledger entries from reconcile().
  • Previously the balance used total_balance (which includes current/future-month activity and out-of-window credits) plus a one-sided current-month debt adjustment. Current-month surplus leaked through, making the balance appear less negative than the actual past-month debt.
  • Pay-All is now max(0, balance) so the two values are derived from a single source and can never disagree.
  • Affected: adults_view() and juniors_view() in app.py.

2026-05-03 19:26 CEST — Fee-aware allocation for multi-month payments

  • reconcile() no longer splits a multi-month payment evenly. Allocation is now per-member with two phases: greedy (if amount ≥ total expected, each month gets exactly its expected fee and overflow → credit) and proportional (otherwise distribute by each month's expected). Fixes the case where e.g. 1250 CZK covering 3 months with mixed fees (750/350/150) marked two months red.
  • Out-of-window months keep the previous even-split-to-credit behavior. Fallback to even split when all matched months have expected = 0 (prepayment before attendance is recorded).
  • Display layer only — no changes to how payments are stored in Google Sheets; Inferred Amount still holds the full bank amount.
  • Files: scripts/match_payments.py, tests/test_reconcile_exceptions.py (6 new test cases).