Deploy to K8s / deploy (push) Successful in 20s
Go (go/) becomes the primary implementation. The original Python version moves to legacy/ and is kept runnable; new features are still built in both (see the Dual implementation rule in CLAUDE.md). - git mv app.py, scripts/, templates/, tests/, pyproject.toml, uv.lock and build/ into legacy/ - Makefile: Python targets point at legacy/, venv in legacy/.venv, CREDENTIALS_PATH exported for the root .secret/ file - CI and make image build the Python image with legacy/ as context; the Makefile is no longer copied into the image - legacy/.dockerignore moved to the context root so it takes effect - CLAUDE.md, docs/README.md, Go comments, fixture README, capture script and VS Code launch config updated to the new layout Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
90 lines
2.6 KiB
Go
90 lines
2.6 KiB
Go
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 legacy/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 ""
|
|
}
|
|
}
|