Merge pull request 'feat(fees): junior rate for 2026-08 is 0' (#50) from feat/junior-fee-2026-08 into main
Reviewed-on: #50
This commit was merged in pull request #50.
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-10-08 21:27 CEST — feat(fees): junior rate for 2026-08 is 0
|
||||||
|
|
||||||
|
- Junior monthly rate for 2026-08: 500 → 0 CZK, same as adults.
|
||||||
|
- A single practice in a 0-rate month is now 0 instead of `?`, since there is nothing to review in a free month. Other months are unchanged.
|
||||||
|
- Mirrored in `go/internal/domain/fees/junior.go` and `legacy/scripts/attendance.py`; two new parity fixtures for `calculate_junior_fee`.
|
||||||
|
|
||||||
## 2026-10-08 12:37 CEST — feat(infer): messages starting with "other:" are not monthly fees
|
## 2026-10-08 12:37 CEST — feat(infer): messages starting with "other:" are not monthly fees
|
||||||
|
|
||||||
- When the bank message starts with `other:` (case-insensitive), inference no longer looks for a month. It copies the trimmed message into Purpose, so the payment is linked to the person as an "other" transaction and not counted as a monthly fee. Messages that contain `other:` elsewhere are inferred as before.
|
- When the bank message starts with `other:` (case-insensitive), inference no longer looks for a month. It copies the trimmed message into Purpose, so the payment is linked to the person as an "other" transaction and not counted as a monthly fee. Messages that contain `other:` elsewhere are inferred as before.
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ Tiers are set in column B of the attendance sheet:
|
|||||||
### Fee calculation
|
### Fee calculation
|
||||||
|
|
||||||
- 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)
|
- 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)
|
- Juniors: 0 → 0, 1 → `"?"` (manual review required; 0 in a 0-rate month), 2+ → monthly rate from `JuniorFeeMonthlyRate` / `JUNIOR_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))`.
|
- 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))`.
|
||||||
|
|
||||||
### Merged months
|
### Merged months
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const JuniorFeeDefault = 500 // CZK fallback for 2+ practices when month not in
|
|||||||
var JuniorFeeMonthlyRate = map[string]int{
|
var JuniorFeeMonthlyRate = map[string]int{
|
||||||
"2025-09": 250,
|
"2025-09": 250,
|
||||||
"2026-03": 250,
|
"2026-03": 250,
|
||||||
|
"2026-08": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expected is the result of a junior fee calculation.
|
// Expected is the result of a junior fee calculation.
|
||||||
@@ -21,17 +22,19 @@ type Expected struct {
|
|||||||
// the given monthKey (format "YYYY-MM").
|
// the given monthKey (format "YYYY-MM").
|
||||||
//
|
//
|
||||||
// 0 practices → Expected{Value: 0}
|
// 0 practices → Expected{Value: 0}
|
||||||
// 1 practice → Expected{Unknown: true} (manual review; Python sentinel "?")
|
// 1 practice → Expected{Unknown: true} (manual review; Python sentinel "?"),
|
||||||
|
// or Expected{Value: 0} when the month's rate is 0
|
||||||
// 2+ → Expected{Value: JuniorFeeMonthlyRate[monthKey] or JuniorFeeDefault}
|
// 2+ → Expected{Value: JuniorFeeMonthlyRate[monthKey] or JuniorFeeDefault}
|
||||||
func CalculateJuniorFee(attendanceCount int, monthKey string) Expected {
|
func CalculateJuniorFee(attendanceCount int, monthKey string) Expected {
|
||||||
if attendanceCount == 0 {
|
if attendanceCount == 0 {
|
||||||
return Expected{Value: 0}
|
return Expected{Value: 0}
|
||||||
}
|
}
|
||||||
if attendanceCount == 1 {
|
rate, ok := JuniorFeeMonthlyRate[monthKey]
|
||||||
|
if !ok {
|
||||||
|
rate = JuniorFeeDefault
|
||||||
|
}
|
||||||
|
if attendanceCount == 1 && rate > 0 {
|
||||||
return Expected{Unknown: true}
|
return Expected{Unknown: true}
|
||||||
}
|
}
|
||||||
if rate, ok := JuniorFeeMonthlyRate[monthKey]; ok {
|
|
||||||
return Expected{Value: rate}
|
return Expected{Value: rate}
|
||||||
}
|
|
||||||
return Expected{Value: JuniorFeeDefault}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ func TestCalculateJuniorFee(t *testing.T) {
|
|||||||
{"two practices reduced sept", 2, "2025-09", mustRate("2025-09")},
|
{"two practices reduced sept", 2, "2025-09", mustRate("2025-09")},
|
||||||
{"two practices reduced march", 2, "2026-03", mustRate("2026-03")},
|
{"two practices reduced march", 2, "2026-03", mustRate("2026-03")},
|
||||||
{"high count same as two", 5, "2025-09", mustRate("2025-09")},
|
{"high count same as two", 5, "2025-09", mustRate("2025-09")},
|
||||||
|
// A 0-rate month is free, and a single practice needs no manual review.
|
||||||
|
{"two in free month is free", 2, "2026-08", Expected{Value: 0}},
|
||||||
|
{"single in free month is free", 1, "2026-08", Expected{Value: 0}},
|
||||||
// Two+ practices for an unknown/future month: must fall back to JuniorFeeDefault.
|
// Two+ practices for an unknown/future month: must fall back to JuniorFeeDefault.
|
||||||
{"unknown future month falls back", 2, "2027-01", Expected{Value: JuniorFeeDefault}},
|
{"unknown future month falls back", 2, "2027-01", Expected{Value: JuniorFeeDefault}},
|
||||||
{"empty month falls back", 2, "", Expected{Value: JuniorFeeDefault}},
|
{"empty month falls back", 2, "", Expected{Value: JuniorFeeDefault}},
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"case": "one_session_free_august",
|
||||||
|
"func": "scripts.attendance.calculate_junior_fee",
|
||||||
|
"captured_at": "2026-10-08",
|
||||||
|
"input": {
|
||||||
|
"attendance_count": 1,
|
||||||
|
"month_key": "2026-08"
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"value": 0,
|
||||||
|
"unknown": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"case": "two_sessions_free_august",
|
||||||
|
"func": "scripts.attendance.calculate_junior_fee",
|
||||||
|
"captured_at": "2026-10-08",
|
||||||
|
"input": {
|
||||||
|
"attendance_count": 2,
|
||||||
|
"month_key": "2026-08"
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"value": 0,
|
||||||
|
"unknown": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -146,6 +146,12 @@ SEEDS[("calculate_junior_fee", "two_sessions_reduced_sep")] = {
|
|||||||
SEEDS[("calculate_junior_fee", "two_sessions_default_fallback")] = {
|
SEEDS[("calculate_junior_fee", "two_sessions_default_fallback")] = {
|
||||||
"attendance_count": 2, "month_key": "2099-06"
|
"attendance_count": 2, "month_key": "2099-06"
|
||||||
}
|
}
|
||||||
|
SEEDS[("calculate_junior_fee", "two_sessions_free_august")] = {
|
||||||
|
"attendance_count": 2, "month_key": "2026-08"
|
||||||
|
}
|
||||||
|
SEEDS[("calculate_junior_fee", "one_session_free_august")] = {
|
||||||
|
"attendance_count": 1, "month_key": "2026-08"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# --- parse_czk_amount ---
|
# --- parse_czk_amount ---
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ ADULT_FEE_MONTHLY_RATE = {
|
|||||||
JUNIOR_FEE_DEFAULT = 500 # CZK for 2+ practices
|
JUNIOR_FEE_DEFAULT = 500 # CZK for 2+ practices
|
||||||
JUNIOR_MONTHLY_RATE = {
|
JUNIOR_MONTHLY_RATE = {
|
||||||
"2025-09": 250,
|
"2025-09": 250,
|
||||||
"2026-03": 250 # reduced fee for March 2026
|
"2026-03": 250, # reduced fee for March 2026
|
||||||
|
"2026-08": 0,
|
||||||
}
|
}
|
||||||
ADULT_MERGED_MONTHS = {
|
ADULT_MERGED_MONTHS = {
|
||||||
"2025-09": "2025-10", # keys are merged into values
|
"2025-09": "2025-10", # keys are merged into values
|
||||||
@@ -105,12 +106,13 @@ def calculate_fee(attendance_count: int, month_key: str) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def calculate_junior_fee(attendance_count: int, month_key: str) -> str | int:
|
def calculate_junior_fee(attendance_count: int, month_key: str) -> str | int:
|
||||||
"""Apply junior fee rules: 0 → 0, 1 → '?', 2+ → Configured Rate (default 500)."""
|
"""Apply junior fee rules: 0 → 0, 1 → '?' (0 in a 0-rate month), 2+ → Configured Rate (default 500)."""
|
||||||
if attendance_count == 0:
|
if attendance_count == 0:
|
||||||
return 0
|
return 0
|
||||||
if attendance_count == 1:
|
rate = JUNIOR_MONTHLY_RATE.get(month_key, JUNIOR_FEE_DEFAULT)
|
||||||
|
if attendance_count == 1 and rate > 0:
|
||||||
return "?"
|
return "?"
|
||||||
return JUNIOR_MONTHLY_RATE.get(month_key, JUNIOR_FEE_DEFAULT)
|
return rate
|
||||||
|
|
||||||
|
|
||||||
def get_members(rows: list[list[str]]) -> list[tuple[str, str, list[str]]]:
|
def get_members(rows: list[list[str]]) -> list[tuple[str, str, list[str]]]:
|
||||||
|
|||||||
Reference in New Issue
Block a user