diff --git a/go/internal/domain/fees/fees.go b/go/internal/domain/fees/fees.go new file mode 100644 index 0000000..2ebee80 --- /dev/null +++ b/go/internal/domain/fees/fees.go @@ -0,0 +1,34 @@ +// Package fees ports fee calculation from scripts/attendance.py. +package fees + +const ( + AdultFeeDefault = 700 // CZK fallback for 2+ practices when month not in AdultFeeMonthlyRate + AdultFeeSingle = 200 // CZK for exactly 1 practice +) + +// AdultFeeMonthlyRate mirrors ADULT_FEE_MONTHLY_RATE in 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, + "2026-01": 750, "2026-02": 750, "2026-03": 350, + "2026-04": 700, "2026-05": 700, +} + +// CalculateFee returns the adult fee in CZK for attendanceCount practices in +// the given monthKey (format "YYYY-MM"). +// +// 0 practices → 0 +// 1 practice → AdultFeeSingle (200) +// 2+ → AdultFeeMonthlyRate[monthKey] or AdultFeeDefault +func CalculateFee(attendanceCount int, monthKey string) int { + if attendanceCount == 0 { + return 0 + } + if attendanceCount == 1 { + return AdultFeeSingle + } + if rate, ok := AdultFeeMonthlyRate[monthKey]; ok { + return rate + } + return AdultFeeDefault +} diff --git a/go/internal/domain/fees/fees_test.go b/go/internal/domain/fees/fees_test.go new file mode 100644 index 0000000..ac7e9e5 --- /dev/null +++ b/go/internal/domain/fees/fees_test.go @@ -0,0 +1,37 @@ +package fees + +import "testing" + +func TestCalculateFee(t *testing.T) { + t.Parallel() + + // All expected outputs verified against live Python implementation on 2026-05-06: + // PYTHONPATH=scripts:. python -c 'from attendance import calculate_fee; print([calculate_fee(c,m) for c,m in [(0,"2026-05"),(0,""),(1,"2026-05"),(1,"unknown"),(2,"2026-05"),(2,"2026-03"),(2,"2025-09"),(5,"2026-05"),(2,"2027-01"),(2,"")]])' + tests := []struct { + name string + count int + month string + want int + }{ + {"zero short-circuits", 0, "2026-05", 0}, + {"zero empty month", 0, "", 0}, + {"single practice", 1, "2026-05", 200}, + {"single ignores monthKey", 1, "unknown", 200}, + {"two practices configured month", 2, "2026-05", 700}, + {"two practices reduced march", 2, "2026-03", 350}, + {"two practices early season", 2, "2025-09", 750}, + {"high count same as two", 5, "2026-05", 700}, + {"unknown future month falls back", 2, "2027-01", 700}, + {"empty month falls back", 2, "", 700}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got := CalculateFee(tc.count, tc.month) + if got != tc.want { + t.Errorf("CalculateFee(%d, %q) = %d, want %d", tc.count, tc.month, got, tc.want) + } + }) + } +} diff --git a/go/internal/domain/fees/junior.go b/go/internal/domain/fees/junior.go new file mode 100644 index 0000000..b1f0c3f --- /dev/null +++ b/go/internal/domain/fees/junior.go @@ -0,0 +1,37 @@ +package fees + +const JuniorFeeDefault = 500 // CZK fallback for 2+ practices when month not in JuniorFeeMonthlyRate + +// JuniorFeeMonthlyRate mirrors JUNIOR_MONTHLY_RATE in scripts/attendance.py. +// Months absent from this map fall back to JuniorFeeDefault. +var JuniorFeeMonthlyRate = map[string]int{ + "2025-09": 250, + "2026-03": 250, +} + +// Expected is the result of a junior fee calculation. +// When Unknown is true the fee requires manual review (Python returns "?"); +// in that case Value is meaningless — always check Unknown first. +type Expected struct { + Value int + Unknown bool +} + +// CalculateJuniorFee returns the junior fee for attendanceCount practices in +// the given monthKey (format "YYYY-MM"). +// +// 0 practices → Expected{Value: 0} +// 1 practice → Expected{Unknown: true} (manual review; Python sentinel "?") +// 2+ → Expected{Value: JuniorFeeMonthlyRate[monthKey] or JuniorFeeDefault} +func CalculateJuniorFee(attendanceCount int, monthKey string) Expected { + if attendanceCount == 0 { + return Expected{Value: 0} + } + if attendanceCount == 1 { + return Expected{Unknown: true} + } + if rate, ok := JuniorFeeMonthlyRate[monthKey]; ok { + return Expected{Value: rate} + } + return Expected{Value: JuniorFeeDefault} +} diff --git a/go/internal/domain/fees/junior_test.go b/go/internal/domain/fees/junior_test.go new file mode 100644 index 0000000..a3525ec --- /dev/null +++ b/go/internal/domain/fees/junior_test.go @@ -0,0 +1,37 @@ +package fees + +import "testing" + +func TestCalculateJuniorFee(t *testing.T) { + t.Parallel() + + // All expected outputs verified against live Python implementation on 2026-05-06: + // PYTHONPATH=scripts:. python -c 'from attendance import calculate_junior_fee; print([calculate_junior_fee(c,m) for c,m in [(0,"2026-05"),(0,""),(1,"2026-05"),(1,"unknown"),(2,"2026-05"),(2,"2025-09"),(2,"2026-03"),(5,"2025-09"),(2,"2027-01"),(2,"")]])' + tests := []struct { + name string + count int + month string + want Expected + }{ + {"zero short-circuits", 0, "2026-05", Expected{Value: 0}}, + {"zero empty month", 0, "", Expected{Value: 0}}, + {"single practice sentinel", 1, "2026-05", Expected{Unknown: true}}, + {"single ignores monthKey", 1, "unknown", Expected{Unknown: true}}, + {"two practices default month", 2, "2026-05", Expected{Value: 500}}, + {"two practices reduced sept", 2, "2025-09", Expected{Value: 250}}, + {"two practices reduced march", 2, "2026-03", Expected{Value: 250}}, + {"high count same as two", 5, "2025-09", Expected{Value: 250}}, + {"unknown future month falls back", 2, "2027-01", Expected{Value: 500}}, + {"empty month falls back", 2, "", Expected{Value: 500}}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got := CalculateJuniorFee(tc.count, tc.month) + if got != tc.want { + t.Errorf("CalculateJuniorFee(%d, %q) = %+v, want %+v", tc.count, tc.month, got, tc.want) + } + }) + } +}