package matching // Expected values verified against legacy/scripts/match_payments.py on 2026-05-06: // // PYTHONPATH=legacy/scripts:legacy 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 TestIsOtherPurpose(t *testing.T) { t.Parallel() cases := map[string]bool{ "other: tournament": true, "other:shirt": true, " Other: Tournament": true, "OTHER:x": true, "leden other: x": false, "other tournament": false, "another: x": false, "2026-01": false, "": false, } for in, want := range cases { if got := IsOtherPurpose(in); got != want { t.Errorf("IsOtherPurpose(%q) = %v, want %v", in, got, want) } } } func TestInferTransactionDetails_OtherMessage(t *testing.T) { t.Parallel() jana := []Match{{Name: "Jana Nováková", Confidence: ConfidenceAuto}} cases := []struct { name string tx Transaction wantMembers []Match wantMonths []string }{ { // Month words and the date must both be ignored. name: "other prefix: message becomes the purpose, no month", tx: Transaction{Sender: "Jana Novakova", Message: " other: turnaj leden 2026 ", Date: "2026-04-15"}, wantMembers: jana, wantMonths: []string{"other: turnaj leden 2026"}, }, { name: "other prefix is case-insensitive and keeps the original text", tx: Transaction{Sender: "Jana Novakova", Message: "Other: Dres", Date: "2026-04-15"}, wantMembers: jana, wantMonths: []string{"Other: Dres"}, }, { name: "other prefix with no known member still yields the purpose", tx: Transaction{Sender: "Unknown Payer", Message: "other: dres", Date: "2026-04-15"}, wantMembers: nil, wantMonths: []string{"other: dres"}, }, { name: "other: not at the start is handled the old way", tx: Transaction{Sender: "Jana Novakova", Message: "leden other: dres", Date: "2026-04-15"}, wantMembers: jana, wantMonths: []string{"2026-01"}, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := InferTransactionDetails(tc.tx, inferMembers, 2026) if len(got.Members) != len(tc.wantMembers) || (len(got.Members) > 0 && !reflect.DeepEqual(got.Members, tc.wantMembers)) { t.Errorf("Members = %v, want %v", got.Members, tc.wantMembers) } if !reflect.DeepEqual(got.Months, tc.wantMonths) { t.Errorf("Months = %v, want %v", got.Months, tc.wantMonths) } }) } } 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) } }) } }