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>
109 lines
4.3 KiB
Go
109 lines
4.3 KiB
Go
package matching
|
|
|
|
// Expected values verified against scripts/match_payments.py on 2026-05-06:
|
|
//
|
|
// PYTHONPATH=scripts:. python3 << 'EOF'
|
|
// from match_payments import infer_transaction_details
|
|
// MEMBERS = ["Tomáš Němeček (Tov)", "Jana Nováková"]
|
|
// cases = [
|
|
// ({"sender":"Tomas Nemecek","message":"clenske 04/2026","user_id":"","date":"2026-04-15"}, "full match"),
|
|
// ({"sender":"Tomas Nemecek","message":"","user_id":"","date":"2026-04-15"}, "sender fallback month"),
|
|
// ({"sender":"Jana Novakova","message":"","user_id":"","date":44197}, "serial int date"),
|
|
// ({"sender":"neznamy","message":"","user_id":"","date":""}, "no match"),
|
|
// ({"sender":"Tomas Nemecek","message":"","user_id":"","date":44197.5}, "serial float date"),
|
|
// ]
|
|
// for tx, label in cases:
|
|
// r = infer_transaction_details(tx, MEMBERS)
|
|
// print(label + ": members=" + repr(r["members"]) + " months=" + repr(r["months"]) + " search_text=" + repr(r["search_text"]))
|
|
// EOF
|
|
//
|
|
// Output:
|
|
//
|
|
// full match: members=[('Tomáš Němeček (Tov)', 'auto')] months=['2026-04'] search_text='Tomas Nemecek clenske 04/2026 '
|
|
// sender fallback month: members=[('Tomáš Němeček (Tov)', 'auto')] months=['2026-04'] search_text='Tomas Nemecek '
|
|
// serial int date: members=[('Jana Nováková', 'auto')] months=['2021-01'] search_text='Jana Novakova '
|
|
// no match: members=[] months=[] search_text='neznamy '
|
|
// serial float date: members=[('Tomáš Němeček (Tov)', 'auto')] months=['2021-01'] search_text='Tomas Nemecek '
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
var inferMembers = []string{"Tomáš Němeček (Tov)", "Jana Nováková"}
|
|
|
|
func TestInferTransactionDetails(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
tx Transaction
|
|
defaultYear int
|
|
wantMembers []Match
|
|
wantMonths []string
|
|
wantSearchText string
|
|
}{
|
|
{
|
|
name: "full match — members and months from search text",
|
|
tx: Transaction{Sender: "Tomas Nemecek", Message: "clenske 04/2026", UserID: "", Date: "2026-04-15"},
|
|
defaultYear: 2026,
|
|
wantMembers: []Match{{Name: "Tomáš Němeček (Tov)", Confidence: ConfidenceAuto}},
|
|
wantMonths: []string{"2026-04"},
|
|
// Python: sender + " " + message + " " + user_id (no trim)
|
|
wantSearchText: "Tomas Nemecek clenske 04/2026 ",
|
|
},
|
|
{
|
|
// months not in message → fall back to date string
|
|
name: "months fall back to date string",
|
|
tx: Transaction{Sender: "Tomas Nemecek", Message: "", UserID: "", Date: "2026-04-15"},
|
|
defaultYear: 2026,
|
|
wantMembers: []Match{{Name: "Tomáš Němeček (Tov)", Confidence: ConfidenceAuto}},
|
|
wantMonths: []string{"2026-04"},
|
|
wantSearchText: "Tomas Nemecek ",
|
|
},
|
|
{
|
|
// months fall back to Sheets serial int date
|
|
name: "months fall back to serial int date",
|
|
tx: Transaction{Sender: "Jana Novakova", Message: "", UserID: "", Date: int(44197)},
|
|
defaultYear: 2026,
|
|
wantMembers: []Match{{Name: "Jana Nováková", Confidence: ConfidenceAuto}},
|
|
wantMonths: []string{"2021-01"},
|
|
wantSearchText: "Jana Novakova ",
|
|
},
|
|
{
|
|
// months fall back to Sheets serial float64 date
|
|
name: "months fall back to serial float date",
|
|
tx: Transaction{Sender: "Tomas Nemecek", Message: "", UserID: "", Date: float64(44197.5)},
|
|
defaultYear: 2026,
|
|
wantMembers: []Match{{Name: "Tomáš Němeček (Tov)", Confidence: ConfidenceAuto}},
|
|
wantMonths: []string{"2021-01"},
|
|
wantSearchText: "Tomas Nemecek ",
|
|
},
|
|
{
|
|
name: "no match — both slices empty not nil",
|
|
tx: Transaction{Sender: "neznamy", Message: "", UserID: "", Date: ""},
|
|
defaultYear: 2026,
|
|
wantMembers: []Match{},
|
|
wantMonths: []string{},
|
|
wantSearchText: "neznamy ",
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got := InferTransactionDetails(tc.tx, inferMembers, tc.defaultYear)
|
|
|
|
if !reflect.DeepEqual(got.Members, tc.wantMembers) {
|
|
t.Errorf("Members\n got %v\n want %v", got.Members, tc.wantMembers)
|
|
}
|
|
if !reflect.DeepEqual(got.Months, tc.wantMonths) {
|
|
t.Errorf("Months\n got %v\n want %v", got.Months, tc.wantMonths)
|
|
}
|
|
if got.SearchText != tc.wantSearchText {
|
|
t.Errorf("SearchText\n got %q\n want %q", got.SearchText, tc.wantSearchText)
|
|
}
|
|
})
|
|
}
|
|
}
|