feat(go/M2.3+M2.4): port domain/fees.CalculateFee and CalculateJuniorFee
All checks were successful
Deploy to K8s / deploy (push) Successful in 6s
All checks were successful
Deploy to K8s / deploy (push) Successful in 6s
Ports calculate_fee and calculate_junior_fee from scripts/attendance.py into a new go/internal/domain/fees package. Introduces the Expected type (Value int, Unknown bool) for the junior "?" sentinel, keeping the Go API strictly typed instead of mirroring Python's str|int return. All 20 table-driven tests pass with -race; golangci-lint clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
34
go/internal/domain/fees/fees.go
Normal file
34
go/internal/domain/fees/fees.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
37
go/internal/domain/fees/fees_test.go
Normal file
37
go/internal/domain/fees/fees_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
37
go/internal/domain/fees/junior.go
Normal file
37
go/internal/domain/fees/junior.go
Normal file
@@ -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}
|
||||||
|
}
|
||||||
37
go/internal/domain/fees/junior_test.go
Normal file
37
go/internal/domain/fees/junior_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user