From 022facc95704c70b2775190b09edc4690e8648d9 Mon Sep 17 00:00:00 2001 From: Jan Novak Date: Thu, 8 Oct 2026 11:33:57 +0200 Subject: [PATCH] refactor: move Python implementation to legacy/, make Go primary 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 --- .gitea/workflows/build.yaml | 4 +- .vscode/launch.json | 8 +- CHANGELOG.md | 8 ++ CLAUDE.md | 110 ++++++++++++++---- Makefile | 60 +++++----- docs/README.md | 2 + go/cmd/parity/main.go | 2 +- go/internal/config/config.go | 4 +- .../czech/parse_month_references_test.go | 2 +- go/internal/domain/fees/fees.go | 4 +- go/internal/domain/fees/junior.go | 2 +- go/internal/domain/matching/doc.go | 2 +- go/internal/domain/matching/format_date.go | 2 +- .../domain/matching/format_date_test.go | 4 +- go/internal/domain/matching/infer.go | 2 +- go/internal/domain/matching/infer_test.go | 4 +- go/internal/domain/matching/match_members.go | 2 +- .../domain/matching/match_members_test.go | 4 +- go/internal/domain/matching/name_variants.go | 2 +- .../domain/matching/name_variants_test.go | 4 +- go/internal/domain/money/money.go | 4 +- go/internal/domain/money/money_test.go | 2 +- go/internal/domain/reconcile/reconcile.go | 6 +- .../domain/reconcile/reconcile_test.go | 4 +- go/internal/domain/synch/synch.go | 4 +- go/internal/domain/synch/synch_test.go | 4 +- go/internal/io/cache/filecache.go | 4 +- go/internal/io/fio/api.go | 2 +- go/internal/io/fio/transparent.go | 2 +- go/internal/services/banksync/infer.go | 2 +- go/internal/services/banksync/sync.go | 4 +- .../services/membership/format_fees.go | 4 +- .../services/membership/format_fees_test.go | 4 +- .../services/membership/format_reconcile.go | 4 +- .../membership/format_reconcile_test.go | 4 +- go/internal/services/membership/sources.go | 18 +-- go/internal/web/api/build_adults.go | 2 +- go/internal/web/api/build_common.go | 6 +- go/internal/web/api/build_juniors.go | 2 +- go/internal/web/api/build_payments.go | 2 +- go/internal/web/api/version.go | 2 +- go/tests/fixtures/README.md | 6 +- .../build_name_variants_parity_test.go | 2 +- .../calculate_fee_parity_test.go | 2 +- .../calculate_junior_fee_parity_test.go | 2 +- .../format_date/format_date_parity_test.go | 2 +- .../generate_sync_id_parity_test.go | 2 +- .../infer_transaction_details_parity_test.go | 2 +- .../match_members_parity_test.go | 2 +- .../pure/normalize/normalize_parity_test.go | 2 +- .../parse_czk_amount_parity_test.go | 2 +- .../parse_month_references_parity_test.go | 2 +- .../parity/reconcile/reconcile_parity_test.go | 2 +- {build => legacy}/.dockerignore | 4 +- app.py => legacy/app.py | 0 {build => legacy/build}/Dockerfile | 2 +- {build => legacy/build}/entrypoint.sh | 0 pyproject.toml => legacy/pyproject.toml | 2 +- {scripts => legacy/scripts}/_fixture_seeds.py | 0 {scripts => legacy/scripts}/attendance.py | 0 {scripts => legacy/scripts}/cache_utils.py | 0 {scripts => legacy/scripts}/calculate_fees.py | 0 .../scripts}/capture_all_fixtures.sh | 13 ++- .../scripts}/capture_fixtures.py | 0 {scripts => legacy/scripts}/config.py | 0 {scripts => legacy/scripts}/czech_utils.py | 0 {scripts => legacy/scripts}/fio_utils.py | 0 {scripts => legacy/scripts}/infer_payments.py | 0 {scripts => legacy/scripts}/match_payments.py | 0 {scripts => legacy/scripts}/scrub_fixtures.py | 0 .../scripts}/sync_fio_to_sheets.py | 0 {scripts => legacy/scripts}/views.py | 0 {templates => legacy/templates}/adults.html | 0 .../templates}/flush-cache.html | 0 {templates => legacy/templates}/juniors.html | 0 {templates => legacy/templates}/payments.html | 0 {templates => legacy/templates}/sync.html | 0 {tests => legacy/tests}/test_app.py | 0 {tests => legacy/tests}/test_match_members.py | 0 .../tests}/test_match_payments.py | 0 .../tests}/test_reconcile_exceptions.py | 0 uv.lock => legacy/uv.lock | 0 82 files changed, 223 insertions(+), 144 deletions(-) rename {build => legacy}/.dockerignore (52%) rename app.py => legacy/app.py (100%) rename {build => legacy/build}/Dockerfile (97%) rename {build => legacy/build}/entrypoint.sh (100%) rename pyproject.toml => legacy/pyproject.toml (72%) rename {scripts => legacy/scripts}/_fixture_seeds.py (100%) rename {scripts => legacy/scripts}/attendance.py (100%) rename {scripts => legacy/scripts}/cache_utils.py (100%) rename {scripts => legacy/scripts}/calculate_fees.py (100%) rename {scripts => legacy/scripts}/capture_all_fixtures.sh (70%) rename {scripts => legacy/scripts}/capture_fixtures.py (100%) rename {scripts => legacy/scripts}/config.py (100%) rename {scripts => legacy/scripts}/czech_utils.py (100%) rename {scripts => legacy/scripts}/fio_utils.py (100%) rename {scripts => legacy/scripts}/infer_payments.py (100%) rename {scripts => legacy/scripts}/match_payments.py (100%) rename {scripts => legacy/scripts}/scrub_fixtures.py (100%) rename {scripts => legacy/scripts}/sync_fio_to_sheets.py (100%) rename {scripts => legacy/scripts}/views.py (100%) rename {templates => legacy/templates}/adults.html (100%) rename {templates => legacy/templates}/flush-cache.html (100%) rename {templates => legacy/templates}/juniors.html (100%) rename {templates => legacy/templates}/payments.html (100%) rename {templates => legacy/templates}/sync.html (100%) rename {tests => legacy/tests}/test_app.py (100%) rename {tests => legacy/tests}/test_match_members.py (100%) rename {tests => legacy/tests}/test_match_payments.py (100%) rename {tests => legacy/tests}/test_reconcile_exceptions.py (100%) rename uv.lock => legacy/uv.lock (100%) diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 9b996ce..6aed862 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -31,11 +31,11 @@ jobs: TAG=${{ inputs.tag }} fi IMAGE=gitea.home.hrajfrisbee.cz/${{ github.repository }}:$TAG - docker build -f build/Dockerfile \ + docker build -f legacy/build/Dockerfile \ --build-arg GIT_TAG=$TAG \ --build-arg GIT_COMMIT=${{ github.sha }} \ --build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \ - -t $IMAGE . + -t $IMAGE legacy/ docker push $IMAGE build-go: diff --git a/.vscode/launch.json b/.vscode/launch.json index 8c6d99d..947fa33 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,14 +2,16 @@ "version": "0.2.0", "configurations": [ { - "name": "Python Debugger: Flask", + "name": "Python Debugger: Flask (legacy)", "type": "debugpy", "request": "launch", "module": "flask", - "python": "${workspaceFolder}/.venv/bin/python", + "python": "${workspaceFolder}/legacy/.venv/bin/python", + "cwd": "${workspaceFolder}/legacy", "env": { "FLASK_APP": "app.py", - "FLASK_DEBUG": "1" + "FLASK_DEBUG": "1", + "CREDENTIALS_PATH": "${workspaceFolder}/.secret/fuj-management-bot-credentials.json" }, "args": [ "run", diff --git a/CHANGELOG.md b/CHANGELOG.md index 2abb628..921322c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2026-10-08 11:33 CEST — refactor: move Python implementation to legacy/, Go is primary + +- Go (`go/`) is now the primary implementation. The original Python version moved to `legacy/` (`app.py`, `scripts/`, `templates/`, `tests/`, `pyproject.toml`, `uv.lock`, `build/`) and is considered legacy; new features are still built in both. +- `CLAUDE.md` rewritten for the Go-first layout, with a "Dual implementation rule" section. +- Makefile Python targets keep their names and point at `legacy/`; virtualenv is `legacy/.venv`, Python cache is `legacy/tmp/`; `CREDENTIALS_PATH` is exported pointing at the root `.secret/` file. +- CI builds the Python image with `legacy/` as context; the image no longer contains the Makefile. `legacy/.dockerignore` now takes effect and excludes nested `__pycache__`. +- Go comments, fixture README, capture script and VS Code launch config updated to the new paths. + ## 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. diff --git a/CLAUDE.md b/CLAUDE.md index ea5661f..6e78e79 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,28 +4,62 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Status -Flask-based financial management system for FUJ (Frisbee Ultimate Jablonec). Handles attendance-based fee calculation, Fio bank transaction sync, payment reconciliation, and a web dashboard. +Financial management system for FUJ (Frisbee Ultimate Jablonec). Handles attendance-based fee calculation, Fio bank transaction sync, payment reconciliation, and a web dashboard. + +- **Go is the primary implementation** (`go/`). It is the version that is built, deployed and developed first. +- **Python is legacy** (`legacy/`). The project was originally developed in Python (Flask); that version is kept runnable and in sync, but is considered legacy from now on. + +## Dual implementation rule + +Until the Python version is retired, **every new feature and every behaviour change must be implemented in both Go and Python**: + +1. Implement and test in Go first (`go/`). +2. Mirror the change in the legacy Python code (`legacy/`), including its tests. +3. Keep the two in parity: shared constants (fee rates, merged months, bank accounts, sheet IDs) must match on both sides, and the JSON API contract (`/api/adults`, `/api/juniors`, `/api/payments`) must stay identical. The HTML frontends are allowed to diverge. +4. If a pure function's behaviour changes, add or update seeds in `legacy/scripts/_fixture_seeds.py`, run `make capture-fixtures`, and make sure `make go-test-all` passes. With both servers running, `make parity` diffs the live JSON APIs. + +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). ## Key Constraints - **PII separation**: Member data (names, emails, payment info) must never be committed to git. Enforce config/data separation from day one. -- **Configuration**: External service IDs, credentials, and tunable parameters are centralized in `scripts/config.py`. Domain-specific constants (fees, merged months) stay in their respective modules. +- **Configuration**: External service IDs, credentials, and tunable parameters are centralized in `go/internal/config/config.go` (mirrored in `legacy/scripts/config.py`). Domain-specific constants (fees, merged months) stay in their respective modules. ## Development Setup -This project uses `uv` for dependency management. +Go (primary): a recent Go toolchain (see `go/go.mod`) and `golangci-lint`. ```bash -uv venv && uv sync -source .venv/bin/activate +make go-build # builds bin/fuj ``` -Set `PYTHONPATH=scripts:.` when running scripts directly (the Makefile does this automatically). +Python (legacy) uses `uv`; the project file and virtualenv live in `legacy/`. + +```bash +make venv # uv sync --project legacy → legacy/.venv +``` + +The Makefile sets `PYTHONPATH` (`legacy/scripts:legacy`) and `CREDENTIALS_PATH` (`.secret/fuj-management-bot-credentials.json` at the repo root) for the Python targets. Set both yourself when running Python scripts directly. ## Commands +Go (primary): + ```bash -make web # Start dashboard at http://localhost:5001 +make web-go # Build and start dashboard at http://localhost:8080 +make go-build # Build bin/fuj +make go-test # Unit tests (go test -race ./...) +make go-parity # Parity tests against the captured Python fixtures +make go-test-all # Both of the above +make go-lint # golangci-lint +make go-run ARGS="fees" # Run a subcommand: server, fees, reconcile, sync, infer, version +make go-sync-debug [DAYS=N] # Dry-run bank sync with debug logs +``` + +Python (legacy): + +```bash +make web-py # Start dashboard at http://localhost:5001 (make web is an alias) make web-debug # Same with FLASK_DEBUG=1 make test # Run all tests (unittest discover) make test-v # Tests with verbose output @@ -33,12 +67,20 @@ make fees # Print fee report from attendance sheet make sync-2026 # Sync Fio bank transactions for 2026 to Google Sheets make infer # Auto-fill Person/Purpose/Amount columns in payments sheet make reconcile # Print balance report from Google Sheets data -make image # Build Docker image +make image # Build the legacy Python Docker image +``` + +Cross-implementation: + +```bash +make capture-fixtures # Regenerate go/tests/fixtures from the Python implementation +make parity # Diff /api/* between web-py (:5001) and web-go (:8080); both must be running ``` Run a single test: ```bash -PYTHONPATH=scripts:. python -m unittest tests.test_app.TestWebApp.test_adults_route +cd go && go test ./internal/domain/reconcile -run TestName +cd legacy && PYTHONPATH=scripts:. .venv/bin/python -m unittest tests.test_app.TestWebApp.test_adults_route ``` ## Architecture @@ -46,25 +88,45 @@ PYTHONPATH=scripts:. python -m unittest tests.test_app.TestWebApp.test_adults_ro ### Data flow ``` -Google Sheets (attendance) ──► attendance.py ──► reconcile() ──► Flask routes ──► templates/ -Google Sheets (payments) ──► match_payments.py ──┘ -Fio Bank API ──► sync_fio_to_sheets.py ──► Google Sheets (payments) +Google Sheets (attendance) ──► attendance loader ──► reconcile ──► HTTP handlers ──► templates +Google Sheets (payments) ──► payments loader ──┘ +Fio Bank API ──► bank sync ──► Google Sheets (payments) ``` -### Key modules +### Repository layout -- `app.py` — Flask app; routes for `/adults`, `/juniors`, `/payments`, `/sync-bank`, `/qr`, `/flush-cache` -- `scripts/attendance.py` — Fetches attendance CSV from Google Sheets, computes per-member per-month fees. Contains fee rate constants (`ADULT_FEE_DEFAULT`, `JUNIOR_FEE_DEFAULT`) and `ADULT_MERGED_MONTHS` / `JUNIOR_MERGED_MONTHS` dicts. -- `scripts/match_payments.py` — `reconcile()` matches transactions to members/months. `fetch_sheet_data()` reads the payments sheet. `fetch_exceptions()` reads the `exceptions` tab. -- `scripts/cache_utils.py` — Invalidation via Google Drive API `modifiedTime`; falls back to 5-minute TTL buckets when Drive API is unavailable. Cache files live in `tmp/`. -- `scripts/sync_fio_to_sheets.py` — Pulls Fio bank transactions and appends them to the payments Google Sheet. -- `scripts/infer_payments.py` — Fills in Person/Purpose/Inferred Amount columns using name-matching heuristics. -- `scripts/config.py` — All external IDs, paths, and tunable TTLs. Override via env vars (`CREDENTIALS_PATH`, `BANK_ACCOUNT`, `CACHE_TTL_SECONDS`). +- `go/` — primary implementation (single binary `fuj`, templates and static assets embedded). +- `legacy/` — legacy Python implementation (`app.py`, `scripts/`, `templates/`, `tests/`, `pyproject.toml`, `build/Dockerfile`). +- `docs/` — documentation; `docs/plans/` holds plans and task lists. +- `.gitea/workflows/` — CI: image builds on tag push, gitops image-update PR. + +### Key modules — Go (primary) + +- `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/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/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. + +### Key modules — Python (legacy) + +- `legacy/app.py` — Flask app; routes for `/adults`, `/juniors`, `/payments`, `/sync-bank`, `/qr`, `/flush-cache`, `/api/*` +- `legacy/scripts/attendance.py` — Fetches attendance CSV from Google Sheets, computes per-member per-month fees. Contains fee rate constants (`ADULT_FEE_DEFAULT`, `JUNIOR_FEE_DEFAULT`) and `ADULT_MERGED_MONTHS` / `JUNIOR_MERGED_MONTHS` dicts. +- `legacy/scripts/match_payments.py` — `reconcile()` matches transactions to members/months. `fetch_sheet_data()` reads the payments sheet. `fetch_exceptions()` reads the `exceptions` tab. +- `legacy/scripts/cache_utils.py` — Invalidation via Google Drive API `modifiedTime`; falls back to 5-minute TTL buckets when Drive API is unavailable. Cache files live in `legacy/tmp/`. +- `legacy/scripts/sync_fio_to_sheets.py` — Pulls Fio bank transactions and appends them to the payments Google Sheet. +- `legacy/scripts/infer_payments.py` — Fills in Person/Purpose/Inferred Amount columns using name-matching heuristics. +- `legacy/scripts/views.py` — View models shared by the HTML routes and the JSON API. +- `legacy/scripts/config.py` — All external IDs, paths, and tunable TTLs. Override via env vars (`CREDENTIALS_PATH`, `CACHE_TTL_SECONDS`, `MONTHS_TO_SHOW`). +- `legacy/scripts/capture_fixtures.py`, `scrub_fixtures.py`, `_fixture_seeds.py` — produce the scrubbed parity fixtures for Go. ### Member tiers Tiers are set in column B of the attendance sheet: -- `A` — Adult, pays fees (per-month rate from `ADULT_FEE_MONTHLY_RATE`, fallback 700 CZK for 2+ sessions; 200 CZK for exactly 1) +- `A` — Adult, pays fees (per-month rate from `AdultFeeMonthlyRate` / `ADULT_FEE_MONTHLY_RATE`, fallback 700 CZK for 2+ sessions; 200 CZK for exactly 1) - `J` — Junior attending adult practices; their attendance is merged with the junior sheet - `X` — Excluded from junior fee calculation (coaches, etc.) @@ -76,17 +138,17 @@ Tiers are set in column B of the attendance sheet: ### Merged months -`ADULT_MERGED_MONTHS` / `JUNIOR_MERGED_MONTHS` in `attendance.py` map a source month to a target month (e.g., `"2025-12": "2026-01"` merges December into January billing). The target month accumulates attendance from both months. +`AdultMergedMonths` / `JuniorMergedMonths` in `go/internal/services/membership/sources.go` (`ADULT_MERGED_MONTHS` / `JUNIOR_MERGED_MONTHS` in `legacy/scripts/attendance.py`) map a source month to a target month (e.g., `"2025-12": "2026-01"` merges December into January billing). The target month accumulates attendance from both months. ### Caching -`get_cached_data()` in `app.py` checks the Drive API `modifiedTime` before each request and serves a JSON file from `tmp/` when the sheet hasn't changed. Cache is warmed up at startup (`warmup_cache()`). Flush via `/flush-cache` (POST) or `flush_cache()`. +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). ### Payments sheet columns `Date | Amount | manual fix | Person | Purpose | Inferred Amount | Sender | VS | Message | Bank ID | Sync ID` -`Person` and `Purpose` are written by `infer_payments.py` and can be manually corrected. `manual fix` column presence disables re-inference for that row. Multiple people or months are comma-separated in Person/Purpose. +`Person` and `Purpose` are written by the inference step (`fuj infer` / `infer_payments.py`) and can be manually corrected. `manual fix` column presence disables re-inference for that row. Multiple people or months are comma-separated in Person/Purpose. ### QR codes diff --git a/Makefile b/Makefile index e7299cb..d062bc1 100644 --- a/Makefile +++ b/Makefile @@ -1,26 +1,30 @@ .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 -export PYTHONPATH := scripts:$(PYTHONPATH) -VENV := .venv -PYTHON := $(VENV)/bin/python3 -CREDENTIALS := .secret/fuj-management-bot-credentials.json +# Legacy Python implementation lives in legacy/ (Go in go/ is primary). +LEGACY := legacy +export PYTHONPATH := $(CURDIR)/$(LEGACY)/scripts:$(CURDIR)/$(LEGACY):$(PYTHONPATH) +VENV := $(LEGACY)/.venv +PYTHON := $(CURDIR)/$(VENV)/bin/python3 +CREDENTIALS := $(CURDIR)/.secret/fuj-management-bot-credentials.json +# Both implementations read CREDENTIALS_PATH; the Python default would resolve under legacy/. +export CREDENTIALS_PATH ?= $(CREDENTIALS) GO_SRC := go GO_BIN := bin/fuj -$(PYTHON): .venv/.last_sync +$(PYTHON): $(VENV)/.last_sync -.venv/.last_sync: pyproject.toml - uv sync - touch .venv/.last_sync +$(VENV)/.last_sync: $(LEGACY)/pyproject.toml + uv sync --project $(LEGACY) + touch $(VENV)/.last_sync help: @echo "Available targets:" @echo " make fees - Calculate monthly fees from the attendance sheet" @echo " make match - Match Fio bank payments against expected attendance fees" - @echo " make web - Start Python dashboard (alias for web-py, until M8)" - @echo " make web-py - Start Python dashboard on :5001" - @echo " make web-go - Build and start Go dashboard on :8080" + @echo " make web - Start legacy Python dashboard (alias for web-py)" + @echo " make web-py - Start legacy Python dashboard on :5001" + @echo " make web-go - Build and start Go dashboard on :8080 (primary)" @echo " make web-debug - Start Python dashboard in debug mode" @echo " make go-build - Build Go binary to bin/fuj" @echo " make go-test - Run Go unit tests" @@ -44,21 +48,21 @@ help: @echo " make docs - Serve documentation in a browser" venv: - uv sync + uv sync --project $(LEGACY) fees: $(PYTHON) - $(PYTHON) scripts/calculate_fees.py + $(PYTHON) $(LEGACY)/scripts/calculate_fees.py match: $(PYTHON) - $(PYTHON) scripts/match_payments.py + $(PYTHON) $(LEGACY)/scripts/match_payments.py web: web-py web-py: $(PYTHON) - $(PYTHON) app.py + $(PYTHON) $(LEGACY)/app.py web-debug: $(PYTHON) - FLASK_DEBUG=1 $(PYTHON) app.py + FLASK_DEBUG=1 $(PYTHON) $(LEGACY)/app.py go-build: cd $(GO_SRC) && go build -trimpath \ @@ -82,10 +86,10 @@ capture-fixtures: $(PYTHON) infer_transaction_details format_date reconcile; do \ dir="go/tests/fixtures/$$([[ $$func == reconcile ]] && echo reconcile || echo pure/$$func)"; \ mkdir -p "$$dir"; \ - PYTHONPATH=scripts:. $(PYTHON) scripts/capture_fixtures.py --func $$func --all \ + $(PYTHON) $(LEGACY)/scripts/capture_fixtures.py --func $$func --all \ | while IFS= read -r line; do \ case_id=$$(echo "$$line" | $(PYTHON) -c "import sys,json; print(json.load(sys.stdin)['case'])"); \ - echo "$$line" | $(PYTHON) scripts/scrub_fixtures.py > "$$dir/$${case_id}.json"; \ + echo "$$line" | $(PYTHON) $(LEGACY)/scripts/scrub_fixtures.py > "$$dir/$${case_id}.json"; \ done; \ echo " $$func done"; \ done @@ -112,34 +116,34 @@ image: --build-arg GIT_TAG=$$(git describe --tags --always 2>/dev/null || echo "untagged") \ --build-arg GIT_COMMIT=$$(git rev-parse --short HEAD) \ --build-arg BUILD_DATE=$$(date -u +%Y-%m-%dT%H:%M:%SZ) \ - -f build/Dockerfile . + -f $(LEGACY)/build/Dockerfile $(LEGACY) run: docker run -it --rm -p 5001:5001 -v $(CURDIR)/.secret:/app/.secret:ro fuj-management:latest sync: $(PYTHON) - $(PYTHON) scripts/sync_fio_to_sheets.py --credentials .secret/fuj-management-bot-credentials.json + $(PYTHON) $(LEGACY)/scripts/sync_fio_to_sheets.py --credentials $(CREDENTIALS) sync-2025: $(PYTHON) - $(PYTHON) scripts/sync_fio_to_sheets.py --credentials .secret/fuj-management-bot-credentials.json --from 2025-10-01 --to 2025-12-31 --sort-by-date + $(PYTHON) $(LEGACY)/scripts/sync_fio_to_sheets.py --credentials $(CREDENTIALS) --from 2025-10-01 --to 2025-12-31 --sort-by-date sync-2026: $(PYTHON) - $(PYTHON) scripts/sync_fio_to_sheets.py --credentials .secret/fuj-management-bot-credentials.json --from 2026-01-01 --to 2026-12-31 --sort-by-date + $(PYTHON) $(LEGACY)/scripts/sync_fio_to_sheets.py --credentials $(CREDENTIALS) --from 2026-01-01 --to 2026-12-31 --sort-by-date sync-debug: $(PYTHON) ## Dry-run Python sync with Fio diagnostics and txn table (default DAYS=30) - $(PYTHON) scripts/sync_fio_to_sheets.py --credentials .secret/fuj-management-bot-credentials.json --days $(DAYS) --dry-run --print-fio-table + $(PYTHON) $(LEGACY)/scripts/sync_fio_to_sheets.py --credentials $(CREDENTIALS) --days $(DAYS) --dry-run --print-fio-table infer: $(PYTHON) - $(PYTHON) scripts/infer_payments.py --credentials $(CREDENTIALS) + $(PYTHON) $(LEGACY)/scripts/infer_payments.py --credentials $(CREDENTIALS) -reconcile: ## Match payments against attendance - export PYTHONPATH=$(PYTHONPATH):$(CURDIR)/scripts && $(PYTHON) scripts/match_payments.py --credentials $(CREDENTIALS) +reconcile: $(PYTHON) ## Match payments against attendance + $(PYTHON) $(LEGACY)/scripts/match_payments.py --credentials $(CREDENTIALS) test: $(PYTHON) ## Run web application tests - export PYTHONPATH=$(PYTHONPATH):$(CURDIR)/scripts:$(CURDIR) && $(PYTHON) -m unittest discover tests + cd $(LEGACY) && $(PYTHON) -m unittest discover tests test-v: $(PYTHON) ## Run tests with verbose output - export PYTHONPATH=$(PYTHONPATH):$(CURDIR)/scripts:$(CURDIR) && $(PYTHON) -m unittest discover -v tests + cd $(LEGACY) && $(PYTHON) -m unittest discover -v tests docs: ## Serve documentation locally @echo "Starting documentation server at http://localhost:8000" diff --git a/docs/README.md b/docs/README.md index 3338afe..ea47cad 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,6 +4,8 @@ Welcome to the documentation for the FUJ Management application. This project automates financial and operational management for the FUJ (Frisbee Ultimate Jablonec) club. +> **Go is the primary implementation** (`go/`). The project was originally developed in Python; that version now lives in `legacy/` and is considered legacy. New features are still built in both. Most guides below predate this change and describe the Python version with its old paths (`app.py`, `scripts/` are now `legacy/app.py`, `legacy/scripts/`). + ## Navigation Use the sidebar to explore the documentation: diff --git a/go/cmd/parity/main.go b/go/cmd/parity/main.go index f6ff8d0..918249d 100644 --- a/go/cmd/parity/main.go +++ b/go/cmd/parity/main.go @@ -21,7 +21,7 @@ var allRoutes = []string{"/api/adults", "/api/juniors", "/api/payments"} // defaultAllowlist holds dotted key paths to strip before diffing. // render_time.total is forward-compatible insurance: today it lives in the Jinja -// template context only (app.py inject_render_time) and is logged via +// template context only (legacy/app.py inject_render_time) and is logged via // middleware/timer.go on the Go side, so it isn't in any JSON response. If either // side ever surfaces it under a render_time envelope, the scrubber handles it. var defaultAllowlist = []string{"render_time.total"} diff --git a/go/internal/config/config.go b/go/internal/config/config.go index babaa00..f2cba64 100644 --- a/go/internal/config/config.go +++ b/go/internal/config/config.go @@ -42,7 +42,7 @@ const ( JuniorSheetGID = "1213318614" // gid=1213318614 — junior practices tab (IMPORTRANGE'd) ) -// CacheSheetMap mirrors scripts/config.py CACHE_SHEET_MAP. +// CacheSheetMap mirrors legacy/scripts/config.py CACHE_SHEET_MAP. // Maps a cache key to the Google Sheet ID whose Drive modifiedTime gates it. // Both attendance keys map to the same spreadsheet — different tabs, one Drive file. var CacheSheetMap = map[string]string{ @@ -53,7 +53,7 @@ var CacheSheetMap = map[string]string{ } // Config holds all runtime configuration loaded from environment variables. -// Mirrors scripts/config.py. +// Mirrors legacy/scripts/config.py. type Config struct { CredentialsPath string QRAccount string // IBAN of the primary account used for QR codes diff --git a/go/internal/domain/czech/parse_month_references_test.go b/go/internal/domain/czech/parse_month_references_test.go index 2f43a47..0c4cc96 100644 --- a/go/internal/domain/czech/parse_month_references_test.go +++ b/go/internal/domain/czech/parse_month_references_test.go @@ -9,7 +9,7 @@ func TestParseMonthReferences(t *testing.T) { t.Parallel() // All expected outputs verified against live Python implementation on 2026-05-05: - // PYTHONPATH=scripts:. python -c 'from czech_utils import parse_month_references; print(parse_month_references("", 2026))' + // PYTHONPATH=legacy/scripts:legacy python -c 'from czech_utils import parse_month_references; print(parse_month_references("", 2026))' tests := []struct { name string input string diff --git a/go/internal/domain/fees/fees.go b/go/internal/domain/fees/fees.go index 1158ee4..f1e8e85 100644 --- a/go/internal/domain/fees/fees.go +++ b/go/internal/domain/fees/fees.go @@ -1,4 +1,4 @@ -// Package fees ports fee calculation from scripts/attendance.py. +// Package fees ports fee calculation from legacy/scripts/attendance.py. package fees const ( @@ -6,7 +6,7 @@ const ( AdultFeeSingle = 200 // CZK for exactly 1 practice ) -// AdultFeeMonthlyRate mirrors ADULT_FEE_MONTHLY_RATE in scripts/attendance.py. +// AdultFeeMonthlyRate mirrors ADULT_FEE_MONTHLY_RATE in legacy/scripts/attendance.py. // Months absent from this map fall back to AdultFeeDefault. var AdultFeeMonthlyRate = map[string]int{ "2025-09": 750, "2025-10": 750, "2025-11": 750, "2025-12": 750, diff --git a/go/internal/domain/fees/junior.go b/go/internal/domain/fees/junior.go index b1f0c3f..b48aa1f 100644 --- a/go/internal/domain/fees/junior.go +++ b/go/internal/domain/fees/junior.go @@ -2,7 +2,7 @@ package fees const JuniorFeeDefault = 500 // CZK fallback for 2+ practices when month not in JuniorFeeMonthlyRate -// JuniorFeeMonthlyRate mirrors JUNIOR_MONTHLY_RATE in scripts/attendance.py. +// JuniorFeeMonthlyRate mirrors JUNIOR_MONTHLY_RATE in legacy/scripts/attendance.py. // Months absent from this map fall back to JuniorFeeDefault. var JuniorFeeMonthlyRate = map[string]int{ "2025-09": 250, diff --git a/go/internal/domain/matching/doc.go b/go/internal/domain/matching/doc.go index 328afbe..4e3f781 100644 --- a/go/internal/domain/matching/doc.go +++ b/go/internal/domain/matching/doc.go @@ -1,2 +1,2 @@ -// Package matching ports name/member matching from scripts/match_payments.py. +// Package matching ports name/member matching from legacy/scripts/match_payments.py. package matching diff --git a/go/internal/domain/matching/format_date.go b/go/internal/domain/matching/format_date.go index e7f306a..888afe0 100644 --- a/go/internal/domain/matching/format_date.go +++ b/go/internal/domain/matching/format_date.go @@ -14,7 +14,7 @@ var sheetsEpoch = time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC) // a pre-formatted "YYYY-MM-DD" string (returned as-is), or any other value // (returned as fmt.Sprint(v).TrimSpace). Never returns an error. // -// Ports scripts/match_payments.py format_date. +// Ports legacy/scripts/match_payments.py format_date. func FormatDate(val any) string { if val == nil { return "" diff --git a/go/internal/domain/matching/format_date_test.go b/go/internal/domain/matching/format_date_test.go index 2daaf52..93ca6c9 100644 --- a/go/internal/domain/matching/format_date_test.go +++ b/go/internal/domain/matching/format_date_test.go @@ -1,8 +1,8 @@ package matching -// Expected values verified against scripts/match_payments.py on 2026-05-06: +// Expected values verified against legacy/scripts/match_payments.py on 2026-05-06: // -// PYTHONPATH=scripts:. python3 -c ' +// PYTHONPATH=legacy/scripts:legacy python3 -c ' // from match_payments import format_date // for v in [None, "", 44197, 44197.5, "2026-04-15", "garbage", " 2026-04-15 "]: // print(repr(format_date(v))) diff --git a/go/internal/domain/matching/infer.go b/go/internal/domain/matching/infer.go index cc8c2a1..6dc4c91 100644 --- a/go/internal/domain/matching/infer.go +++ b/go/internal/domain/matching/infer.go @@ -33,7 +33,7 @@ type InferredDetails struct { // defaultYear seeds czech.ParseMonthReferences (Python defaulted to the current year; // callers should pass time.Now().Year() or a fixed year for deterministic tests). // -// Ports scripts/match_payments.py infer_transaction_details. +// Ports legacy/scripts/match_payments.py infer_transaction_details. func InferTransactionDetails(tx Transaction, memberNames []string, defaultYear int) InferredDetails { searchText := fmt.Sprintf("%s %s %s", tx.Sender, tx.Message, tx.UserID) diff --git a/go/internal/domain/matching/infer_test.go b/go/internal/domain/matching/infer_test.go index 938e2e4..a898dbc 100644 --- a/go/internal/domain/matching/infer_test.go +++ b/go/internal/domain/matching/infer_test.go @@ -1,8 +1,8 @@ package matching -// Expected values verified against scripts/match_payments.py on 2026-05-06: +// Expected values verified against legacy/scripts/match_payments.py on 2026-05-06: // -// PYTHONPATH=scripts:. python3 << 'EOF' +// PYTHONPATH=legacy/scripts:legacy python3 << 'EOF' // from match_payments import infer_transaction_details // MEMBERS = ["Tomáš Němeček (Tov)", "Jana Nováková"] // cases = [ diff --git a/go/internal/domain/matching/match_members.go b/go/internal/domain/matching/match_members.go index 9d92c9f..0145f7c 100644 --- a/go/internal/domain/matching/match_members.go +++ b/go/internal/domain/matching/match_members.go @@ -28,7 +28,7 @@ var commonSurnames = map[string]bool{ // MatchMembers finds members mentioned in text and returns them with a // confidence level of "auto" (reliable) or "review" (needs human verification). // -// Algorithm (ported verbatim from scripts/match_payments.py match_members): +// Algorithm (ported verbatim from legacy/scripts/match_payments.py match_members): // 1. Exact short-circuit: if any member's full normalized name appears as whole // words in normalize(text), return ONLY those matches as auto. This prevents // nickname "tov" from matching inside surname "ottova". diff --git a/go/internal/domain/matching/match_members_test.go b/go/internal/domain/matching/match_members_test.go index f0b0cda..4de512f 100644 --- a/go/internal/domain/matching/match_members_test.go +++ b/go/internal/domain/matching/match_members_test.go @@ -1,9 +1,9 @@ package matching -// Expected values verified against scripts/match_payments.py and +// Expected values verified against legacy/scripts/match_payments.py and // tests/test_match_members.py on 2026-05-06: // -// PYTHONPATH=scripts:. python3 -c ' +// PYTHONPATH=legacy/scripts:legacy python3 -c ' // from match_payments import match_members // MEMBERS = ["Henrietta Ottová", "Tomáš Němeček (Tov)", "František Vrbík (Štrúdl)", "Jana Nováková"] // cases = [ diff --git a/go/internal/domain/matching/name_variants.go b/go/internal/domain/matching/name_variants.go index 20e426c..f5e1db6 100644 --- a/go/internal/domain/matching/name_variants.go +++ b/go/internal/domain/matching/name_variants.go @@ -19,7 +19,7 @@ var ( // this invariant for the exact short-circuit pass. Variants shorter than 3 characters are // dropped. // -// Ports scripts/match_payments.py _build_name_variants. +// Ports legacy/scripts/match_payments.py _build_name_variants. func BuildNameVariants(name string) []string { var nickname string if m := nicknameRe.FindStringSubmatch(name); m != nil { diff --git a/go/internal/domain/matching/name_variants_test.go b/go/internal/domain/matching/name_variants_test.go index ae9c983..58a229f 100644 --- a/go/internal/domain/matching/name_variants_test.go +++ b/go/internal/domain/matching/name_variants_test.go @@ -1,8 +1,8 @@ package matching -// Expected values verified against scripts/match_payments.py on 2026-05-06: +// Expected values verified against legacy/scripts/match_payments.py on 2026-05-06: // -// PYTHONPATH=scripts:. python3 -c ' +// PYTHONPATH=legacy/scripts:legacy python3 -c ' // from match_payments import _build_name_variants // for n in ["František Vrbík (Štrúdl)", "Tov (St)", "Jana", " Petr Novák ( Jenda ) "]: // print(repr(n), "->", _build_name_variants(n)) diff --git a/go/internal/domain/money/money.go b/go/internal/domain/money/money.go index a82316d..761fca8 100644 --- a/go/internal/domain/money/money.go +++ b/go/internal/domain/money/money.go @@ -1,4 +1,4 @@ -// Package money ports Czech-locale currency parsing from scripts/infer_payments.py. +// Package money ports Czech-locale currency parsing from legacy/scripts/infer_payments.py. package money import ( @@ -11,7 +11,7 @@ import ( var ErrInvalidAmount = errors.New("money: invalid CZK amount") // ParseCZK parses a Czech-locale amount string and returns the value in CZK -// as a float64. Mirrors scripts/infer_payments.py parse_czk_amount: +// as a float64. Mirrors legacy/scripts/infer_payments.py parse_czk_amount: // // - empty input → (0, nil) // - "Kč"/"CZK" suffixes stripped (case-sensitive, like Python) diff --git a/go/internal/domain/money/money_test.go b/go/internal/domain/money/money_test.go index fd41739..18edf5a 100644 --- a/go/internal/domain/money/money_test.go +++ b/go/internal/domain/money/money_test.go @@ -8,7 +8,7 @@ func TestParseCZK(t *testing.T) { t.Parallel() // All expected outputs verified against live Python implementation on 2026-05-06: - // PYTHONPATH=scripts:. python -c ' + // PYTHONPATH=legacy/scripts:legacy python -c ' // from infer_payments import parse_czk_amount // for v in [None, "", "0", "500", "500 Kč", "500 CZK", // "1 500", "1500.00", "1 500.00", diff --git a/go/internal/domain/reconcile/reconcile.go b/go/internal/domain/reconcile/reconcile.go index 36bffae..72437cf 100644 --- a/go/internal/domain/reconcile/reconcile.go +++ b/go/internal/domain/reconcile/reconcile.go @@ -1,4 +1,4 @@ -// Package reconcile ports the three-phase payment reconciliation from scripts/match_payments.py. +// Package reconcile ports the three-phase payment reconciliation from legacy/scripts/match_payments.py. package reconcile import ( @@ -105,7 +105,7 @@ var questionMarkRe = regexp.MustCompile(`\[\?\]\s*`) // canonicalMemberKey returns a diacritic-, case-, and whitespace-insensitive key // used to resolve Person-column values that drift from canonical attendance-sheet names. -// Ports scripts/match_payments.py canonical_member_key. +// Ports legacy/scripts/match_payments.py canonical_member_key. func canonicalMemberKey(name string) string { return strings.Join(strings.Fields(czech.Normalize(name)), " ") } @@ -124,7 +124,7 @@ type monthExpected struct { // defaultYear seeds czech.ParseMonthReferences in the inference fallback. // Pass time.Now().Year() in production; pass a fixed year in tests. // -// Ports scripts/match_payments.py reconcile. +// Ports legacy/scripts/match_payments.py reconcile. func Reconcile( members []Member, sortedMonths []string, diff --git a/go/internal/domain/reconcile/reconcile_test.go b/go/internal/domain/reconcile/reconcile_test.go index 807b9c1..c5520f5 100644 --- a/go/internal/domain/reconcile/reconcile_test.go +++ b/go/internal/domain/reconcile/reconcile_test.go @@ -1,8 +1,8 @@ package reconcile -// Expected values verified against scripts/match_payments.py on 2026-05-06: +// Expected values verified against legacy/scripts/match_payments.py on 2026-05-06: // -// PYTHONPATH=scripts:. python3 -m unittest tests.test_reconcile_exceptions tests.test_match_payments -v +// PYTHONPATH=legacy/scripts:legacy python3 -m unittest tests.test_reconcile_exceptions tests.test_match_payments -v // // All Python test cases are ported below. Additional Go-only cases are marked with [Go]. diff --git a/go/internal/domain/synch/synch.go b/go/internal/domain/synch/synch.go index 9b92de9..2df3fba 100644 --- a/go/internal/domain/synch/synch.go +++ b/go/internal/domain/synch/synch.go @@ -1,5 +1,5 @@ // Package synch ports the bank-sync deduplication helper from -// scripts/sync_fio_to_sheets.py. +// legacy/scripts/sync_fio_to_sheets.py. package synch import ( @@ -30,7 +30,7 @@ type Transaction struct { // "date|amount|currency|sender|vs|message|bank_id" (lower-cased), used // as the dedup key in column K of the payments sheet. // -// Byte-stable with scripts/sync_fio_to_sheets.py generate_sync_id. +// Byte-stable with legacy/scripts/sync_fio_to_sheets.py generate_sync_id. func GenerateSyncID(tx Transaction) string { currency := tx.Currency if currency == "" { diff --git a/go/internal/domain/synch/synch_test.go b/go/internal/domain/synch/synch_test.go index 4664b71..8c74fd9 100644 --- a/go/internal/domain/synch/synch_test.go +++ b/go/internal/domain/synch/synch_test.go @@ -6,7 +6,7 @@ import ( // All expected digests verified against the live Python implementation on 2026-05-06: // -// PYTHONPATH=scripts:. python -c ' +// PYTHONPATH=legacy/scripts:legacy python -c ' // from sync_fio_to_sheets import generate_sync_id // cases = [ // {"date":"2026-01-15","amount":500.0,"currency":"CZK","sender":"Jan Novak","vs":"123","message":"clenske 1/2026","bank_id":"abc123"}, @@ -88,7 +88,7 @@ func TestGenerateSyncID(t *testing.T) { // All expected strings verified against the live Python implementation on 2026-05-06: // -// PYTHONPATH=scripts:. python -c ' +// PYTHONPATH=legacy/scripts:legacy python -c ' // for v in [0.0, 500.0, -500.0, 0.1, 1234.56, 99999.99, 1500000.0, 1e16, 1e-5]: // print(repr(v), "->", repr(str(v))) // ' diff --git a/go/internal/io/cache/filecache.go b/go/internal/io/cache/filecache.go index 70b8ebb..e40486a 100644 --- a/go/internal/io/cache/filecache.go +++ b/go/internal/io/cache/filecache.go @@ -1,5 +1,5 @@ // Package cache implements a Drive-modifiedTime-gated JSON file cache, -// mirroring scripts/cache_utils.py. +// mirroring legacy/scripts/cache_utils.py. package cache import ( @@ -26,7 +26,7 @@ type cacheFile struct { // FileCache wraps a Drive client to gate JSON file caching on sheet modifiedTime. // -// Two TTL knobs mirror scripts/cache_utils.py: +// Two TTL knobs mirror legacy/scripts/cache_utils.py: // - ttl: if the cache file on disk is younger than this, skip the Drive check entirely. // - apiCheckTTL: debounces in-memory Drive API calls per sheet ID. // diff --git a/go/internal/io/fio/api.go b/go/internal/io/fio/api.go index be32f91..29de3c5 100644 --- a/go/internal/io/fio/api.go +++ b/go/internal/io/fio/api.go @@ -17,7 +17,7 @@ type httpDoer interface { } // apiClient fetches transactions from the Fio REST API (JSON). -// Ports scripts/fio_utils.py fetch_transactions_api. +// Ports legacy/scripts/fio_utils.py fetch_transactions_api. type apiClient struct { token string hc httpDoer diff --git a/go/internal/io/fio/transparent.go b/go/internal/io/fio/transparent.go index 31184d7..87685a4 100644 --- a/go/internal/io/fio/transparent.go +++ b/go/internal/io/fio/transparent.go @@ -15,7 +15,7 @@ import ( ) // transparentClient fetches transactions from the Fio transparent account page (HTML). -// Ports scripts/fio_utils.py FioTableParser + fetch_transactions_transparent. +// Ports legacy/scripts/fio_utils.py FioTableParser + fetch_transactions_transparent. type transparentClient struct { accountNum string hc httpDoer diff --git a/go/internal/services/banksync/infer.go b/go/internal/services/banksync/infer.go index 82f431a..0eaf21a 100644 --- a/go/internal/services/banksync/infer.go +++ b/go/internal/services/banksync/infer.go @@ -30,7 +30,7 @@ type sheetReadWriter interface { // InferPayments fills empty Person/Purpose/Inferred Amount cells in the payments // sheet using name and month matching against the member list. // Returns the number of rows updated (or that would be updated on dry-run). -// Ports scripts/infer_payments.py infer_payments. +// Ports legacy/scripts/infer_payments.py infer_payments. func InferPayments( ctx context.Context, spreadsheetID string, diff --git a/go/internal/services/banksync/sync.go b/go/internal/services/banksync/sync.go index a33bcda..887f65d 100644 --- a/go/internal/services/banksync/sync.go +++ b/go/internal/services/banksync/sync.go @@ -12,7 +12,7 @@ import ( ) // columnLabels is the canonical header for the payments sheet. -// Mirrors COLUMN_LABELS in scripts/sync_fio_to_sheets.py. +// Mirrors COLUMN_LABELS in legacy/scripts/sync_fio_to_sheets.py. var columnLabels = []string{ "Date", "Amount", "manual fix", "Person", "Purpose", "Inferred Amount", "Sender", "VS", "Message", "Bank ID", "Sync ID", @@ -37,7 +37,7 @@ type SyncOpts struct { // SyncToSheets fetches Fio transactions from all provided clients and appends // new ones to the payments sheet. Returns the number of rows appended. -// Ports scripts/sync_fio_to_sheets.py sync_to_sheets. +// Ports legacy/scripts/sync_fio_to_sheets.py sync_to_sheets. func SyncToSheets( ctx context.Context, spreadsheetID string, diff --git a/go/internal/services/membership/format_fees.go b/go/internal/services/membership/format_fees.go index e304236..a930522 100644 --- a/go/internal/services/membership/format_fees.go +++ b/go/internal/services/membership/format_fees.go @@ -9,11 +9,11 @@ import ( ) // printFeesTable writes a fixed-width adult-fees table to w. -// Mirrors scripts/calculate_fees.py main(). +// Mirrors legacy/scripts/calculate_fees.py main(). // // Verify with: // -// PYTHONPATH=scripts:. python scripts/calculate_fees.py +// PYTHONPATH=legacy/scripts:legacy python legacy/scripts/calculate_fees.py func printFeesTable(w io.Writer, members []reconcile.Member, sortedMonths []string) { type row struct { name string diff --git a/go/internal/services/membership/format_fees_test.go b/go/internal/services/membership/format_fees_test.go index 5527af2..f48952a 100644 --- a/go/internal/services/membership/format_fees_test.go +++ b/go/internal/services/membership/format_fees_test.go @@ -1,8 +1,8 @@ package membership -// Golden strings verified against scripts/calculate_fees.py on 2026-05-06: +// Golden strings verified against legacy/scripts/calculate_fees.py on 2026-05-06: // -// PYTHONPATH=scripts:. python scripts/calculate_fees.py +// PYTHONPATH=legacy/scripts:legacy python legacy/scripts/calculate_fees.py // // (feed equivalent fixture data via attendance sheet or local CSV) diff --git a/go/internal/services/membership/format_reconcile.go b/go/internal/services/membership/format_reconcile.go index e1cff4f..f5abb3c 100644 --- a/go/internal/services/membership/format_reconcile.go +++ b/go/internal/services/membership/format_reconcile.go @@ -10,11 +10,11 @@ import ( ) // printReconcileReport writes the full balance report to w. -// Mirrors scripts/match_payments.py print_report(). +// Mirrors legacy/scripts/match_payments.py print_report(). // // Verify with: // -// PYTHONPATH=scripts:. python -c ' +// PYTHONPATH=legacy/scripts:legacy python -c ' // from match_payments import print_report, reconcile, fetch_sheet_data, fetch_exceptions // ...' func printReconcileReport(w io.Writer, result reconcile.Result, sortedMonths []string) { diff --git a/go/internal/services/membership/format_reconcile_test.go b/go/internal/services/membership/format_reconcile_test.go index e123d2d..b27ada8 100644 --- a/go/internal/services/membership/format_reconcile_test.go +++ b/go/internal/services/membership/format_reconcile_test.go @@ -1,8 +1,8 @@ package membership -// Golden strings verified against scripts/match_payments.py print_report() on 2026-05-06: +// Golden strings verified against legacy/scripts/match_payments.py print_report() on 2026-05-06: // -// PYTHONPATH=scripts:. python -c ' +// PYTHONPATH=legacy/scripts:legacy python -c ' // from match_payments import print_report // result = { // "members": { diff --git a/go/internal/services/membership/sources.go b/go/internal/services/membership/sources.go index ed006a8..c2f9cca 100644 --- a/go/internal/services/membership/sources.go +++ b/go/internal/services/membership/sources.go @@ -18,20 +18,20 @@ import ( "time" ) -// Attendance CSV column indices (mirrors COL_* in scripts/attendance.py) +// Attendance CSV column indices (mirrors COL_* in legacy/scripts/attendance.py) const ( colName = 0 colTier = 1 firstDateCol = 3 ) -// AdultMergedMonths mirrors ADULT_MERGED_MONTHS in scripts/attendance.py. +// AdultMergedMonths mirrors ADULT_MERGED_MONTHS in legacy/scripts/attendance.py. // Source month → target month (source attendance accumulated into target). var AdultMergedMonths = map[string]string{ "2025-09": "2025-10", } -// JuniorMergedMonths mirrors JUNIOR_MERGED_MONTHS in scripts/attendance.py. +// JuniorMergedMonths mirrors JUNIOR_MERGED_MONTHS in legacy/scripts/attendance.py. var JuniorMergedMonths = map[string]string{ "2025-12": "2026-01", "2025-09": "2025-10", @@ -127,11 +127,11 @@ func (s *realSources) LoadExceptions(ctx context.Context) (map[reconcile.Excepti } // --------------------------------------------------------------------------- -// Attendance CSV parsing (ports scripts/attendance.py) +// Attendance CSV parsing (ports legacy/scripts/attendance.py) // --------------------------------------------------------------------------- // parseDates returns (columnIndex, YYYY-MM) pairs for all date columns. -// Ports scripts/attendance.py parse_dates + strftime("%Y-%m"). +// Ports legacy/scripts/attendance.py parse_dates + strftime("%Y-%m"). func parseDates(header []string) []struct { col int month string @@ -200,7 +200,7 @@ func countTrue(row []string, cols []int) int { // parseAdultRows converts raw CSV rows to []reconcile.Member. // Includes all tiers; fee is 0 for non-A tiers (reconcile filters downstream). -// Ports scripts/attendance.py get_members_with_fees. +// Ports legacy/scripts/attendance.py get_members_with_fees. func parseAdultRows(rows [][]string) ([]reconcile.Member, []string, error) { if len(rows) < 2 { return nil, nil, nil @@ -246,7 +246,7 @@ func parseAdultRows(rows [][]string) ([]reconcile.Member, []string, error) { // parseJuniorRows builds junior members by merging tier-J rows from the adult tab // with the junior sheet, then calling CalculateJuniorFee. -// Ports scripts/attendance.py get_junior_members_with_fees. +// Ports legacy/scripts/attendance.py get_junior_members_with_fees. func parseJuniorRows(adultRows, juniorRows [][]string) ([]reconcile.Member, []string, error) { if len(adultRows) < 2 || len(juniorRows) < 2 { return nil, nil, nil @@ -362,7 +362,7 @@ func parseJuniorRows(adultRows, juniorRows [][]string) ([]reconcile.Member, []st } // --------------------------------------------------------------------------- -// Payments sheet row parsing (ports scripts/match_payments.py fetch_sheet_data) +// Payments sheet row parsing (ports legacy/scripts/match_payments.py fetch_sheet_data) // --------------------------------------------------------------------------- func parseTransactionRows(rows [][]any) ([]reconcile.Transaction, error) { @@ -466,7 +466,7 @@ func parseFloat(v any) float64 { } // --------------------------------------------------------------------------- -// Exceptions tab parsing (ports scripts/match_payments.py fetch_exceptions) +// Exceptions tab parsing (ports legacy/scripts/match_payments.py fetch_exceptions) // --------------------------------------------------------------------------- func parseExceptionRows(rows [][]any) map[reconcile.ExceptionKey]reconcile.Exception { diff --git a/go/internal/web/api/build_adults.go b/go/internal/web/api/build_adults.go index 008f317..fcd4769 100644 --- a/go/internal/web/api/build_adults.go +++ b/go/internal/web/api/build_adults.go @@ -13,7 +13,7 @@ import ( type monthSums struct{ expected, paid int } // buildAdultsResponse constructs the AdultsResponse wire type from reconcile output. -// Mirrors scripts/views.py:build_adults_view_model. +// Mirrors legacy/scripts/views.py:build_adults_view_model. func buildAdultsResponse( members []domreconcile.Member, sortedMonths []string, diff --git a/go/internal/web/api/build_common.go b/go/internal/web/api/build_common.go index df12212..434c36b 100644 --- a/go/internal/web/api/build_common.go +++ b/go/internal/web/api/build_common.go @@ -12,7 +12,7 @@ import ( // getMonthLabels builds display labels for sortedMonths, merging month names // (e.g. "Dec+Jan 2026") when mergedMonths maps a source month into this target. -// Mirrors scripts/views.py:get_month_labels. +// Mirrors legacy/scripts/views.py:get_month_labels. func getMonthLabels(sortedMonths []string, mergedMonths map[string]string) map[string]string { labels := make(map[string]string, len(sortedMonths)) for _, m := range sortedMonths { @@ -72,14 +72,14 @@ func labelsForMonths(sortedMonths []string, labels map[string]string) []string { var questionMarkRe = regexp.MustCompile(`\[\?\]\s*`) // canonicalKey returns a normalized form of a person name used for deduplication. -// Mirrors scripts/match_payments.py:canonical_member_key. +// Mirrors legacy/scripts/match_payments.py:canonical_member_key. func canonicalKey(name string) string { return strings.Join(strings.Fields(czech.Normalize(name)), " ") } // groupRawPaymentsByPerson groups transactions by the "person" column, // canonicalizing names against memberNames where possible. -// Mirrors scripts/views.py:group_payments_by_person (without the +// Mirrors legacy/scripts/views.py:group_payments_by_person (without the // "Unmatched / Unknown" bucket that is payments-view-specific). func groupRawPaymentsByPerson(txns []domreconcile.Transaction, memberNames []string) map[string][]RawTransaction { canonicalByKey := make(map[string]string, len(memberNames)) diff --git a/go/internal/web/api/build_juniors.go b/go/internal/web/api/build_juniors.go index e4b8043..bee21e0 100644 --- a/go/internal/web/api/build_juniors.go +++ b/go/internal/web/api/build_juniors.go @@ -11,7 +11,7 @@ import ( ) // buildJuniorsResponse constructs the JuniorsResponse wire type from reconcile output. -// Mirrors scripts/views.py:build_juniors_view_model. +// Mirrors legacy/scripts/views.py:build_juniors_view_model. func buildJuniorsResponse( members []domreconcile.Member, sortedMonths []string, diff --git a/go/internal/web/api/build_payments.go b/go/internal/web/api/build_payments.go index 57d874c..5510513 100644 --- a/go/internal/web/api/build_payments.go +++ b/go/internal/web/api/build_payments.go @@ -9,7 +9,7 @@ import ( ) // buildPaymentsResponse constructs the PaymentsResponse wire type. -// Mirrors scripts/views.py:build_payments_view_model. +// Mirrors legacy/scripts/views.py:build_payments_view_model. func buildPaymentsResponse( txns []domreconcile.Transaction, memberNames []string, diff --git a/go/internal/web/api/version.go b/go/internal/web/api/version.go index 21593a6..414d2c3 100644 --- a/go/internal/web/api/version.go +++ b/go/internal/web/api/version.go @@ -1,7 +1,7 @@ package api // VersionResponse is the JSON contract for GET /api/version. -// Keys match Python's BUILD_META dict (see app.py). +// Keys match Python's BUILD_META dict (see legacy/app.py). type VersionResponse struct { Tag string `json:"tag"` Commit string `json:"commit"` diff --git a/go/tests/fixtures/README.md b/go/tests/fixtures/README.md index 4225f68..18c6a02 100644 --- a/go/tests/fixtures/README.md +++ b/go/tests/fixtures/README.md @@ -85,10 +85,10 @@ git diff go/tests/fixtures/ # review changes before committing To refresh a single function: ```bash -PYTHONPATH=scripts:. python3 scripts/capture_fixtures.py --func normalize --all \ +PYTHONPATH=legacy/scripts:legacy python3 legacy/scripts/capture_fixtures.py --func normalize --all \ | while IFS= read -r line; do id=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin)['case'])") - echo "$line" | python3 scripts/scrub_fixtures.py \ + echo "$line" | python3 legacy/scripts/scrub_fixtures.py \ > go/tests/fixtures/pure/normalize/${id}.json done ``` @@ -120,7 +120,7 @@ verify that no real roster names appear in their `member_names` arrays. ## Adding a new fixture -1. Add a seed to `scripts/_fixture_seeds.py` under `SEEDS[("func_name", "case_id")]`. +1. Add a seed to `legacy/scripts/_fixture_seeds.py` under `SEEDS[("func_name", "case_id")]`. 2. Add `In`/`Out` struct fields to `go/tests/parity/parityio.go` if the function is new. 3. Run the single-file capture recipe above and review the diff. diff --git a/go/tests/parity/pure/build_name_variants/build_name_variants_parity_test.go b/go/tests/parity/pure/build_name_variants/build_name_variants_parity_test.go index be35e2c..f19da50 100644 --- a/go/tests/parity/pure/build_name_variants/build_name_variants_parity_test.go +++ b/go/tests/parity/pure/build_name_variants/build_name_variants_parity_test.go @@ -13,7 +13,7 @@ import ( // Verify expected values against live Python: // -// PYTHONPATH=scripts:. python3 -c "from match_payments import _build_name_variants; print(_build_name_variants('František Vrbík (Štrúdl)'))" +// PYTHONPATH=legacy/scripts:legacy python3 -c "from match_payments import _build_name_variants; print(_build_name_variants('František Vrbík (Štrúdl)'))" func TestBuildNameVariantsParity(t *testing.T) { t.Parallel() diff --git a/go/tests/parity/pure/calculate_fee/calculate_fee_parity_test.go b/go/tests/parity/pure/calculate_fee/calculate_fee_parity_test.go index 81ec5a9..180c373 100644 --- a/go/tests/parity/pure/calculate_fee/calculate_fee_parity_test.go +++ b/go/tests/parity/pure/calculate_fee/calculate_fee_parity_test.go @@ -12,7 +12,7 @@ import ( // Verify expected values against live Python: // -// PYTHONPATH=scripts:. python3 -c "from attendance import calculate_fee; print(calculate_fee(2, '2026-01'))" +// PYTHONPATH=legacy/scripts:legacy python3 -c "from attendance import calculate_fee; print(calculate_fee(2, '2026-01'))" func TestCalculateFeeParity(t *testing.T) { t.Parallel() diff --git a/go/tests/parity/pure/calculate_junior_fee/calculate_junior_fee_parity_test.go b/go/tests/parity/pure/calculate_junior_fee/calculate_junior_fee_parity_test.go index 7f900a8..41977bd 100644 --- a/go/tests/parity/pure/calculate_junior_fee/calculate_junior_fee_parity_test.go +++ b/go/tests/parity/pure/calculate_junior_fee/calculate_junior_fee_parity_test.go @@ -12,7 +12,7 @@ import ( // Verify expected values against live Python: // -// PYTHONPATH=scripts:. python3 -c "from attendance import calculate_junior_fee; print(calculate_junior_fee(1, '2026-01'))" +// PYTHONPATH=legacy/scripts:legacy python3 -c "from attendance import calculate_junior_fee; print(calculate_junior_fee(1, '2026-01'))" func TestCalculateJuniorFeeParity(t *testing.T) { t.Parallel() diff --git a/go/tests/parity/pure/format_date/format_date_parity_test.go b/go/tests/parity/pure/format_date/format_date_parity_test.go index b5d0b5a..fe84526 100644 --- a/go/tests/parity/pure/format_date/format_date_parity_test.go +++ b/go/tests/parity/pure/format_date/format_date_parity_test.go @@ -12,7 +12,7 @@ import ( // Verify expected values against live Python: // -// PYTHONPATH=scripts:. python3 -c "from match_payments import format_date; print(format_date(46027))" +// PYTHONPATH=legacy/scripts:legacy python3 -c "from match_payments import format_date; print(format_date(46027))" func TestFormatDateParity(t *testing.T) { t.Parallel() diff --git a/go/tests/parity/pure/generate_sync_id/generate_sync_id_parity_test.go b/go/tests/parity/pure/generate_sync_id/generate_sync_id_parity_test.go index 0ba7ef9..da546c7 100644 --- a/go/tests/parity/pure/generate_sync_id/generate_sync_id_parity_test.go +++ b/go/tests/parity/pure/generate_sync_id/generate_sync_id_parity_test.go @@ -12,7 +12,7 @@ import ( // Verify expected values against live Python: // -// PYTHONPATH=scripts:. python3 -c " +// PYTHONPATH=legacy/scripts:legacy python3 -c " // from sync_fio_to_sheets import generate_sync_id // print(generate_sync_id({'date':'2026-01-15','amount':750.0,'currency':'CZK','sender':'Test','vs':'123','message':'x','bank_id':'1'}))" // diff --git a/go/tests/parity/pure/infer_transaction_details/infer_transaction_details_parity_test.go b/go/tests/parity/pure/infer_transaction_details/infer_transaction_details_parity_test.go index 4bec7b0..c38ecf0 100644 --- a/go/tests/parity/pure/infer_transaction_details/infer_transaction_details_parity_test.go +++ b/go/tests/parity/pure/infer_transaction_details/infer_transaction_details_parity_test.go @@ -12,7 +12,7 @@ import ( // Verify expected values against live Python: // -// PYTHONPATH=scripts:. python3 -c " +// PYTHONPATH=legacy/scripts:legacy python3 -c " // from match_payments import infer_transaction_details // print(infer_transaction_details({'sender':'Henrietta Ottová','message':'leden 2026','user_id':'','date':'2026-01-15'}, ['Henrietta Ottová']))" diff --git a/go/tests/parity/pure/match_members/match_members_parity_test.go b/go/tests/parity/pure/match_members/match_members_parity_test.go index b4eb4b5..7bbc885 100644 --- a/go/tests/parity/pure/match_members/match_members_parity_test.go +++ b/go/tests/parity/pure/match_members/match_members_parity_test.go @@ -12,7 +12,7 @@ import ( // Verify expected values against live Python: // -// PYTHONPATH=scripts:. python3 -c "from match_payments import match_members; print(match_members('henrietta ottova leden', ['Henrietta Ottová','Jan Novák']))" +// PYTHONPATH=legacy/scripts:legacy python3 -c "from match_payments import match_members; print(match_members('henrietta ottova leden', ['Henrietta Ottová','Jan Novák']))" func TestMatchMembersParity(t *testing.T) { t.Parallel() diff --git a/go/tests/parity/pure/normalize/normalize_parity_test.go b/go/tests/parity/pure/normalize/normalize_parity_test.go index 5fd7e47..394c7d3 100644 --- a/go/tests/parity/pure/normalize/normalize_parity_test.go +++ b/go/tests/parity/pure/normalize/normalize_parity_test.go @@ -12,7 +12,7 @@ import ( // Verify expected values against live Python: // -// PYTHONPATH=scripts:. python3 -c "from czech_utils import normalize; print(normalize('štefan čakrtový'))" +// PYTHONPATH=legacy/scripts:legacy python3 -c "from czech_utils import normalize; print(normalize('štefan čakrtový'))" func TestNormalizeParity(t *testing.T) { t.Parallel() diff --git a/go/tests/parity/pure/parse_czk_amount/parse_czk_amount_parity_test.go b/go/tests/parity/pure/parse_czk_amount/parse_czk_amount_parity_test.go index da3f18c..274f028 100644 --- a/go/tests/parity/pure/parse_czk_amount/parse_czk_amount_parity_test.go +++ b/go/tests/parity/pure/parse_czk_amount/parse_czk_amount_parity_test.go @@ -12,7 +12,7 @@ import ( // Verify expected values against live Python: // -// PYTHONPATH=scripts:. python3 -c "from infer_payments import parse_czk_amount; print(parse_czk_amount('1.500,00'))" +// PYTHONPATH=legacy/scripts:legacy python3 -c "from infer_payments import parse_czk_amount; print(parse_czk_amount('1.500,00'))" // // Note: Go's ParseCZK returns an error for unparseable input; Python returns 0.0. // Callers should discard the error to match Python semantics: v, _ := money.ParseCZK(s) diff --git a/go/tests/parity/pure/parse_month_references/parse_month_references_parity_test.go b/go/tests/parity/pure/parse_month_references/parse_month_references_parity_test.go index e57a091..d885d58 100644 --- a/go/tests/parity/pure/parse_month_references/parse_month_references_parity_test.go +++ b/go/tests/parity/pure/parse_month_references/parse_month_references_parity_test.go @@ -13,7 +13,7 @@ import ( // Verify expected values against live Python: // -// PYTHONPATH=scripts:. python3 -c "from czech_utils import parse_month_references; print(parse_month_references('prosinec-leden', 2026))" +// PYTHONPATH=legacy/scripts:legacy python3 -c "from czech_utils import parse_month_references; print(parse_month_references('prosinec-leden', 2026))" func TestParseMonthReferencesParity(t *testing.T) { t.Parallel() diff --git a/go/tests/parity/reconcile/reconcile_parity_test.go b/go/tests/parity/reconcile/reconcile_parity_test.go index a6cf225..bf18784 100644 --- a/go/tests/parity/reconcile/reconcile_parity_test.go +++ b/go/tests/parity/reconcile/reconcile_parity_test.go @@ -6,7 +6,7 @@ // // Verify expected values against live Python: // -// PYTHONPATH=scripts:. python3 -c " +// PYTHONPATH=legacy/scripts:legacy python3 -c " // from match_payments import reconcile // # ... build members/months/txs/exceptions... // import json; print(json.dumps(reconcile(...)))" diff --git a/build/.dockerignore b/legacy/.dockerignore similarity index 52% rename from build/.dockerignore rename to legacy/.dockerignore index cc3255f..9842ac5 100644 --- a/build/.dockerignore +++ b/legacy/.dockerignore @@ -1,6 +1,6 @@ .git .venv -__pycache__ -*.pyc +**/__pycache__ +**/*.pyc .claude .gemini diff --git a/app.py b/legacy/app.py similarity index 100% rename from app.py rename to legacy/app.py diff --git a/build/Dockerfile b/legacy/build/Dockerfile similarity index 97% rename from build/Dockerfile rename to legacy/build/Dockerfile index d5e7438..c104b47 100644 --- a/build/Dockerfile +++ b/legacy/build/Dockerfile @@ -17,7 +17,7 @@ RUN pip install --no-cache-dir \ pillow \ gunicorn -COPY app.py Makefile ./ +COPY app.py ./ COPY scripts/ ./scripts/ COPY templates/ ./templates/ diff --git a/build/entrypoint.sh b/legacy/build/entrypoint.sh similarity index 100% rename from build/entrypoint.sh rename to legacy/build/entrypoint.sh diff --git a/pyproject.toml b/legacy/pyproject.toml similarity index 72% rename from pyproject.toml rename to legacy/pyproject.toml index 3a5deec..a8b2cbb 100644 --- a/pyproject.toml +++ b/legacy/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "fuj-management" version = "0.10" -description = "Management tools for FUJ (Frisbee Ultimate Jablonec)" +description = "Legacy Python implementation of the FUJ (Frisbee Ultimate Jablonec) management tools; the Go implementation in ../go is primary" dependencies = [ "flask>=3.1.3", "google-api-python-client>=2.162.0", diff --git a/scripts/_fixture_seeds.py b/legacy/scripts/_fixture_seeds.py similarity index 100% rename from scripts/_fixture_seeds.py rename to legacy/scripts/_fixture_seeds.py diff --git a/scripts/attendance.py b/legacy/scripts/attendance.py similarity index 100% rename from scripts/attendance.py rename to legacy/scripts/attendance.py diff --git a/scripts/cache_utils.py b/legacy/scripts/cache_utils.py similarity index 100% rename from scripts/cache_utils.py rename to legacy/scripts/cache_utils.py diff --git a/scripts/calculate_fees.py b/legacy/scripts/calculate_fees.py similarity index 100% rename from scripts/calculate_fees.py rename to legacy/scripts/calculate_fees.py diff --git a/scripts/capture_all_fixtures.sh b/legacy/scripts/capture_all_fixtures.sh similarity index 70% rename from scripts/capture_all_fixtures.sh rename to legacy/scripts/capture_all_fixtures.sh index 9fb554c..d6abd63 100755 --- a/scripts/capture_all_fixtures.sh +++ b/legacy/scripts/capture_all_fixtures.sh @@ -1,14 +1,15 @@ #!/usr/bin/env bash # Regenerate the full fixture corpus. # Safe to re-run — always overwrites. -# Requires: tmp/*_cache.json present (for real-data seeds for parse_month_references and match_members). +# Requires: legacy/tmp/*_cache.json present (for real-data seeds for parse_month_references and match_members). set -euo pipefail -REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +LEGACY="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REPO="$(cd "$LEGACY/.." && pwd)" FIXTURES="$REPO/go/tests/fixtures" -CAPTURE_CMD="PYTHONPATH=$REPO/scripts:. python3 $REPO/scripts/capture_fixtures.py" -SCRUB_CMD="python3 $REPO/scripts/scrub_fixtures.py" +CAPTURE_CMD="PYTHONPATH=$LEGACY/scripts:$LEGACY python3 $LEGACY/scripts/capture_fixtures.py" +SCRUB_CMD="python3 $LEGACY/scripts/scrub_fixtures.py" run_func() { local func="$1" @@ -18,7 +19,7 @@ run_func() { eval "$CAPTURE_CMD --func $func --all" | while IFS= read -r line; do case_id="$(python3 -c "import sys,json; print(json.loads('''$line''')['case'])" 2>/dev/null || \ echo "$line" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['case'])")" - echo "$line" | python3 "$REPO/scripts/scrub_fixtures.py" > "$dir/${case_id}.json" + echo "$line" | python3 "$LEGACY/scripts/scrub_fixtures.py" > "$dir/${case_id}.json" done } @@ -39,7 +40,7 @@ echo "==> Capturing reconcile fixtures..." mkdir -p "$FIXTURES/reconcile" eval "$CAPTURE_CMD --func reconcile --all" | while IFS= read -r line; do case_id="$(echo "$line" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['case'])")" - echo "$line" | python3 "$REPO/scripts/scrub_fixtures.py" > "$FIXTURES/reconcile/${case_id}.json" + echo "$line" | python3 "$LEGACY/scripts/scrub_fixtures.py" > "$FIXTURES/reconcile/${case_id}.json" done echo "==> Done. Review with: git diff go/tests/fixtures/" diff --git a/scripts/capture_fixtures.py b/legacy/scripts/capture_fixtures.py similarity index 100% rename from scripts/capture_fixtures.py rename to legacy/scripts/capture_fixtures.py diff --git a/scripts/config.py b/legacy/scripts/config.py similarity index 100% rename from scripts/config.py rename to legacy/scripts/config.py diff --git a/scripts/czech_utils.py b/legacy/scripts/czech_utils.py similarity index 100% rename from scripts/czech_utils.py rename to legacy/scripts/czech_utils.py diff --git a/scripts/fio_utils.py b/legacy/scripts/fio_utils.py similarity index 100% rename from scripts/fio_utils.py rename to legacy/scripts/fio_utils.py diff --git a/scripts/infer_payments.py b/legacy/scripts/infer_payments.py similarity index 100% rename from scripts/infer_payments.py rename to legacy/scripts/infer_payments.py diff --git a/scripts/match_payments.py b/legacy/scripts/match_payments.py similarity index 100% rename from scripts/match_payments.py rename to legacy/scripts/match_payments.py diff --git a/scripts/scrub_fixtures.py b/legacy/scripts/scrub_fixtures.py similarity index 100% rename from scripts/scrub_fixtures.py rename to legacy/scripts/scrub_fixtures.py diff --git a/scripts/sync_fio_to_sheets.py b/legacy/scripts/sync_fio_to_sheets.py similarity index 100% rename from scripts/sync_fio_to_sheets.py rename to legacy/scripts/sync_fio_to_sheets.py diff --git a/scripts/views.py b/legacy/scripts/views.py similarity index 100% rename from scripts/views.py rename to legacy/scripts/views.py diff --git a/templates/adults.html b/legacy/templates/adults.html similarity index 100% rename from templates/adults.html rename to legacy/templates/adults.html diff --git a/templates/flush-cache.html b/legacy/templates/flush-cache.html similarity index 100% rename from templates/flush-cache.html rename to legacy/templates/flush-cache.html diff --git a/templates/juniors.html b/legacy/templates/juniors.html similarity index 100% rename from templates/juniors.html rename to legacy/templates/juniors.html diff --git a/templates/payments.html b/legacy/templates/payments.html similarity index 100% rename from templates/payments.html rename to legacy/templates/payments.html diff --git a/templates/sync.html b/legacy/templates/sync.html similarity index 100% rename from templates/sync.html rename to legacy/templates/sync.html diff --git a/tests/test_app.py b/legacy/tests/test_app.py similarity index 100% rename from tests/test_app.py rename to legacy/tests/test_app.py diff --git a/tests/test_match_members.py b/legacy/tests/test_match_members.py similarity index 100% rename from tests/test_match_members.py rename to legacy/tests/test_match_members.py diff --git a/tests/test_match_payments.py b/legacy/tests/test_match_payments.py similarity index 100% rename from tests/test_match_payments.py rename to legacy/tests/test_match_payments.py diff --git a/tests/test_reconcile_exceptions.py b/legacy/tests/test_reconcile_exceptions.py similarity index 100% rename from tests/test_reconcile_exceptions.py rename to legacy/tests/test_reconcile_exceptions.py diff --git a/uv.lock b/legacy/uv.lock similarity index 100% rename from uv.lock rename to legacy/uv.lock -- 2.54.0