Skip to content

Commit

Permalink
feat: filter by all ancestors (#1015)
Browse files Browse the repository at this point in the history
  • Loading branch information
morremeyer authored Apr 21, 2024
1 parent 5b41236 commit 8160786
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 41 deletions.
19 changes: 18 additions & 1 deletion pkg/controllers/v4/envelope.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package v4

import (
"fmt"
"net/http"

"github.com/envelope-zero/backend/v5/pkg/httputil"
Expand Down Expand Up @@ -142,11 +143,27 @@ func GetEnvelopes(c *gin.Context) {
}

q := models.DB.
Order("name ASC").
Order("envelopes.name ASC").
Where(&model, queryFields...)

q = stringFilters(models.DB, q, setFields, filter.Name, filter.Note, filter.Search)

if filter.BudgetID != "" {
budgetID, err := httputil.UUIDFromString(filter.BudgetID)
if err != nil {
s := fmt.Sprintf("Error parsing budget ID for filtering: %s", err.Error())
c.JSON(status(err), EnvelopeListResponse{
Error: &s,
})
return
}

q = q.
Joins("JOIN categories on categories.id = envelopes.category_id").
Joins("JOIN budgets on budgets.id = categories.budget_id").
Where("budgets.id = ?", budgetID)
}

// Set the offset. Does not need checking since the default is 0
q = q.Offset(int(filter.Offset))

Expand Down
36 changes: 21 additions & 15 deletions pkg/controllers/v4/envelope_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,11 @@ func (suite *TestSuiteStandard) TestEnvelopesGetSingle() {
}

func (suite *TestSuiteStandard) TestEnvelopesGetFilter() {
c1 := createTestCategory(suite.T(), v4.CategoryEditable{})
c2 := createTestCategory(suite.T(), v4.CategoryEditable{})
b1 := createTestBudget(suite.T(), v4.BudgetEditable{})
b2 := createTestBudget(suite.T(), v4.BudgetEditable{})

c1 := createTestCategory(suite.T(), v4.CategoryEditable{BudgetID: b1.Data.ID})
c2 := createTestCategory(suite.T(), v4.CategoryEditable{BudgetID: b2.Data.ID})

_ = createTestEnvelope(suite.T(), v4.EnvelopeEditable{
Name: "Groceries",
Expand All @@ -170,38 +173,41 @@ func (suite *TestSuiteStandard) TestEnvelopesGetFilter() {
len int
checkFunc func(t *testing.T, envelopes []v4.Envelope)
}{
{"Archived", "archived=true", 1, func(t *testing.T, envelopes []v4.Envelope) {
for _, e := range envelopes {
assert.True(t, e.Archived)
}
}},
{"Budget 1", fmt.Sprintf("budget=%s", b1.Data.ID), 1, nil},
{"Budget 2", fmt.Sprintf("budget=%s", b2.Data.ID), 2, nil},
{"Category 2", fmt.Sprintf("category=%s", c2.Data.ID), 2, nil},
{"Category Not Existing", "category=e0f9ff7a-9f07-463c-bbd2-0d72d09d3cc6", 0, nil},
{"Empty Note", "note=", 0, nil},
{"Empty Name", "name=", 0, nil},
{"Name & Note", "name=Groceries&note=For the stuff bought in supermarkets", 1, nil},
{"Empty Note", "note=", 0, nil},
{"Fuzzy name", "name=es", 2, nil},
{"Fuzzy note", "note=Because", 2, nil},
{"Limit -1", "limit=-1", 3, nil},
{"Limit 0", "limit=0", 0, nil},
{"Limit 4", "limit=4", 3, nil},
{"Offset 0, limit 2", "offset=0&limit=2", 2, nil},
{"Name & Note", "name=Groceries&note=For the stuff bought in supermarkets", 1, nil},
{"Non-matching budget", fmt.Sprintf("budget=%s", uuid.New()), 0, nil},
{"Not archived", "archived=false", 2, func(t *testing.T, envelopes []v4.Envelope) {
for _, e := range envelopes {
assert.False(t, e.Archived)
}
}},
{"Archived", "archived=true", 1, func(t *testing.T, envelopes []v4.Envelope) {
for _, e := range envelopes {
assert.True(t, e.Archived)
}
}},
{"Offset 2", "offset=2", 1, nil},
{"Search for 'hair'", "search=hair", 2, nil},
{"Search for 'st'", "search=st", 2, nil},
{"Search for 'STUFF'", "search=STUFF", 1, nil},
{"Offset 2", "offset=2", 1, nil},
{"Offset 0, limit 2", "offset=0&limit=2", 2, nil},
{"Limit 4", "limit=4", 3, nil},
{"Limit 0", "limit=0", 0, nil},
{"Limit -1", "limit=-1", 3, nil},
}

for _, tt := range tests {
suite.T().Run(tt.name, func(t *testing.T) {
var re v4.EnvelopeListResponse
r := test.Request(t, http.MethodGet, fmt.Sprintf("/v4/envelopes?%s", tt.query), "")
test.AssertHTTPStatus(suite.T(), &r, http.StatusOK)
test.AssertHTTPStatus(t, &r, http.StatusOK)
test.DecodeResponse(t, &r, &re)

assert.Equal(t, tt.len, len(re.Data), "Request ID: %s", r.Result().Header.Get("x-request-id"))
Expand Down
1 change: 1 addition & 0 deletions pkg/controllers/v4/envelope_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ type EnvelopeResponse struct {
}

type EnvelopeQueryFilter struct {
BudgetID string `form:"budget" filterField:"false"` // By budget ID
CategoryID string `form:"category"` // By the ID of the category
Name string `form:"name" filterField:"false"` // By name
Note string `form:"note" filterField:"false"` // By the note
Expand Down
36 changes: 35 additions & 1 deletion pkg/controllers/v4/goal.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package v4

import (
"fmt"
"net/http"

"github.com/envelope-zero/backend/v5/internal/types"
Expand Down Expand Up @@ -149,7 +150,7 @@ func GetGoals(c *gin.Context) {
}

q := models.DB.
Order("date(month) ASC, name ASC").
Order("date(goals.month) ASC, goals.name ASC").
Where(&where, queryFields...)

q = stringFilters(models.DB, q, setFields, filter.Name, filter.Note, filter.Search)
Expand Down Expand Up @@ -198,6 +199,39 @@ func GetGoals(c *gin.Context) {
q = q.Where("goals.amount >= ?", filter.AmountMoreOrEqual)
}

if filter.CategoryID != "" {
categoryID, err := httputil.UUIDFromString(filter.CategoryID)
if err != nil {
s := fmt.Sprintf("Error parsing category ID for filtering: %s", err.Error())
c.JSON(status(err), GoalListResponse{
Error: &s,
})
return
}

q = q.
Joins("JOIN envelopes AS category_filter_envelopes on category_filter_envelopes.id = goals.envelope_id").
Joins("JOIN categories AS category_filter_categories on category_filter_categories.id = category_filter_envelopes.category_id").
Where("category_filter_categories.id = ?", categoryID)
}

if filter.BudgetID != "" {
budgetID, err := httputil.UUIDFromString(filter.BudgetID)
if err != nil {
s := fmt.Sprintf("Error parsing budget ID for filtering: %s", err.Error())
c.JSON(status(err), GoalListResponse{
Error: &s,
})
return
}

q = q.
Joins("JOIN envelopes on envelopes.id = goals.envelope_id").
Joins("JOIN categories on categories.id = envelopes.category_id").
Joins("JOIN budgets on budgets.id = categories.budget_id").
Where("budgets.id = ?", budgetID)
}

var goals []models.Goal
err = q.Find(&goals).Error
if err != nil {
Expand Down
49 changes: 28 additions & 21 deletions pkg/controllers/v4/goal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,11 @@ func (suite *TestSuiteStandard) TestGoalsGet() {
func (suite *TestSuiteStandard) TestGoalsGetFilter() {
b := createTestBudget(suite.T(), v4.BudgetEditable{})

c := createTestCategory(suite.T(), v4.CategoryEditable{BudgetID: b.Data.ID})
c1 := createTestCategory(suite.T(), v4.CategoryEditable{BudgetID: b.Data.ID})
c2 := createTestCategory(suite.T(), v4.CategoryEditable{BudgetID: b.Data.ID})

e1 := createTestEnvelope(suite.T(), v4.EnvelopeEditable{CategoryID: c.Data.ID})
e2 := createTestEnvelope(suite.T(), v4.EnvelopeEditable{CategoryID: c.Data.ID})
e1 := createTestEnvelope(suite.T(), v4.EnvelopeEditable{CategoryID: c1.Data.ID})
e2 := createTestEnvelope(suite.T(), v4.EnvelopeEditable{CategoryID: c2.Data.ID})

_ = createTestGoal(suite.T(), v4.GoalEditable{
Name: "Test Goal",
Expand Down Expand Up @@ -194,33 +195,39 @@ func (suite *TestSuiteStandard) TestGoalsGetFilter() {
query string
len int
}{
{"Same month", fmt.Sprintf("month=%s", types.NewMonth(2024, 1)), 2},
{"After month", fmt.Sprintf("fromMonth=%s", types.NewMonth(2024, 2)), 1},
{"Before month", fmt.Sprintf("untilMonth=%s", types.NewMonth(2024, 2)), 3},
{"After all months", fmt.Sprintf("fromMonth=%s", types.NewMonth(2024, 6)), 0},
{"Before all months", fmt.Sprintf("untilMonth=%s", types.NewMonth(2023, 6)), 0},
{"Impossible between two months", fmt.Sprintf("fromMonth=%s&untilMonth=%s", types.NewMonth(2024, 11), types.NewMonth(2024, 10)), 0},
{"Exact Amount", fmt.Sprintf("amount=%s", decimal.NewFromFloat(200).String()), 1},
{"Note", "note=can", 1},
{"No note", "note=", 1},
{"Fuzzy note", "note=so", 2},
{"After month", fmt.Sprintf("fromMonth=%s", types.NewMonth(2024, 2)), 1},
{"Amount less or equal to 99", "amountLessOrEqual=99", 0},
{"Amount less or equal to 200", "amountLessOrEqual=200", 2},
{"Amount more or equal to 3", "amountMoreOrEqual=3", 3},
{"Amount more or equal to 50 and less than 500", "amountMoreOrEqual=50&amountLessOrEqual=500", 2},
{"Amount more or equal to 100 and less than 10", "amountMoreOrEqual=100&amountLessOrEqual=10", 0},
{"Amount more or equal to 500.813", "amountMoreOrEqual=500.813", 1},
{"Amount more or equal to 99999", "amountMoreOrEqual=99999", 0},
{"Amount more or equal to 100 and less than 10", "amountMoreOrEqual=100&amountLessOrEqual=10", 0},
{"Amount more or equal to 50 and less than 500", "amountMoreOrEqual=50&amountLessOrEqual=500", 2},
{"Before all months", fmt.Sprintf("untilMonth=%s", types.NewMonth(2023, 6)), 0},
{"Before month", fmt.Sprintf("untilMonth=%s", types.NewMonth(2024, 2)), 3},
{"Budget matches", fmt.Sprintf("budget=%s", b.Data.ID), 3},
{"Budget does not match", fmt.Sprintf("budget=%s", uuid.New()), 0},
{"Category 1", fmt.Sprintf("category=%s", c1.Data.ID), 2},
{"Category 1, but budget does not match", fmt.Sprintf("category=%s&budget=%s", c1.Data.ID, uuid.New()), 0},
{"Category 2", fmt.Sprintf("category=%s", c2.Data.ID), 1},
{"Category does not match", fmt.Sprintf("category=%s", uuid.New()), 0},
{"Exact Amount", fmt.Sprintf("amount=%s", decimal.NewFromFloat(200).String()), 1},
{"Fuzzy note", "note=so", 2},
{"Impossible between two months", fmt.Sprintf("fromMonth=%s&untilMonth=%s", types.NewMonth(2024, 11), types.NewMonth(2024, 10)), 0},
{"Limit and Fuzzy Note", "limit=1&note=so", 1},
{"Limit and Offset", "limit=1&offset=1", 1},
{"Limit negative", "limit=-123", 3},
{"Limit positive", "limit=2", 2},
{"Limit zero", "limit=0", 0},
{"Limit unset", "limit=-1", 3},
{"Limit negative", "limit=-123", 3},
{"Offset zero", "offset=0", 3},
{"Offset positive", "offset=2", 1},
{"Offset higher than number", "offset=5", 0},
{"Limit and Offset", "limit=1&offset=1", 1},
{"Limit and Fuzzy Note", "limit=1&note=so", 1},
{"Limit zero", "limit=0", 0},
{"No note", "note=", 1},
{"Note", "note=can", 1},
{"Offset and Fuzzy Note", "offset=2&note=they", 0},
{"Offset higher than number", "offset=5", 0},
{"Offset positive", "offset=2", 1},
{"Offset zero", "offset=0", 3},
{"Same month", fmt.Sprintf("month=%s", types.NewMonth(2024, 1)), 2},
}

for _, tt := range tests {
Expand Down
2 changes: 2 additions & 0 deletions pkg/controllers/v4/goal_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ type GoalResponse struct {
}

type GoalQueryFilter struct {
BudgetID string `form:"budget" filterField:"false"` // By budget ID
CategoryID string `form:"category" filterField:"false"` // By category ID
Name string `form:"name" filterField:"false"` // By name
Note string `form:"note" filterField:"false"` // By the note
Search string `form:"search" filterField:"false"` // By string in name or note
Expand Down
16 changes: 16 additions & 0 deletions pkg/controllers/v4/match_rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,22 @@ func GetMatchRules(c *gin.Context) {
q = q.Where("match = ''")
}

if filter.BudgetID != "" {
budgetID, err := httputil.UUIDFromString(filter.BudgetID)
if err != nil {
s := fmt.Sprintf("Error parsing budget ID for filtering: %s", err.Error())
c.JSON(status(err), MatchRuleListResponse{
Error: &s,
})
return
}

q = q.
Joins("JOIN accounts on accounts.id = match_rules.account_id").
Joins("JOIN budgets on budgets.id = accounts.budget_id").
Where("budgets.id = ?", budgetID)
}

// Set the offset. Does not need checking since the default is 0
q = q.Offset(int(filter.Offset))

Expand Down
10 changes: 7 additions & 3 deletions pkg/controllers/v4/match_rule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,11 @@ func (suite *TestSuiteStandard) TestMatchRulesDatabaseError() {

// TestMatchRulesGetFilter verifies that filtering Match Rules works as expected.
func (suite *TestSuiteStandard) TestMatchRulesGetFilter() {
b := createTestBudget(suite.T(), v4.BudgetEditable{})
b1 := createTestBudget(suite.T(), v4.BudgetEditable{})
b2 := createTestBudget(suite.T(), v4.BudgetEditable{})

a1 := createTestAccount(suite.T(), v4.AccountEditable{BudgetID: b.Data.ID, Name: "TestMatchRulesGetFilter 1"})
a2 := createTestAccount(suite.T(), v4.AccountEditable{BudgetID: b.Data.ID, Name: "TestMatchRulesGetFilter 2"})
a1 := createTestAccount(suite.T(), v4.AccountEditable{BudgetID: b1.Data.ID, Name: "TestMatchRulesGetFilter 1"})
a2 := createTestAccount(suite.T(), v4.AccountEditable{BudgetID: b2.Data.ID, Name: "TestMatchRulesGetFilter 2"})

_ = createTestMatchRule(suite.T(), v4.MatchRuleEditable{
Priority: 1,
Expand All @@ -215,6 +216,9 @@ func (suite *TestSuiteStandard) TestMatchRulesGetFilter() {
query string
len int
}{
{"Budget 1", fmt.Sprintf("budget=%s", b1.Data.ID), 1},
{"Budget 2", fmt.Sprintf("budget=%s", b2.Data.ID), 2},
{"Budget does not match", fmt.Sprintf("budget=%s", uuid.New()), 0},
{"Limit over count", "limit=5", 3},
{"Limit under count", "limit=2", 2},
{"Offset", "offset=2", 1},
Expand Down
1 change: 1 addition & 0 deletions pkg/controllers/v4/match_rule_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ func newMatchRule(c *gin.Context, model models.MatchRule) MatchRule {

// MatchRuleQueryFilter contains the fields that Match Rules can be filtered with.
type MatchRuleQueryFilter struct {
BudgetID string `form:"budget" filterField:"false"` // By budget ID
Priority uint `form:"priority"` // By priority
Match string `form:"match" filterField:"false"` // By match
AccountID string `form:"account"` // By ID of the Account they map to
Expand Down

0 comments on commit 8160786

Please sign in to comment.