refactor: move Python implementation to legacy/, make Go primary
Deploy to K8s / deploy (push) Successful in 20s
Deploy to K8s / deploy (push) Successful in 20s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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("<input>", 2026))'
|
||||
// PYTHONPATH=legacy/scripts:legacy python -c 'from czech_utils import parse_month_references; print(parse_month_references("<input>", 2026))'
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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].
|
||||
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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)))
|
||||
// '
|
||||
|
||||
Reference in New Issue
Block a user