When the bank message starts with "other:" (case-insensitive),
inference does not look for a month, neither in the text nor from the
payment date. The trimmed message is written to Purpose, so reconcile
books the payment as an "other" transaction for the person instead of
a monthly fee. Messages containing "other:" elsewhere are unchanged.
- matching.IsOtherPurpose / is_other_purpose shared by inference and
reconcile
- reconcile applies the rule to rows not yet processed by inference
- no person found: Purpose is still written, row shows as unmatched
- implemented in Go and legacy Python; four new parity fixtures
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
/sync-bank now uses one shared default window in both implementations:
1 November of the previous year to 31 December of the current year.
Payments dated around New Year are picked up in January; duplicates
are still filtered by Sync ID.
Python used a hardcoded 2026 window and would have stopped syncing on
2027-01-01; Go used the current calendar year only.
Adds make sync-year [YEAR=N] with the same window. The CLI default
without flags (last 30 days) is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Go stringified sheet cells with fmt.Sprint, so a numeric VS read with
UNFORMATTED_VALUE was rendered as 2.609767878e+09. Text columns of the
payments sheet now go through sheets.CellString, which never uses
exponent form; bank inference uses the same helper.
Both sync implementations now append VS as a text cell so Sheets keeps
it a string and leading zeros survive. Existing rows are not rewritten.
Python already read VS as a digit string; a regression test pins that.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Go (go/) becomes the primary implementation. The original Python
version moves to legacy/ and is kept runnable; new features are still
built in both (see the Dual implementation rule in CLAUDE.md).
- git mv app.py, scripts/, templates/, tests/, pyproject.toml, uv.lock
and build/ into legacy/
- Makefile: Python targets point at legacy/, venv in legacy/.venv,
CREDENTIALS_PATH exported for the root .secret/ file
- CI and make image build the Python image with legacy/ as context;
the Makefile is no longer copied into the image
- legacy/.dockerignore moved to the context root so it takes effect
- CLAUDE.md, docs/README.md, Go comments, fixture README, capture
script and VS Code launch config updated to the new layout
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add second Fio account (CZ0820100000002502035405 / 2502035405/2010).
Both accounts are fetched on every sync run and combined before dedup,
so the payments sheet accumulates transactions from either account.
QR codes now default to the new account.
Go:
- config.go: hardcoded Accounts/LoadedAccount slice replaces scalar
BankAccount + FioAPIToken; Config.BankAccount renamed QRAccount;
per-account tokens via FIO_API_TOKEN_NEW / FIO_API_TOKEN_OLD
- banksync.SyncToSheets: accepts []fio.Client, loops to combine txns
- cmd/fuj/main.go: buildFioClients helper; both sync call sites updated
- html_handler + build_adults/juniors: use Config.QRAccount
- New TestSyncToSheets_MultiAccount covers cross-account dedup
Python:
- config.py: ACCOUNTS list + LOADED_ACCOUNTS (tokens from env)
- fio_utils.py: fetch_transactions_for (per-account) +
fetch_transactions_all (loops all accounts)
- sync_fio_to_sheets.py: uses fetch_transactions_all
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
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>
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>