feat(infer): messages starting with "other:" are not monthly fees
When the bank message starts with "other:" (case-insensitive), inference does not look for a month, neither in the text nor from the payment date. The trimmed message is written to Purpose, so reconcile books the payment as an "other" transaction for the person instead of a monthly fee. Messages containing "other:" elsewhere are unchanged. - matching.IsOtherPurpose / is_other_purpose shared by inference and reconcile - reconcile applies the rule to rows not yet processed by inference - no person found: Purpose is still written, row shows as unmatched - implemented in Go and legacy Python; four new parity fixtures Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package matching
|
||||
import (
|
||||
"fmt"
|
||||
"fuj-management/go/internal/domain/czech"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -17,12 +18,22 @@ type Transaction struct {
|
||||
}
|
||||
|
||||
// InferredDetails is the result of InferTransactionDetails.
|
||||
// Months holds "YYYY-MM" keys, or exactly one "other:…" purpose when the
|
||||
// payment is not a monthly fee (see IsOtherPurpose).
|
||||
type InferredDetails struct {
|
||||
Members []Match
|
||||
Months []string
|
||||
SearchText string
|
||||
}
|
||||
|
||||
// IsOtherPurpose reports whether s marks a payment that is not a monthly fee:
|
||||
// it starts with "other:" (case-insensitive, leading whitespace ignored).
|
||||
// Used for both the Purpose column and the bank message.
|
||||
// Ports legacy/scripts/match_payments.py is_other_purpose.
|
||||
func IsOtherPurpose(s string) bool {
|
||||
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(s)), "other:")
|
||||
}
|
||||
|
||||
// InferTransactionDetails infers which member(s) and month(s) a transaction belongs to.
|
||||
//
|
||||
// Search text for member matching: sender + message + user_id.
|
||||
@@ -30,6 +41,10 @@ type InferredDetails struct {
|
||||
// Fallback 1: if no members found, retry match on sender alone.
|
||||
// Fallback 2: if no months found, derive from tx.Date (Sheets serial or YYYY-MM-DD).
|
||||
//
|
||||
// A message that starts with "other:" is not a monthly fee: no month is
|
||||
// inferred (neither from the text nor from the date) and Months holds the
|
||||
// trimmed message as the single purpose.
|
||||
//
|
||||
// defaultYear seeds czech.ParseMonthReferences (Python defaulted to the current year;
|
||||
// callers should pass time.Now().Year() or a fixed year for deterministic tests).
|
||||
//
|
||||
@@ -38,12 +53,20 @@ func InferTransactionDetails(tx Transaction, memberNames []string, defaultYear i
|
||||
searchText := fmt.Sprintf("%s %s %s", tx.Sender, tx.Message, tx.UserID)
|
||||
|
||||
members := MatchMembers(searchText, memberNames)
|
||||
months := czech.ParseMonthReferences(tx.Message+" "+tx.UserID, defaultYear)
|
||||
|
||||
if len(members) == 0 {
|
||||
members = MatchMembers(tx.Sender, memberNames)
|
||||
}
|
||||
|
||||
if IsOtherPurpose(tx.Message) {
|
||||
return InferredDetails{
|
||||
Members: members,
|
||||
Months: []string{strings.TrimSpace(tx.Message)},
|
||||
SearchText: searchText,
|
||||
}
|
||||
}
|
||||
|
||||
months := czech.ParseMonthReferences(tx.Message+" "+tx.UserID, defaultYear)
|
||||
|
||||
if len(months) == 0 && tx.Date != nil && tx.Date != "" {
|
||||
if ym := inferMonthFromDate(tx.Date); ym != "" {
|
||||
months = []string{ym}
|
||||
|
||||
Reference in New Issue
Block a user