package matching import ( "fmt" "fuj-management/go/internal/domain/czech" "time" ) // Transaction is the subset of a payment row used by InferTransactionDetails. // Date accepts string ("YYYY-MM-DD"), float64 (Sheets serial), or int — matching // the heterogeneous types returned by the Sheets API and the FIO scraper. type Transaction struct { Sender string Message string UserID string Date any } // InferredDetails is the result of InferTransactionDetails. type InferredDetails struct { Members []Match Months []string SearchText string } // InferTransactionDetails infers which member(s) and month(s) a transaction belongs to. // // Search text for member matching: sender + message + user_id. // Month search text: message + user_id only (sender excluded, matching Python). // 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). // // defaultYear seeds czech.ParseMonthReferences (Python defaulted to the current year; // callers should pass time.Now().Year() or a fixed year for deterministic tests). // // Ports scripts/match_payments.py infer_transaction_details. func InferTransactionDetails(tx Transaction, memberNames []string, defaultYear int) InferredDetails { 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 len(months) == 0 && tx.Date != nil && tx.Date != "" { if ym := inferMonthFromDate(tx.Date); ym != "" { months = []string{ym} } } if months == nil { months = []string{} } return InferredDetails{ Members: members, Months: months, SearchText: searchText, } } // inferMonthFromDate converts a date value to "YYYY-MM" for the month fallback. // Returns "" on any error, matching Python's bare except pass. func inferMonthFromDate(val any) string { switch v := val.(type) { case int: dt := sheetsEpoch.Add(time.Duration(float64(v) * 24 * float64(time.Hour))) return dt.Format("2006-01") case int64: dt := sheetsEpoch.Add(time.Duration(float64(v) * 24 * float64(time.Hour))) return dt.Format("2006-01") case float64: dt := sheetsEpoch.Add(time.Duration(v * 24 * float64(time.Hour))) return dt.Format("2006-01") case string: if v == "" { return "" } dt, err := time.Parse("2006-01-02", v) if err != nil { return "" } return dt.Format("2006-01") default: return "" } }