feat(go/M2.7-2.9): port domain/matching package

New go/internal/domain/matching package porting three helpers from
scripts/match_payments.py:

- BuildNameVariants: normalized ASCII variants from a member name (nickname
  in parens, last/first split, len<3 filtered); variants[0] is always the
  full base name — MatchMembers relies on this invariant.
- MatchMembers: auto/review confidence matching with an exact-name
  short-circuit pass that prevents nickname substrings (tov) from firing
  inside longer surnames (ottova); common-surname filter for review tier.
- FormatDate: nil/empty/""/serial int/float64 (since 1899-12-30, fractional
  days supported)/YYYY-MM-DD passthrough/garbage → never errors.
- InferTransactionDetails: composes BuildNameVariants+MatchMembers+
  ParseMonthReferences; falls back to sender-only member match and
  date-derived month when text carries no signal.

21 table-driven tests; all expected values verified against live Python
on 2026-05-06. go-build, go-test, go-lint all clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 13:19:42 +02:00
parent c2bffed1b8
commit e596f0000e
12 changed files with 835 additions and 3 deletions

View File

@@ -0,0 +1,49 @@
package matching
// Expected values verified against scripts/match_payments.py on 2026-05-06:
//
// PYTHONPATH=scripts:. 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)))
// '
//
// Output:
//
// ''
// ''
// '2021-01-01'
// '2021-01-01'
// '2026-04-15'
// 'garbage'
// '2026-04-15'
import "testing"
func TestFormatDate(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input any
want string
}{
{name: "nil", input: nil, want: ""},
{name: "empty string", input: "", want: ""},
{name: "serial int", input: int(44197), want: "2021-01-01"},
{name: "serial float fractional", input: float64(44197.5), want: "2021-01-01"},
{name: "already formatted", input: "2026-04-15", want: "2026-04-15"},
{name: "garbage string", input: "garbage", want: "garbage"},
{name: "padded date string trimmed", input: " 2026-04-15 ", want: "2026-04-15"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := FormatDate(tc.input)
if got != tc.want {
t.Errorf("FormatDate(%v) = %q, want %q", tc.input, got, tc.want)
}
})
}
}