mirror of
https://github.com/chenasraf/cospend-cli.git
synced 2026-05-17 17:38:04 +00:00
fix: payment method & category matching logic
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
32
internal/cache/cache.go
vendored
32
internal/cache/cache.go
vendored
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
6
internal/cache/cache_test.go
vendored
6
internal/cache/cache_test.go
vendored
@@ -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},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user