diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a11e98..ab7f1e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 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. +- 2026-07 and 2026-08: 600 → 0 CZK; 2026-09 through 2026-12 added at 600 CZK (September and October were previously billed at the old 700 default). +- The single-session fee (200 CZK) is now capped at the month's rate, so a 0-rate month is free for one session too. +- Mirrored in `go/internal/domain/fees/fees.go` and `legacy/scripts/attendance.py`; three new parity fixtures for `calculate_fee`. + ## 2026-10-08 11:48 CEST — fix(ci): remove kubernetes-deploy workflow that leaked secrets to logs - Deleted `.gitea/workflows/kubernetes-deploy.yaml`. It ran on every push to every branch, deployed nothing, and echoed the Vault login response, the Kanidm API token response and the OIDC token response to the job log. diff --git a/CLAUDE.md b/CLAUDE.md index 6e78e79..b4b27ed 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -126,13 +126,13 @@ Fio Bank API ──► bank sync ──► Google Sheets (payments ### Member tiers Tiers are set in column B of the attendance sheet: -- `A` — Adult, pays fees (per-month rate from `AdultFeeMonthlyRate` / `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 600 CZK for 2+ sessions; 200 CZK for exactly 1, capped at the month's rate) - `J` — Junior attending adult practices; their attendance is merged with the junior sheet - `X` — Excluded from junior fee calculation (coaches, etc.) ### Fee calculation -- Adults: 0 sessions → 0, 1 session → 200 CZK, 2+ sessions → monthly rate (default 700 CZK) +- Adults: 0 sessions → 0, 1 session → 200 CZK (never more than the month's rate, so a 0-rate month is free), 2+ sessions → monthly rate (default 600 CZK) - Juniors: 0 → 0, 1 → `"?"` (manual review required), 2+ → monthly rate (default 500 CZK) - Per-member per-month overrides live in the `exceptions` tab of the payments sheet (columns: Name, Period YYYY-MM, Amount, Note). Exceptions are keyed by `(normalize(name), normalize(period))`. 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 0739e69..c7f35f2 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 @@ -42,7 +42,9 @@ before new feature work starts. Companion to the Go rewrite tracker, - After deletion the repo secrets `VAULT_ROLE_ID`, `VAULT_SECRET_ID` and `K8S_CA_CERT` are unused and can be removed. Old job logs still contain the leaked tokens until they are deleted or expire. -- [ ] **T2** Confirm the fee rates for 2026-09 onward. +- [x] **T2** Confirm the fee rates for 2026-09 onward. Done 2026-10-08: + adult default 600, 2026-07/08 at 0, 2026-09 to 2026-12 at 600. Junior + rates were not changed. - `ADULT_FEE_MONTHLY_RATE` ends at 2026-08 in `scripts/attendance.py` and `go/internal/domain/fees/fees.go`. - September and October 2026 are billed at the defaults: 700 CZK adults, diff --git a/go/internal/domain/fees/fees.go b/go/internal/domain/fees/fees.go index f1e8e85..33735c0 100644 --- a/go/internal/domain/fees/fees.go +++ b/go/internal/domain/fees/fees.go @@ -2,7 +2,7 @@ package fees const ( - AdultFeeDefault = 700 // CZK fallback for 2+ practices when month not in AdultFeeMonthlyRate + AdultFeeDefault = 600 // CZK fallback for 2+ practices when month not in AdultFeeMonthlyRate AdultFeeSingle = 200 // CZK for exactly 1 practice ) @@ -11,24 +11,27 @@ const ( var AdultFeeMonthlyRate = map[string]int{ "2025-09": 750, "2025-10": 750, "2025-11": 750, "2025-12": 750, "2026-01": 750, "2026-02": 750, "2026-03": 350, - "2026-04": 700, "2026-05": 450, "2026-06": 600, "2026-07": 600, "2026-08": 600, + "2026-04": 700, "2026-05": 450, "2026-06": 600, "2026-07": 0, "2026-08": 0, + "2026-09": 600, "2026-10": 600, "2026-11": 600, "2026-12": 600, } // CalculateFee returns the adult fee in CZK for attendanceCount practices in // the given monthKey (format "YYYY-MM"). // // 0 practices → 0 -// 1 practice → AdultFeeSingle (200) +// 1 practice → AdultFeeSingle (200), capped at the month's rate so a +// 0-rate month is free for everyone // 2+ → AdultFeeMonthlyRate[monthKey] or AdultFeeDefault func CalculateFee(attendanceCount int, monthKey string) int { if attendanceCount == 0 { return 0 } + rate, ok := AdultFeeMonthlyRate[monthKey] + if !ok { + rate = AdultFeeDefault + } if attendanceCount == 1 { - return AdultFeeSingle + return min(AdultFeeSingle, rate) } - if rate, ok := AdultFeeMonthlyRate[monthKey]; ok { - return rate - } - return AdultFeeDefault + return rate } diff --git a/go/internal/domain/fees/fees_test.go b/go/internal/domain/fees/fees_test.go index 39d4388..c4fa289 100644 --- a/go/internal/domain/fees/fees_test.go +++ b/go/internal/domain/fees/fees_test.go @@ -26,9 +26,11 @@ func TestCalculateFee(t *testing.T) { // Zero attendance always returns 0. {"zero short-circuits", 0, "2026-05", 0}, {"zero empty month", 0, "", 0}, - // Single practice returns AdultFeeSingle regardless of month. + // Single practice returns AdultFeeSingle, capped at the month's rate. {"single practice", 1, "2026-05", AdultFeeSingle}, - {"single ignores monthKey", 1, "unknown", AdultFeeSingle}, + {"single unknown month", 1, "unknown", AdultFeeSingle}, + {"single in free month is free", 1, "2026-07", 0}, + {"two in free month is free", 2, "2026-08", 0}, // Two+ practices for a configured month: must use the map value, not the default. // Expected values are read from AdultFeeMonthlyRate so this test stays correct // when rates are updated — the assertion verifies dispatch logic, not rate values. diff --git a/go/tests/fixtures/pure/calculate_fee/one_session_free_july.json b/go/tests/fixtures/pure/calculate_fee/one_session_free_july.json new file mode 100644 index 0000000..882980e --- /dev/null +++ b/go/tests/fixtures/pure/calculate_fee/one_session_free_july.json @@ -0,0 +1,12 @@ +{ + "case": "one_session_free_july", + "func": "scripts.attendance.calculate_fee", + "captured_at": "2026-10-08", + "input": { + "attendance_count": 1, + "month_key": "2026-07" + }, + "output": { + "fee": 0 + } +} diff --git a/go/tests/fixtures/pure/calculate_fee/two_sessions_default_fallback.json b/go/tests/fixtures/pure/calculate_fee/two_sessions_default_fallback.json index fc0b814..f04940d 100644 --- a/go/tests/fixtures/pure/calculate_fee/two_sessions_default_fallback.json +++ b/go/tests/fixtures/pure/calculate_fee/two_sessions_default_fallback.json @@ -1,12 +1,12 @@ { "case": "two_sessions_default_fallback", "func": "scripts.attendance.calculate_fee", - "captured_at": "2026-05-06", + "captured_at": "2026-10-08", "input": { "attendance_count": 2, "month_key": "2099-01" }, "output": { - "fee": 700 + "fee": 600 } } diff --git a/go/tests/fixtures/pure/calculate_fee/two_sessions_free_july.json b/go/tests/fixtures/pure/calculate_fee/two_sessions_free_july.json new file mode 100644 index 0000000..7b3098d --- /dev/null +++ b/go/tests/fixtures/pure/calculate_fee/two_sessions_free_july.json @@ -0,0 +1,12 @@ +{ + "case": "two_sessions_free_july", + "func": "scripts.attendance.calculate_fee", + "captured_at": "2026-10-08", + "input": { + "attendance_count": 2, + "month_key": "2026-07" + }, + "output": { + "fee": 0 + } +} diff --git a/go/tests/fixtures/pure/calculate_fee/two_sessions_october.json b/go/tests/fixtures/pure/calculate_fee/two_sessions_october.json new file mode 100644 index 0000000..c554c60 --- /dev/null +++ b/go/tests/fixtures/pure/calculate_fee/two_sessions_october.json @@ -0,0 +1,12 @@ +{ + "case": "two_sessions_october", + "func": "scripts.attendance.calculate_fee", + "captured_at": "2026-10-08", + "input": { + "attendance_count": 2, + "month_key": "2026-10" + }, + "output": { + "fee": 600 + } +} diff --git a/legacy/scripts/_fixture_seeds.py b/legacy/scripts/_fixture_seeds.py index 1ad53a2..b464592 100644 --- a/legacy/scripts/_fixture_seeds.py +++ b/legacy/scripts/_fixture_seeds.py @@ -115,6 +115,15 @@ SEEDS[("calculate_fee", "two_sessions_reduced_march")] = { SEEDS[("calculate_fee", "two_sessions_default_fallback")] = { "attendance_count": 2, "month_key": "2099-01" } +SEEDS[("calculate_fee", "two_sessions_free_july")] = { + "attendance_count": 2, "month_key": "2026-07" +} +SEEDS[("calculate_fee", "one_session_free_july")] = { + "attendance_count": 1, "month_key": "2026-07" +} +SEEDS[("calculate_fee", "two_sessions_october")] = { + "attendance_count": 2, "month_key": "2026-10" +} # --- calculate_junior_fee --- diff --git a/legacy/scripts/attendance.py b/legacy/scripts/attendance.py index c858b2d..20ceb61 100644 --- a/legacy/scripts/attendance.py +++ b/legacy/scripts/attendance.py @@ -10,7 +10,7 @@ from config import ATTENDANCE_SHEET_ID as SHEET_ID, JUNIOR_SHEET_GID EXPORT_URL = f"https://docs.google.com/spreadsheets/d/{SHEET_ID}/export?format=csv&gid=0" JUNIOR_EXPORT_URL = f"https://docs.google.com/spreadsheets/d/{SHEET_ID}/export?format=csv&gid={JUNIOR_SHEET_GID}" -ADULT_FEE_DEFAULT = 700 # CZK, for 2+ practices in a month +ADULT_FEE_DEFAULT = 600 # CZK, for 2+ practices in a month ADULT_FEE_SINGLE = 200 # CZK, for exactly 1 practice in a month ADULT_FEE_MONTHLY_RATE = { "2025-09": 750, @@ -23,8 +23,12 @@ ADULT_FEE_MONTHLY_RATE = { "2026-04": 700, "2026-05": 450, "2026-06": 600, - "2026-07": 600, - "2026-08": 600, + "2026-07": 0, + "2026-08": 0, + "2026-09": 600, + "2026-10": 600, + "2026-11": 600, + "2026-12": 600, } JUNIOR_FEE_DEFAULT = 500 # CZK for 2+ practices @@ -91,12 +95,13 @@ def group_by_month(dates: list[tuple[int, datetime]], merged_months: dict[str, s def calculate_fee(attendance_count: int, month_key: str) -> int: - """Apply fee rules: 0 → 0, 1 → 200, 2+ → configured rate (default 750).""" + """Apply fee rules: 0 → 0, 1 → 200 (capped at the month's rate), 2+ → configured rate (default 600).""" if attendance_count == 0: return 0 + rate = ADULT_FEE_MONTHLY_RATE.get(month_key, ADULT_FEE_DEFAULT) if attendance_count == 1: - return ADULT_FEE_SINGLE - return ADULT_FEE_MONTHLY_RATE.get(month_key, ADULT_FEE_DEFAULT) + return min(ADULT_FEE_SINGLE, rate) + return rate def calculate_junior_fee(attendance_count: int, month_key: str) -> str | int: