fix: payment method & category matching logic

This commit is contained in:
2026-02-09 23:10:18 +02:00
parent cafd1c1571
commit b545c803bb
4 changed files with 90 additions and 6 deletions

View File

@@ -124,7 +124,10 @@ func parseCategories(data json.RawMessage) []Category {
var obj map[string]Category
if err := json.Unmarshal(data, &obj); err == nil {
result := make([]Category, 0, len(obj))
for _, cat := range obj {
for idStr, cat := range obj {
if id, err := strconv.Atoi(idStr); err == nil {
cat.ID = id
}
result = append(result, cat)
}
return result
@@ -142,7 +145,10 @@ func parsePaymentModes(data json.RawMessage) []PaymentMode {
var obj map[string]PaymentMode
if err := json.Unmarshal(data, &obj); err == nil {
result := make([]PaymentMode, 0, len(obj))
for _, pm := range obj {
for idStr, pm := range obj {
if id, err := strconv.Atoi(idStr); err == nil {
pm.ID = id
}
result = append(result, pm)
}
return result

View File

@@ -547,6 +547,54 @@ func TestProjectCurrencyName(t *testing.T) {
}
}
func TestProjectUnmarshalObjectKeyed(t *testing.T) {
// Real API returns categories and payment modes as objects keyed by ID
projectJSON := `{
"id": "test",
"name": "Test",
"currencyname": "₪",
"members": [],
"currencies": [],
"categories": {
"5": {"name": "Food", "icon": "🍔", "color": "#ff0000"},
"12": {"name": "Transport", "icon": "🚗", "color": "#00ff00"}
},
"paymentmodes": {
"3": {"name": "Credit Card", "icon": "💳", "color": "#0000ff"},
"7": {"name": "Cash", "icon": "💵", "color": "#00ff00"}
}
}`
var project Project
if err := json.Unmarshal([]byte(projectJSON), &project); err != nil {
t.Fatalf("Unmarshal error: %v", err)
}
// Verify categories have correct IDs from map keys
catByName := make(map[string]int)
for _, c := range project.Categories {
catByName[c.Name] = c.ID
}
if catByName["Food"] != 5 {
t.Errorf("Category Food ID = %d, want 5", catByName["Food"])
}
if catByName["Transport"] != 12 {
t.Errorf("Category Transport ID = %d, want 12", catByName["Transport"])
}
// Verify payment modes have correct IDs from map keys
pmByName := make(map[string]int)
for _, pm := range project.PaymentModes {
pmByName[pm.Name] = pm.ID
}
if pmByName["Credit Card"] != 3 {
t.Errorf("PaymentMode Credit Card ID = %d, want 3", pmByName["Credit Card"])
}
if pmByName["Cash"] != 7 {
t.Errorf("PaymentMode Cash ID = %d, want 7", pmByName["Cash"])
}
}
func mustMarshal(v any) json.RawMessage {
data, err := json.Marshal(v)
if err != nil {

View File

@@ -302,8 +302,12 @@ func ResolveMember(project *api.Project, username string) (int, error) {
return 0, fmt.Errorf("member not found: %s", username)
}
// ResolveCategory finds a category by name (case-insensitive) or ID and returns the ID
// ResolveCategory finds a category by name (case-insensitive, substring) or ID and returns the ID
func ResolveCategory(project *api.Project, nameOrID string) (int, error) {
if nameOrID == "" {
return 0, fmt.Errorf("category not found: %s", nameOrID)
}
// Try parsing as ID first
if id, err := strconv.Atoi(nameOrID); err == nil {
for _, c := range project.Categories {
@@ -313,19 +317,31 @@ func ResolveCategory(project *api.Project, nameOrID string) (int, error) {
}
}
// Try matching by name (case-insensitive)
lowerName := strings.ToLower(nameOrID)
// Try exact match first
for _, c := range project.Categories {
if strings.ToLower(c.Name) == lowerName {
return c.ID, nil
}
}
// Fallback to substring match
for _, c := range project.Categories {
if strings.Contains(strings.ToLower(c.Name), lowerName) {
return c.ID, nil
}
}
return 0, fmt.Errorf("category not found: %s", nameOrID)
}
// ResolvePaymentMode finds a payment mode by name (case-insensitive) or ID and returns the ID
// ResolvePaymentMode finds a payment mode by name (case-insensitive, substring) or ID and returns the ID
func ResolvePaymentMode(project *api.Project, nameOrID string) (int, error) {
if nameOrID == "" {
return 0, fmt.Errorf("payment mode not found: %s", nameOrID)
}
// Try parsing as ID first
if id, err := strconv.Atoi(nameOrID); err == nil {
for _, pm := range project.PaymentModes {
@@ -335,14 +351,22 @@ func ResolvePaymentMode(project *api.Project, nameOrID string) (int, error) {
}
}
// Try matching by name (case-insensitive)
lowerName := strings.ToLower(nameOrID)
// Try exact match first
for _, pm := range project.PaymentModes {
if strings.ToLower(pm.Name) == lowerName {
return pm.ID, nil
}
}
// Fallback to substring match
for _, pm := range project.PaymentModes {
if strings.Contains(strings.ToLower(pm.Name), lowerName) {
return pm.ID, nil
}
}
return 0, fmt.Errorf("payment mode not found: %s", nameOrID)
}

View File

@@ -71,6 +71,9 @@ func TestResolveCategory(t *testing.T) {
{"by name lowercase", "groceries", 1, false},
{"by name uppercase", "RESTAURANT", 2, false},
{"by name mixed case", "tRaNsPoRt", 10, false},
{"by substring", "grocer", 1, false},
{"by substring middle", "estauran", 2, false},
{"by substring case insensitive", "TRANS", 10, false},
{"id not found", "99", 0, true},
{"name not found", "Unknown", 0, true},
{"empty string", "", 0, true},
@@ -112,6 +115,9 @@ func TestResolvePaymentMode(t *testing.T) {
{"by name with space", "Credit Card", 2, false},
{"by name with space lowercase", "credit card", 2, false},
{"by name uppercase", "BANK TRANSFER", 3, false},
{"by substring", "credit", 2, false},
{"by substring single word", "card", 2, false},
{"by substring case insensitive", "BANK", 3, false},
{"id not found", "99", 0, true},
{"name not found", "Bitcoin", 0, true},
}