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:
@@ -42,7 +42,7 @@ const (
|
||||
JuniorSheetGID = "1213318614" // gid=1213318614 — junior practices tab (IMPORTRANGE'd)
|
||||
)
|
||||
|
||||
// CacheSheetMap mirrors scripts/config.py CACHE_SHEET_MAP.
|
||||
// CacheSheetMap mirrors legacy/scripts/config.py CACHE_SHEET_MAP.
|
||||
// Maps a cache key to the Google Sheet ID whose Drive modifiedTime gates it.
|
||||
// Both attendance keys map to the same spreadsheet — different tabs, one Drive file.
|
||||
var CacheSheetMap = map[string]string{
|
||||
@@ -53,7 +53,7 @@ var CacheSheetMap = map[string]string{
|
||||
}
|
||||
|
||||
// Config holds all runtime configuration loaded from environment variables.
|
||||
// Mirrors scripts/config.py.
|
||||
// Mirrors legacy/scripts/config.py.
|
||||
type Config struct {
|
||||
CredentialsPath string
|
||||
QRAccount string // IBAN of the primary account used for QR codes
|
||||
|
||||
@@ -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)))
|
||||
// '
|
||||
|
||||
Vendored
+2
-2
@@ -1,5 +1,5 @@
|
||||
// Package cache implements a Drive-modifiedTime-gated JSON file cache,
|
||||
// mirroring scripts/cache_utils.py.
|
||||
// mirroring legacy/scripts/cache_utils.py.
|
||||
package cache
|
||||
|
||||
import (
|
||||
@@ -26,7 +26,7 @@ type cacheFile struct {
|
||||
|
||||
// FileCache wraps a Drive client to gate JSON file caching on sheet modifiedTime.
|
||||
//
|
||||
// Two TTL knobs mirror scripts/cache_utils.py:
|
||||
// Two TTL knobs mirror legacy/scripts/cache_utils.py:
|
||||
// - ttl: if the cache file on disk is younger than this, skip the Drive check entirely.
|
||||
// - apiCheckTTL: debounces in-memory Drive API calls per sheet ID.
|
||||
//
|
||||
|
||||
@@ -17,7 +17,7 @@ type httpDoer interface {
|
||||
}
|
||||
|
||||
// apiClient fetches transactions from the Fio REST API (JSON).
|
||||
// Ports scripts/fio_utils.py fetch_transactions_api.
|
||||
// Ports legacy/scripts/fio_utils.py fetch_transactions_api.
|
||||
type apiClient struct {
|
||||
token string
|
||||
hc httpDoer
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
// transparentClient fetches transactions from the Fio transparent account page (HTML).
|
||||
// Ports scripts/fio_utils.py FioTableParser + fetch_transactions_transparent.
|
||||
// Ports legacy/scripts/fio_utils.py FioTableParser + fetch_transactions_transparent.
|
||||
type transparentClient struct {
|
||||
accountNum string
|
||||
hc httpDoer
|
||||
|
||||
@@ -30,7 +30,7 @@ type sheetReadWriter interface {
|
||||
// InferPayments fills empty Person/Purpose/Inferred Amount cells in the payments
|
||||
// sheet using name and month matching against the member list.
|
||||
// Returns the number of rows updated (or that would be updated on dry-run).
|
||||
// Ports scripts/infer_payments.py infer_payments.
|
||||
// Ports legacy/scripts/infer_payments.py infer_payments.
|
||||
func InferPayments(
|
||||
ctx context.Context,
|
||||
spreadsheetID string,
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
// columnLabels is the canonical header for the payments sheet.
|
||||
// Mirrors COLUMN_LABELS in scripts/sync_fio_to_sheets.py.
|
||||
// Mirrors COLUMN_LABELS in legacy/scripts/sync_fio_to_sheets.py.
|
||||
var columnLabels = []string{
|
||||
"Date", "Amount", "manual fix", "Person", "Purpose",
|
||||
"Inferred Amount", "Sender", "VS", "Message", "Bank ID", "Sync ID",
|
||||
@@ -37,7 +37,7 @@ type SyncOpts struct {
|
||||
|
||||
// SyncToSheets fetches Fio transactions from all provided clients and appends
|
||||
// new ones to the payments sheet. Returns the number of rows appended.
|
||||
// Ports scripts/sync_fio_to_sheets.py sync_to_sheets.
|
||||
// Ports legacy/scripts/sync_fio_to_sheets.py sync_to_sheets.
|
||||
func SyncToSheets(
|
||||
ctx context.Context,
|
||||
spreadsheetID string,
|
||||
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
)
|
||||
|
||||
// printFeesTable writes a fixed-width adult-fees table to w.
|
||||
// Mirrors scripts/calculate_fees.py main().
|
||||
// Mirrors legacy/scripts/calculate_fees.py main().
|
||||
//
|
||||
// Verify with:
|
||||
//
|
||||
// PYTHONPATH=scripts:. python scripts/calculate_fees.py
|
||||
// PYTHONPATH=legacy/scripts:legacy python legacy/scripts/calculate_fees.py
|
||||
func printFeesTable(w io.Writer, members []reconcile.Member, sortedMonths []string) {
|
||||
type row struct {
|
||||
name string
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package membership
|
||||
|
||||
// Golden strings verified against scripts/calculate_fees.py on 2026-05-06:
|
||||
// Golden strings verified against legacy/scripts/calculate_fees.py on 2026-05-06:
|
||||
//
|
||||
// PYTHONPATH=scripts:. python scripts/calculate_fees.py
|
||||
// PYTHONPATH=legacy/scripts:legacy python legacy/scripts/calculate_fees.py
|
||||
//
|
||||
// (feed equivalent fixture data via attendance sheet or local CSV)
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ import (
|
||||
)
|
||||
|
||||
// printReconcileReport writes the full balance report to w.
|
||||
// Mirrors scripts/match_payments.py print_report().
|
||||
// Mirrors legacy/scripts/match_payments.py print_report().
|
||||
//
|
||||
// Verify with:
|
||||
//
|
||||
// PYTHONPATH=scripts:. python -c '
|
||||
// PYTHONPATH=legacy/scripts:legacy python -c '
|
||||
// from match_payments import print_report, reconcile, fetch_sheet_data, fetch_exceptions
|
||||
// ...'
|
||||
func printReconcileReport(w io.Writer, result reconcile.Result, sortedMonths []string) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package membership
|
||||
|
||||
// Golden strings verified against scripts/match_payments.py print_report() on 2026-05-06:
|
||||
// Golden strings verified against legacy/scripts/match_payments.py print_report() on 2026-05-06:
|
||||
//
|
||||
// PYTHONPATH=scripts:. python -c '
|
||||
// PYTHONPATH=legacy/scripts:legacy python -c '
|
||||
// from match_payments import print_report
|
||||
// result = {
|
||||
// "members": {
|
||||
|
||||
@@ -18,20 +18,20 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Attendance CSV column indices (mirrors COL_* in scripts/attendance.py)
|
||||
// Attendance CSV column indices (mirrors COL_* in legacy/scripts/attendance.py)
|
||||
const (
|
||||
colName = 0
|
||||
colTier = 1
|
||||
firstDateCol = 3
|
||||
)
|
||||
|
||||
// AdultMergedMonths mirrors ADULT_MERGED_MONTHS in scripts/attendance.py.
|
||||
// AdultMergedMonths mirrors ADULT_MERGED_MONTHS in legacy/scripts/attendance.py.
|
||||
// Source month → target month (source attendance accumulated into target).
|
||||
var AdultMergedMonths = map[string]string{
|
||||
"2025-09": "2025-10",
|
||||
}
|
||||
|
||||
// JuniorMergedMonths mirrors JUNIOR_MERGED_MONTHS in scripts/attendance.py.
|
||||
// JuniorMergedMonths mirrors JUNIOR_MERGED_MONTHS in legacy/scripts/attendance.py.
|
||||
var JuniorMergedMonths = map[string]string{
|
||||
"2025-12": "2026-01",
|
||||
"2025-09": "2025-10",
|
||||
@@ -127,11 +127,11 @@ func (s *realSources) LoadExceptions(ctx context.Context) (map[reconcile.Excepti
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Attendance CSV parsing (ports scripts/attendance.py)
|
||||
// Attendance CSV parsing (ports legacy/scripts/attendance.py)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// parseDates returns (columnIndex, YYYY-MM) pairs for all date columns.
|
||||
// Ports scripts/attendance.py parse_dates + strftime("%Y-%m").
|
||||
// Ports legacy/scripts/attendance.py parse_dates + strftime("%Y-%m").
|
||||
func parseDates(header []string) []struct {
|
||||
col int
|
||||
month string
|
||||
@@ -200,7 +200,7 @@ func countTrue(row []string, cols []int) int {
|
||||
|
||||
// parseAdultRows converts raw CSV rows to []reconcile.Member.
|
||||
// Includes all tiers; fee is 0 for non-A tiers (reconcile filters downstream).
|
||||
// Ports scripts/attendance.py get_members_with_fees.
|
||||
// Ports legacy/scripts/attendance.py get_members_with_fees.
|
||||
func parseAdultRows(rows [][]string) ([]reconcile.Member, []string, error) {
|
||||
if len(rows) < 2 {
|
||||
return nil, nil, nil
|
||||
@@ -246,7 +246,7 @@ func parseAdultRows(rows [][]string) ([]reconcile.Member, []string, error) {
|
||||
|
||||
// parseJuniorRows builds junior members by merging tier-J rows from the adult tab
|
||||
// with the junior sheet, then calling CalculateJuniorFee.
|
||||
// Ports scripts/attendance.py get_junior_members_with_fees.
|
||||
// Ports legacy/scripts/attendance.py get_junior_members_with_fees.
|
||||
func parseJuniorRows(adultRows, juniorRows [][]string) ([]reconcile.Member, []string, error) {
|
||||
if len(adultRows) < 2 || len(juniorRows) < 2 {
|
||||
return nil, nil, nil
|
||||
@@ -362,7 +362,7 @@ func parseJuniorRows(adultRows, juniorRows [][]string) ([]reconcile.Member, []st
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Payments sheet row parsing (ports scripts/match_payments.py fetch_sheet_data)
|
||||
// Payments sheet row parsing (ports legacy/scripts/match_payments.py fetch_sheet_data)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func parseTransactionRows(rows [][]any) ([]reconcile.Transaction, error) {
|
||||
@@ -466,7 +466,7 @@ func parseFloat(v any) float64 {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Exceptions tab parsing (ports scripts/match_payments.py fetch_exceptions)
|
||||
// Exceptions tab parsing (ports legacy/scripts/match_payments.py fetch_exceptions)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func parseExceptionRows(rows [][]any) map[reconcile.ExceptionKey]reconcile.Exception {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
type monthSums struct{ expected, paid int }
|
||||
|
||||
// buildAdultsResponse constructs the AdultsResponse wire type from reconcile output.
|
||||
// Mirrors scripts/views.py:build_adults_view_model.
|
||||
// Mirrors legacy/scripts/views.py:build_adults_view_model.
|
||||
func buildAdultsResponse(
|
||||
members []domreconcile.Member,
|
||||
sortedMonths []string,
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
// getMonthLabels builds display labels for sortedMonths, merging month names
|
||||
// (e.g. "Dec+Jan 2026") when mergedMonths maps a source month into this target.
|
||||
// Mirrors scripts/views.py:get_month_labels.
|
||||
// Mirrors legacy/scripts/views.py:get_month_labels.
|
||||
func getMonthLabels(sortedMonths []string, mergedMonths map[string]string) map[string]string {
|
||||
labels := make(map[string]string, len(sortedMonths))
|
||||
for _, m := range sortedMonths {
|
||||
@@ -72,14 +72,14 @@ func labelsForMonths(sortedMonths []string, labels map[string]string) []string {
|
||||
var questionMarkRe = regexp.MustCompile(`\[\?\]\s*`)
|
||||
|
||||
// canonicalKey returns a normalized form of a person name used for deduplication.
|
||||
// Mirrors scripts/match_payments.py:canonical_member_key.
|
||||
// Mirrors legacy/scripts/match_payments.py:canonical_member_key.
|
||||
func canonicalKey(name string) string {
|
||||
return strings.Join(strings.Fields(czech.Normalize(name)), " ")
|
||||
}
|
||||
|
||||
// groupRawPaymentsByPerson groups transactions by the "person" column,
|
||||
// canonicalizing names against memberNames where possible.
|
||||
// Mirrors scripts/views.py:group_payments_by_person (without the
|
||||
// Mirrors legacy/scripts/views.py:group_payments_by_person (without the
|
||||
// "Unmatched / Unknown" bucket that is payments-view-specific).
|
||||
func groupRawPaymentsByPerson(txns []domreconcile.Transaction, memberNames []string) map[string][]RawTransaction {
|
||||
canonicalByKey := make(map[string]string, len(memberNames))
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
// buildJuniorsResponse constructs the JuniorsResponse wire type from reconcile output.
|
||||
// Mirrors scripts/views.py:build_juniors_view_model.
|
||||
// Mirrors legacy/scripts/views.py:build_juniors_view_model.
|
||||
func buildJuniorsResponse(
|
||||
members []domreconcile.Member,
|
||||
sortedMonths []string,
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
// buildPaymentsResponse constructs the PaymentsResponse wire type.
|
||||
// Mirrors scripts/views.py:build_payments_view_model.
|
||||
// Mirrors legacy/scripts/views.py:build_payments_view_model.
|
||||
func buildPaymentsResponse(
|
||||
txns []domreconcile.Transaction,
|
||||
memberNames []string,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package api
|
||||
|
||||
// VersionResponse is the JSON contract for GET /api/version.
|
||||
// Keys match Python's BUILD_META dict (see app.py).
|
||||
// Keys match Python's BUILD_META dict (see legacy/app.py).
|
||||
type VersionResponse struct {
|
||||
Tag string `json:"tag"`
|
||||
Commit string `json:"commit"`
|
||||
|
||||
Reference in New Issue
Block a user