From 723152cdadd2ea2ec2fe3fc766672c57723a3f6f Mon Sep 17 00:00:00 2001 From: Jan Novak Date: Thu, 7 May 2026 23:17:45 +0200 Subject: [PATCH] fix(go): pass raw value to FormatDate so numeric serial-day dates format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The transaction-row parser in services/membership/sources.go used a helper (`getVal`) that did `fmt.Sprint(row[i])` before passing to `matching.FormatDate`. The Sheets API returns date-formatted cells as `float64` (Sheets serial-day numbers); pre-stringifying defeated `FormatDate`'s `case float64:` dispatch, so values like 46147 leaked through unchanged as the string "46147" instead of being converted to "2026-05-05". Surfaced by `make parity` (M5.4) — every `transactions[].date` on /api/adults and /api/juniors differed between Python and Go. Python side passes the raw value through directly (`isinstance(val, (int, float))` in scripts/match_payments.py format_date), so it was always correct. Added a `getRaw` helper that returns row[i] without stringifying; only the date column needs it. Extended TestLoadTransactions with a numeric-serial-day row to lock in the regression. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 6 ++++++ go/internal/services/membership/sources.go | 12 +++++++++++- go/internal/services/membership/sources_test.go | 11 ++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b24900..c96f0eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2026-05-07 23:17 CEST — fix(go): pass raw value to FormatDate so numeric serial-day dates format + +- `go/internal/services/membership/sources.go`: transaction-row parser now passes `row[idxDate]` directly to `matching.FormatDate` (via a new `getRaw` helper) instead of stringifying first via `getVal`. The Sheets API returns numeric serial-day values as `float64` for date-formatted cells; pre-stringifying them defeated `FormatDate`'s `case float64:` dispatch, causing all numeric dates to leak through as `"46147"` style strings instead of `"2026-05-05"`. +- Surfaced by `make parity` (M5.4): every `transactions[].date` field on `/api/adults` and `/api/juniors` differed between Python and Go. +- `sources_test.go::TestLoadTransactions` extended with a numeric-serial-day row covering the regression. + ## 2026-05-07 23:05 CEST — fix(go): default CacheDir to `tmp/go` to avoid Python collision - `go/internal/config/config.go`: `CacheDir` default changed from `tmp` to `tmp/go`. Override via `CACHE_DIR` env var still works. diff --git a/go/internal/services/membership/sources.go b/go/internal/services/membership/sources.go index 5ea07e1..8f70507 100644 --- a/go/internal/services/membership/sources.go +++ b/go/internal/services/membership/sources.go @@ -394,9 +394,19 @@ func parseTransactionRows(rows [][]any) ([]reconcile.Transaction, error) { return fmt.Sprint(row[i]) } + // getRaw returns row[i] without stringifying — needed for FormatDate to + // dispatch on the underlying numeric type (Sheets returns serial-day + // numbers as float64). Stringifying first defeats that dispatch. + getRaw := func(row []any, i int) any { + if i < 0 || i >= len(row) { + return nil + } + return row[i] + } + var txns []reconcile.Transaction for _, row := range rows[1:] { - dateStr := matching.FormatDate(getVal(row, idxDate)) + dateStr := matching.FormatDate(getRaw(row, idxDate)) amountRaw := row[idxAmount] if idxAmount < 0 || idxAmount >= len(row) { amountRaw = "" diff --git a/go/internal/services/membership/sources_test.go b/go/internal/services/membership/sources_test.go index 828a009..e18c3c2 100644 --- a/go/internal/services/membership/sources_test.go +++ b/go/internal/services/membership/sources_test.go @@ -114,12 +114,15 @@ func TestLoadJuniors(t *testing.T) { func TestLoadTransactions(t *testing.T) { // Sheets fake keyed by "/" — use the real constant. + // Row 1 uses a pre-formatted date string; row 2 uses the numeric Sheets + // serial-day form (float64) — the API returns either depending on cell + // formatting, and FormatDate must handle both. paymentsKey := config.PaymentsSheetID + "/A1:Z" sh := &sheets.Fake{Values: map[string][][]any{ paymentsKey: { {"Date", "Amount", "manual fix", "Person", "Purpose", "Inferred Amount", "Sender", "VS", "Message", "Bank ID", "Sync ID"}, {"2026-04-01", 700.0, "", "Alice", "2026-04", "", "Alice Bank", "", "fee", "", "abc"}, - {"2026-05-01", 500.0, "", "", "", "", "Bob Bank", "", "platba", "", "def"}, + {46147.0, 500.0, "", "", "", "", "Bob Bank", "", "platba", "", "def"}, // 46147 serial-day = 2026-05-05 }, }} s := buildSources(t, &attendance.Fake{}, sh) @@ -137,6 +140,12 @@ func TestLoadTransactions(t *testing.T) { if txns[0].Amount != 700 { t.Errorf("txn[0].Amount: want 700, got %v", txns[0].Amount) } + if txns[0].Date != "2026-04-01" { + t.Errorf("txn[0].Date: want 2026-04-01, got %q", txns[0].Date) + } + if txns[1].Date != "2026-05-05" { + t.Errorf("txn[1].Date (numeric serial-day): want 2026-05-05, got %q", txns[1].Date) + } } func TestLoadExceptions(t *testing.T) { -- 2.49.1