From 62c6dc2ca0d037ff3d3fbddd07cf7fc262317cb1 Mon Sep 17 00:00:00 2001 From: Jan Novak Date: Thu, 8 Oct 2026 12:04:59 +0200 Subject: [PATCH] fix(sync): dashboard sync covers current year plus Nov-Dec of previous /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 --- CHANGELOG.md | 7 +++++++ CLAUDE.md | 2 +- Makefile | 7 ++++++- ...26-10-08-1026-pre-feature-cleanup-tasks.md | 4 +++- go/cmd/fuj/main.go | 4 +--- go/internal/services/banksync/sync.go | 12 +++++++++++ go/internal/services/banksync/sync_test.go | 20 +++++++++++++++++++ legacy/app.py | 11 +++++----- legacy/scripts/sync_fio_to_sheets.py | 12 +++++++++++ legacy/tests/test_app.py | 14 +++++++++++++ legacy/tests/test_sync_fio.py | 10 +++++++++- 11 files changed, 91 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab7f1e0..50e3907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 2026-10-08 12:04 CEST — fix(sync): dashboard sync covers the current year plus Nov–Dec of the previous one + +- `/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 previously used a hardcoded `2026-01-01`..`2026-12-31` and would have stopped syncing on 2027-01-01; Go used the current calendar year only. +- Added `make sync-year [YEAR=N]` with the same window; `make sync-2026` is kept. The CLI default without flags (last 30 days) is unchanged. +- Key files: `go/internal/services/banksync/sync.go` (`DefaultWindow`), `go/cmd/fuj/main.go`, `legacy/scripts/sync_fio_to_sheets.py` (`default_sync_window`), `legacy/app.py`. + ## 2026-10-08 11:53 CEST — feat(fees): adult rates for 2026-07 through 2026-12, default 600 - Adult default monthly rate (months not in the table): 700 → 600 CZK. diff --git a/CLAUDE.md b/CLAUDE.md index b4b27ed..7c861b8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,7 +64,7 @@ make web-debug # Same with FLASK_DEBUG=1 make test # Run all tests (unittest discover) make test-v # Tests with verbose output make fees # Print fee report from attendance sheet -make sync-2026 # Sync Fio bank transactions for 2026 to Google Sheets +make sync-year # Sync Fio transactions for the current year plus Nov-Dec of the previous one (YEAR=N to override) make infer # Auto-fill Person/Purpose/Amount columns in payments sheet make reconcile # Print balance report from Google Sheets data make image # Build the legacy Python Docker image diff --git a/Makefile b/Makefile index d062bc1..bc01d83 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help fees match web web-py web-debug web-go go-build go-test go-test-all go-parity go-run go-sync-debug go-lint capture-fixtures parity image run sync sync-2026 test test-v docs +.PHONY: help fees match web web-py web-debug web-go go-build go-test go-test-all go-parity go-run go-sync-debug go-lint capture-fixtures parity image run sync sync-2026 sync-year test test-v docs # Legacy Python implementation lives in legacy/ (Go in go/ is primary). LEGACY := legacy @@ -39,6 +39,7 @@ help: @echo " make sync - Sync Fio transactions to Google Sheets" @echo " make sync-2025 - Sync Fio transactions for Q4 2025 (Oct-Dec)" @echo " make sync-2026 - Sync Fio transactions for the whole year of 2026" + @echo " make sync-year [YEAR=N] - Sync Fio transactions for YEAR plus Nov-Dec of the year before (default: current year)" @echo " make sync-debug [DAYS=N] - Dry-run Python sync with Fio diagnostics and txn table (default DAYS=30)" @echo " make infer - Infer payment details (Person, Purpose, Amount) in the sheet" @echo " make reconcile - Show balance report using Google Sheets data" @@ -130,6 +131,10 @@ sync-2025: $(PYTHON) sync-2026: $(PYTHON) $(PYTHON) $(LEGACY)/scripts/sync_fio_to_sheets.py --credentials $(CREDENTIALS) --from 2026-01-01 --to 2026-12-31 --sort-by-date +YEAR ?= $(shell date +%Y) +sync-year: $(PYTHON) + $(PYTHON) $(LEGACY)/scripts/sync_fio_to_sheets.py --credentials $(CREDENTIALS) --from $(shell expr $(YEAR) - 1)-11-01 --to $(YEAR)-12-31 --sort-by-date + sync-debug: $(PYTHON) ## Dry-run Python sync with Fio diagnostics and txn table (default DAYS=30) $(PYTHON) $(LEGACY)/scripts/sync_fio_to_sheets.py --credentials $(CREDENTIALS) --days $(DAYS) --dry-run --print-fio-table diff --git a/docs/plans/2026-10-08-1026-pre-feature-cleanup-tasks.md b/docs/plans/2026-10-08-1026-pre-feature-cleanup-tasks.md index c7f35f2..e4a3f62 100644 --- a/docs/plans/2026-10-08-1026-pre-feature-cleanup-tasks.md +++ b/docs/plans/2026-10-08-1026-pre-feature-cleanup-tasks.md @@ -50,7 +50,9 @@ before new feature work starts. Companion to the Go rewrite tracker, - September and October 2026 are billed at the defaults: 700 CZK adults, 500 CZK juniors. - If those are not the intended rates, add the months in both files. -- [ ] **T3** Remove the hardcoded 2026 window from Python `/sync-bank` +- [x] **T3** Done 2026-10-08, extended: both dashboards now sync the + current year plus November–December of the previous one. Original task: + remove the hardcoded 2026 window from Python `/sync-bank` (`app.py`, the `sync_to_sheets` call with `2026-01-01` / `2026-12-31`). - It stops picking up transactions on 2027-01-01. Go already uses the current year. diff --git a/go/cmd/fuj/main.go b/go/cmd/fuj/main.go index 3f9f7a6..7d54c93 100644 --- a/go/cmd/fuj/main.go +++ b/go/cmd/fuj/main.go @@ -92,9 +92,7 @@ func serverCmd(args []string) { actions := web.ActionHandlers{ BankSync: func(ctx context.Context, out io.Writer) error { - yr := time.Now().Year() - from := time.Date(yr, 1, 1, 0, 0, 0, 0, time.UTC) - to := time.Date(yr, 12, 31, 23, 59, 59, 0, time.UTC) + from, to := banksync.DefaultWindow(time.Now()) fmt.Fprintln(out, "=== Sync Fio Transactions ===") n, err := banksync.SyncToSheets(ctx, config.PaymentsSheetID, fioClients, sheetsCli, diff --git a/go/internal/services/banksync/sync.go b/go/internal/services/banksync/sync.go index 7a20961..7ee6956 100644 --- a/go/internal/services/banksync/sync.go +++ b/go/internal/services/banksync/sync.go @@ -36,6 +36,18 @@ type SyncOpts struct { PrintFioTable bool // with DryRun: print every fetched Fio txn with NEW/DUP status } +// DefaultWindow is the window used by the dashboard sync: the whole current +// calendar year plus the last two months of the previous one, so payments +// dated around New Year are still picked up in January. Re-fetching is safe +// because rows are deduplicated by Sync ID. +// Mirrors default_sync_window in legacy/scripts/sync_fio_to_sheets.py. +func DefaultWindow(now time.Time) (from, to time.Time) { + yr := now.Year() + from = time.Date(yr-1, time.November, 1, 0, 0, 0, 0, time.UTC) + to = time.Date(yr, time.December, 31, 23, 59, 59, 0, time.UTC) + return from, to +} + // SyncToSheets fetches Fio transactions from all provided clients and appends // new ones to the payments sheet. Returns the number of rows appended. // Ports legacy/scripts/sync_fio_to_sheets.py sync_to_sheets. diff --git a/go/internal/services/banksync/sync_test.go b/go/internal/services/banksync/sync_test.go index 2f3935b..e7d30b5 100644 --- a/go/internal/services/banksync/sync_test.go +++ b/go/internal/services/banksync/sync_test.go @@ -193,3 +193,23 @@ func syncIDFor(tx fio.Transaction) string { Sender: tx.Sender, VS: tx.VS, Message: tx.Message, BankID: tx.BankID, }) } + +func TestDefaultWindow(t *testing.T) { + cases := []struct { + now time.Time + from, to string + }{ + {time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC), "2025-11-01", "2026-12-31"}, + {time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC), "2026-11-01", "2027-12-31"}, + {time.Date(2027, 12, 31, 23, 0, 0, 0, time.UTC), "2026-11-01", "2027-12-31"}, + } + for _, tc := range cases { + from, to := DefaultWindow(tc.now) + if got := from.Format("2006-01-02"); got != tc.from { + t.Errorf("DefaultWindow(%s) from: want %s, got %s", tc.now.Format("2006-01-02"), tc.from, got) + } + if got := to.Format("2006-01-02"); got != tc.to { + t.Errorf("DefaultWindow(%s) to: want %s, got %s", tc.now.Format("2006-01-02"), tc.to, got) + } + } +} diff --git a/legacy/app.py b/legacy/app.py index 6e186ee..9134392 100644 --- a/legacy/app.py +++ b/legacy/app.py @@ -30,7 +30,7 @@ from views import ( adapt_junior_members, ) from cache_utils import get_sheet_modified_time, read_cache, write_cache, _LAST_CHECKED, flush_cache -from sync_fio_to_sheets import sync_to_sheets +from sync_fio_to_sheets import sync_to_sheets, default_sync_window from infer_payments import infer_payments def get_cached_data(cache_key, sheet_id, fetch_func, *args, serialize=None, deserialize=None, **kwargs): @@ -128,13 +128,14 @@ def sync_bank(): success = True try: with contextlib.redirect_stdout(output), contextlib.redirect_stderr(output): - # sync_to_sheets: equivalent of make sync-2026 - output.write("=== Syncing Fio transactions (2026) ===\n") + # sync_to_sheets: equivalent of make sync-year (same default window as Go) + date_from, date_to = default_sync_window() + output.write(f"=== Syncing Fio transactions ({date_from} to {date_to}) ===\n") sync_to_sheets( spreadsheet_id=PAYMENTS_SHEET_ID, credentials_path=CREDENTIALS_PATH, - date_from_str="2026-01-01", - date_to_str="2026-12-31", + date_from_str=date_from, + date_to_str=date_to, sort_by_date=True, ) output.write("\n=== Inferring payment details ===\n") diff --git a/legacy/scripts/sync_fio_to_sheets.py b/legacy/scripts/sync_fio_to_sheets.py index 7fee367..c5529e3 100644 --- a/legacy/scripts/sync_fio_to_sheets.py +++ b/legacy/scripts/sync_fio_to_sheets.py @@ -59,6 +59,18 @@ def get_sheets_service(credentials_path: str): return _SHEETS_SERVICE +def default_sync_window(today=None) -> tuple[str, str]: + """Window used by the dashboard sync, as (from, to) in YYYY-MM-DD. + + The whole current calendar year plus the last two months of the previous + one, so payments dated around New Year are still picked up in January. + Re-fetching is safe because rows are deduplicated by Sync ID. + Mirrors DefaultWindow in go/internal/services/banksync/sync.go. + """ + year = (today or datetime.now()).year + return f"{year - 1}-11-01", f"{year}-12-31" + + def as_text_cell(value) -> str: """Mark a value to be stored as text when written with USER_ENTERED. diff --git a/legacy/tests/test_app.py b/legacy/tests/test_app.py index a64f532..693e6df 100644 --- a/legacy/tests/test_app.py +++ b/legacy/tests/test_app.py @@ -23,6 +23,20 @@ class TestWebApp(unittest.TestCase): app.config['TESTING'] = True self.client = app.test_client() + @patch('app.flush_cache', return_value=0) + @patch('app.infer_payments') + @patch('app.sync_to_sheets') + def test_sync_bank_uses_default_window(self, mock_sync, mock_infer, mock_flush): + """/sync-bank syncs the current year plus Nov-Dec of the previous one, not a hardcoded year.""" + from datetime import datetime + year = datetime.now().year + response = self.client.get('/sync-bank') + self.assertEqual(response.status_code, 200) + kwargs = mock_sync.call_args.kwargs + self.assertEqual(kwargs['date_from_str'], f"{year - 1}-11-01") + self.assertEqual(kwargs['date_to_str'], f"{year}-12-31") + mock_infer.assert_called_once() + def test_index_page(self): """Test that / returns the refresh meta tag""" response = self.client.get('/') diff --git a/legacy/tests/test_sync_fio.py b/legacy/tests/test_sync_fio.py index 262c280..a2e8e0c 100644 --- a/legacy/tests/test_sync_fio.py +++ b/legacy/tests/test_sync_fio.py @@ -1,9 +1,17 @@ import contextlib import io import unittest +from datetime import datetime from unittest.mock import MagicMock, patch -from scripts.sync_fio_to_sheets import COLUMN_LABELS, as_text_cell, sync_to_sheets +from scripts.sync_fio_to_sheets import COLUMN_LABELS, as_text_cell, default_sync_window, sync_to_sheets + + +class TestDefaultSyncWindow(unittest.TestCase): + def test_current_year_plus_last_two_months_of_previous(self): + self.assertEqual(default_sync_window(datetime(2026, 10, 8)), ("2025-11-01", "2026-12-31")) + self.assertEqual(default_sync_window(datetime(2027, 1, 1)), ("2026-11-01", "2027-12-31")) + self.assertEqual(default_sync_window(datetime(2027, 12, 31)), ("2026-11-01", "2027-12-31")) class TestAsTextCell(unittest.TestCase): -- 2.54.0