feat(go): scheduled bank sync (Go only)

The Go server now runs sync -> infer -> flush-cache by itself at the
times in SYNC_SCHEDULE (comma-separated HH:MM, default 04:00, "off"
disables), read in SYNC_TZ (default Europe/Prague). Nothing runs at
startup; an invalid value stops the server with an error.

- internal/scheduler: time-of-day parsing, Next, and the run loop
- banksync.Runner: one lock shared by /sync-bank and the scheduler so
  runs never overlap, plus the status of the last run
- footer shows the last sync since start and the next scheduled run
- binary embeds time/tzdata because the Alpine image has no zone data

Deliberately not implemented in the legacy Python app, which is being
phased out; recorded in CLAUDE.md under "Deliberately Go-only features".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 12:17:15 +02:00
co-authored by Claude Opus 5.5
parent 1d5a1c95d2
commit 2c2db60fe7
19 changed files with 741 additions and 19 deletions
+16 -2
View File
@@ -20,6 +20,12 @@ Until the Python version is retired, **every new feature and every behaviour cha
Go-only or Python-only changes are fine for things that exist on one side only (build, tooling, HTML/CSS/JS, refactors with no behaviour change).
### Deliberately Go-only features
These exist only in the Go app on purpose. Do not port them to the legacy Python app, which is being phased out:
- **Scheduled bank sync** — the Go server runs sync → infer → flush-cache by itself at the times in `SYNC_SCHEDULE`. Python's multi-worker gunicorn setup would need a cross-process lock for this, which is not worth building in code that is going away. In Python the sync still runs only from `/sync-bank` or the CLI.
## Key Constraints
- **PII separation**: Member data (names, emails, payment info) must never be committed to git. Enforce config/data separation from day one.
@@ -104,10 +110,11 @@ Fio Bank API ──► bank sync ──► Google Sheets (payments
- `go/cmd/fuj` — CLI entry point; subcommands `server`, `fees`, `reconcile`, `sync`, `infer`, `version`.
- `go/cmd/parity` — diffs the JSON API of the Go and Python servers.
- `go/internal/config` — sheet IDs, bank accounts, env-driven settings (`CREDENTIALS_PATH`, `CACHE_DIR`, `CACHE_TTL_SECONDS`, `CACHE_API_CHECK_TTL_SECONDS`, `MONTHS_TO_SHOW`, `SERVER_ADDR`, `LOG_LEVEL`, `FIO_API_TOKEN_NEW` / `FIO_API_TOKEN_OLD`).
- `go/internal/config` — sheet IDs, bank accounts, env-driven settings (`CREDENTIALS_PATH`, `CACHE_DIR`, `CACHE_TTL_SECONDS`, `CACHE_API_CHECK_TTL_SECONDS`, `MONTHS_TO_SHOW`, `SERVER_ADDR`, `LOG_LEVEL`, `FIO_API_TOKEN_NEW` / `FIO_API_TOKEN_OLD`, `SYNC_SCHEDULE`, `SYNC_TZ`).
- `go/internal/domain` — pure logic: `fees` (rates, `CalculateFee`, `CalculateJuniorFee`), `reconcile`, `matching` (name matching, inference), `czech` (normalize, month parsing), `money`, `synch` (sync IDs).
- `go/internal/io` — external systems: `attendance` (CSV export), `sheets`, `drive`, `fio` (API + transparent-page scraper), `cache` (file cache). Each has a fake for tests.
- `go/internal/services` — `membership` (loading, merged months, fee and reconcile reports) and `banksync` (Fio sync + inference).
- `go/internal/services` — `membership` (loading, merged months, fee and reconcile reports) and `banksync` (Fio sync + inference; `Runner` serialises manual and scheduled runs).
- `go/internal/scheduler` — runs a job at fixed times of day; used by `fuj server` for the scheduled bank sync.
- `go/internal/web` — routes for `/adults`, `/juniors`, `/payments`, `/sync-bank`, `/flush-cache`, `/qr`, `/version`, `/api/*`; `templates/` and `static/` are embedded.
- `go/tests` — parity tests and the scrubbed fixture corpus captured from Python.
@@ -144,6 +151,13 @@ Tiers are set in column B of the attendance sheet:
Both implementations check the Drive API `modifiedTime` and serve a cached JSON file when the sheet hasn't changed, falling back to 5-minute TTL buckets when the Drive API is unavailable. Go: `go/internal/io/cache`, files in `tmp/go/` (`CACHE_DIR`). Python: `get_cached_data()` in `legacy/app.py`, files in `legacy/tmp/`, warmed up at startup (`warmup_cache()`). Flush via `/flush-cache` (POST).
### Bank sync
- Manual: open `/sync-bank` (both apps) or run `fuj sync` + `fuj infer` / `make sync-year` + `make infer`.
- Scheduled (Go server only): at each time in `SYNC_SCHEDULE` (comma-separated `HH:MM`, default `04:00`; `off` disables) read in `SYNC_TZ` (default `Europe/Prague`). Nothing runs at startup. Manual and scheduled runs share one lock and never overlap. The page footer shows the last run since start and the next one.
- Both use the same window: 1 November of the previous year to 31 December of the current year; rows are de-duplicated by Sync ID.
- The schedule runs per process: keep the deployment at one replica, and note that a local `make web-go` with real credentials also syncs at the scheduled time unless `SYNC_SCHEDULE=off`.
### Payments sheet columns
`Date | Amount | manual fix | Person | Purpose | Inferred Amount | Sender | VS | Message | Bank ID | Sync ID`